diff --git a/sigstore/_cli.py b/sigstore/_cli.py index cf10b8d56..4f4f1398e 100644 --- a/sigstore/_cli.py +++ b/sigstore/_cli.py @@ -22,6 +22,7 @@ from typing import Optional, TextIO, Union, cast from sigstore import __version__ +from sigstore._internal.ctfe import CTKeyring from sigstore._internal.fulcio.client import DEFAULT_FULCIO_URL, FulcioClient from sigstore._internal.oidc.ambient import ( GitHubOidcPermissionCredentialError, @@ -35,6 +36,7 @@ ) from sigstore._internal.rekor.client import DEFAULT_REKOR_URL, RekorClient from sigstore._sign import Signer +from sigstore._utils import load_pem_public_key from sigstore._verify import ( CertificateVerificationFailure, RekorEntryMissing, @@ -357,10 +359,11 @@ def _sign(args: argparse.Namespace) -> None: elif args.fulcio_url == DEFAULT_FULCIO_URL and args.rekor_url == DEFAULT_REKOR_URL: signer = Signer.production() else: + ct_keyring = CTKeyring([load_pem_public_key(args.ctfe_pem.read())]) signer = Signer( fulcio=FulcioClient(args.fulcio_url), rekor=RekorClient( - args.rekor_url, args.rekor_root_pubkey.read(), args.ctfe_pem.read() + args.rekor_url, args.rekor_root_pubkey.read(), ct_keyring ), ) diff --git a/sigstore/_internal/ctfe.py b/sigstore/_internal/ctfe.py new file mode 100644 index 000000000..bc58101c4 --- /dev/null +++ b/sigstore/_internal/ctfe.py @@ -0,0 +1,127 @@ +# Copyright 2022 The Sigstore Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Functionality for interacting with CT ("CTFE") signing keys. +""" + +from __future__ import annotations + +from importlib import resources +from typing import List + +import cryptography.hazmat.primitives.asymmetric.padding as padding +from cryptography.exceptions import InvalidSignature +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import ec, rsa + +from sigstore._utils import PublicKey, key_id, load_pem_public_key + + +class CTKeyringError(Exception): + """ + Raised on failure by `CTKeyring.verify()`. + """ + + pass + + +class CTKeyringLookupError(CTKeyringError): + """ + A specialization of `CTKeyringError`, indicating that the specified + key ID wasn't found in the keyring. + """ + + pass + + +class CTKeyring: + """ + Represents a set of CT signing keys, each of which is a potentially + valid signer for a Signed Certificate Timestamp (SCT). + + This structure exists to facilitate key rotation in a CT log. + """ + + def __init__(self, keys: List[PublicKey] = []): + self._keyring = {} + for key in keys: + self._keyring[key_id(key)] = key + + @classmethod + def staging(cls) -> CTKeyring: + """ + Returns a `CTKeyring` instance capable of verifying SCTs from + Sigstore's staging deployment. + """ + keyring = cls() + keyring._add_resource("ctfe.staging.pub") + keyring._add_resource("ctfe_2022.staging.pub") + keyring._add_resource("ctfe_2022.2.staging.pub") + + return keyring + + @classmethod + def production(cls) -> CTKeyring: + """ + Returns a `CTKeyring` instance capable of verifying SCTs from + Sigstore's production deployment. + """ + keyring = cls() + keyring._add_resource("ctfe.pub") + keyring._add_resource("ctfe_2022.pub") + + return keyring + + def _add_resource(self, name: str) -> None: + """ + Adds a key to the current keyring, as identified by its + resource name under `sigstore._store`. + """ + key_pem = resources.read_binary("sigstore._store", name) + self.add(key_pem) + + def add(self, key_pem: bytes) -> None: + """ + Adds a PEM-encoded key to the current keyring. + """ + key = load_pem_public_key(key_pem) + self._keyring[key_id(key)] = key + + def verify(self, *, key_id: bytes, signature: bytes, data: bytes) -> None: + key = self._keyring.get(key_id) + if key is None: + # If we don't have a key corresponding to this key ID, we can't + # possibly verify the signature. + raise CTKeyringLookupError(f"no known key for key ID {key_id.hex()}") + + try: + if isinstance(key, rsa.RSAPublicKey): + key.verify( + signature=signature, + data=data, + padding=padding.PKCS1v15(), + algorithm=hashes.SHA256(), + ) + elif isinstance(key, ec.EllipticCurvePublicKey): + key.verify( + signature=signature, + data=data, + signature_algorithm=ec.ECDSA(hashes.SHA256()), + ) + else: + # NOTE(ww): Unreachable without API misuse. + raise CTKeyringError(f"unsupported key type: {key}") + except InvalidSignature as exc: + raise CTKeyringError("invalid signature") from exc diff --git a/sigstore/_internal/rekor/client.py b/sigstore/_internal/rekor/client.py index f9e75bb2f..5669a93cc 100644 --- a/sigstore/_internal/rekor/client.py +++ b/sigstore/_internal/rekor/client.py @@ -27,9 +27,11 @@ import requests from cryptography.hazmat.primitives import serialization -from cryptography.hazmat.primitives.asymmetric import ec, rsa +from cryptography.hazmat.primitives.asymmetric import ec from pydantic import BaseModel, Field, StrictInt, StrictStr, validator +from sigstore._internal.ctfe import CTKeyring + logger = logging.getLogger(__name__) DEFAULT_REKOR_URL = "https://rekor.sigstore.dev" @@ -272,7 +274,7 @@ def post( class RekorClient: """The internal Rekor client""" - def __init__(self, url: str, pubkey: bytes, ctfe_pubkey: bytes) -> None: + def __init__(self, url: str, pubkey: bytes, ct_keyring: CTKeyring) -> None: self.url = urljoin(url, "api/v1/") self.session = requests.Session() self.session.headers.update( @@ -287,16 +289,7 @@ def __init__(self, url: str, pubkey: bytes, ctfe_pubkey: bytes) -> None: raise RekorClientError(f"Invalid public key type: {pubkey}") self._pubkey = pubkey - ctfe_pubkey = serialization.load_pem_public_key(ctfe_pubkey) - if not isinstance( - ctfe_pubkey, - ( - rsa.RSAPublicKey, - ec.EllipticCurvePublicKey, - ), - ): - raise RekorClientError(f"Invalid CTFE public key type: {ctfe_pubkey}") - self._ctfe_pubkey = ctfe_pubkey + self._ct_keyring = ct_keyring def __del__(self) -> None: self.session.close() @@ -304,14 +297,12 @@ def __del__(self) -> None: @classmethod def production(cls) -> RekorClient: return cls( - DEFAULT_REKOR_URL, _DEFAULT_REKOR_ROOT_PUBKEY, _DEFAULT_REKOR_CTFE_PUBKEY + DEFAULT_REKOR_URL, _DEFAULT_REKOR_ROOT_PUBKEY, CTKeyring.production() ) @classmethod def staging(cls) -> RekorClient: - return cls( - STAGING_REKOR_URL, _STAGING_REKOR_ROOT_PUBKEY, _STAGING_REKOR_CTFE_PUBKEY - ) + return cls(STAGING_REKOR_URL, _STAGING_REKOR_ROOT_PUBKEY, CTKeyring.staging()) @property def log(self) -> RekorLog: diff --git a/sigstore/_internal/sct.py b/sigstore/_internal/sct.py index ec50ef328..1c5fbfdca 100644 --- a/sigstore/_internal/sct.py +++ b/sigstore/_internal/sct.py @@ -16,29 +16,33 @@ Utilities for verifying signed certificate timestamps. """ -import hashlib import logging import struct from datetime import timezone -from typing import List, Optional, Union +from textwrap import dedent +from typing import List, Optional -import cryptography.hazmat.primitives.asymmetric.padding as padding -from cryptography.exceptions import InvalidSignature from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import ec, rsa from cryptography.x509 import Certificate, ExtendedKeyUsage from cryptography.x509.certificate_transparency import ( LogEntryType, - SignatureAlgorithm, SignedCertificateTimestamp, ) from cryptography.x509.oid import ExtendedKeyUsageOID +from sigstore._internal.ctfe import ( + CTKeyring, + CTKeyringError, + CTKeyringLookupError, +) +from sigstore._utils import key_id + logger = logging.getLogger(__name__) def _pack_signed_entry( - sct: SignedCertificateTimestamp, cert: Certificate, issuer_key_hash: Optional[bytes] + sct: SignedCertificateTimestamp, cert: Certificate, issuer_key_id: Optional[bytes] ) -> bytes: fields = [] if sct.entry_type == LogEntryType.X509_CERTIFICATE: @@ -48,18 +52,18 @@ def _pack_signed_entry( pack_format = "!BBB{cert_der_len}s" cert_der = cert.public_bytes(encoding=serialization.Encoding.DER) elif sct.entry_type == LogEntryType.PRE_CERTIFICATE: - if not issuer_key_hash or len(issuer_key_hash) != 32: - raise InvalidSctError("API misuse: issuer key hash missing") + if not issuer_key_id or len(issuer_key_id) != 32: + raise InvalidSctError("API misuse: issuer key ID missing") # When dealing with a precertificate, our signed entry looks like this: # - # [0]: issuer_key_hash[32] + # [0]: issuer_key_id[32] # [1]: opaque TBSCertificate<1..2^24-1> pack_format = "!32sBBB{cert_der_len}s" # Precertificates must have their SCT list extension filtered out. cert_der = cert.tbs_precertificate_bytes - fields.append(issuer_key_hash) + fields.append(issuer_key_id) else: raise InvalidSctError(f"unknown SCT log entry type: {sct.entry_type!r}") @@ -81,7 +85,7 @@ def _pack_signed_entry( def _pack_digitally_signed( sct: SignedCertificateTimestamp, cert: Certificate, - issuer_key_hash: Optional[bytes], + issuer_key_id: Optional[bytes], ) -> bytes: """ Packs the contents of `cert` (and some pieces of `sct`) into a structured @@ -99,7 +103,7 @@ def _pack_digitally_signed( # This constructs the "core" `signed_entry` field, which is either # the public bytes of the cert *or* the TBSPrecertificate (with some # filtering), depending on whether our SCT is for a precertificate. - signed_entry = _pack_signed_entry(sct, cert, issuer_key_hash) + signed_entry = _pack_signed_entry(sct, cert, issuer_key_id) # Assemble a format string with the certificate length baked in and then pack the digitally # signed data @@ -133,15 +137,6 @@ def _get_issuer_cert(chain: List[Certificate]) -> Certificate: return issuer -def _issuer_key_hash(cert: Certificate) -> bytes: - issuer_key: bytes = cert.public_key().public_bytes( - encoding=serialization.Encoding.DER, - format=serialization.PublicFormat.SubjectPublicKeyInfo, - ) - - return hashlib.sha256(issuer_key).digest() - - class InvalidSctError(Exception): pass @@ -150,19 +145,26 @@ def verify_sct( sct: SignedCertificateTimestamp, cert: Certificate, chain: List[Certificate], - ctfe_key: Union[rsa.RSAPublicKey, ec.EllipticCurvePublicKey], + ct_keyring: CTKeyring, ) -> None: """Verify a signed certificate timestamp""" - issuer_key_hash = None + issuer_key_id = None if sct.entry_type == LogEntryType.PRE_CERTIFICATE: # If we're verifying an SCT for a precertificate, we need to # find its issuer in the chain and calculate a hash over # its public key information, as part of the "binding" proof # that ties the issuer to the final certificate. - issuer_key_hash = _issuer_key_hash(_get_issuer_cert(chain)) + issuer_pubkey = _get_issuer_cert(chain).public_key() - digitally_signed = _pack_digitally_signed(sct, cert, issuer_key_hash) + if not isinstance(issuer_pubkey, (rsa.RSAPublicKey, ec.EllipticCurvePublicKey)): + raise InvalidSctError( + f"invalid issuer pubkey format (not ECDSA or RSA): {issuer_pubkey}" + ) + + issuer_key_id = key_id(issuer_pubkey) + + digitally_signed = _pack_digitally_signed(sct, cert, issuer_key_id) if not isinstance(sct.signature_hash_algorithm, hashes.SHA256): raise InvalidSctError( @@ -171,27 +173,37 @@ def verify_sct( ) try: - if sct.signature_algorithm == SignatureAlgorithm.RSA and isinstance( - ctfe_key, rsa.RSAPublicKey - ): - ctfe_key.verify( - signature=sct.signature, - data=digitally_signed, - padding=padding.PKCS1v15(), - algorithm=hashes.SHA256(), - ) - elif sct.signature_algorithm == SignatureAlgorithm.ECDSA and isinstance( - ctfe_key, ec.EllipticCurvePublicKey - ): - ctfe_key.verify( - signature=sct.signature, - data=digitally_signed, - signature_algorithm=ec.ECDSA(hashes.SHA256()), - ) - else: - raise InvalidSctError( - "Found unexpected signature type in SCT: signature type of" - f"{sct.signature_algorithm} and CTFE key type of {type(ctfe_key)}" - ) - except InvalidSignature as inval_sig: - raise InvalidSctError from inval_sig + logger.debug(f"attempting to verify SCT with key ID {sct.log_id.hex()}") + # NOTE(ww): In terms of the DER structure, the SCT's `LogID` contains a + # singular `opaque key_id[32]`. Cryptography's APIs don't bother + # to expose this trivial single member, so we use the `log_id` + # attribute directly. + ct_keyring.verify( + key_id=sct.log_id, signature=sct.signature, data=digitally_signed + ) + except CTKeyringLookupError as exc: + # We specialize this error case, since it usually indicates one of + # two conditions: either the current sigstore client is out-of-date, + # or that the SCT is well-formed but invalid for the current configuration + # (indicating that the user has asked for the wrong instance). + # + # TODO(ww): Longer term, this should be specialized elsewhere. + raise InvalidSctError( + dedent( + f""" + Invalid key ID in SCT: not found in current keyring. + + This may be a result of an outdated `sigstore` installation. + + Consider upgrading with: + + python -m pip install --upgrade sigstore + + Additional context: + + {exc} + """ + ), + ) + except CTKeyringError as exc: + raise InvalidSctError from exc diff --git a/sigstore/_sign.py b/sigstore/_sign.py index da822477b..696db1183 100644 --- a/sigstore/_sign.py +++ b/sigstore/_sign.py @@ -97,7 +97,7 @@ def sign( cert = certificate_response.cert # noqa chain = certificate_response.chain - verify_sct(sct, cert, chain, self._rekor._ctfe_pubkey) + verify_sct(sct, cert, chain, self._rekor._ct_keyring) logger.debug("Successfully verified SCT...") diff --git a/sigstore/_store/ctfe_2022.pub b/sigstore/_store/ctfe_2022.pub new file mode 100644 index 000000000..32fa2ad10 --- /dev/null +++ b/sigstore/_store/ctfe_2022.pub @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEiPSlFi0CmFTfEjCUqF9HuCEcYXNK +AaYalIJmBZ8yyezPjTqhxrKBpMnaocVtLJBI1eM3uXnQzQGAJdJ4gs9Fyw== +-----END PUBLIC KEY----- diff --git a/sigstore/_utils.py b/sigstore/_utils.py new file mode 100644 index 000000000..a407eaaea --- /dev/null +++ b/sigstore/_utils.py @@ -0,0 +1,61 @@ +# Copyright 2022 The Sigstore Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Shared utilities. +""" + +import hashlib +from typing import Union + +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import ec, rsa + +PublicKey = Union[rsa.RSAPublicKey, ec.EllipticCurvePublicKey] + + +class InvalidKey(Exception): + pass + + +def load_pem_public_key(key_pem: bytes) -> PublicKey: + """ + A specialization of `cryptography`'s `serialization.load_pem_public_key` + with a uniform exception type (`InvalidKey`) and additional restrictions + on key validity (only RSA and ECDSA keys are valid). + """ + + try: + key = serialization.load_pem_public_key(key_pem) + except Exception as exc: + raise InvalidKey("could not load PEM-formatted public key") from exc + + if not isinstance(key, (rsa.RSAPublicKey, ec.EllipticCurvePublicKey)): + raise InvalidKey(f"invalid key format (not ECDSA or RSA): {key}") + + return key + + +def key_id(key: PublicKey) -> bytes: + """ + Returns an RFC 6962-style "key ID" for the given public key. + + See: + """ + public_bytes = key.public_bytes( + encoding=serialization.Encoding.DER, + format=serialization.PublicFormat.SubjectPublicKeyInfo, + ) + + return hashlib.sha256(public_bytes).digest() diff --git a/test/internal/test_ctfe.py b/test/internal/test_ctfe.py new file mode 100644 index 000000000..7ec171586 --- /dev/null +++ b/test/internal/test_ctfe.py @@ -0,0 +1,41 @@ +# Copyright 2022 The Sigstore Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from sigstore._internal.ctfe import CTKeyring + + +class TestCTKeyring: + def test_keyring_cardinalities(self): + production = CTKeyring.production() + staging = CTKeyring.staging() + + assert len(production._keyring) == 2 + assert len(staging._keyring) == 3 + + def test_production_staging_both_initialize(self): + keyrings = [CTKeyring.production(), CTKeyring.staging()] + for keyring in keyrings: + assert keyring is not None + + def test_production_staging_keyrings_are_disjoint(self): + production = CTKeyring.production() + staging = CTKeyring.staging() + + production_key_ids = production._keyring.keys() + staging_key_ids = staging._keyring.keys() + + # The key IDs (and therefore keys) in the production and staging instances + # should never overlap. Overlapping would imply loading keys intended + # for the wrong instance. + assert production_key_ids.isdisjoint(staging_key_ids) diff --git a/test/internal/test_sct.py b/test/internal/test_sct.py index 9d1442cff..6fd2b0b64 100644 --- a/test/internal/test_sct.py +++ b/test/internal/test_sct.py @@ -13,13 +13,10 @@ # limitations under the License. import datetime -import hashlib import struct import pretend import pytest -from cryptography import x509 -from cryptography.hazmat.primitives import serialization from cryptography.x509.certificate_transparency import LogEntryType from sigstore._internal import sct @@ -62,39 +59,3 @@ def test_pack_digitally_signed(precert_bytes): + b"\x00\x00" # extensions length + b"" # extensions ) - - -def test_issuer_key_hash(): - # Taken from certificate-transparency-go: - # https://github.com/google/certificate-transparency-go/blob/88227ce0/trillian/ctfe/testonly/certificates.go#L213-L231 - precert_pem = b"""-----BEGIN CERTIFICATE----- -MIIC3zCCAkigAwIBAgIBBzANBgkqhkiG9w0BAQUFADBVMQswCQYDVQQGEwJHQjEk -MCIGA1UEChMbQ2VydGlmaWNhdGUgVHJhbnNwYXJlbmN5IENBMQ4wDAYDVQQIEwVX -YWxlczEQMA4GA1UEBxMHRXJ3IFdlbjAeFw0xMjA2MDEwMDAwMDBaFw0yMjA2MDEw -MDAwMDBaMFIxCzAJBgNVBAYTAkdCMSEwHwYDVQQKExhDZXJ0aWZpY2F0ZSBUcmFu -c3BhcmVuY3kxDjAMBgNVBAgTBVdhbGVzMRAwDgYDVQQHEwdFcncgV2VuMIGfMA0G -CSqGSIb3DQEBAQUAA4GNADCBiQKBgQC+75jnwmh3rjhfdTJaDB0ym+3xj6r015a/ -BH634c4VyVui+A7kWL19uG+KSyUhkaeb1wDDjpwDibRc1NyaEgqyHgy0HNDnKAWk -EM2cW9tdSSdyba8XEPYBhzd+olsaHjnu0LiBGdwVTcaPfajjDK8VijPmyVCfSgWw -FAn/Xdh+tQIDAQABo4HBMIG+MB0GA1UdDgQWBBQgMVQa8lwF/9hli2hDeU9ekDb3 -tDB9BgNVHSMEdjB0gBRfnYgNyHPmVNT4DdjmsMEktEfDVaFZpFcwVTELMAkGA1UE -BhMCR0IxJDAiBgNVBAoTG0NlcnRpZmljYXRlIFRyYW5zcGFyZW5jeSBDQTEOMAwG -A1UECBMFV2FsZXMxEDAOBgNVBAcTB0VydyBXZW6CAQAwCQYDVR0TBAIwADATBgor -BgEEAdZ5AgQDAQH/BAIFADANBgkqhkiG9w0BAQUFAAOBgQACocOeAVr1Tf8CPDNg -h1//NDdVLx8JAb3CVDFfM3K3I/sV+87MTfRxoM5NjFRlXYSHl/soHj36u0YtLGhL -BW/qe2O0cP8WbjLURgY1s9K8bagkmyYw5x/DTwjyPdTuIo+PdPY9eGMR3QpYEUBf -kGzKLC0+6/yBmWTr2M98CIY/vg== - -----END CERTIFICATE-----""" - - precert = x509.load_pem_x509_certificate(precert_pem) - - public_key = precert.public_key().public_bytes( - encoding=serialization.Encoding.DER, - format=serialization.PublicFormat.SubjectPublicKeyInfo, - ) - - assert sct._issuer_key_hash(precert) == hashlib.sha256(public_key).digest() - assert ( - hashlib.sha256(public_key).hexdigest() - == "086c0ea25b60e3c44a994d0d5f40b81a0d44f21d63df19315e6ddfbe47373817" - ) diff --git a/test/test_utils.py b/test/test_utils.py new file mode 100644 index 000000000..6cc7d4120 --- /dev/null +++ b/test/test_utils.py @@ -0,0 +1,58 @@ +# Copyright 2022 The Sigstore Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import hashlib + +from cryptography import x509 +from cryptography.hazmat.primitives import serialization + +from sigstore import _utils as utils + + +def test_key_id(): + # Taken from certificate-transparency-go: + # https://github.com/google/certificate-transparency-go/blob/88227ce0/trillian/ctfe/testonly/certificates.go#L213-L231 + precert_pem = b"""-----BEGIN CERTIFICATE----- +MIIC3zCCAkigAwIBAgIBBzANBgkqhkiG9w0BAQUFADBVMQswCQYDVQQGEwJHQjEk +MCIGA1UEChMbQ2VydGlmaWNhdGUgVHJhbnNwYXJlbmN5IENBMQ4wDAYDVQQIEwVX +YWxlczEQMA4GA1UEBxMHRXJ3IFdlbjAeFw0xMjA2MDEwMDAwMDBaFw0yMjA2MDEw +MDAwMDBaMFIxCzAJBgNVBAYTAkdCMSEwHwYDVQQKExhDZXJ0aWZpY2F0ZSBUcmFu +c3BhcmVuY3kxDjAMBgNVBAgTBVdhbGVzMRAwDgYDVQQHEwdFcncgV2VuMIGfMA0G +CSqGSIb3DQEBAQUAA4GNADCBiQKBgQC+75jnwmh3rjhfdTJaDB0ym+3xj6r015a/ +BH634c4VyVui+A7kWL19uG+KSyUhkaeb1wDDjpwDibRc1NyaEgqyHgy0HNDnKAWk +EM2cW9tdSSdyba8XEPYBhzd+olsaHjnu0LiBGdwVTcaPfajjDK8VijPmyVCfSgWw +FAn/Xdh+tQIDAQABo4HBMIG+MB0GA1UdDgQWBBQgMVQa8lwF/9hli2hDeU9ekDb3 +tDB9BgNVHSMEdjB0gBRfnYgNyHPmVNT4DdjmsMEktEfDVaFZpFcwVTELMAkGA1UE +BhMCR0IxJDAiBgNVBAoTG0NlcnRpZmljYXRlIFRyYW5zcGFyZW5jeSBDQTEOMAwG +A1UECBMFV2FsZXMxEDAOBgNVBAcTB0VydyBXZW6CAQAwCQYDVR0TBAIwADATBgor +BgEEAdZ5AgQDAQH/BAIFADANBgkqhkiG9w0BAQUFAAOBgQACocOeAVr1Tf8CPDNg +h1//NDdVLx8JAb3CVDFfM3K3I/sV+87MTfRxoM5NjFRlXYSHl/soHj36u0YtLGhL +BW/qe2O0cP8WbjLURgY1s9K8bagkmyYw5x/DTwjyPdTuIo+PdPY9eGMR3QpYEUBf +kGzKLC0+6/yBmWTr2M98CIY/vg== + -----END CERTIFICATE-----""" + + precert = x509.load_pem_x509_certificate(precert_pem) + + public_key = precert.public_key().public_bytes( + encoding=serialization.Encoding.DER, + format=serialization.PublicFormat.SubjectPublicKeyInfo, + ) + + key_id = utils.key_id(precert.public_key()) + assert key_id == hashlib.sha256(public_key).digest() + assert ( + hashlib.sha256(public_key).hexdigest() + == "086c0ea25b60e3c44a994d0d5f40b81a0d44f21d63df19315e6ddfbe47373817" + )