Skip to content

Verify artifact signing time against all timestamps #1381

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
May 16, 2025
Merged
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,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))

* 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)

* CI: Timestamp Authority tests use latest release, not latest tag, of
[sigstore/timestamp-authority](https://github.com/sigstore/timestamp-authority)
[#1377](https://github.com/sigstore/sigstore-python/pull/1377)
Expand Down
22 changes: 11 additions & 11 deletions sigstore/verify/verifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -372,17 +372,17 @@ 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 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
<= 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
Expand Down
1 change: 1 addition & 0 deletions test/assets/tsa/bundle.txt.late_timestamp.sigstore
Original file line number Diff line number Diff line change
@@ -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=="}}
5 changes: 3 additions & 2 deletions test/unit/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
53 changes: 53 additions & 0 deletions test/unit/verify/test_verifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@


import hashlib
import json
import logging
from datetime import datetime, timezone

Expand Down Expand Up @@ -222,6 +223,36 @@ 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_dict = json.loads(asset("tsa/bundle.txt.sigstore").read_bytes())
(entry_dict,) = bundle_dict["verificationMaterial"]["tlogEntries"]
for field in fields_to_delete:
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,
null_policy,
)

def test_verifier_without_timestamp(
self, verifier, asset, null_policy, monkeypatch
):
Expand Down Expand Up @@ -301,6 +332,28 @@ 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
):
Expand Down