Skip to content

Commit 45819f3

Browse files
committed
Fail if attestations have unsupported predicate type
1 parent 4075073 commit 45819f3

File tree

4 files changed

+229
-29
lines changed

4 files changed

+229
-29
lines changed

requirements/main.in

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ rfc3986
6363
sentry-sdk
6464
setuptools
6565
sigstore~=3.0.0
66-
pypi-attestation-models==0.0.4
66+
pypi-attestation-models==0.0.5
6767
sqlalchemy[asyncio]>=2.0,<3.0
6868
stdlib-list
6969
stripe

requirements/main.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1736,9 +1736,9 @@ pyparsing==3.1.2 \
17361736
--hash=sha256:a1bac0ce561155ecc3ed78ca94d3c9378656ad4c94c1270de543f621420f94ad \
17371737
--hash=sha256:f9db75911801ed778fe61bb643079ff86601aca99fcae6345aa67292038fb742
17381738
# via linehaul
1739-
pypi-attestation-models==0.0.4 \
1740-
--hash=sha256:72693503fc636959f0cf15ca5f067f011e28829600cc3f2f1cd8340eeccc7af6 \
1741-
--hash=sha256:9ee0d7151c1a02b89b77332cd204ff1334d5808bcd4bad11685e9c70bea12740
1739+
pypi-attestation-models==0.0.5 \
1740+
--hash=sha256:cceb48aec1c9d93d880d2a6c8c9581bedb503b66203e37081e1ba2e863b6bac9 \
1741+
--hash=sha256:d105bc4cf167d4d1db180177bc464bcc4cea8437cdc583c6598424b712c8b068
17421742
# via -r requirements/main.in
17431743
pyqrcode==1.2.1 \
17441744
--hash=sha256:1b2812775fa6ff5c527977c4cd2ccb07051ca7d0bc0aecf937a43864abe5eff6 \

tests/unit/forklift/test_legacy.py

Lines changed: 177 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3446,7 +3446,12 @@ def test_upload_with_valid_attestation_succeeds(
34463446
)
34473447
monkeypatch.setattr(HasEvents, "record_event", record_event)
34483448

3449-
verify = pretend.call_recorder(lambda _self, _verifier, _policy, _dist: None)
3449+
verify = pretend.call_recorder(
3450+
lambda _self, _verifier, _policy, _dist: (
3451+
"https://docs.pypi.org/attestations/publish/v1",
3452+
None,
3453+
)
3454+
)
34503455
monkeypatch.setattr(Attestation, "verify", verify)
34513456
monkeypatch.setattr(Verifier, "production", lambda: pretend.stub())
34523457

