diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e8b4361..4d42205 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -61,6 +61,4 @@ jobs: run: sudo apt-get install -y firejail - name: run tests offline - run: | - make dev INSTALL_EXTRA=test - firejail --noprofile --net=none --env=TEST_OFFLINE=1 make test-nocoverage + run: make test-offline INSTALL_EXTRA=test diff --git a/Makefile b/Makefile index e128ce8..f3c3d19 100644 --- a/Makefile +++ b/Makefile @@ -75,6 +75,15 @@ test-nocoverage: $(VENV)/pyvenv.cfg . $(VENV_BIN)/activate && \ pytest $(T) $(TEST_ARGS) +# test-offline requires firejail +.PHONY: test-offline +test-offline: $(VENV)/pyvenv.cfg + # ensure trust root is updated, then run tests inside no-network firejail + . $(VENV_BIN)/activate && \ + python -m sigstore plumbing update-trust-root && \ + python -m sigstore --staging plumbing update-trust-root && \ + firejail --noprofile --net=none --env=TEST_OFFLINE=1 pytest $(T) $(TEST_ARGS) + .PHONY: doc doc: $(VENV)/pyvenv.cfg . $(VENV_BIN)/activate && \ diff --git a/src/pypi_attestations/_cli.py b/src/pypi_attestations/_cli.py index 4b5a6a9..93a66d5 100644 --- a/src/pypi_attestations/_cli.py +++ b/src/pypi_attestations/_cli.py @@ -3,6 +3,7 @@ from __future__ import annotations import argparse +import base64 import json import logging import typing @@ -20,6 +21,7 @@ parse_wheel_filename, ) from pydantic import ValidationError +from rfc3161_client import decode_timestamp_response from rfc3986 import exceptions, uri_reference, validators from sigstore.models import Bundle, ClientTrustConfig, InvalidBundle from sigstore.oidc import IdentityError, IdentityToken, Issuer @@ -428,7 +430,7 @@ def _sign(args: argparse.Namespace) -> None: _die(f"Failed to detect identity: {identity_error}") trust_config = ClientTrustConfig.staging() if args.staging else ClientTrustConfig.production() - # Make sure we use rekor v1 until attestations are compatible with v2 + # Make sure we choose the rekor version: currently v1 trust_config.force_tlog_version = 1 signing_ctx = SigningContext.from_trust_config(trust_config) @@ -464,7 +466,7 @@ def _inspect(args: argparse.Namespace) -> None: Warning: The information displayed from the attestations are not verified. """ - attestation_files = [f for f in args.files if f.suffix == ".attestation"] + attestation_files = args.files _validate_files(attestation_files, should_exist=True) for file_path in attestation_files: try: @@ -513,6 +515,14 @@ def _inspect(args: argparse.Namespace) -> None: ) for idx, entry in enumerate(verification_material.transparency_entries): _logger.info(f"\tLog Index: {entry['logIndex']}") + kv = entry["kindVersion"] + _logger.info(f"\tEntry type: {kv['kind']} {kv['version']}") + + # Timestamps + _logger.info(f"Timestamps ({len(verification_material.timestamps)}):") + for data in verification_material.timestamps: + ts = decode_timestamp_response(base64.b64decode(data)) + _logger.info(f"\tTime: {ts.tst_info.gen_time}") def _verify_attestation(args: argparse.Namespace) -> None: diff --git a/src/pypi_attestations/_impl.py b/src/pypi_attestations/_impl.py index 3388601..765245f 100644 --- a/src/pypi_attestations/_impl.py +++ b/src/pypi_attestations/_impl.py @@ -24,6 +24,7 @@ from pydantic import Base64Bytes, BaseModel, ConfigDict, Field, field_validator from pydantic.alias_generators import to_snake from pydantic_core import ValidationError +from rfc3161_client import decode_timestamp_response from sigstore._utils import _sha256_streaming from sigstore.dsse import DigestSet, StatementBuilder, Subject, _Statement from sigstore.dsse import Envelope as DsseEnvelope @@ -136,6 +137,7 @@ def __init__(self: VerificationError, msg: str) -> None: TransparencyLogEntry = NewType("TransparencyLogEntry", dict[str, Any]) +Timestamp = NewType("Timestamp", bytes) class VerificationMaterial(BaseModel): @@ -152,6 +154,11 @@ class VerificationMaterial(BaseModel): and certificate. """ + timestamps: list[Timestamp] = [] + """ + list of RFC3161 timestamps. List may be empty if all transparency entries are rekor v1. + """ + class Attestation(BaseModel): """Attestation object as defined in PEP 740.""" @@ -347,10 +354,16 @@ def to_bundle(self) -> Bundle: except (ValidationError, sigstore.errors.Error) as err: raise ConversionError("invalid transparency log entry") from err + timestamps = [ + decode_timestamp_response(base64.b64decode(t)) + for t in self.verification_material.timestamps + ] + return Bundle._from_parts( # noqa: SLF001 cert=certificate, content=evp, log_entry=log_entry, + signed_timestamp=timestamps, ) @classmethod @@ -368,6 +381,11 @@ def from_bundle(cls, sigstore_bundle: Bundle) -> Attestation: if len(envelope.signatures) != 1: raise ConversionError(f"expected exactly one signature, got {len(envelope.signatures)}") + timestamps = [] + if sigstore_bundle.verification_material.timestamp_verification_data: + ts_data = sigstore_bundle.verification_material.timestamp_verification_data + timestamps = [base64.b64encode(ts.as_bytes()) for ts in ts_data.rfc3161_timestamps] + return cls( version=1, verification_material=VerificationMaterial( @@ -375,6 +393,7 @@ def from_bundle(cls, sigstore_bundle: Bundle) -> Attestation: transparency_entries=[ sigstore_bundle.log_entry._inner.to_dict() # noqa: SLF001 ], + timestamps=timestamps, ), envelope=Envelope( statement=base64.b64encode(envelope.payload), diff --git a/test/assets/pypi_attestations-0.0.19.tar.gz.publish.attestation.with_rekor2_timestamp b/test/assets/pypi_attestations-0.0.19.tar.gz.publish.attestation.with_rekor2_timestamp new file mode 100644 index 0000000..3956732 --- /dev/null +++ b/test/assets/pypi_attestations-0.0.19.tar.gz.publish.attestation.with_rekor2_timestamp @@ -0,0 +1 @@ +{"version":1,"verification_material":{"certificate":"MIICyjCCAlGgAwIBAgIUCwSld2TPMfzGf7dWDCWJ/usimb0wCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjUxMDEwMDgyMzQxWhcNMjUxMDEwMDgzMzQxWjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEpxjPe0rDTmkfNdf3BYfERviPS1HwIraC9anAY6nvb50XOV3UdRaOuhrcqK7eKEfDX5CIfuTbla7V4sNHo3b3W6OCAXAwggFsMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUi7a3nCqG5XXtGAudLFN0kH2Rc8gwHwYDVR0jBBgwFoAUcYYwphR8Ym/599b0BRp/X//rb6wwGQYDVR0RAQH/BA8wDYELamt1QGdvdG8uZmkwLAYKKwYBBAGDvzABAQQeaHR0cHM6Ly9naXRodWIuY29tL2xvZ2luL29hdXRoMC4GCisGAQQBg78wAQgEIAweaHR0cHM6Ly9naXRodWIuY29tL2xvZ2luL29hdXRoMIGLBgorBgEEAdZ5AgQCBH0EewB5AHcAKzC83GiIyeLh2CYpXnQfSDkxlgLynDPLXkNA/rKshnoAAAGZzTf3nwAABAMASDBGAiEAqwr4LJOIaxM8Bc0kcNEPNCxlDzZpos/f1rS7BK76nv0CIQCLIb2V/2i4FqvOGqHvuz3fF0nAKAEtW6VNiH+LnPOm0zAKBggqhkjOPQQDAwNnADBkAjBwVJpXj7XmjRSi7+KbiPoFUlyWX2N5DYvWFOLLKruvOqSYYYDVCqg/8doBMT99v4gCMHFSLEA0E9vSM03a5zkjO8vgY1ZP8LsJ/CJH1Z/MOYjJ9owPGLAt87WaN1QA76hKsg==","transparency_entries":[{"logIndex":"27301","logId":{"keyId":"09OnDKEw7/hpZiYVPoTRzRbglHk0sylsUovegnRUlJY="},"kindVersion":{"kind":"dsse","version":"0.0.2"},"integratedTime":"0","inclusionProof":{"logIndex":"27301","rootHash":"d+mZtqsmI19pROxvjpvI49MvCN3K+OQz+eU7kMYIf7I=","treeSize":"27302","hashes":["5PW4MIHqlleW+qI0c7/S3Y5mGSUBtShnVY6MNrxy+Z8=","AErw4uGrtK1FSaK0K6kdEHDyEbSBepPyZn3M/V0kjPc=","mPEssHL3To5i1DwEbUH7A30Ncl0+tvJKqprgM5KdsmE=","3Z1CsJikTA+Jxb5U90HaRQ6szln3OgKZGfo0GtC8NHI=","LpmrDMP/j7qB/x+7ZpcsyNWjCEFpW4r89LOgxXe4Si4=","3po/6lFr3EfEwUGBEcW8VrwzTFYiozZdE+fqrjQYcQ0=","X9zV3aAQKYVKUMZNhpSw5DeU9UGYo8IgrRbL7YPss40=","/F5BrQjoXk4Ob9cWhsK690rvVXNd/UwynjOz51kuYSM="],"checkpoint":{"envelope":"log2025-alpha3.rekor.sigstage.dev\n27302\nd+mZtqsmI19pROxvjpvI49MvCN3K+OQz+eU7kMYIf7I=\n\n— log2025-alpha3.rekor.sigstage.dev 09OnDBORH0Cui/+phd++9rIxf12lCq+ueOqrzKdUiyWHbJQzcg8IGLmE2vor7d5rC3EHGMjCY3aQht9n1Abp74Xjjgk=\n"}},"canonicalizedBody":"eyJhcGlWZXJzaW9uIjoiMC4wLjIiLCJraW5kIjoiZHNzZSIsInNwZWMiOnsiZHNzZVYwMDIiOnsicGF5bG9hZEhhc2giOnsiYWxnb3JpdGhtIjoiU0hBMl8yNTYiLCJkaWdlc3QiOiIrWGtPaW9mUUdKalFEU3poeW16a0sveHM1Witxb3h1cUNyWkFaS2FXZ0FBPSJ9LCJzaWduYXR1cmVzIjpbeyJjb250ZW50IjoiTUVRQ0lBOWszWTRYZ3gyWFYwajVXVTZIYjFiZXNOYURzQ0FNdktPQjFGak5hdEF1QWlBQjY2cWtSQW1mQmhPdXdQUHhNVnJ4Rm16VjBGSkl2NldLblQvdEdrZUJxUT09IiwidmVyaWZpZXIiOnsia2V5RGV0YWlscyI6IlBLSVhfRUNEU0FfUDI1Nl9TSEFfMjU2IiwieDUwOUNlcnRpZmljYXRlIjp7InJhd0J5dGVzIjoiTUlJQ3lqQ0NBbEdnQXdJQkFnSVVDd1NsZDJUUE1mekdmN2RXRENXSi91c2ltYjB3Q2dZSUtvWkl6ajBFQXdNd056RVZNQk1HQTFVRUNoTU1jMmxuYzNSdmNtVXVaR1YyTVI0d0hBWURWUVFERXhWemFXZHpkRzl5WlMxcGJuUmxjbTFsWkdsaGRHVXdIaGNOTWpVeE1ERXdNRGd5TXpReFdoY05NalV4TURFd01EZ3pNelF4V2pBQU1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRXB4alBlMHJEVG1rZk5kZjNCWWZFUnZpUFMxSHdJcmFDOWFuQVk2bnZiNTBYT1YzVWRSYU91aHJjcUs3ZUtFZkRYNUNJZnVUYmxhN1Y0c05IbzNiM1c2T0NBWEF3Z2dGc01BNEdBMVVkRHdFQi93UUVBd0lIZ0RBVEJnTlZIU1VFRERBS0JnZ3JCZ0VGQlFjREF6QWRCZ05WSFE0RUZnUVVpN2EzbkNxRzVYWHRHQXVkTEZOMGtIMlJjOGd3SHdZRFZSMGpCQmd3Rm9BVWNZWXdwaFI4WW0vNTk5YjBCUnAvWC8vcmI2d3dHUVlEVlIwUkFRSC9CQTh3RFlFTGFtdDFRR2R2ZEc4dVpta3dMQVlLS3dZQkJBR0R2ekFCQVFRZWFIUjBjSE02THk5bmFYUm9kV0l1WTI5dEwyeHZaMmx1TDI5aGRYUm9NQzRHQ2lzR0FRUUJnNzh3QVFnRUlBd2VhSFIwY0hNNkx5OW5hWFJvZFdJdVkyOXRMMnh2WjJsdUwyOWhkWFJvTUlHTEJnb3JCZ0VFQWRaNUFnUUNCSDBFZXdCNUFIY0FLekM4M0dpSXllTGgyQ1lwWG5RZlNEa3hsZ0x5bkRQTFhrTkEvcktzaG5vQUFBR1p6VGYzbndBQUJBTUFTREJHQWlFQXF3cjRMSk9JYXhNOEJjMGtjTkVQTkN4bER6WnBvcy9mMXJTN0JLNzZudjBDSVFDTEliMlYvMmk0RnF2T0dxSHZ1ejNmRjBuQUtBRXRXNlZOaUgrTG5QT20wekFLQmdncWhrak9QUVFEQXdObkFEQmtBakJ3VkpwWGo3WG1qUlNpNytLYmlQb0ZVbHlXWDJONURZdldGT0xMS3J1dk9xU1lZWURWQ3FnLzhkb0JNVDk5djRnQ01IRlNMRUEwRTl2U00wM2E1emtqTzh2Z1kxWlA4THNKL0NKSDFaL01PWWpKOW93UEdMQXQ4N1dhTjFRQTc2aEtzZz09In19fV19fX0="}],"timestamps":["MIIE6jADAgEAMIIE4QYJKoZIhvcNAQcCoIIE0jCCBM4CAQMxDTALBglghkgBZQMEAgEwgcMGCyqGSIb3DQEJEAEEoIGzBIGwMIGtAgEBBgkrBgEEAYO/MAIwMTANBglghkgBZQMEAgEFAAQgfC4NT5HwVGPGQCO/haHFdiCiQMSQ/fyL6nOLil48PyYCFQDR+SzWjCiB32Mq1s4ntWsFmx3uXxgPMjAyNTEwMTAwODIzNDFaMAMCAQECCQD5vCCFKUu0uqAypDAwLjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MRUwEwYDVQQDEwxzaWdzdG9yZS10c2GgggITMIICDzCCAZagAwIBAgIUCjWhBmHV4kFzxomWp/J98n4DfKcwCgYIKoZIzj0EAwMwOTEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSAwHgYDVQQDExdzaWdzdG9yZS10c2Etc2VsZnNpZ25lZDAeFw0yNTAzMjgwOTE0MDZaFw0zNTAzMjYwODE0MDZaMC4xFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEVMBMGA1UEAxMMc2lnc3RvcmUtdHNhMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEx1v5F3HpD9egHuknpBFlRz7QBRDJu4aeVzt9zJLRY0lvmx1lF7WBM2c9AN8ZGPQsmDqHlJN2R/7+RxLkvlLzkc19IOx38t7mGGEcB7agUDdCF/Ky3RTLSK0Xo/0AgHQdo2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFKj8ZPYo3i7mO3NPVIxSxOGc3VOlMB8GA1UdIwQYMBaAFDsgRlletTJNRzDObmPuc3RH8gR9MBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMDA2cAMGQCMESvVS6GGtF33+J19TfwENWJXjRv4i0/HQFwLUSkX6TfV7g0nG8VnqNHJLvEpAtOjQIwUD3uywTXorQP1DgbV09rF9Yen+CEqs/iEpieJWPst280SSOZ5Na+dyPVk9/8SFk6MYIB2zCCAdcCAQEwUTA5MRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxIDAeBgNVBAMTF3NpZ3N0b3JlLXRzYS1zZWxmc2lnbmVkAhQKNaEGYdXiQXPGiZan8n3yfgN8pzALBglghkgBZQMEAgGggfwwGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNTEwMTAwODIzNDFaMC8GCSqGSIb3DQEJBDEiBCDDNLfOiDbENhYGQNGrMwLkPKhAPwxEnmIPJuPgx/7mYjCBjgYLKoZIhvcNAQkQAi8xfzB9MHsweQQgBvT/4Ef+s1mZtzOw16MjUBz8GOTAM2aoRdd1NudLJ0QwVTA9pDswOTEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSAwHgYDVQQDExdzaWdzdG9yZS10c2Etc2VsZnNpZ25lZAIUCjWhBmHV4kFzxomWp/J98n4DfKcwCgYIKoZIzj0EAwIEZzBlAjEAnEXFAwkb8GHuC6Id7lxA9W6gUzV+6PmrgCtz5KHs/ud0QVtGvzDxA3uBJHZm7t1JAjANQFBCyGWdOINu2/cfLwpVjJ0TvXr8M+dFAOSf/JeQbK0gD93OMtZ7F7jH3lES3zY="]},"envelope":{"statement":"eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoicHlwaV9hdHRlc3RhdGlvbnMtMC4wLjE5LnRhci5neiIsImRpZ2VzdCI6eyJzaGEyNTYiOiI5YmIxYWRkMDRiMWI0ZTE4MmJlNmIwYjgwOTMxNTkzZjdhMjkxZWI0OWQ2OWI0ZmQ3MjhhNWQ0Y2JjZGM0YmQzIn19XSwicHJlZGljYXRlVHlwZSI6Imh0dHBzOi8vZG9jcy5weXBpLm9yZy9hdHRlc3RhdGlvbnMvcHVibGlzaC92MSIsInByZWRpY2F0ZSI6bnVsbH0=","signature":"MEQCIA9k3Y4Xgx2XV0j5WU6Hb1besNaDsCAMvKOB1FjNatAuAiAB66qkRAmfBhOuwPPxMVrxFmzV0FJIv6WKnT/tGkeBqQ=="}} \ No newline at end of file diff --git a/test/assets/pypi_attestations-0.0.19.tar.gz.publish.attestation.with_timestamp b/test/assets/pypi_attestations-0.0.19.tar.gz.publish.attestation.with_timestamp new file mode 100644 index 0000000..8a47dc0 --- /dev/null +++ b/test/assets/pypi_attestations-0.0.19.tar.gz.publish.attestation.with_timestamp @@ -0,0 +1 @@ +{"version":1,"verification_material":{"certificate":"MIICyjCCAlGgAwIBAgIUCsKmvvvMOxljmIrNu7XsdHuxA1UwCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjUxMDEwMDgyMjI4WhcNMjUxMDEwMDgzMjI4WjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEQL/FHnzx8tbzUWNgJNCiOmqZ+PFNYjECUklO9kcwK0FylPr8WLqxnxJIyu4CAq6BHMdXApib/t1LZCBpiCkKW6OCAXAwggFsMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUPrNp830jURXObbZcz0De5AXtZfwwHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4YZD8wGQYDVR0RAQH/BA8wDYELamt1QGdvdG8uZmkwLAYKKwYBBAGDvzABAQQeaHR0cHM6Ly9naXRodWIuY29tL2xvZ2luL29hdXRoMC4GCisGAQQBg78wAQgEIAweaHR0cHM6Ly9naXRodWIuY29tL2xvZ2luL29hdXRoMIGLBgorBgEEAdZ5AgQCBH0EewB5AHcA3T0wasbHETJjGR4cmWc3AqJKXrjePK3/h4pygC8p7o4AAAGZzTbZ2AAABAMASDBGAiEA6iKnHAj/opQOry1f9GcEE1Kmhk4eHjsR7lypmeG9Me0CIQCub0pTY/XBfHjahPr8C1R26HFfmEGa+DPunjHW1ZZurjAKBggqhkjOPQQDAwNnADBkAjAru0NtGlCsjwOOCnH/beIFl0jXYuZ6NS2DzpD5OV3kNpMEJp8wsBeCxFtuSMePF30CMBqySI7MrueB7/2SUur3DJv7A/OLUslnfN0WAjPzMGsoJrMpwqVD4cady2mXwhK/eQ==","transparency_entries":[{"logIndex":"598461431","logId":{"keyId":"wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0="},"kindVersion":{"kind":"dsse","version":"0.0.1"},"integratedTime":"1760084548","inclusionPromise":{"signedEntryTimestamp":"MEUCIQCx6X6FZCepxmGbNtzw3+ttHWCvslmkUSHdSjGoc6A8CgIgLzJrx4K0XP+nPChNIgM8R2e2OX+nQ49bn5dbsjsDKcE="},"inclusionProof":{"logIndex":"476557169","rootHash":"5rmfI5fg+GLgW1u9vyFGa5CGRWCuWfqD9KN9ChB8mB0=","treeSize":"476557170","hashes":["Z4U0ToAixBcbhfvBzcFZ0aP7CxhW7Ql4zjDkU/UcXZI=","IMm3BxjyrXKFqCUjNVImFffyzp9lGd85VywPwPpOkYk=","XQl2LB3lN5KPbizzBG/FSu/PzUXjB8/9CJ7Emxf5CfE=","OSXATwZmQbaIxVOkMhKJhHruKbXPyw23sE0vPAf4LTU=","5F0HtxEvz9wzVBJDJicASVB1EQVwyT02M3EyBZCjsN4=","pGA1v60Ji6EbUgbRVHoTQ/r5fhlMetBrO6XM5F5ZCJU=","ePisi8uUsxle18YHgyMhOSB6feLySmaFVzmDHNTGIzQ=","VOdGKmbb9n2TrE3xEQ/QehtkLm4+7UQgycAIDwL7tOM=","pTOZwpCP0yDqia4ethoqTejq6XusxsbezkyHrZ9wim0=","1ph/oLtygbE0j91MF5D3qGzpB7t5+wmEvheP9AJwN1k=","6V8DhhxWbMfN9pfeP/KTwoCvgbJlDiesecM5nYGTONs=","a2ac6t7rl39TPjIcwg8cvKVHuwE1SIkI7M5kC6iA7UA=","oRq5SAm/9ovZ2TYAsT6iBMkqWElTdvMY6PBVze65E8g=","mxhgKnbFidkcY9Sspg3POkzMS3zMgeWy033i8AHVyWQ=","EGaD/cNavzxGYLx1Gl0uNNWBZvyXlSHSdlIeH7m+63A=","2Wv4GiithwNukRKV06clevnQQYCzXmSS/+/OJtXgsXQ=","1mfy94KpcItqshH9+gwqV6jccupcaMpVsF28New8zDY=","vS7O4ozHIQZJWBiov+mkpI27GE8zAmVCEkRcP3NDyNE="],"checkpoint":{"envelope":"rekor.sigstore.dev - 1193050959916656506\n476557170\n5rmfI5fg+GLgW1u9vyFGa5CGRWCuWfqD9KN9ChB8mB0=\n\n— rekor.sigstore.dev wNI9ajBFAiAJ5EzPNjtvhuhuueWp4MstQItoEcdtX3+LbBPJg8RllQIhALDctcPGO1VzCP2FTbfvCo16X8MuVjHdigS1m2f5LAjC\n"}},"canonicalizedBody":"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiZHNzZSIsInNwZWMiOnsiZW52ZWxvcGVIYXNoIjp7ImFsZ29yaXRobSI6InNoYTI1NiIsInZhbHVlIjoiOTg5NmEzNDljYTFlZWYzNWIwNjFkOWQ0ZjM3Mzc2NzE3MWJmZjVlMjUwMmY2ZjNjNDVhYTYzMzU4MzA5NWVkMSJ9LCJwYXlsb2FkSGFzaCI6eyJhbGdvcml0aG0iOiJzaGEyNTYiLCJ2YWx1ZSI6ImY5NzkwZThhODdkMDE4OThkMDBkMmNlMWNhNmNlNDJiZmM2Y2U1OWZhYWEzMWJhYTBhYjY0MDY0YTY5NjgwMDAifSwic2lnbmF0dXJlcyI6W3sic2lnbmF0dXJlIjoiTUVVQ0lRRGJiUWpQR1h5YmZZeU5nRk9hRmRqamlEMi80K1VRWUJSTjZwamJ5SytTaHdJZ0NnZ1ljeHZ1RWF2NnZTeFFJaTFlckhMc1M4dmh1bFIvYmNVNlhxYVRSbG89IiwidmVyaWZpZXIiOiJMUzB0TFMxQ1JVZEpUaUJEUlZKVVNVWkpRMEZVUlMwdExTMHRDazFKU1VONWFrTkRRV3hIWjBGM1NVSkJaMGxWUTNOTGJYWjJkazFQZUd4cWJVbHlUblUzV0hOa1NIVjRRVEZWZDBObldVbExiMXBKZW1vd1JVRjNUWGNLVG5wRlZrMUNUVWRCTVZWRlEyaE5UV015Ykc1ak0xSjJZMjFWZFZwSFZqSk5ValIzU0VGWlJGWlJVVVJGZUZaNllWZGtlbVJIT1hsYVV6RndZbTVTYkFwamJURnNXa2RzYUdSSFZYZElhR05PVFdwVmVFMUVSWGROUkdkNVRXcEpORmRvWTA1TmFsVjRUVVJGZDAxRVozcE5ha2swVjJwQlFVMUdhM2RGZDFsSUNrdHZXa2w2YWpCRFFWRlpTVXR2V2tsNmFqQkVRVkZqUkZGblFVVlJUQzlHU0c1NmVEaDBZbnBWVjA1blNrNURhVTl0Y1ZvclVFWk9XV3BGUTFWcmJFOEtPV3RqZDBzd1JubHNVSEk0VjB4eGVHNTRTa2w1ZFRSRFFYRTJRa2hOWkZoQmNHbGlMM1F4VEZwRFFuQnBRMnRMVnpaUFEwRllRWGRuWjBaelRVRTBSd3BCTVZWa1JIZEZRaTkzVVVWQmQwbElaMFJCVkVKblRsWklVMVZGUkVSQlMwSm5aM0pDWjBWR1FsRmpSRUY2UVdSQ1owNVdTRkUwUlVablVWVlFjazV3Q2pnek1HcFZVbGhQWW1KYVkzb3dSR1UxUVZoMFdtWjNkMGgzV1VSV1VqQnFRa0puZDBadlFWVXpPVkJ3ZWpGWmEwVmFZalZ4VG1wd1MwWlhhWGhwTkZrS1drUTRkMGRSV1VSV1VqQlNRVkZJTDBKQk9IZEVXVVZNWVcxME1WRkhaSFprUnpoMVdtMXJkMHhCV1V0TGQxbENRa0ZIUkhaNlFVSkJVVkZsWVVoU01BcGpTRTAyVEhrNWJtRllVbTlrVjBsMVdUSTVkRXd5ZUhaYU1teDFUREk1YUdSWVVtOU5RelJIUTJselIwRlJVVUpuTnpoM1FWRm5SVWxCZDJWaFNGSXdDbU5JVFRaTWVUbHVZVmhTYjJSWFNYVlpNamwwVERKNGRsb3liSFZNTWpsb1pGaFNiMDFKUjB4Q1oyOXlRbWRGUlVGa1dqVkJaMUZEUWtnd1JXVjNRalVLUVVoalFUTlVNSGRoYzJKSVJWUktha2RTTkdOdFYyTXpRWEZLUzFoeWFtVlFTek12YURSd2VXZERPSEEzYnpSQlFVRkhXbnBVWWxveVFVRkJRa0ZOUVFwVFJFSkhRV2xGUVRacFMyNUlRV292YjNCUlQzSjVNV1k1UjJORlJURkxiV2hyTkdWSWFuTlNOMng1Y0cxbFJ6bE5aVEJEU1ZGRGRXSXdjRlJaTDFoQ0NtWklhbUZvVUhJNFF6RlNNalpJUm1adFJVZGhLMFJRZFc1cVNGY3hXbHAxY21wQlMwSm5aM0ZvYTJwUFVGRlJSRUYzVG01QlJFSnJRV3BCY25Vd1RuUUtSMnhEYzJwM1QwOURia2d2WW1WSlJtd3dhbGhaZFZvMlRsTXlSSHB3UkRWUFZqTnJUbkJOUlVwd09IZHpRbVZEZUVaMGRWTk5aVkJHTXpCRFRVSnhlUXBUU1RkTmNuVmxRamN2TWxOVmRYSXpSRXAyTjBFdlQweFZjMnh1Wms0d1YwRnFVSHBOUjNOdlNuSk5jSGR4VmtRMFkyRmtlVEp0V0hkb1N5OWxVVDA5Q2kwdExTMHRSVTVFSUVORlVsUkpSa2xEUVZSRkxTMHRMUzBLIn1dfX0="}],"timestamps":["MIIE7DADAgEAMIIE4wYJKoZIhvcNAQcCoIIE1DCCBNACAQMxDTALBglghkgBZQMEAgEwgcMGCyqGSIb3DQEJEAEEoIGzBIGwMIGtAgEBBgkrBgEEAYO/MAIwMTANBglghkgBZQMEAgEFAAQg8KlV7NgkK/FnHgIWxUJXSXwkp5vWaelxfxdGgVBszw4CFQCqAXNo5MneAMM9Roy7KoYTJxYFkBgPMjAyNTEwMTAwODIyMjhaMAMCAQECCQCxPnBshVVTnKAypDAwLjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MRUwEwYDVQQDEwxzaWdzdG9yZS10c2GgggIUMIICEDCCAZagAwIBAgIUOhNULwyQYe68wUMvy4qOiyojiwwwCgYIKoZIzj0EAwMwOTEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSAwHgYDVQQDExdzaWdzdG9yZS10c2Etc2VsZnNpZ25lZDAeFw0yNTA0MDgwNjU5NDNaFw0zNTA0MDYwNjU5NDNaMC4xFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEVMBMGA1UEAxMMc2lnc3RvcmUtdHNhMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE4ra2Z8hKNig2T9kFjCAToGG30jky+WQv3BzL+mKvh1SKNR/UwuwsfNCg4sryoYAd8E6isovVA3M4aoNdm9QDi50Z8nTEyvqgfDPtTIwXItfiW/AFf1V7uwkbkAoj0xxco2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFIn9eUOHz9BlRsMCRscsc1t9tOsDMB8GA1UdIwQYMBaAFJjsAe9/u1H/1JUeb4qImFMHic6/MBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMDA2gAMGUCMDtpsV/6KaO0qyF/UMsX2aSUXKQFdoGTptQGc0ftq1csulHPGG6dsmyMNd3JB+G3EQIxAOajvBcjpJmKb4Nv+2Taoj8Uc5+b6ih6FXCCKraSqupe07zqswMcXJTe1cExvHvvlzGCAdwwggHYAgEBMFEwOTEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSAwHgYDVQQDExdzaWdzdG9yZS10c2Etc2VsZnNpZ25lZAIUOhNULwyQYe68wUMvy4qOiyojiwwwCwYJYIZIAWUDBAIBoIH8MBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAcBgkqhkiG9w0BCQUxDxcNMjUxMDEwMDgyMjI4WjAvBgkqhkiG9w0BCQQxIgQgLjDQB0fviWQEUJvQcAa3MLNvzeZAawcvopv+4ZsHWzIwgY4GCyqGSIb3DQEJEAIvMX8wfTB7MHkEIIX5J7wHq2LKw7RDVsEO/IGyxog/2nq55thw2dE6zQW3MFUwPaQ7MDkxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEgMB4GA1UEAxMXc2lnc3RvcmUtdHNhLXNlbGZzaWduZWQCFDoTVC8MkGHuvMFDL8uKjosqI4sMMAoGCCqGSM49BAMCBGgwZgIxANkYjeM8aGQ3d9IB5INg1wnvcH6zn8whwSiQKDK6I+3Xy3EydisbDPs+ygpoRw55MAIxAJIb7+HPPc6TJmKB8vSfTwrOonRSw6YVWckJbLeUWO4SIrO26ASszUuXNzaA24Rd3A=="]},"envelope":{"statement":"eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoicHlwaV9hdHRlc3RhdGlvbnMtMC4wLjE5LnRhci5neiIsImRpZ2VzdCI6eyJzaGEyNTYiOiI5YmIxYWRkMDRiMWI0ZTE4MmJlNmIwYjgwOTMxNTkzZjdhMjkxZWI0OWQ2OWI0ZmQ3MjhhNWQ0Y2JjZGM0YmQzIn19XSwicHJlZGljYXRlVHlwZSI6Imh0dHBzOi8vZG9jcy5weXBpLm9yZy9hdHRlc3RhdGlvbnMvcHVibGlzaC92MSIsInByZWRpY2F0ZSI6bnVsbH0=","signature":"MEUCIQDbbQjPGXybfYyNgFOaFdjjiD2/4+UQYBRN6pjbyK+ShwIgCggYcxvuEav6vSxQIi1erHLsS8vhulR/bcU6XqaTRlo="}} \ No newline at end of file diff --git a/test/test_cli.py b/test/test_cli.py index 940a401..a44b2a3 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -36,6 +36,10 @@ publish_attestation_identity = "https://github.com/trailofbits/pypi-attestations/.github/workflows/release.yml@refs/tags/v0.0.19" publish_attestation_path = _ASSETS / "pypi_attestations-0.0.19.tar.gz.publish.attestation" slsa_attestation_path = _ASSETS / "pypi_attestations-0.0.19.tar.gz.slsa.attestation" +rekor2_attestation_path = ( + _ASSETS / "pypi_attestations-0.0.19.tar.gz.publish.attestation.with_rekor2_timestamp" +) + pypi_wheel_url = "https://files.pythonhosted.org/packages/fb/f2/3e026065773b84c5b2345e2548a08b10105d324b9b95c72643f57a25fcbb/pypi_attestations-0.0.19-py3-none-any.whl" pypi_sdist_url = "https://files.pythonhosted.org/packages/c5/4d/a114bdd186903426bd9c1e9c3700761ec5eaac260fa3dfdef14bf84b751b/pypi_attestations-0.0.19.tar.gz" @@ -229,6 +233,11 @@ def test_inspect_command(caplog: pytest.LogCaptureFixture) -> None: run_main_with_command(["inspect", "--dump-bytes", publish_attestation_path.as_posix()]) assert "Signature:" in caplog.text + # Happy path with annotation that contains rekor2 entry and a timestamp + run_main_with_command(["inspect", rekor2_attestation_path.as_posix()]) + assert "Entry type: dsse 0.0.2" in caplog.text + assert "Timestamps (1):" in caplog.text + # Failure paths caplog.clear() diff --git a/test/test_impl.py b/test/test_impl.py index 9fe22fe..4d704f6 100644 --- a/test/test_impl.py +++ b/test/test_impl.py @@ -35,6 +35,7 @@ dist = impl.Distribution.from_file(dist_path) dist_bundle_path = _ASSETS / "rfc8785-0.1.2-py3-none-any.whl.sigstore" dist_attestation_path = _ASSETS / "rfc8785-0.1.2-py3-none-any.whl.attestation" +pypi_attestations_dist = impl.Distribution.from_file(_ASSETS / "pypi_attestations-0.0.19.tar.gz") pypi_attestations_attestation = _ASSETS / "pypi_attestations-0.0.19.tar.gz.publish.attestation" # produced by actions/attest@v1 @@ -46,6 +47,11 @@ gl_signed_dist = impl.Distribution.from_file(gl_signed_dist_path) gl_attestation_path = _ASSETS / "gitlab_oidc_project-0.0.3.tar.gz.publish.attestation" +# contains timestamp but still rekor v1 entry from production +attestation_with_ts = Path(str(pypi_attestations_attestation) + ".with_timestamp") +# contains timestamp and rekorv2 entry from staging +attestation_with_rekor2 = Path(str(pypi_attestations_attestation) + ".with_rekor2_timestamp") + class TestDistribution: def test_from_file_nonexistent(self, tmp_path: Path) -> None: @@ -70,7 +76,7 @@ class TestAttestation: @online def test_roundtrip(self, id_token: IdentityToken) -> None: trust_config = ClientTrustConfig.staging() - # Make sure we use rekor v1 until attestations are compatible with v2 + # Make sure we choose the rekor version: currently v1 trust_config.force_tlog_version = 1 sign_ctx = SigningContext.from_trust_config(trust_config) @@ -79,6 +85,10 @@ def test_roundtrip(self, id_token: IdentityToken) -> None: attestation.verify(policy.UnsafeNoOp(), dist, staging=True) + # ensure we only produce attestations with rekor v1 entries for now: + for entry in attestation.verification_material.transparency_entries: + assert entry["kindVersion"] == {"kind": "dsse", "version": "0.0.1"} + # converting to a bundle and verifying as a bundle also works bundle = attestation.to_bundle() Verifier.staging().verify_dsse(bundle, policy.UnsafeNoOp()) @@ -107,7 +117,7 @@ def in_validity_period(_: IdentityToken) -> bool: monkeypatch.setattr(IdentityToken, "in_validity_period", in_validity_period) trust_config = ClientTrustConfig.staging() - # Make sure we use rekor v1 until attestations are compatible with v2 + # Make sure we choose the rekor version: currently v1 trust_config.force_tlog_version = 1 sign_ctx = SigningContext.from_trust_config(trust_config) @@ -128,7 +138,7 @@ def get_bundle(*_: Any) -> Bundle: monkeypatch.setattr(sigstore.sign.Signer, "sign_dsse", get_bundle) trust_config = ClientTrustConfig.staging() - # Make sure we use rekor v1 until attestations are compatible with v2 + # Make sure we choose the rekor version: currently v1 trust_config.force_tlog_version = 1 sign_ctx = SigningContext.from_trust_config(trust_config) @@ -220,6 +230,49 @@ def test_verify(self) -> None: bundle = attestation.to_bundle() Verifier.staging(offline=True).verify_dsse(bundle, policy.UnsafeNoOp()) + def test_verify_with_timestamp(self) -> None: + # Our checked-in asset has this identity. + pol = policy.Identity(identity="jku@goto.fi", issuer="https://github.com/login/oauth") + + attestation = impl.Attestation.model_validate_json(attestation_with_ts.read_bytes()) + predicate_type, predicate = attestation.verify(pol, pypi_attestations_dist, offline=True) + + assert attestation.statement["_type"] == "https://in-toto.io/Statement/v1" + assert ( + predicate_type + == attestation.statement["predicateType"] + == "https://docs.pypi.org/attestations/publish/v1" + ) + assert predicate is None and attestation.statement["predicate"] is None + + # convert the attestation to a bundle and verify it that way too + bundle = attestation.to_bundle() + Verifier.production(offline=True).verify_dsse(bundle, policy.UnsafeNoOp()) + + def test_verify_with_timestamp_and_rekor2_entry(self) -> None: + # Note that the pypi-attestations does not currently create attestatations with rekor2 + # entries. This test still asserts that verification works + + # Our checked-in asset has this identity. + pol = policy.Identity(identity="jku@goto.fi", issuer="https://github.com/login/oauth") + + attestation = impl.Attestation.model_validate_json(attestation_with_rekor2.read_bytes()) + predicate_type, predicate = attestation.verify( + pol, pypi_attestations_dist, staging=True, offline=True + ) + + assert attestation.statement["_type"] == "https://in-toto.io/Statement/v1" + assert ( + predicate_type + == attestation.statement["predicateType"] + == "https://docs.pypi.org/attestations/publish/v1" + ) + assert predicate is None and attestation.statement["predicate"] is None + + # convert the attestation to a bundle and verify it that way too + bundle = attestation.to_bundle() + Verifier.staging(offline=True).verify_dsse(bundle, policy.UnsafeNoOp()) + def test_verify_digest_mismatch(self, tmp_path: Path) -> None: # Our checked-in asset has this identity. pol = policy.Identity(