sigstore.verify
API for verifying artifact signatures.
Example:
import base64
from pathlib import Path
from sigstore.models import Bundle
from sigstore.verify import Verifier
from sigstore.verify.policy import Identity
# The input to verify
input_ = Path("foo.txt").read_bytes()
# The bundle to verify with
bundle = Bundle.from_json(Path("foo.txt.sigstore.json").read_bytes())
verifier = Verifier.production()
result = verifier.verify(
input_,
bundle,
Identity(
identity="foo@bar.com",
issuer="https://accounts.google.com",
),
)
print(result)
1# Copyright 2022 The Sigstore Authors 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15""" 16API for verifying artifact signatures. 17 18Example: 19```python 20import base64 21from pathlib import Path 22 23from sigstore.models import Bundle 24from sigstore.verify import Verifier 25from sigstore.verify.policy import Identity 26 27# The input to verify 28input_ = Path("foo.txt").read_bytes() 29 30# The bundle to verify with 31bundle = Bundle.from_json(Path("foo.txt.sigstore.json").read_bytes()) 32 33verifier = Verifier.production() 34result = verifier.verify( 35 input_, 36 bundle, 37 Identity( 38 identity="foo@bar.com", 39 issuer="https://accounts.google.com", 40 ), 41) 42print(result) 43``` 44""" 45 46from sigstore.verify.verifier import Verifier 47 48__all__ = [ 49 "Verifier", 50 "policy", 51 "verifier", 52]
58class Verifier: 59 """ 60 The primary API for verification operations. 61 """ 62 63 def __init__(self, *, rekor: RekorClient, trusted_root: TrustedRoot): 64 """ 65 Create a new `Verifier`. 66 67 `rekor` is a `RekorClient` capable of connecting to a Rekor instance 68 containing logs for the file(s) being verified. 69 70 `fulcio_certificate_chain` is a list of PEM-encoded X.509 certificates, 71 establishing the trust chain for the signing certificate and signature. 72 """ 73 self._rekor = rekor 74 self._fulcio_certificate_chain: List[X509] = [ 75 X509.from_cryptography(parent_cert) 76 for parent_cert in trusted_root.get_fulcio_certs() 77 ] 78 self._trusted_root = trusted_root 79 80 @classmethod 81 def production(cls) -> Verifier: 82 """ 83 Return a `Verifier` instance configured against Sigstore's production-level services. 84 """ 85 return cls( 86 rekor=RekorClient.production(), 87 trusted_root=TrustedRoot.production(), 88 ) 89 90 @classmethod 91 def staging(cls) -> Verifier: 92 """ 93 Return a `Verifier` instance configured against Sigstore's staging-level services. 94 """ 95 return cls( 96 rekor=RekorClient.staging(), 97 trusted_root=TrustedRoot.staging(), 98 ) 99 100 @classmethod 101 def _from_trust_config(cls, trust_config: ClientTrustConfig) -> Verifier: 102 """ 103 Create a `Verifier` from the given `ClientTrustConfig`. 104 105 @api private 106 """ 107 return cls( 108 rekor=RekorClient(trust_config._inner.signing_config.tlog_urls[0]), 109 trusted_root=trust_config.trusted_root, 110 ) 111 112 def _verify_common_signing_cert( 113 self, bundle: Bundle, policy: VerificationPolicy 114 ) -> None: 115 """ 116 Performs the signing certificate verification steps that are shared between 117 `verify_dsse` and `verify_artifact`. 118 119 Raises `VerificationError` on all failures. 120 """ 121 122 # In order to verify an artifact, we need to achieve the following: 123 # 124 # 1. Verify that the signing certificate chains to the root of trust 125 # and is valid at the time of signing. 126 # 2. Verify the signing certificate's SCT. 127 # 3. Verify that the signing certificate conforms to the Sigstore 128 # X.509 profile as well as the passed-in `VerificationPolicy`. 129 # 4. Verify the inclusion proof and signed checkpoint for the log 130 # entry. 131 # 5. Verify the inclusion promise for the log entry, if present. 132 # 6. Verify the timely insertion of the log entry against the validity 133 # period for the signing certificate. 134 # 7. Verify the signature and input against the signing certificate's 135 # public key. 136 # 8. Verify the transparency log entry's consistency against the other 137 # materials, to prevent variants of CVE-2022-36056. 138 # 139 # This method performs steps (1) through (6) above. Its caller 140 # MUST perform steps (7) and (8) separately, since they vary based on 141 # the kind of verification being performed (i.e. hashedrekord, DSSE, etc.) 142 143 cert = bundle.signing_certificate 144 145 # NOTE: The `X509Store` object currently cannot have its time reset once the `set_time` 146 # method been called on it. To get around this, we construct a new one for every `verify` 147 # call. 148 store = X509Store() 149 # NOTE: By explicitly setting the flags here, we ensure that OpenSSL's 150 # PARTIAL_CHAIN default does not change on us. Enabling PARTIAL_CHAIN 151 # would be strictly more conformant of OpenSSL, but we currently 152 # *want* the "long" chain behavior of performing path validation 153 # down to a self-signed root. 154 store.set_flags(X509StoreFlags.X509_STRICT) 155 for parent_cert_ossl in self._fulcio_certificate_chain: 156 store.add_cert(parent_cert_ossl) 157 158 # (1): verify that the signing certificate is signed by the root 159 # certificate and that the signing certificate was valid at the 160 # time of signing. 161 sign_date = cert.not_valid_before_utc 162 cert_ossl = X509.from_cryptography(cert) 163 164 store.set_time(sign_date) 165 store_ctx = X509StoreContext(store, cert_ossl) 166 try: 167 # get_verified_chain returns the full chain including the end-entity certificate 168 # and chain should contain only CA certificates 169 chain = store_ctx.get_verified_chain()[1:] 170 except X509StoreContextError as e: 171 raise VerificationError(f"failed to build chain: {e}") 172 173 # (2): verify the signing certificate's SCT. 174 sct = _get_precertificate_signed_certificate_timestamps(cert)[0] 175 try: 176 verify_sct( 177 sct, 178 cert, 179 [parent_cert.to_cryptography() for parent_cert in chain], 180 self._trusted_root.ct_keyring(KeyringPurpose.VERIFY), 181 ) 182 except VerificationError as e: 183 raise VerificationError(f"failed to verify SCT on signing certificate: {e}") 184 185 # (3): verify the signing certificate against the Sigstore 186 # X.509 profile and verify against the given `VerificationPolicy`. 187 usage_ext = cert.extensions.get_extension_for_class(KeyUsage) 188 if not usage_ext.value.digital_signature: 189 raise VerificationError("Key usage is not of type `digital signature`") 190 191 extended_usage_ext = cert.extensions.get_extension_for_class(ExtendedKeyUsage) 192 if ExtendedKeyUsageOID.CODE_SIGNING not in extended_usage_ext.value: 193 raise VerificationError("Extended usage does not contain `code signing`") 194 195 policy.verify(cert) 196 197 _logger.debug("Successfully verified signing certificate validity...") 198 199 # (4): verify the inclusion proof and signed checkpoint for the 200 # log entry. 201 # (5): verify the inclusion promise for the log entry, if present. 202 entry = bundle.log_entry 203 try: 204 entry._verify(self._trusted_root.rekor_keyring(KeyringPurpose.VERIFY)) 205 except VerificationError as exc: 206 raise VerificationError(f"invalid log entry: {exc}") 207 208 # (6): verify that log entry was integrated circa the signing certificate's 209 # validity period. 210 integrated_time = datetime.fromtimestamp(entry.integrated_time, tz=timezone.utc) 211 if not ( 212 bundle.signing_certificate.not_valid_before_utc 213 <= integrated_time 214 <= bundle.signing_certificate.not_valid_after_utc 215 ): 216 raise VerificationError( 217 "invalid signing cert: expired at time of Rekor entry" 218 ) 219 220 def verify_dsse( 221 self, bundle: Bundle, policy: VerificationPolicy 222 ) -> tuple[str, bytes]: 223 """ 224 Verifies an bundle's DSSE envelope, returning the encapsulated payload 225 and its content type. 226 227 This method is only for DSSE-enveloped payloads. To verify 228 an arbitrary input against a bundle, use the `verify_artifact` 229 method. 230 231 `bundle` is the Sigstore `Bundle` to both verify and verify against. 232 233 `policy` is the `VerificationPolicy` to verify against. 234 235 Returns a tuple of `(type, payload)`, where `type` is the payload's 236 type as encoded in the DSSE envelope and `payload` is the raw `bytes` 237 of the payload. No validation of either `type` or `payload` is 238 performed; users of this API **must** assert that `type` is known 239 to them before proceeding to handle `payload` in an application-dependent 240 manner. 241 """ 242 243 # (1) through (6) are performed by `_verify_common_signing_cert`. 244 self._verify_common_signing_cert(bundle, policy) 245 246 # (7): verify the bundle's signature and DSSE envelope against the 247 # signing certificate's public key. 248 envelope = bundle._dsse_envelope 249 if envelope is None: 250 raise VerificationError( 251 "cannot perform DSSE verification on a bundle without a DSSE envelope" 252 ) 253 254 signing_key = bundle.signing_certificate.public_key() 255 signing_key = cast(ec.EllipticCurvePublicKey, signing_key) 256 dsse._verify(signing_key, envelope) 257 258 # (8): verify the consistency of the log entry's body against 259 # the other bundle materials. 260 # NOTE: This is very slightly weaker than the consistency check 261 # for hashedrekord entries, due to how inclusion is recorded for DSSE: 262 # the included entry for DSSE includes an envelope hash that we 263 # *cannot* verify, since the envelope is uncanonicalized JSON. 264 # Instead, we manually pick apart the entry body below and verify 265 # the parts we can (namely the payload hash and signature list). 266 entry = bundle.log_entry 267 try: 268 entry_body = rekor_types.Dsse.model_validate_json( 269 base64.b64decode(entry.body) 270 ) 271 except ValidationError as exc: 272 raise VerificationError(f"invalid DSSE log entry: {exc}") 273 274 payload_hash = sha256_digest(envelope._inner.payload).digest.hex() 275 if ( 276 entry_body.spec.root.payload_hash.algorithm # type: ignore[union-attr] 277 != rekor_types.dsse.Algorithm.SHA256 278 ): 279 raise VerificationError("expected SHA256 payload hash in DSSE log entry") 280 if payload_hash != entry_body.spec.root.payload_hash.value: # type: ignore[union-attr] 281 raise VerificationError("log entry payload hash does not match bundle") 282 283 # NOTE: Like `dsse._verify`: multiple signatures would be frivolous here, 284 # but we handle them just in case the signer has somehow produced multiple 285 # signatures for their envelope with the same signing key. 286 signatures = [ 287 rekor_types.dsse.Signature( 288 signature=base64.b64encode(signature.sig).decode(), 289 verifier=base64_encode_pem_cert(bundle.signing_certificate), 290 ) 291 for signature in envelope._inner.signatures 292 ] 293 if signatures != entry_body.spec.root.signatures: 294 raise VerificationError("log entry signatures do not match bundle") 295 296 return (envelope._inner.payload_type, envelope._inner.payload) 297 298 def verify_artifact( 299 self, 300 input_: bytes | Hashed, 301 bundle: Bundle, 302 policy: VerificationPolicy, 303 ) -> None: 304 """ 305 Public API for verifying. 306 307 `input_` is the input to verify, either as a buffer of contents or as 308 a prehashed `Hashed` object. 309 310 `bundle` is the Sigstore `Bundle` to verify against. 311 312 `policy` is the `VerificationPolicy` to verify against. 313 314 On failure, this method raises `VerificationError`. 315 """ 316 317 # (1) through (6) are performed by `_verify_common_signing_cert`. 318 self._verify_common_signing_cert(bundle, policy) 319 320 hashed_input = sha256_digest(input_) 321 322 # (7): verify that the signature was signed by the public key in the signing certificate. 323 try: 324 signing_key = bundle.signing_certificate.public_key() 325 signing_key = cast(ec.EllipticCurvePublicKey, signing_key) 326 signing_key.verify( 327 bundle._inner.message_signature.signature, 328 hashed_input.digest, 329 ec.ECDSA(hashed_input._as_prehashed()), 330 ) 331 except InvalidSignature: 332 raise VerificationError("Signature is invalid for input") 333 334 _logger.debug("Successfully verified signature...") 335 336 # (8): verify the consistency of the log entry's body against 337 # the other bundle materials (and input being verified). 338 entry = bundle.log_entry 339 340 expected_body = _hashedrekord_from_parts( 341 bundle.signing_certificate, 342 bundle._inner.message_signature.signature, 343 hashed_input, 344 ) 345 actual_body = rekor_types.Hashedrekord.model_validate_json( 346 base64.b64decode(entry.body) 347 ) 348 if expected_body != actual_body: 349 raise VerificationError( 350 "transparency log entry is inconsistent with other materials" 351 )
The primary API for verification operations.
63 def __init__(self, *, rekor: RekorClient, trusted_root: TrustedRoot): 64 """ 65 Create a new `Verifier`. 66 67 `rekor` is a `RekorClient` capable of connecting to a Rekor instance 68 containing logs for the file(s) being verified. 69 70 `fulcio_certificate_chain` is a list of PEM-encoded X.509 certificates, 71 establishing the trust chain for the signing certificate and signature. 72 """ 73 self._rekor = rekor 74 self._fulcio_certificate_chain: List[X509] = [ 75 X509.from_cryptography(parent_cert) 76 for parent_cert in trusted_root.get_fulcio_certs() 77 ] 78 self._trusted_root = trusted_root
Create a new Verifier
.
rekor
is a RekorClient
capable of connecting to a Rekor instance
containing logs for the file(s) being verified.
fulcio_certificate_chain
is a list of PEM-encoded X.509 certificates,
establishing the trust chain for the signing certificate and signature.
80 @classmethod 81 def production(cls) -> Verifier: 82 """ 83 Return a `Verifier` instance configured against Sigstore's production-level services. 84 """ 85 return cls( 86 rekor=RekorClient.production(), 87 trusted_root=TrustedRoot.production(), 88 )
Return a Verifier
instance configured against Sigstore's production-level services.
90 @classmethod 91 def staging(cls) -> Verifier: 92 """ 93 Return a `Verifier` instance configured against Sigstore's staging-level services. 94 """ 95 return cls( 96 rekor=RekorClient.staging(), 97 trusted_root=TrustedRoot.staging(), 98 )
Return a Verifier
instance configured against Sigstore's staging-level services.
220 def verify_dsse( 221 self, bundle: Bundle, policy: VerificationPolicy 222 ) -> tuple[str, bytes]: 223 """ 224 Verifies an bundle's DSSE envelope, returning the encapsulated payload 225 and its content type. 226 227 This method is only for DSSE-enveloped payloads. To verify 228 an arbitrary input against a bundle, use the `verify_artifact` 229 method. 230 231 `bundle` is the Sigstore `Bundle` to both verify and verify against. 232 233 `policy` is the `VerificationPolicy` to verify against. 234 235 Returns a tuple of `(type, payload)`, where `type` is the payload's 236 type as encoded in the DSSE envelope and `payload` is the raw `bytes` 237 of the payload. No validation of either `type` or `payload` is 238 performed; users of this API **must** assert that `type` is known 239 to them before proceeding to handle `payload` in an application-dependent 240 manner. 241 """ 242 243 # (1) through (6) are performed by `_verify_common_signing_cert`. 244 self._verify_common_signing_cert(bundle, policy) 245 246 # (7): verify the bundle's signature and DSSE envelope against the 247 # signing certificate's public key. 248 envelope = bundle._dsse_envelope 249 if envelope is None: 250 raise VerificationError( 251 "cannot perform DSSE verification on a bundle without a DSSE envelope" 252 ) 253 254 signing_key = bundle.signing_certificate.public_key() 255 signing_key = cast(ec.EllipticCurvePublicKey, signing_key) 256 dsse._verify(signing_key, envelope) 257 258 # (8): verify the consistency of the log entry's body against 259 # the other bundle materials. 260 # NOTE: This is very slightly weaker than the consistency check 261 # for hashedrekord entries, due to how inclusion is recorded for DSSE: 262 # the included entry for DSSE includes an envelope hash that we 263 # *cannot* verify, since the envelope is uncanonicalized JSON. 264 # Instead, we manually pick apart the entry body below and verify 265 # the parts we can (namely the payload hash and signature list). 266 entry = bundle.log_entry 267 try: 268 entry_body = rekor_types.Dsse.model_validate_json( 269 base64.b64decode(entry.body) 270 ) 271 except ValidationError as exc: 272 raise VerificationError(f"invalid DSSE log entry: {exc}") 273 274 payload_hash = sha256_digest(envelope._inner.payload).digest.hex() 275 if ( 276 entry_body.spec.root.payload_hash.algorithm # type: ignore[union-attr] 277 != rekor_types.dsse.Algorithm.SHA256 278 ): 279 raise VerificationError("expected SHA256 payload hash in DSSE log entry") 280 if payload_hash != entry_body.spec.root.payload_hash.value: # type: ignore[union-attr] 281 raise VerificationError("log entry payload hash does not match bundle") 282 283 # NOTE: Like `dsse._verify`: multiple signatures would be frivolous here, 284 # but we handle them just in case the signer has somehow produced multiple 285 # signatures for their envelope with the same signing key. 286 signatures = [ 287 rekor_types.dsse.Signature( 288 signature=base64.b64encode(signature.sig).decode(), 289 verifier=base64_encode_pem_cert(bundle.signing_certificate), 290 ) 291 for signature in envelope._inner.signatures 292 ] 293 if signatures != entry_body.spec.root.signatures: 294 raise VerificationError("log entry signatures do not match bundle") 295 296 return (envelope._inner.payload_type, envelope._inner.payload)
Verifies an bundle's DSSE envelope, returning the encapsulated payload and its content type.
This method is only for DSSE-enveloped payloads. To verify
an arbitrary input against a bundle, use the verify_artifact
method.
bundle
is the Sigstore Bundle
to both verify and verify against.
policy
is the VerificationPolicy
to verify against.
Returns a tuple of (type, payload)
, where type
is the payload's
type as encoded in the DSSE envelope and payload
is the raw bytes
of the payload. No validation of either type
or payload
is
performed; users of this API must assert that type
is known
to them before proceeding to handle payload
in an application-dependent
manner.
298 def verify_artifact( 299 self, 300 input_: bytes | Hashed, 301 bundle: Bundle, 302 policy: VerificationPolicy, 303 ) -> None: 304 """ 305 Public API for verifying. 306 307 `input_` is the input to verify, either as a buffer of contents or as 308 a prehashed `Hashed` object. 309 310 `bundle` is the Sigstore `Bundle` to verify against. 311 312 `policy` is the `VerificationPolicy` to verify against. 313 314 On failure, this method raises `VerificationError`. 315 """ 316 317 # (1) through (6) are performed by `_verify_common_signing_cert`. 318 self._verify_common_signing_cert(bundle, policy) 319 320 hashed_input = sha256_digest(input_) 321 322 # (7): verify that the signature was signed by the public key in the signing certificate. 323 try: 324 signing_key = bundle.signing_certificate.public_key() 325 signing_key = cast(ec.EllipticCurvePublicKey, signing_key) 326 signing_key.verify( 327 bundle._inner.message_signature.signature, 328 hashed_input.digest, 329 ec.ECDSA(hashed_input._as_prehashed()), 330 ) 331 except InvalidSignature: 332 raise VerificationError("Signature is invalid for input") 333 334 _logger.debug("Successfully verified signature...") 335 336 # (8): verify the consistency of the log entry's body against 337 # the other bundle materials (and input being verified). 338 entry = bundle.log_entry 339 340 expected_body = _hashedrekord_from_parts( 341 bundle.signing_certificate, 342 bundle._inner.message_signature.signature, 343 hashed_input, 344 ) 345 actual_body = rekor_types.Hashedrekord.model_validate_json( 346 base64.b64decode(entry.body) 347 ) 348 if expected_body != actual_body: 349 raise VerificationError( 350 "transparency log entry is inconsistent with other materials" 351 )
Public API for verifying.
input_
is the input to verify, either as a buffer of contents or as
a prehashed Hashed
object.
bundle
is the Sigstore Bundle
to verify against.
policy
is the VerificationPolicy
to verify against.
On failure, this method raises VerificationError
.