From 146f85ef3e3c039951fca99f00ffd1cd9507d051 Mon Sep 17 00:00:00 2001 From: Ramon Petgrave Date: Fri, 9 May 2025 19:22:25 +0000 Subject: [PATCH 1/9] use all verified timestamps Signed-off-by: Ramon Petgrave --- CHANGELOG.md | 5 +++++ q | 20 ++++++++++++++++++++ sigstore/verify/verifier.py | 23 ++++++++++++----------- test/unit/test_models.py | 5 +++-- 4 files changed, 40 insertions(+), 13 deletions(-) create mode 100644 q diff --git a/CHANGELOG.md b/CHANGELOG.md index e77467913..1364e4bdc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,11 @@ All versions prior to 0.9.0 are untracked. * API: Make Rekor APIs compatible with Rekor v2 by removing trailing slashes from endpoints ([#1366](https://github.com/sigstore/sigstore-python/pull/1366)) +* Timestamps: Verify the signature date against the validity period of both the + Timestamp Authority or the Transperency Service, if either of such timestamps + are present in the Bundle. We still require at lease one of such timestamps + [#PRLINK][PRLINK] + ### Changed * `--trust-config` now requires a file with SigningConfig v0.2, and is able to fully diff --git a/q b/q new file mode 100644 index 000000000..c02264732 --- /dev/null +++ b/q @@ -0,0 +1,20 @@ +diff --git a/test/unit/test_models.py b/test/unit/test_models.py +index 95f297f..088e86f 100644 +--- a/test/unit/test_models.py ++++ b/test/unit/test_models.py +@@ -30,12 +30,13 @@ from sigstore.models import ( +  +  + class TestLogEntry: +- def test_missing_inclusion_proof(self): ++ @pytest.mark.parametrize('integrated_time', [0, 1746819403]) ++ def test_missing_inclusion_proof(self, integrated_time: int): + with pytest.raises(ValueError, match=r"inclusion_proof"): + LogEntry( + uuid="fake", + body=b64encode(b"fake"), +- integrated_time=0, ++ integrated_time=integrated_time, + log_id="1234", + log_index=1, + inclusion_proof=None, diff --git a/sigstore/verify/verifier.py b/sigstore/verify/verifier.py index 30e7e8e59..24efa665b 100644 --- a/sigstore/verify/verifier.py +++ b/sigstore/verify/verifier.py @@ -372,17 +372,18 @@ def _verify_common_signing_cert( except VerificationError as exc: raise VerificationError(f"invalid log entry: {exc}") - # (6): verify that log entry was integrated circa the signing certificate's - # validity period. - integrated_time = datetime.fromtimestamp(entry.integrated_time, tz=timezone.utc) - if not ( - bundle.signing_certificate.not_valid_before_utc - <= integrated_time - <= bundle.signing_certificate.not_valid_after_utc - ): - raise VerificationError( - "invalid signing cert: expired at time of Rekor entry" - ) + # (6): verify the log entry was signed circa circa the signing certificate's + # validity period, via the verified timestamps. In the case of timestamps from + # the transparency service, the integrated time will have been used. See `_establish_time()`. + for vts in verified_timestamps: + if not ( + bundle.signing_certificate.not_valid_before_utc + <= vts.time + <= bundle.signing_certificate.not_valid_after_utc + ): + raise VerificationError( + f"invalid signing cert: expired at time of signing, time via {vts}" + ) def verify_dsse( self, bundle: Bundle, policy: VerificationPolicy diff --git a/test/unit/test_models.py b/test/unit/test_models.py index 95f297f07..088e86f3f 100644 --- a/test/unit/test_models.py +++ b/test/unit/test_models.py @@ -30,12 +30,13 @@ class TestLogEntry: - def test_missing_inclusion_proof(self): + @pytest.mark.parametrize('integrated_time', [0, 1746819403]) + def test_missing_inclusion_proof(self, integrated_time: int): with pytest.raises(ValueError, match=r"inclusion_proof"): LogEntry( uuid="fake", body=b64encode(b"fake"), - integrated_time=0, + integrated_time=integrated_time, log_id="1234", log_index=1, inclusion_proof=None, From 830ea640e2f04ec781b243d93c80f1eb44dd53cc Mon Sep 17 00:00:00 2001 From: Ramon Petgrave Date: Fri, 9 May 2025 20:16:02 +0000 Subject: [PATCH 2/9] additional test Signed-off-by: Ramon Petgrave --- test/unit/test_models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/test_models.py b/test/unit/test_models.py index 088e86f3f..f1d345e13 100644 --- a/test/unit/test_models.py +++ b/test/unit/test_models.py @@ -30,7 +30,7 @@ class TestLogEntry: - @pytest.mark.parametrize('integrated_time', [0, 1746819403]) + @pytest.mark.parametrize("integrated_time", [0, 1746819403]) def test_missing_inclusion_proof(self, integrated_time: int): with pytest.raises(ValueError, match=r"inclusion_proof"): LogEntry( From 17a97164942b3a7a1970df49023658a74ee7a94f Mon Sep 17 00:00:00 2001 From: Ramon Petgrave Date: Fri, 9 May 2025 20:36:51 +0000 Subject: [PATCH 3/9] cleanup Signed-off-by: Ramon Petgrave --- q | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 q diff --git a/q b/q deleted file mode 100644 index c02264732..000000000 --- a/q +++ /dev/null @@ -1,20 +0,0 @@ -diff --git a/test/unit/test_models.py b/test/unit/test_models.py -index 95f297f..088e86f 100644 ---- a/test/unit/test_models.py -+++ b/test/unit/test_models.py -@@ -30,12 +30,13 @@ from sigstore.models import ( -  -  - class TestLogEntry: -- def test_missing_inclusion_proof(self): -+ @pytest.mark.parametrize('integrated_time', [0, 1746819403]) -+ def test_missing_inclusion_proof(self, integrated_time: int): - with pytest.raises(ValueError, match=r"inclusion_proof"): - LogEntry( - uuid="fake", - body=b64encode(b"fake"), -- integrated_time=0, -+ integrated_time=integrated_time, - log_id="1234", - log_index=1, - inclusion_proof=None, From 2e93051a73a27a7a22b3823cf68d9a6e7b31d41f Mon Sep 17 00:00:00 2001 From: Ramon Petgrave Date: Fri, 9 May 2025 20:38:29 +0000 Subject: [PATCH 4/9] pr backlink Signed-off-by: Ramon Petgrave --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1364e4bdc..e0430ed65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,8 +22,8 @@ All versions prior to 0.9.0 are untracked. * Timestamps: Verify the signature date against the validity period of both the Timestamp Authority or the Transperency Service, if either of such timestamps - are present in the Bundle. We still require at lease one of such timestamps - [#PRLINK][PRLINK] + are present in the Bundle. We still require at lease one of such timestamps. + [#1381](https://github.com/sigstore/sigstore-python/pull/1381) ### Changed From 6642f12a15f5eb21b1368ecefe02e8814d92c40a Mon Sep 17 00:00:00 2001 From: Ramon Petgrave Date: Mon, 12 May 2025 19:57:14 +0000 Subject: [PATCH 5/9] add test and data for a late timestamp Signed-off-by: Ramon Petgrave --- .../tsa/bundle.txt.late_timestamp.sigstore | 1 + test/unit/verify/test_verifier.py | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 test/assets/tsa/bundle.txt.late_timestamp.sigstore diff --git a/test/assets/tsa/bundle.txt.late_timestamp.sigstore b/test/assets/tsa/bundle.txt.late_timestamp.sigstore new file mode 100644 index 000000000..5b5e0f2fd --- /dev/null +++ b/test/assets/tsa/bundle.txt.late_timestamp.sigstore @@ -0,0 +1 @@ +{"mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json", "verificationMaterial": {"certificate": {"rawBytes": "MIIDBTCCAougAwIBAgIUIs3M2DgogCj3KotUVZg8Mok6IhMwCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjUwNTEyMTg0ODI2WhcNMjUwNTEyMTg1ODI2WjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEn2elp6N4BmBpOaQbpbiYY5EBXJq5+f0tPnffeJTbLVzPgUbpX4T5ZS7KDuQFQSPrljgIZAO3+ZmFSFFnwVrNv6OCAaowggGmMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQU+j0g8S3mHrEo3eautm7T4RnwWwUwHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4YZD8wWQYDVR0RAQH/BE8wTYFLaW5zZWN1cmUtY2xvdWR0b3Atc2hhcmVkLXVzZXJAY2xvdWR0b3AtcHJvZC11cy1lYXN0LmlhbS5nc2VydmljZWFjY291bnQuY29tMCkGCisGAQQBg78wAQEEG2h0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbTArBgorBgEEAYO/MAEIBB0MG2h0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbTCBiwYKKwYBBAHWeQIEAgR9BHsAeQB3AN09MGrGxxEyYxkeHJlnNwKiSl643jyt/4eKcoAvKe6OAAABlsXTr40AAAQDAEgwRgIhAN790LnuqDZwfqzyilH4qtk7zVvVZUQXB0Q0YfX9tNWXAiEAzH649BUx15UYsZUGihsBfNUQXov87UYzfYE2Zw2L174wCgYIKoZIzj0EAwMDaAAwZQIwCJ8+cVdfOc5SPoQnjY6rrIxIlYqLgtW65YrX8GzbRW4NpP37m6nxi6cjqtgwGFMeAjEAp4JgaETMFRgSBSSZLB7uhqr1fY97LPcHmAebKFpqFQERELMUmmqk5uHB2wgtvzB2"}, "tlogEntries": [{"logIndex": "42066373", "logId": {"keyId": "0y8wo8MtY5wrdiIFohx7sHeI5oKDpK5vQhGHI6G+pJY="}, "kindVersion": {"kind": "hashedrekord", "version": "0.0.1"}, "integratedTime": "1747075707", "inclusionPromise": {"signedEntryTimestamp": "MEUCIDxpagNcBytw52lZI3CwbTA6lfydnHlIGogI1Jfu13PKAiEAri9BAnNJDCZV3gEj9MuLEPw6jVbAiKfmrUmoaAIqSsc="}, "inclusionProof": {"logIndex": "10383961", "rootHash": "K6ZWztp1qbjIuzexDwMUOhf/+S+wqz4iQEDFTcEnNGQ=", "treeSize": "10383962", "hashes": ["HjQ6YJcBoxxKm4Uxs0zJCqC/LM/phnZMGiOiDLXo5wg=", "HSzuxscITh6g9k7vt64/9Z8zPwGwcQJv7NfnX92ULng=", "EEVPMqL5AIgHaYl2NbjmSTvn31oGEjhpTPbpgowrPM4=", "WbnH9wLRq4lD3Ju3FWOBZ+PEfvXT2c0Ugqy78gFgR0M=", "Kv0MBtfoWuGMfuJhPQiwSV7qUt+ALTQMx9BWYUrusb0=", "vld+lIPewmCjCp7W2cwUZD59sPgCK0rC0T2GpveXsmM=", "//dvSvxZzO+sVgkN0WfDdWVO4VGUsVGNT6bSmn5b/Qg=", "AzkFO8X9eKMNJxy+AuVjKe/2ObuNc4pGFzYucDuH87I=", "BVBT6KGWJPrAI4T7Zzt529+ZxU+G5UR0UMqPDcKUYzk=", "1CmXahercpSNPyH2ATDpK8S80Gim/GrKkm/8V5Ozue0=", "ZyAV6AeFLhv6n2Ya599XWHwy3HCr/y0+RF0P6Smg8IU=", "GoHRwlhYuJIYJdmRnHX5HWLr2ngxzHnAIIqBewovBi0=", "OdoqbUqBYHhj2W1RLM8APkQOnM2K9gzGm1KPFmwIIeQ="], "checkpoint": {"envelope": "rekor.sigstage.dev - 8202293616175992157\n10383962\nK6ZWztp1qbjIuzexDwMUOhf/+S+wqz4iQEDFTcEnNGQ=\n\n\u2014 rekor.sigstage.dev 0y8wozBFAiEA+12tjmkJ2CeZlW4baTsLtnVfdSeWNyW8ZFykmBcAn4QCIB6OZTD/bVgAsuq5FgSQZzwn0RPYl7+S1IFRYAoHIP5G\n"}}, "canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI4MDJkZDYwZmY4ODMzMzgwMmYyNTg1ZTczMDQzYmQyMWMzNDEyODVlMTk5MmZlNWIzMTc1NWUxY2FkZWFlMzBlIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FUUNJRmhsZU1oMjlRRW5QbVc3Mjhkd1h1ZkdGTVo0NG8zNkNseGVxRWVWaUxSdEFpQjIzUkRHenArbjF3aDVjVTF0cC9CampIc3RBQjdsWmY5S0tKbnpwM3ViV0E9PSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVUkNWRU5EUVc5MVowRjNTVUpCWjBsVlNYTXpUVEpFWjI5blEyb3pTMjkwVlZaYVp6aE5iMnMyU1doTmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcFZkMDVVUlhsTlZHY3dUMFJKTWxkb1kwNU5hbFYzVGxSRmVVMVVaekZQUkVreVYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVZ1TW1Wc2NEWk9ORUp0UW5CUFlWRmljR0pwV1ZrMVJVSllTbkUxSzJZd2RGQnVabVlLWlVwVVlreFdlbEJuVldKd1dEUlVOVnBUTjB0RWRWRkdVVk5RY214cVowbGFRVTh6SzFwdFJsTkdSbTUzVm5KT2RqWlBRMEZoYjNkblowZHRUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlVyYWpCbkNqaFRNMjFJY2tWdk0yVmhkWFJ0TjFRMFVtNTNWM2RWZDBoM1dVUldVakJxUWtKbmQwWnZRVlV6T1ZCd2VqRlphMFZhWWpWeFRtcHdTMFpYYVhocE5Ga0tXa1E0ZDFkUldVUldVakJTUVZGSUwwSkZPSGRVV1VaTVlWYzFlbHBYVGpGamJWVjBXVEo0ZG1SWFVqQmlNMEYwWXpKb2FHTnRWbXRNV0ZaNldsaEtRUXBaTW5oMlpGZFNNR0l6UVhSalNFcDJXa014TVdONU1XeFpXRTR3VEcxc2FHSlROVzVqTWxaNVpHMXNhbHBYUm1wWk1qa3hZbTVSZFZreU9YUk5RMnRIQ2tOcGMwZEJVVkZDWnpjNGQwRlJSVVZITW1nd1pFaENlazlwT0haWlYwNXFZak5XZFdSSVRYVmFNamwyV2pKNGJFeHRUblppVkVGeVFtZHZja0puUlVVS1FWbFBMMDFCUlVsQ1FqQk5SekpvTUdSSVFucFBhVGgyV1ZkT2FtSXpWblZrU0UxMVdqSTVkbG95ZUd4TWJVNTJZbFJEUW1sM1dVdExkMWxDUWtGSVZ3cGxVVWxGUVdkU09VSkljMEZsVVVJelFVNHdPVTFIY2tkNGVFVjVXWGhyWlVoS2JHNU9kMHRwVTJ3Mk5ETnFlWFF2TkdWTFkyOUJka3RsTms5QlFVRkNDbXh6V0ZSeU5EQkJRVUZSUkVGRlozZFNaMGxvUVU0M09UQk1iblZ4UkZwM1puRjZlV2xzU0RSeGRHczNlbFoyVmxwVlVWaENNRkV3V1daWU9YUk9WMWdLUVdsRlFYcElOalE1UWxWNE1UVlZXWE5hVlVkcGFITkNaazVWVVZodmRqZzNWVmw2WmxsRk1scDNNa3d4TnpSM1EyZFpTVXR2V2tsNmFqQkZRWGROUkFwaFFVRjNXbEZKZDBOS09DdGpWbVJtVDJNMVUxQnZVVzVxV1RaeWNrbDRTV3haY1V4bmRGYzJOVmx5V0RoSGVtSlNWelJPY0ZBek4yMDJibmhwTm1OcUNuRjBaM2RIUmsxbFFXcEZRWEEwU21kaFJWUk5SbEpuVTBKVFUxcE1RamQxYUhGeU1XWlpPVGRNVUdOSWJVRmxZa3RHY0hGR1VVVlNSVXhOVlcxdGNXc0tOWFZJUWpKM1ozUjJla0l5Q2kwdExTMHRSVTVFSUVORlVsUkpSa2xEUVZSRkxTMHRMUzBLIn19fX0="}], "timestampVerificationData": {"rfc3161Timestamps": [{"signedTimestamp": "MIIE6TADAgEAMIIE4AYJKoZIhvcNAQcCoIIE0TCCBM0CAQMxDTALBglghkgBZQMEAgEwgcIGCyqGSIb3DQEJEAEEoIGyBIGvMIGsAgEBBgkrBgEEAYO/MAIwMTANBglghkgBZQMEAgEFAAQgLenXcJBzdjua5V/WDyNWpTIysBnS9xKUPS0plFLqG0gCFQD3x9GVccz7Cvui6lxEdDQtb7L3uBgPMjAyNTA1MTIxOTAwMjdaMAMCAQECCHSCL66M6EByoDKkMDAuMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxFTATBgNVBAMTDHNpZ3N0b3JlLXRzYaCCAhMwggIPMIIBlqADAgECAhQKNaEGYdXiQXPGiZan8n3yfgN8pzAKBggqhkjOPQQDAzA5MRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxIDAeBgNVBAMTF3NpZ3N0b3JlLXRzYS1zZWxmc2lnbmVkMB4XDTI1MDMyODA5MTQwNloXDTM1MDMyNjA4MTQwNlowLjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MRUwEwYDVQQDEwxzaWdzdG9yZS10c2EwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATHW/kXcekP16Ae6SekEWVHPtAFEMm7hp5XO33MktFjSW+bHWUXtYEzZz0A3xkY9CyYOoeUk3ZH/v5HEuS+UvORzX0g7Hfy3uYYYRwHtqBQN0IX8rLdFMtIrRej/QCAdB2jajBoMA4GA1UdDwEB/wQEAwIHgDAdBgNVHQ4EFgQUqPxk9ijeLuY7c09UjFLE4ZzdU6UwHwYDVR0jBBgwFoAUOyBGWV61Mk1HMM5uY+5zdEfyBH0wFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwCgYIKoZIzj0EAwMDZwAwZAIwRK9VLoYa0Xff4nX1N/AQ1YleNG/iLT8dAXAtRKRfpN9XuDScbxWeo0cku8SkC06NAjBQPe7LBNeitA/UOBtXT2sX1h6f4ISqz+ISmJ4lY+y3bzRJI5nk1r53I9WT3/xIWToxggHbMIIB1wIBATBRMDkxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEgMB4GA1UEAxMXc2lnc3RvcmUtdHNhLXNlbGZzaWduZWQCFAo1oQZh1eJBc8aJlqfyffJ+A3ynMAsGCWCGSAFlAwQCAaCB/DAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwHAYJKoZIhvcNAQkFMQ8XDTI1MDUxMjE5MDAyN1owLwYJKoZIhvcNAQkEMSIEIB+SgwjYmkSbLhZNvWnGj/KrNAOr+sqpO38OpoIYSOSZMIGOBgsqhkiG9w0BCRACLzF/MH0wezB5BCAG9P/gR/6zWZm3M7DXoyNQHPwY5MAzZqhF13U250snRDBVMD2kOzA5MRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxIDAeBgNVBAMTF3NpZ3N0b3JlLXRzYS1zZWxmc2lnbmVkAhQKNaEGYdXiQXPGiZan8n3yfgN8pzAKBggqhkjOPQQDAgRnMGUCMFMwn1mx1D3q+vKwf57UDA96286zoTJ+ITJG5IQVypKLqnKSEX8Gm7GIRDXR06PJPgIxANj1zJ+cVXxoYuH4H8yobeqVeztGLZNd+YqbkyuvTkcX46CTCH0e6imE+Z4yTCRiYw=="}]}}, "messageSignature": {"messageDigest": {"algorithm": "SHA2_256", "digest": "gC3WD/iDM4AvJYXnMEO9IcNBKF4Zkv5bMXVeHK3q4w4="}, "signature": "MEQCIFhleMh29QEnPmW728dwXufGFMZ44o36ClxeqEeViLRtAiB23RDGzp+n1wh5cU1tp/BjjHstAB7lZf9KKJnzp3ubWA=="}} diff --git a/test/unit/verify/test_verifier.py b/test/unit/verify/test_verifier.py index 5b0fbe401..292a4d514 100644 --- a/test/unit/verify/test_verifier.py +++ b/test/unit/verify/test_verifier.py @@ -301,6 +301,24 @@ def test_verifier_no_authorities(self, asset, null_policy): null_policy, ) + def test_late_timestamp(self, caplog, verifier, asset, null_policy): + ''' + Ensures that verifying the signing certificate fails because the timestamp + is outside the certificate's validity window. The sample bundle + "tsa/bundle.txt.late_timestamp.sigstore" was generated by adding `time.sleep(12*60)` + into `sigstore.sign.Signer._finalize_sign()`, just after the entry is posted to Rekor + but before the timestamp is requested. + ''' + with pytest.raises(VerificationError, match="not enough timestamps"): + verifier.verify_artifact( + asset("tsa/bundle.txt").read_bytes(), + Bundle.from_json( + asset("tsa/bundle.txt.late_timestamp.sigstore").read_bytes()), + null_policy, + ) + + assert caplog.records[0].message == "Error while verifying certificates: Unable to verify certificate" + def test_verifier_not_enough_timestamp( self, verifier, asset, null_policy, monkeypatch ): From 0375ed218fb4bbfde5545669ac6abad1f930564c Mon Sep 17 00:00:00 2001 From: Ramon Petgrave Date: Mon, 12 May 2025 19:59:54 +0000 Subject: [PATCH 6/9] better documentation Signed-off-by: Ramon Petgrave --- CHANGELOG.md | 6 +++--- sigstore/verify/verifier.py | 5 ++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e0430ed65..1792169c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,9 +20,9 @@ All versions prior to 0.9.0 are untracked. * API: Make Rekor APIs compatible with Rekor v2 by removing trailing slashes from endpoints ([#1366](https://github.com/sigstore/sigstore-python/pull/1366)) -* Timestamps: Verify the signature date against the validity period of both the - Timestamp Authority or the Transperency Service, if either of such timestamps - are present in the Bundle. We still require at lease one of such timestamps. +* Verify: verify that all established times (timestamps or the log integration time) + are within the signing certificate validity period. At least one established time is + still required. [#1381](https://github.com/sigstore/sigstore-python/pull/1381) ### Changed diff --git a/sigstore/verify/verifier.py b/sigstore/verify/verifier.py index 24efa665b..471b5e1f4 100644 --- a/sigstore/verify/verifier.py +++ b/sigstore/verify/verifier.py @@ -372,9 +372,8 @@ def _verify_common_signing_cert( except VerificationError as exc: raise VerificationError(f"invalid log entry: {exc}") - # (6): verify the log entry was signed circa circa the signing certificate's - # validity period, via the verified timestamps. In the case of timestamps from - # the transparency service, the integrated time will have been used. See `_establish_time()`. + # (6): verify our established times (timestamps or the log integration time) are + # within signing certificate validity period. for vts in verified_timestamps: if not ( bundle.signing_certificate.not_valid_before_utc From 6d9d7a81edef98be4799d3a287bb09a1aecdb763 Mon Sep 17 00:00:00 2001 From: Ramon Petgrave Date: Mon, 12 May 2025 20:00:49 +0000 Subject: [PATCH 7/9] lint Signed-off-by: Ramon Petgrave --- test/unit/verify/test_verifier.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/test/unit/verify/test_verifier.py b/test/unit/verify/test_verifier.py index 292a4d514..123bd00b5 100644 --- a/test/unit/verify/test_verifier.py +++ b/test/unit/verify/test_verifier.py @@ -302,22 +302,26 @@ def test_verifier_no_authorities(self, asset, null_policy): ) def test_late_timestamp(self, caplog, verifier, asset, null_policy): - ''' + """ Ensures that verifying the signing certificate fails because the timestamp is outside the certificate's validity window. The sample bundle "tsa/bundle.txt.late_timestamp.sigstore" was generated by adding `time.sleep(12*60)` into `sigstore.sign.Signer._finalize_sign()`, just after the entry is posted to Rekor but before the timestamp is requested. - ''' + """ with pytest.raises(VerificationError, match="not enough timestamps"): verifier.verify_artifact( asset("tsa/bundle.txt").read_bytes(), Bundle.from_json( - asset("tsa/bundle.txt.late_timestamp.sigstore").read_bytes()), + asset("tsa/bundle.txt.late_timestamp.sigstore").read_bytes() + ), null_policy, ) - assert caplog.records[0].message == "Error while verifying certificates: Unable to verify certificate" + assert ( + caplog.records[0].message + == "Error while verifying certificates: Unable to verify certificate" + ) def test_verifier_not_enough_timestamp( self, verifier, asset, null_policy, monkeypatch From f47ef161fa52b4dcd4c281fc6e5df8c866ad5bb0 Mon Sep 17 00:00:00 2001 From: Ramon Petgrave Date: Wed, 14 May 2025 16:16:06 +0000 Subject: [PATCH 8/9] test with missing inclusion promise Signed-off-by: Ramon Petgrave --- test/unit/verify/test_verifier.py | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/test/unit/verify/test_verifier.py b/test/unit/verify/test_verifier.py index 123bd00b5..5ab336c97 100644 --- a/test/unit/verify/test_verifier.py +++ b/test/unit/verify/test_verifier.py @@ -24,7 +24,7 @@ from sigstore._internal.trust import CertificateAuthority from sigstore.dsse import StatementBuilder, Subject from sigstore.errors import VerificationError -from sigstore.models import Bundle +from sigstore.models import Bundle, LogEntry from sigstore.verify import policy from sigstore.verify.verifier import Verifier @@ -222,6 +222,35 @@ def test_verifier_no_validity_end(self, verifier, asset, null_policy): null_policy, ) + @pytest.mark.parametrize( + "fields_to_delete", + ( + [], + ["inclusionPromise"], + # integratedTime is required to verify the inclusionPromise. + pytest.param(["integratedTime"], marks=pytest.mark.xfail), + ["inclusionPromise", "integratedTime"], + ), + ) + def test_vierifier_verify_no_inclusion_promise_and_integrated_time( + self, verifier, asset, null_policy, fields_to_delete + ): + """ + Ensure that we can still verify a Bundle with a rfc3161 timestamp if the SET can't be verified or isn't present. + There is one exception: When inclusionPromise is present, but integratedTime is not, then we expect a failure + because the integratedTime is required to verify the inclusionPromise. + """ + bundle = Bundle.from_json(asset("tsa/bundle.txt.sigstore").read_bytes()) + _dict = bundle.log_entry._to_rekor().to_dict() + for field in fields_to_delete: + del _dict[field] + bundle._log_entry = LogEntry._from_dict_rekor(_dict) + verifier.verify_artifact( + asset("tsa/bundle.txt").read_bytes(), + bundle, + null_policy, + ) + def test_verifier_without_timestamp( self, verifier, asset, null_policy, monkeypatch ): From c0c44dba805593be07cfd37cb0b26a87edd84db7 Mon Sep 17 00:00:00 2001 From: Ramon Petgrave Date: Thu, 15 May 2025 16:09:14 +0000 Subject: [PATCH 9/9] edit the fields before instantiating the bundle Signed-off-by: Ramon Petgrave --- test/unit/verify/test_verifier.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/test/unit/verify/test_verifier.py b/test/unit/verify/test_verifier.py index 5ab336c97..057b35e50 100644 --- a/test/unit/verify/test_verifier.py +++ b/test/unit/verify/test_verifier.py @@ -14,6 +14,7 @@ import hashlib +import json import logging from datetime import datetime, timezone @@ -24,7 +25,7 @@ from sigstore._internal.trust import CertificateAuthority from sigstore.dsse import StatementBuilder, Subject from sigstore.errors import VerificationError -from sigstore.models import Bundle, LogEntry +from sigstore.models import Bundle from sigstore.verify import policy from sigstore.verify.verifier import Verifier @@ -240,11 +241,12 @@ def test_vierifier_verify_no_inclusion_promise_and_integrated_time( There is one exception: When inclusionPromise is present, but integratedTime is not, then we expect a failure because the integratedTime is required to verify the inclusionPromise. """ - bundle = Bundle.from_json(asset("tsa/bundle.txt.sigstore").read_bytes()) - _dict = bundle.log_entry._to_rekor().to_dict() + bundle_dict = json.loads(asset("tsa/bundle.txt.sigstore").read_bytes()) + (entry_dict,) = bundle_dict["verificationMaterial"]["tlogEntries"] for field in fields_to_delete: - del _dict[field] - bundle._log_entry = LogEntry._from_dict_rekor(_dict) + del entry_dict[field] + # Bundle.from_json() also validates the bundle's layout. + bundle = Bundle.from_json(json.dumps(bundle_dict)) verifier.verify_artifact( asset("tsa/bundle.txt").read_bytes(), bundle,