@@ -371,6 +371,92 @@ def _is_duplicate_file(db_session, filename, hashes):
371
371
return None
372
372
373
373
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
+
374
460
@view_config (
375
461
route_name = "forklift.legacy.file_upload" ,
376
462
uses_session = True ,
@@ -1069,79 +1155,9 @@ def file_upload(request):
1069
1155
k : h .hexdigest ().lower () for k , h in metadata_file_hashes .items ()
1070
1156
}
1071
1157
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
+ )
1145
1161
1146
1162
# TODO: This should be handled by some sort of database trigger or a
1147
1163
# SQLAlchemy hook or the like instead of doing it inline in this
0 commit comments