Skip to content

Commit 900295e

Browse files
committed
Add key-signed attestation verification support
closes #1340 Assisted By: Claude Opus 5.5
1 parent 0a5065a commit 900295e

12 files changed

Lines changed: 667 additions & 12 deletions

File tree

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
-----BEGIN PRIVATE KEY-----
2+
MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQDLYCPSdloA94Te
3+
+68CLqtHJ3qTKttyWyM1uHkcb+AEAExtQGLoKtysmE2QQz8xwpiBKsByAlPTAHZZ
4+
pYIGzof45xEwiRzBqKxE4um1Uhtq+WCth96UeUkjz2G0xMxkFycjHDdpQQ92Hlg+
5+
mvU8VOrs4Xi7PvG5E+lTY7zez89QQ2ZY3OcbFSHVkC1b0COjYqJ7m5TmVWaRyNbW
6+
Nn+00/uZ+hFzcsxAuKa9B9s4ngWELLA1TnWp5yi4q2Pkold4prI3PwF7IfGWtrfo
7+
TgyzCKji12QLLjbd5KiKecGqx9zaV8xs7GnCif47JmOFAEDW5dtUstepH8ysy8wC
8+
IXoZYvOSOsOv2mVz99JikyaWeV0rjNk7p9UPrhiaGKOfzfn+g6bzCJYBlo425pmJ
9+
S/d1EoBoqgeCGniyLkFByQ9zDmzr1NlCkNKtNII1qNoRSMyqJY0qKPAd2bNMHUfT
10+
FB3Eb/AvcOjb4fa5kkxl4gMQwasNaFmDgusqt1JjHK4XcL8UIcvzGPj8LsDrx5iT
11+
HCFSF1gJvAXiLG0o6KFgrHfTBw2V3mxaKZNGr2NA5IMTs2UeGud590VmSvsKhr6R
12+
tekHHGLCrZT9kPxbicBq6OZOhSb0usSpclSvwMzMSIOFNzACXq33nPYqlY0r1vGN
13+
5Gi2P9W2CxAYbnOGgqpzHbt4YoLVrQIDAQABAoICAA2SR3xZPtL8XCGFKhM7NLLK
14+
2k3NI60TPTDt3nxx+sD0RCVbkT4XniI7skauNh6xSFFWSQFSpnADgoz46R8LKTJd
15+
jl1uyOcjb7DTyO9l9e5tixtetbuysZlyJ/2oJFDe5VMHzwAhwfu1NVj4KOVIcA+J
16+
UZqCg3RA9TuwrCnc3uNm8VgnZZn+ZFjxW2raY4ZuTpQbuGlRHvHGNTqWPcS+C0wl
17+
zoRQZNDs1t5GZ+/0m5RLvHZ82zKQpRGts5IjmIgJ7QVCxGvdwqwMBWRl0PMhi3jt
18+
EVVYVXuj2/C3BKAg4NwGf93E6PR9FjoG6x00/HQFYrLZkbVM1JuzUynRPL9KRcvz
19+
It1vxbmW71oWAXXkWcW7MHWR3n7LoQ8x0zcuf2A1PRuAG4zb8hTDyWud9d10NmSd
20+
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
51+
Y3acysRQ2KI9Ero+Ga3hDfaohueVFg==
52+
-----END PRIVATE KEY-----

