Skip to content

Commit

Permalink
Merge branch 'main' into dependabot/pip/cryptography-44.0.0
Browse files Browse the repository at this point in the history
  • Loading branch information
woodruffw authored Dec 3, 2024
2 parents 1d44888 + dad57a2 commit 46d98e0
Show file tree
Hide file tree
Showing 5 changed files with 28 additions and 324 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 0 additions & 2 deletions sigstore/_internal/fulcio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,12 @@
"""

from .client import (
DetachedFulcioSCT,
ExpiredCertificate,
FulcioCertificateSigningResponse,
FulcioClient,
)

__all__ = [
"DetachedFulcioSCT",
"ExpiredCertificate",
"FulcioCertificateSigningResponse",
"FulcioClient",
Expand Down
177 changes: 14 additions & 163 deletions sigstore/_internal/fulcio/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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`.
Expand All @@ -95,81 +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:]


# SignedCertificateTimestamp is an ABC, so register our DetachedFulcioSCT as
# virtual subclass.
SignedCertificateTimestamp.register(DetachedFulcioSCT)


class ExpiredCertificate(Exception):
"""An error raised when the Certificate is expired."""

Expand Down Expand Up @@ -243,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
Expand All @@ -269,40 +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_json = json.loads(base64.b64decode(sct_b64).decode())
except ValueError as exc:
raise FulcioClientError from exc

try:
sct = DetachedFulcioSCT.parse_obj(sct_json)
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)

Expand Down
9 changes: 8 additions & 1 deletion test/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,14 @@ def _has_oidc_id():
# On GitHub Actions, forks do not have access to OIDC identities.
# We differentiate this case from other GitHub credential errors,
# since it's a case where we want to skip (i.e. return False).
if os.getenv("GITHUB_EVENT_NAME") == "pull_request":
#
# We also skip when the repo isn't our own, since downstream
# regression testers (e.g. PyCA Cryptography) don't necessarily
# want to give our unit tests access to an OIDC identity.
if (
os.getenv("GITHUB_REPOSITORY") != "sigstore/sigstore-python"
or os.getenv("GITHUB_EVENT_NAME") == "pull_request"
):
return False
return True
except AmbientCredentialError:
Expand Down
Loading

0 comments on commit 46d98e0

Please sign in to comment.