@@ -371,6 +371,92 @@ def _is_duplicate_file(db_session, filename, hashes):
371371 return None
372372
373373
374+ def _process_attestations (request , metrics , artifact_path : Path ):
375+ """
376+ Process any attestations included in a file upload request
377+
378+ Attestations, if present, will be parsed and verified against the uploaded
379+ artifact. Attestations are only allowed when uploading via a Trusted
380+ Publisher, because a Trusted Publisher provides the identity that will be
381+ used to verify the attestations.
382+ Currently, only GitHub Actions Trusted Publishers are supported, and
383+ attestations are discarded after verification.
384+ """
385+
386+ # Check that if the file was uploaded with attestations, verification
387+ # passes
388+ if "attestations" in request .POST :
389+ publisher = request .oidc_publisher
390+ if not publisher or not publisher .publisher_name == "GitHub" :
391+ raise _exc_with_message (
392+ HTTPBadRequest ,
393+ "Attestations are currently only supported when using Trusted "
394+ "Publishing with GitHub Actions." ,
395+ )
396+ try :
397+ attestations = TypeAdapter (list [Attestation ]).validate_json (
398+ request .POST ["attestations" ]
399+ )
400+ except ValidationError as e :
401+ # Log invalid (malformed) attestation upload
402+ metrics .increment ("warehouse.upload.attestations.malformed" )
403+ raise _exc_with_message (
404+ HTTPBadRequest ,
405+ f"Error while decoding the included attestation: { e } " ,
406+ )
407+
408+ if len (attestations ) > 1 :
409+ metrics .increment (
410+ "warehouse.upload.attestations." "failed_multiple_attestations"
411+ )
412+ raise _exc_with_message (
413+ HTTPBadRequest ,
414+ "Only a single attestation per-file is supported at the moment." ,
415+ )
416+
417+ verification_policy = publisher .publisher_verification_policy (
418+ request .oidc_claims
419+ )
420+ for attestation_model in attestations :
421+ try :
422+ # For now, attestations are not stored, just verified
423+ predicate_type , _ = attestation_model .verify (
424+ Verifier .production (),
425+ verification_policy ,
426+ artifact_path ,
427+ )
428+ except VerificationError as e :
429+ # Log invalid (failed verification) attestation upload
430+ metrics .increment ("warehouse.upload.attestations.failed_verify" )
431+ raise _exc_with_message (
432+ HTTPBadRequest ,
433+ f"Could not verify the uploaded artifact using the included "
434+ f"attestation: { e } " ,
435+ )
436+ except Exception as e :
437+ sentry_sdk .capture_message (
438+ f"Unexpected error while verifying attestation: { e } "
439+ )
440+ raise _exc_with_message (
441+ HTTPBadRequest ,
442+ f"Unknown error while trying to verify included "
443+ f"attestations: { e } " ,
444+ )
445+
446+ if predicate_type != "https://docs.pypi.org/attestations/publish/v1" :
447+ metrics .increment (
448+ "warehouse.upload.attestations." "failed_unsupported_predicate_type"
449+ )
450+ raise _exc_with_message (
451+ HTTPBadRequest ,
452+ f"Attestation with unsupported predicate type: "
453+ f"{ predicate_type } " ,
454+ )
455+
456+ # Log successful attestation upload
457+ metrics .increment ("warehouse.upload.attestations.ok" )
458+
459+
374460@view_config (
375461 route_name = "forklift.legacy.file_upload" ,
376462 uses_session = True ,
@@ -1069,79 +1155,9 @@ def file_upload(request):
10691155 k : h .hexdigest ().lower () for k , h in metadata_file_hashes .items ()
10701156 }
10711157
1072- # Check that if the file was uploaded with attestations, verification
1073- # passes
1074- if "attestations" in request .POST :
1075- publisher = request .oidc_publisher
1076- if not publisher or not publisher .publisher_name == "GitHub" :
1077- raise _exc_with_message (
1078- HTTPBadRequest ,
1079- "Attestations are currently only supported when using Trusted "
1080- "Publishing with GitHub Actions." ,
1081- )
1082- try :
1083- attestations = TypeAdapter (list [Attestation ]).validate_json (
1084- request .POST ["attestations" ]
1085- )
1086- except ValidationError as e :
1087- # Log invalid (malformed) attestation upload
1088- metrics .increment ("warehouse.upload.attestations.malformed" )
1089- raise _exc_with_message (
1090- HTTPBadRequest ,
1091- f"Error while decoding the included attestation: { e } " ,
1092- )
1093-
1094- if len (attestations ) > 1 :
1095- metrics .increment (
1096- "warehouse.upload.attestations." "failed_multiple_attestations"
1097- )
1098- raise _exc_with_message (
1099- HTTPBadRequest ,
1100- "Only a single attestation per-file is supported at the moment." ,
1101- )
1102-
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" )
1158+ _process_attestations (
1159+ request = request , metrics = metrics , artifact_path = Path (temporary_filename )
1160+ )
11451161
11461162 # TODO: This should be handled by some sort of database trigger or a
11471163 # SQLAlchemy hook or the like instead of doing it inline in this
0 commit comments