‎.ci/assets/keys/test-key.pem‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
-----BEGIN PUBLIC KEY-----
2+
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAy2Aj0nZaAPeE3vuvAi6r
3+
Ryd6kyrbclsjNbh5HG/gBABMbUBi6CrcrJhNkEM/McKYgSrAcgJT0wB2WaWCBs6H
4+
+OcRMIkcwaisROLptVIbavlgrYfelHlJI89htMTMZBcnIxw3aUEPdh5YPpr1PFTq
5+
7OF4uz7xuRPpU2O83s/PUENmWNznGxUh1ZAtW9Ajo2Kie5uU5lVmkcjW1jZ/tNP7
6+
mfoRc3LMQLimvQfbOJ4FhCywNU51qecouKtj5KJXeKayNz8BeyHxlra36E4Mswio
7+
4tdkCy423eSoinnBqsfc2lfMbOxpwon+OyZjhQBA1uXbVLLXqR/MrMvMAiF6GWLz
8+
kjrDr9plc/fSYpMmlnldK4zZO6fVD64Ymhijn835/oOm8wiWAZaONuaZiUv3dRKA
9+
aKoHghp4si5BQckPcw5s69TZQpDSrTSCNajaEUjMqiWNKijwHdmzTB1H0xQdxG/w
10+
L3Do2+H2uZJMZeIDEMGrDWhZg4LrKrdSYxyuF3C/FCHL8xj4/C7A68eYkxwhUhdY
11+
CbwF4ixtKOihYKx30wcNld5sWimTRq9jQOSDE7NlHhrnefdFZkr7Coa+kbXpBxxi
12+
wq2U/ZD8W4nAaujmToUm9LrEqXJUr8DMzEiDhTcwAl6t95z2KpWNK9bxjeRotj/V
13+
tgsQGG5zhoKqcx27eGKC1a0CAwEAAQ==
14+
-----END PUBLIC KEY-----

‎.github/workflows/scripts/before_install.sh‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ legacy_component_name: "pulp_python"
5050
component_name: "python"
5151
component_version: "${COMPONENT_VERSION}"
5252
pulp_env: {}
53-
pulp_settings: {"allowed_export_paths": "/tmp", "allowed_import_paths": "/tmp", "api_root": "/pulp/", "orphan_protection_time": 0, "pypi_api_hostname": "https://pulp:443"}
53+
pulp_settings: {"allowed_export_paths": "/tmp", "allowed_import_paths": "/tmp", "api_root": "/pulp/", "attestation_verification_keys": {"default": "/etc/pki/attestation/test-key.pem"}, "orphan_protection_time": 0, "pypi_api_hostname": "https://pulp:443"}
5454
pulp_scheme: "https"
5555
image:
5656
name: "pulp"
@@ -64,6 +64,10 @@ image:
6464
extra_files:
6565
- origin: "pulp_python"
6666
destination: "pulp_python"
67+
- origin: "pulp_python/.ci/assets/keys/test-key.pem"
68+
destination: "/etc/pki/attestation/test-key.pem"
69+
- origin: "pulp_python/.ci/assets/keys/test-key-private.pem"
70+
destination: "/etc/pki/attestation/test-key-private.pem"
6771
services:
6872
- name: "pulp"
6973
image: "pulp:ci_build"

