diff --git a/CHANGELOG.md b/CHANGELOG.md index 74599df53..c590da40e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,12 @@ All versions prior to 0.9.0 are untracked. Trusted Root contains one or more Timestamp Authorities ([#1216](https://github.com/sigstore/sigstore-python/pull/1216)) +### Removed + +* Support for "detached" SCTs has been fully removed, aligning + sigstore-python with other sigstore clients + ([#1236](https://github.com/sigstore/sigstore-python/pull/1236)) + ### Fixed * Fixed a CLI parsing bug introduced in 3.5.1 where a warning about diff --git a/sigstore/_internal/fulcio/__init__.py b/sigstore/_internal/fulcio/__init__.py index c37b68beb..4681dafcd 100644 --- a/sigstore/_internal/fulcio/__init__.py +++ b/sigstore/_internal/fulcio/__init__.py @@ -17,14 +17,12 @@ """ from .client import ( - DetachedFulcioSCT, ExpiredCertificate, FulcioCertificateSigningResponse, FulcioClient, ) __all__ = [ - "DetachedFulcioSCT", "ExpiredCertificate", "FulcioCertificateSigningResponse", "FulcioClient", diff --git a/sigstore/_internal/fulcio/client.py b/sigstore/_internal/fulcio/client.py index 9664ccafd..062519186 100644 --- a/sigstore/_internal/fulcio/client.py +++ b/sigstore/_internal/fulcio/client.py @@ -19,30 +19,21 @@ from __future__ import annotations import base64 -import datetime import json import logging -import struct from abc import ABC from dataclasses import dataclass -from enum import IntEnum from typing import List from urllib.parse import urljoin import requests -from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives import serialization from cryptography.x509 import ( Certificate, CertificateSigningRequest, load_pem_x509_certificate, ) -from cryptography.x509.certificate_transparency import ( - LogEntryType, - SignatureAlgorithm, - SignedCertificateTimestamp, - Version, -) -from pydantic import BaseModel, ConfigDict, Field, field_validator +from cryptography.x509.certificate_transparency import SignedCertificateTimestamp from sigstore._internal import USER_AGENT from sigstore._internal.sct import ( @@ -60,33 +51,6 @@ TRUST_BUNDLE_ENDPOINT = "/api/v2/trustBundle" -class SCTHashAlgorithm(IntEnum): - """ - Hash algorithms that are valid for SCTs. - - These are exactly the same as the HashAlgorithm enum in RFC 5246 (TLS 1.2). - - See: https://datatracker.ietf.org/doc/html/rfc5246#section-7.4.1.4.1 - """ - - NONE = 0 - MD5 = 1 - SHA1 = 2 - SHA224 = 3 - SHA256 = 4 - SHA384 = 5 - SHA512 = 6 - - def to_cryptography(self) -> hashes.HashAlgorithm: - """ - Converts this `SCTHashAlgorithm` into a `cryptography.hashes` object. - """ - if self != SCTHashAlgorithm.SHA256: - raise FulcioSCTError(f"unexpected hash algorithm: {self!r}") - - return hashes.SHA256() - - class FulcioSCTError(Exception): """ Raised on errors when constructing a `FulcioSignedCertificateTimestamp`. @@ -95,76 +59,6 @@ class FulcioSCTError(Exception): pass -class DetachedFulcioSCT(BaseModel): - """ - Represents a "detached" SignedCertificateTimestamp from Fulcio. - """ - - model_config = ConfigDict(populate_by_name=True, arbitrary_types_allowed=True) - - version: Version = Field(..., alias="sct_version") - log_id: bytes = Field(..., alias="id") - timestamp: datetime.datetime - digitally_signed: bytes = Field(..., alias="signature") - extension_bytes: bytes = Field(..., alias="extensions") - - @field_validator("timestamp") - def _validate_timestamp(cls, v: datetime.datetime) -> datetime.datetime: - return v.replace(tzinfo=datetime.timezone.utc) - - @field_validator("digitally_signed", mode="before") - def _validate_digitally_signed(cls, v: bytes) -> bytes: - digitally_signed = base64.b64decode(v) - - if len(digitally_signed) <= 4: - raise ValueError("impossibly small digitally-signed struct") - - return digitally_signed - - @field_validator("log_id", mode="before") - def _validate_log_id(cls, v: bytes) -> bytes: - return base64.b64decode(v) - - @field_validator("extension_bytes", mode="before") - def _validate_extensions(cls, v: bytes) -> bytes: - return base64.b64decode(v) - - @property - def entry_type(self) -> LogEntryType: - """ - Returns the kind of CT log entry this detached SCT is signing for. - """ - return LogEntryType.X509_CERTIFICATE - - @property - def signature_hash_algorithm(self) -> hashes.HashAlgorithm: - """ - Returns the hash algorithm used in this detached SCT's signature. - """ - hash_ = SCTHashAlgorithm(self.digitally_signed[0]) - return hash_.to_cryptography() - - @property - def signature_algorithm(self) -> SignatureAlgorithm: - """ - Returns the signature algorithm used in this detached SCT's signature. - """ - return SignatureAlgorithm(self.digitally_signed[1]) - - @property - def signature(self) -> bytes: - """ - Returns the raw signature inside the detached SCT. - """ - (sig_size,) = struct.unpack("!H", self.digitally_signed[2:4]) - if len(self.digitally_signed[4:]) != sig_size: - raise FulcioSCTError( - f"signature size mismatch: expected {sig_size} bytes, " - f"got {len(self.digitally_signed[4:])}" - ) - return self.digitally_signed[4:] - - class ExpiredCertificate(Exception): """An error raised when the Certificate is expired.""" @@ -238,22 +132,12 @@ def post( raise FulcioClientError(text["message"]) from http_error raise FulcioClientError from http_error - if resp.json().get("signedCertificateEmbeddedSct"): - sct_embedded = True - try: - certificates = resp.json()["signedCertificateEmbeddedSct"]["chain"][ - "certificates" - ] - except KeyError: - raise FulcioClientError("Fulcio response missing certificate chain") - else: - sct_embedded = False - try: - certificates = resp.json()["signedCertificateDetachedSct"]["chain"][ - "certificates" - ] - except KeyError: - raise FulcioClientError("Fulcio response missing certificate chain") + try: + certificates = resp.json()["signedCertificateEmbeddedSct"]["chain"][ + "certificates" + ] + except KeyError: + raise FulcioClientError("Fulcio response missing certificate chain") # Cryptography doesn't have chain verification/building built in # https://github.com/pyca/cryptography/issues/2381 @@ -264,35 +148,12 @@ def post( cert = load_pem_x509_certificate(certificates[0].encode()) chain = [load_pem_x509_certificate(c.encode()) for c in certificates[1:]] - if sct_embedded: - try: - # The SignedCertificateTimestamp should be acessed by the index 0 - sct = _get_precertificate_signed_certificate_timestamps(cert)[0] - - except UnexpectedSctCountException as ex: - raise FulcioClientError(ex) - - else: - # If we don't have any embedded SCTs, then we might be dealing - # with a Fulcio instance that provides detached SCTs. - - # The detached SCT is a base64-encoded payload, which in turn - # is a JSON representation of the SignedCertificateTimestamp - # in RFC 6962 (subsec. 3.2). - try: - sct_b64 = resp.json()["signedCertificateDetachedSct"][ - "signedCertificateTimestamp" - ] - except KeyError: - raise FulcioClientError( - "Fulcio response did not include a detached SCT" - ) - - try: - sct = DetachedFulcioSCT.model_validate_json(base64.b64decode(sct_b64)) - except Exception as exc: - # Ideally we'd catch something less generic here. - raise FulcioClientError from exc + try: + # The SignedCertificateTimestamp should be accessed by the index 0 + sct = _get_precertificate_signed_certificate_timestamps(cert)[0] + + except UnexpectedSctCountException as ex: + raise FulcioClientError(ex) return FulcioCertificateSigningResponse(cert, chain, sct) diff --git a/test/unit/internal/fulcio/test_client.py b/test/unit/internal/fulcio/test_client.py index 490b13efd..d7e77e7a0 100644 --- a/test/unit/internal/fulcio/test_client.py +++ b/test/unit/internal/fulcio/test_client.py @@ -12,157 +12,3 @@ # See the License for the specific language governing permissions and # limitations under the License. -import json -from base64 import b64encode -from datetime import datetime, timezone - -import pytest -from cryptography.hazmat.primitives import hashes -from cryptography.x509.certificate_transparency import ( - LogEntryType, - SignatureAlgorithm, - Version, -) -from pydantic import ValidationError - -from sigstore._internal.fulcio import client - - -def enc(v: bytes) -> str: - return b64encode(v).decode() - - -class TestSCTHashAlgorithm: - def test_sct_hash_sha256(self): - hash_algorithm_sha256 = client.SCTHashAlgorithm(4) - assert isinstance(hash_algorithm_sha256.to_cryptography(), hashes.SHA256) - - def test_sct_hash_none(self): - hash_algorithm_none = client.SCTHashAlgorithm(0) - with pytest.raises( - client.FulcioSCTError, - match="unexpected hash algorithm: ", - ): - hash_algorithm_none.to_cryptography() - - -class TestDetachedFulcioSCT: - def test_fields(self): - blob = enc(b"this is a base64-encoded blob") - now = datetime.now(tz=timezone.utc) - sct = client.DetachedFulcioSCT( - version=0, - log_id=blob, - timestamp=int(now.timestamp() * 1000), - digitally_signed=enc(b"\x04\x00\x00\x04abcd"), - extensions=blob, - ) - - assert sct is not None - - # Each of these fields is transformed, as expected. - assert sct.version == Version.v1 - assert enc(sct.log_id) == blob - # NOTE: We only preserve the millisecond fidelity for timestamps, - # since that's what CT needs. So we need to convert both sides - # into millisecond timestamps before comparing, to avoid - # failing on microseconds. - assert int(sct.timestamp.timestamp() * 1000) == int(now.timestamp() * 1000) - assert sct.digitally_signed == b"\x04\x00\x00\x04abcd" - assert enc(sct.extension_bytes) == blob - - # Computed fields are also correct. - assert sct.entry_type == LogEntryType.X509_CERTIFICATE - - assert type(sct.signature_hash_algorithm) is hashes.SHA256 - assert sct.signature_algorithm == SignatureAlgorithm.ANONYMOUS - assert sct.signature == sct.digitally_signed[4:] == b"abcd" - - def test_constructor_equivalence(self): - blob = enc(b"this is a base64-encoded blob") - now = datetime.now() - payload = dict( - version=0, - log_id=blob, - timestamp=int(now.timestamp() * 1000), - digitally_signed=enc(b"\x00\x00\x00\x04abcd"), - extensions=blob, - ) - - sct1 = client.DetachedFulcioSCT(**payload) - sct2 = client.DetachedFulcioSCT.model_validate(payload) - sct3 = client.DetachedFulcioSCT.model_validate_json(json.dumps(payload)) - - assert sct1 == sct2 == sct3 - - @pytest.mark.parametrize("version", [-1, 1, 2, 3]) - def test_invalid_version(self, version): - with pytest.raises( - ValidationError, - match=r"1 validation error for DetachedFulcioSCT.*", - ): - client.DetachedFulcioSCT( - version=version, - log_id=enc(b"fakeid"), - timestamp=1, - digitally_signed=enc(b"fakesigned"), - extensions=b"", - ) - - @pytest.mark.parametrize( - ("digitally_signed", "reason"), - [ - (enc(b""), "impossibly small digitally-signed struct"), - (enc(b"0"), "impossibly small digitally-signed struct"), - (enc(b"00"), "impossibly small digitally-signed struct"), - (enc(b"000"), "impossibly small digitally-signed struct"), - (enc(b"0000"), "impossibly small digitally-signed struct"), - (b"invalid base64", "Invalid base64-encoded string"), - ], - ) - def test_digitally_signed_invalid(self, digitally_signed, reason): - payload = dict( - version=0, - log_id=enc(b"fakeid"), - timestamp=1, - digitally_signed=digitally_signed, - extensions=b"", - ) - - with pytest.raises(ValidationError, match=reason): - client.DetachedFulcioSCT(**payload) - - with pytest.raises(ValidationError, match=reason): - client.DetachedFulcioSCT.model_validate(payload) - - def test_log_id_invalid(self): - with pytest.raises(ValidationError, match="Invalid base64-encoded string"): - client.DetachedFulcioSCT( - version=0, - log_id=b"invalid base64", - timestamp=1, - digitally_signed=enc(b"fakesigned"), - extensions=b"", - ) - - def test_extensions_invalid(self): - with pytest.raises(ValidationError, match="Invalid base64-encoded string"): - client.DetachedFulcioSCT( - version=0, - log_id=enc(b"fakeid"), - timestamp=1, - digitally_signed=enc(b"fakesigned"), - extensions=b"invalid base64", - ) - - def test_digitally_signed_invalid_size(self): - sct = client.DetachedFulcioSCT( - version=0, - log_id=enc(b"fakeid"), - timestamp=1, - digitally_signed=enc(b"\x00\x00\x00\x05abcd"), - extensions=b"", - ) - - with pytest.raises(client.FulcioSCTError, match="expected 5 bytes, got 4"): - sct.signature