@@ -3456,6 +3461,177 @@ def test_upload_with_valid_attestation_succeeds(
34563461

34573462
assert len(verify.calls) == 1
34583463

3464+
def test_upload_with_invalid_attestation_predicate_type_fails(
3465+
self,
3466+
monkeypatch,
3467+
pyramid_config,
3468+
db_request,
3469+
metrics,
3470+
):
3471+
from warehouse.events.models import HasEvents
3472+
3473+
project = ProjectFactory.create()
3474+
version = "1.0"
3475+
publisher = GitHubPublisherFactory.create(projects=[project])
3476+
claims = {
3477+
"sha": "somesha",
3478+
"repository": f"{publisher.repository_owner}/{publisher.repository_name}",
3479+
"workflow": "workflow_name",
3480+
}
3481+
identity = PublisherTokenContext(publisher, SignedClaims(claims))
3482+
db_request.oidc_publisher = identity.publisher
3483+
db_request.oidc_claims = identity.claims
3484+
3485+
db_request.db.add(Classifier(classifier="Environment :: Other Environment"))
3486+
db_request.db.add(Classifier(classifier="Programming Language :: Python"))
3487+
3488+
filename = "{}-{}.tar.gz".format(project.name, "1.0")
3489+
attestation = Attestation(
3490+
version=1,
3491+
verification_material=VerificationMaterial(
3492+
certificate="somebase64string", transparency_entries=[dict()]
3493+
),
3494+
envelope=Envelope(
3495+
statement="somebase64string",
3496+
signature="somebase64string",
3497+
),
3498+
)
3499+
3500+
pyramid_config.testing_securitypolicy(identity=identity)
3501+
db_request.user = None
3502+
db_request.user_agent = "warehouse-tests/6.6.6"
3503+
db_request.POST = MultiDict(
3504+
{
3505+
"metadata_version": "1.2",
3506+
"name": project.name,
3507+
"attestations": f"[{attestation.model_dump_json()}]",
3508+
"version": version,
3509+
"summary": "This is my summary!",
3510+
"filetype": "sdist",
3511+
"md5_digest": _TAR_GZ_PKG_MD5,
3512+
"content": pretend.stub(
3513+
filename=filename,
3514+
file=io.BytesIO(_TAR_GZ_PKG_TESTDATA),
3515+
type="application/tar",
3516+
),
3517+
}
3518+
)
3519+
3520+
storage_service = pretend.stub(store=lambda path, filepath, meta: None)
3521+
db_request.find_service = lambda svc, name=None, context=None: {
3522+
IFileStorage: storage_service,
3523+
IMetricsService: metrics,
3524+
}.get(svc)
3525+
3526+
record_event = pretend.call_recorder(
3527+
lambda self, *, tag, request=None, additional: None
3528+
)
3529+
monkeypatch.setattr(HasEvents, "record_event", record_event)
3530+
3531+
invalid_predicate_type = "Unsupported predicate type"
3532+
verify = pretend.call_recorder(
3533+
lambda _self, _verifier, _policy, _dist: (invalid_predicate_type, None)
3534+
)
3535+
monkeypatch.setattr(Attestation, "verify", verify)
3536+
monkeypatch.setattr(Verifier, "production", lambda: pretend.stub())
3537+
3538+
with pytest.raises(HTTPBadRequest) as excinfo:
3539+
legacy.file_upload(db_request)
3540+
3541+
resp = excinfo.value
3542+
3543+
assert resp.status_code == 400
3544+
assert resp.status.startswith(
3545+
f"400 Attestation with unsupported predicate type: {invalid_predicate_type}"
3546+
)
3547+
3548+
def test_upload_with_multiple_attestations_fails(
3549+
self,
3550+
monkeypatch,
3551+
pyramid_config,
3552+
db_request,
3553+
metrics,
3554+
):
3555+
from warehouse.events.models import HasEvents
3556+
3557+
project = ProjectFactory.create()
3558+
version = "1.0"
3559+
publisher = GitHubPublisherFactory.create(projects=[project])
3560+
claims = {
3561+
"sha": "somesha",
3562+
"repository": f"{publisher.repository_owner}/{publisher.repository_name}",
3563+
"workflow": "workflow_name",
3564+
}
3565+
identity = PublisherTokenContext(publisher, SignedClaims(claims))
3566+
db_request.oidc_publisher = identity.publisher
3567+
db_request.oidc_claims = identity.claims
3568+
3569+
db_request.db.add(Classifier(classifier="Environment :: Other Environment"))
3570+
db_request.db.add(Classifier(classifier="Programming Language :: Python"))
3571+
3572+
filename = "{}-{}.tar.gz".format(project.name, "1.0")
3573+
attestation = Attestation(
3574+
version=1,
3575+
verification_material=VerificationMaterial(
3576+
certificate="somebase64string", transparency_entries=[dict()]
3577+
),
3578+
envelope=Envelope(
3579+
statement="somebase64string",
3580+
signature="somebase64string",
3581+
),
3582+
)
3583+
3584+
pyramid_config.testing_securitypolicy(identity=identity)
3585+
db_request.user = None
3586+
db_request.user_agent = "warehouse-tests/6.6.6"
3587+
db_request.POST = MultiDict(
3588+
{
3589+
"metadata_version": "1.2",
3590+
"name": project.name,
3591+
"attestations": f"[{attestation.model_dump_json()},"
3592+
f" {attestation.model_dump_json()}]",
3593+
"version": version,
3594+
"summary": "This is my summary!",
3595+
"filetype": "sdist",
3596+
"md5_digest": _TAR_GZ_PKG_MD5,
3597+
"content": pretend.stub(
3598+
filename=filename,
3599+
file=io.BytesIO(_TAR_GZ_PKG_TESTDATA),
3600+
type="application/tar",
3601+
),
3602+
}
3603+
)
3604+
3605+
storage_service = pretend.stub(store=lambda path, filepath, meta: None)
3606+
db_request.find_service = lambda svc, name=None, context=None: {
3607+
IFileStorage: storage_service,
3608+
IMetricsService: metrics,
3609+
}.get(svc)
3610+
3611+
record_event = pretend.call_recorder(
3612+
lambda self, *, tag, request=None, additional: None
3613+
)
3614+
monkeypatch.setattr(HasEvents, "record_event", record_event)
3615+
3616+
verify = pretend.call_recorder(
3617+
lambda _self, _verifier, _policy, _dist: (
3618+
"https://docs.pypi.org/attestations/publish/v1",
3619+
None,
3620+
)
3621+
)
3622+
monkeypatch.setattr(Attestation, "verify", verify)
3623+
monkeypatch.setattr(Verifier, "production", lambda: pretend.stub())
3624+
3625+
with pytest.raises(HTTPBadRequest) as excinfo:
3626+
legacy.file_upload(db_request)
3627+
3628+
resp = excinfo.value
3629+
3630+
assert resp.status_code == 400
3631+
assert resp.status.startswith(
3632+
"400 Only a single attestation per-file is supported at the moment."
3633+
)
3634+
34593635
def test_upload_with_malformed_attestation_fails(
34603636
self,
34613637
monkeypatch,

warehouse/forklift/legacy.py

Lines changed: 48 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1083,42 +1083,66 @@ def file_upload(request):
10831083
attestations = TypeAdapter(list[Attestation]).validate_json(
10841084
request.POST["attestations"]
10851085
)
1086-
verification_policy = publisher.publisher_verification_policy(
1087-
request.oidc_claims
1088-
)
1089-
for attestation_model in attestations:
1090-
# For now, attestations are not stored, just verified
1091-
attestation_model.verify(
1092-
Verifier.production(),
1093-
verification_policy,
1094-
Path(temporary_filename),
1095-
)
1096-
# Log successful attestation upload
1097-
metrics.increment("warehouse.upload.attestations.ok")
10981086
except ValidationError as e:
10991087
# Log invalid (malformed) attestation upload
11001088
metrics.increment("warehouse.upload.attestations.malformed")
11011089
raise _exc_with_message(
11021090
HTTPBadRequest,
11031091
f"Error while decoding the included attestation: {e}",
11041092
)
1105-
except VerificationError as e:
1106-
# Log invalid (failed verification) attestation upload
1107-
metrics.increment("warehouse.upload.attestations.failed_verify")
1108-
raise _exc_with_message(
1109-
HTTPBadRequest,
1110-
f"Could not verify the uploaded artifact using the included "
1111-
f"attestation: {e}",
1112-
)
1113-
except Exception as e:
1114-
sentry_sdk.capture_message(
1115-
f"Unexpected error while verifying attestation: {e}"
1093+
1094+
if len(attestations) > 1:
1095+
metrics.increment(
1096+
"warehouse.upload.attestations." "failed_multiple_attestations"
11161097
)
11171098
raise _exc_with_message(
11181099
HTTPBadRequest,
1119-
f"Unknown error while trying to verify included attestations: {e}",
1100+
"Only a single attestation per-file is supported at the moment.",
11201101
)
11211102

1103+
verification_policy = publisher.publisher_verification_policy(
1104+
request.oidc_claims
1105+
)
1106+
for attestation_model in attestations:
1107+
try:
1108+
# For now, attestations are not stored, just verified
1109+
predicate_type, _ = attestation_model.verify(
1110+
Verifier.production(),
1111+
verification_policy,
1112+
Path(temporary_filename),
1113+
)
1114+
except VerificationError as e:
1115+
# Log invalid (failed verification) attestation upload
1116+
metrics.increment("warehouse.upload.attestations.failed_verify")
1117+
raise _exc_with_message(
1118+
HTTPBadRequest,
1119+
f"Could not verify the uploaded artifact using the included "
1120+
f"attestation: {e}",
1121+
)
1122+
except Exception as e:
1123+
sentry_sdk.capture_message(
1124+
f"Unexpected error while verifying attestation: {e}"
1125+
)
1126+
raise _exc_with_message(
1127+
HTTPBadRequest,
1128+
f"Unknown error while trying to verify included "
1129+
f"attestations: {e}",
1130+
)
1131+
1132+
if predicate_type != "https://docs.pypi.org/attestations/publish/v1":
1133+
metrics.increment(
1134+
"warehouse.upload.attestations."
1135+
"failed_unsupported_predicate_type"
1136+
)
1137+
raise _exc_with_message(
1138+
HTTPBadRequest,
1139+
f"Attestation with unsupported predicate type: "
1140+
f"{predicate_type}",
1141+
)
1142+
1143+
# Log successful attestation upload
1144+
metrics.increment("warehouse.upload.attestations.ok")
1145+
11221146
# TODO: This should be handled by some sort of database trigger or a
11231147
# SQLAlchemy hook or the like instead of doing it inline in this
11241148
# view.

0 commit comments

Comments
 (0)