‎.gitleaks.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
[allowlist]
22
description = "Allow specific test-keys."
33
paths = [
4+
'''.ci/assets/keys/test-key-private.pem''',
45
]
56
regexes = [
67
'''AKIAIT2Z5TDYPX3ARJBA''',

‎CHANGES/1340.feature‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added custom (non-PEP 740) support for verifying attestations signed with a per-domain public key configured via `ATTESTATION_VERIFICATION_KEYS`.

‎docs/admin/reference/settings.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
`pulp_python` adds configuration options to those offered by `pulpcore`.
44

5+
## ATTESTATION_VERIFICATION_KEYS
6+
7+
> A mapping of domain names to PEM public key files used to verify attestations signed with a
8+
> custom key. See [Custom Key Verification](site:pulp_python/docs/user/guides/attestation/) for
9+
> details. Defaults to `{}`.
10+
511
## PYTHON_GROUP_UPLOADS
612

713
> This setting controls whether the uploading of packages through the PyPI APIs can be grouped

‎docs/user/guides/attestation.md‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,49 @@ http $PULP_API/pypi/foo/simple/twine/ "Accept:application/vnd.pypi.simple.v1+jso
8989

9090
http $PULP_API/pypi/foo/integrity/twine/6.2.0/twine-6.2.0.tar.gz/provenance/
9191
```
92+
93+
## Custom Key Verification (non-PEP 740)
94+
95+
!!! note
96+
This is a custom extension outside the PEP 740 spec. Standard PEP 740 users using Sigstore
97+
keyless signing do not need this feature.
98+
99+
Pulp Python supports verifying attestations signed with a custom public key instead of a Sigstore
100+
certificate. This is useful for environments that sign packages with their own key (e.g. internal
101+
build systems) rather than using Sigstore's keyless signing.
102+
103+
To enable this, configure the `ATTESTATION_VERIFICATION_KEYS` setting, which maps a domain name to
104+
the path of a PEM public key file. Each domain trusts only its own key. Without domains enabled, all
105+
content belongs to the `default` domain.
106+
107+
```python
108+
# settings.py
109+
ATTESTATION_VERIFICATION_KEYS = {"default": "/path/to/public-key.pem"}
110+
```
111+
112+
```bash
113+
# or as an environment variable
114+
export PULP_ATTESTATION_VERIFICATION_KEYS='@json {"default": "/path/to/public-key.pem"}'
115+
```
116+
117+
Pulp uses two verification paths that coexist on the same server:
118+
119+
- **Attestations with a Sigstore certificate** are verified through the standard PEP 740 path,
120+
regardless of `ATTESTATION_VERIFICATION_KEYS`.
121+
- **Attestations without a Sigstore certificate** are verified against the key configured for the
122+
current domain using RSA PKCS1v15 signatures. In a domain without a configured key, they are
123+
rejected.
124+
125+
This means a single Pulp instance can host packages with Sigstore certificates and
126+
internally-signed packages with key-based attestations. Each attestation is independently
127+
routed to the appropriate verification path based on whether it contains a certificate.
128+
Key-signed attestations must omit `verification_material` or set it to `null`.
129+
After downloading, key-signed attestations cannot be verified with `sigstore` or
130+
`pypi-attestations`, since both require a certificate. Consumers must verify them against the same
131+
public key, with a tool that supports DSSE signatures, such as `cosign`.
132+
Attestations with a certificate that can't be parsed are rejected.
133+
134+
### Limitations
135+
136+
- Each domain can have only one key.
137+
- The key-based path only supports RSA PKCS1v15 with SHA-256 signatures.

‎pulp_python/app/provenance.py‎

Lines changed: 149 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,28 @@
1+
import json
12
from typing import Annotated, Literal, Union, get_args
23

3-
from pydantic import BaseModel, ConfigDict, Field
4+
from cryptography.exceptions import InvalidSignature, UnsupportedAlgorithm
5+
from cryptography.hazmat.primitives import hashes, serialization
6+
from cryptography.hazmat.primitives.asymmetric import padding as crypto_padding
7+
from cryptography.hazmat.primitives.asymmetric import rsa
8+
from cryptography.x509 import load_der_x509_certificate
9+
from django.conf import settings
10+
from pydantic import BaseModel, ConfigDict, Field, ValidationError
411
from pydantic.alias_generators import to_snake
12+
from pypi_attestations import Attestation as _UpstreamAttestation
513
from pypi_attestations import (
6-
Attestation,
714
Distribution,
15+
Envelope, # noqa - needed in module namespace for Pydantic model rebuild
816
Publisher,
17+
VerificationError,
18+
VerificationMaterial,
919
)
20+
from sigstore.dsse import Envelope as DSSEEnvelope
21+
from sigstore.dsse import _pae
22+
23+
from pulpcore.plugin.util import get_domain
24+
25+
_verification_key_cache = {}
1026

1127

1228
class _PermissivePolicy:
@@ -32,13 +48,30 @@ def _as_policy(self):
3248
# Get the underlying Union type of the original Publisher
3349
# Publisher is Annotated[Union[...], Field(discriminator="kind")]
3450
_OriginalPublisherTypes = get_args(Publisher.__origin__)
51+
# Get the kinds of the original publisher types, they can only be proven by a Sigstore certificate
52+
_TrustedPublisherKinds = {t.model_fields["kind"].default for t in _OriginalPublisherTypes}
3553
# Add AnyPublisher to the list of original publisher types
3654
_ExtendedPublisherTypes = (*_OriginalPublisherTypes, AnyPublisher)
3755
_ExtendedPublisherUnion = Union[_ExtendedPublisherTypes]
3856
# Create a new type that fallbacks to AnyPublisher
3957
ExtendedPublisher = Annotated[_ExtendedPublisherUnion, Field(union_mode="left_to_right")]
4058

4159

60+
class Attestation(_UpstreamAttestation):
61+
"""
62+
Attestation object as defined in PEP 740.
63+
64+
Inherits from the upstream pypi_attestations.Attestation to keep Sigstore
65+
verification methods (to_bundle, verify), but makes verification_material
66+
optional to support attestations signed with a custom key instead of Sigstore.
67+
"""
68+
69+
verification_material: VerificationMaterial | None = None
70+
"""
71+
Cryptographic materials used to verify `message_signature`.
72+
"""
73+
74+
4275
class AttestationBundle(BaseModel):
4376
"""
4477
AttestationBundle object as defined in PEP740.
@@ -58,14 +91,122 @@ class Provenance(BaseModel):
5891
attestation_bundles: list[AttestationBundle]
5992

6093

94+
def _load_verification_key(domain_name):
95+
"""Load the attestation verification public key configured for the domain, with caching."""
96+
keys = settings.ATTESTATION_VERIFICATION_KEYS
97+
if not isinstance(keys, dict):
98+
raise VerificationError(
99+
"ATTESTATION_VERIFICATION_KEYS must map domain names to public key paths"
100+
)
101+
key_path = keys.get(domain_name)
102+
if not key_path:
103+
return None
104+
if key_path not in _verification_key_cache:
105+
try:
106+
with open(key_path, "rb") as f:
107+
key = serialization.load_pem_public_key(f.read())
108+
except (OSError, ValueError, UnsupportedAlgorithm) as e:
109+
raise VerificationError(
110+
f"could not load the verification key for domain '{domain_name}' "
111+
f"from {key_path}: {e}"
112+
)
113+
if not isinstance(key, rsa.RSAPublicKey):
114+
raise VerificationError(
115+
f"the verification key for domain '{domain_name}' in {key_path} "
116+
"must be an RSA public key"
117+
)
118+
_verification_key_cache[key_path] = key
119+
return _verification_key_cache[key_path]
120+
121+
122+
def _has_certificate(attestation):
123+
"""Check whether the attestation carries a certificate, rejecting unparseable ones."""
124+
vm = attestation.verification_material
125+
if vm is None:
126+
return False
127+
try:
128+
load_der_x509_certificate(vm.certificate)
129+
except ValueError as e:
130+
raise VerificationError(f"invalid X.509 certificate in verification_material: {e}")
131+
return True
132+
133+
134+
def _verify_statement_subject(attestation, dist):
135+
"""Validate that the in-toto statement subject matches the distribution."""
136+
try:
137+
stmt = json.loads(attestation.envelope.statement)
138+
except (json.JSONDecodeError, UnicodeDecodeError) as e:
139+
raise VerificationError(f"invalid statement: {e}")
140+
141+
try:
142+
subjects = stmt.get("subject", [])
143+
if len(subjects) != 1:
144+
raise VerificationError("expected exactly one subject in statement")
145+
146+
subject = subjects[0]
147+
name = subject.get("name", "")
148+
if name != dist.name:
149+
raise VerificationError(
150+
f"subject does not match distribution name: {name} != {dist.name}"
151+
)
152+
153+
digest = subject.get("digest", {}).get("sha256")
154+
if digest != dist.digest:
155+
raise VerificationError("subject does not match distribution digest")
156+
except (AttributeError, TypeError) as e:
157+
raise VerificationError(f"malformed statement: {e}")
158+
159+
160+
def _verify_signature(attestation, public_key):
161+
"""Verify the attestation's RSA signature over the DSSE PAE bytes."""
162+
statement_bytes = attestation.envelope.statement
163+
signature_bytes = attestation.envelope.signature
164+
pae = _pae(DSSEEnvelope._TYPE, statement_bytes)
165+
try:
166+
public_key.verify(
167+
signature_bytes,
168+
pae,
169+
crypto_padding.PKCS1v15(),
170+
hashes.SHA256(),
171+
)
172+
except InvalidSignature:
173+
raise VerificationError("signature verification failed")
174+
175+
61176
def verify_provenance(filename, sha256, provenance, offline=True):
62-
"""Verify the provenance object is valid for the package."""
63-
dist = Distribution(name=filename, digest=sha256)
177+
"""Verify the provenance object is valid for the package.
178+
179+
Each attestation is routed to one of two coexisting verification paths:
180+
1. Attestations with a valid Sigstore certificate follow the standard PEP 740 path.
181+
2. Attestations without a certificate are verified against the key configured for the
182+
current domain in ATTESTATION_VERIFICATION_KEYS using RSA PKCS1v15 signatures.
183+
Attestations with a certificate that can't be parsed are rejected.
184+
"""
185+
try:
186+
dist = Distribution(name=filename, digest=sha256)
187+
except ValidationError as e:
188+
raise VerificationError(f"invalid distribution filename {filename}: {e}")
64189
for bundle in provenance.attestation_bundles:
65190
publisher = bundle.publisher
66191
policy = publisher._as_policy()
67192
for attestation in bundle.attestations:
68-
sig_bundle = attestation.to_bundle()
69-
checkpoint = sig_bundle.log_entry._inner.inclusion_proof.checkpoint
70-
staging = "sigstage.dev" in checkpoint.envelope
71-
attestation.verify(policy, dist, staging=staging, offline=offline)
193+
if _has_certificate(attestation):
194+
sig_bundle = attestation.to_bundle()
195+
checkpoint = sig_bundle.log_entry._inner.inclusion_proof.checkpoint
196+
staging = "sigstage.dev" in checkpoint.envelope
197+
attestation.verify(policy, dist, staging=staging, offline=offline)
198+
else:
199+
if publisher.kind in _TrustedPublisherKinds:
200+
raise VerificationError(
201+
f"publisher kind '{publisher.kind}' requires a Sigstore certificate"
202+
)
203+
domain_name = get_domain().name
204+
verification_key = _load_verification_key(domain_name)
205+
if verification_key:
206+
_verify_statement_subject(attestation, dist)
207+
_verify_signature(attestation, verification_key)
208+
else:
209+
raise VerificationError(
210+
"attestation has no Sigstore certificate, and "
211+
f"ATTESTATION_VERIFICATION_KEYS has no key for domain '{domain_name}'"
212+
)

‎pulp_python/app/pypi/serializers.py‎

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,19 @@
33

44
from django.db.utils import IntegrityError
55
from pydantic import TypeAdapter, ValidationError
6+
from pypi_attestations import AttestationError
67
from rest_framework import serializers
78

89
from pulpcore.plugin.models import Artifact
910
from pulpcore.plugin.util import get_domain
1011

11-
from pulp_python.app.provenance import Attestation
12+
from pulp_python.app.provenance import (
13+
AnyPublisher,
14+
Attestation,
15+
AttestationBundle,
16+
Provenance,
17+
verify_provenance,
18+
)
1219
from pulp_python.app.utils import DIST_EXTENSIONS, SUPPORTED_METADATA_VERSIONS
1320

1421
log = logging.getLogger(__name__)
@@ -110,15 +117,25 @@ def validate(self, data):
110117
}
111118
)
112119

120+
sha256 = data.get("sha256_digest")
113121
if attestations := data.get("attestations"):
114122
try:
115123
attestations = TypeAdapter(list[Attestation]).validate_python(attestations)
116124
except ValidationError as e:
117125
raise serializers.ValidationError(
118126
{"attestations": _("The uploaded attestations are not valid: {}").format(e)}
119127
)
128+
if attestations and sha256:
129+
publisher = AnyPublisher(kind="Pulp User")
130+
att_bundle = AttestationBundle(publisher=publisher, attestations=attestations)
131+
provenance = Provenance(attestation_bundles=[att_bundle])
132+
try:
133+
verify_provenance(file.name, sha256, provenance, offline=True)
134+
except AttestationError as e:
135+
raise serializers.ValidationError(
136+
{"attestations": _("Attestations failed verification: {}").format(e)}
137+
)
120138

121-
sha256 = data.get("sha256_digest")
122139
digests = {"sha256": sha256} if sha256 else None
123140
artifact = Artifact.init_and_validate(file, expected_digests=digests)
124141
try:

0 commit comments

Comments
 (0)