Skip to content

Commit 6993d75

Browse files
Switch to Fulcio v2 API (sigstore#159)
The main differences are: * RootCert -> TrustBundle now returns a list of chains * SigningCertificate differentiates between the embedded and detached SCT, and it's no longer returned in a header. * Support for gRPC, but this continues to use HTTP. Signed-off-by: Hayden Blauzvern <hblauzvern@google.com>
1 parent 72ce89b commit 6993d75

File tree

1 file changed

+47
-27
lines changed

1 file changed

+47
-27
lines changed

sigstore/_internal/fulcio/client.py

Lines changed: 47 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,11 @@
2929
from typing import List, Optional
3030
from urllib.parse import urljoin
3131

32-
import pem
3332
import requests
3433
from cryptography.hazmat.primitives import hashes, serialization
3534
from cryptography.x509 import (
3635
Certificate,
3736
CertificateSigningRequest,
38-
ExtensionNotFound,
3937
PrecertificateSignedCertificateTimestamps,
4038
load_pem_x509_certificate,
4139
)
@@ -51,8 +49,8 @@
5149

5250
DEFAULT_FULCIO_URL = "https://fulcio.sigstore.dev"
5351
STAGING_FULCIO_URL = "https://fulcio.sigstage.dev"
54-
SIGNING_CERT_ENDPOINT = "/api/v1/signingCert"
55-
ROOT_CERT_ENDPOINT = "/api/v1/rootCert"
52+
SIGNING_CERT_ENDPOINT = "/api/v2/signingCert"
53+
TRUST_BUNDLE_ENDPOINT = "/api/v2/trustBundle"
5654

5755

5856
class SCTHashAlgorithm(IntEnum):
@@ -162,10 +160,10 @@ class FulcioCertificateSigningResponse:
162160

163161

164162
@dataclass(frozen=True)
165-
class FulcioRootResponse:
166-
"""Root certificate response"""
163+
class FulcioTrustBundleResponse:
164+
"""Trust bundle response, containing a list of certificate chains"""
167165

168-
root_cert: Certificate
166+
trust_bundle: List[List[Certificate]]
169167

170168

171169
class FulcioClientError(Exception):
@@ -212,16 +210,29 @@ def post(
212210
except (AttributeError, KeyError):
213211
raise FulcioClientError from http_error
214212

213+
if resp.json().get("signedCertificateEmbeddedSct"):
214+
sct_embedded = True
215+
try:
216+
certificates = resp.json()["signedCertificateEmbeddedSct"]["chain"]["certificates"]
217+
except KeyError:
218+
raise FulcioClientError("Fulcio response missing certificate chain")
219+
else:
220+
sct_embedded = False
221+
try:
222+
certificates = resp.json()["signedCertificateDetachedSct"]["chain"]["certificates"]
223+
except KeyError:
224+
raise FulcioClientError("Fulcio response missing certificate chain")
225+
215226
# Cryptography doesn't have chain verification/building built in
216227
# https://github.com/pyca/cryptography/issues/2381
217-
try:
218-
cert_pem, *chain_pems = pem.parse(resp.content)
219-
cert = load_pem_x509_certificate(cert_pem.as_bytes())
220-
chain = [load_pem_x509_certificate(c.as_bytes()) for c in chain_pems]
221-
except ValueError:
222-
raise FulcioClientError(f"Did not find a cert in Fulcio response: {resp}")
228+
if len(certificates) < 2:
229+
raise FulcioClientError(
230+
f"Certificate chain is too short: {len(certificates)} < 2"
231+
)
232+
cert = load_pem_x509_certificate(certificates[0].encode())
233+
chain = [load_pem_x509_certificate(c.encode()) for c in certificates[1:]]
223234

224-
try:
235+
if sct_embedded:
225236
# Try to retrieve the embedded SCTs within the cert.
226237
precert_scts_extension = cert.extensions.get_extension_for_class(
227238
PrecertificateSignedCertificateTimestamps
@@ -262,16 +273,17 @@ def _opaque16(value: bytes) -> bytes:
262273
raw_sct = _opaque16(_opaque16(raw_sct_list_bytes))
263274

264275
sct = precert_scts_extension[0]
265-
except ExtensionNotFound:
276+
else:
266277
# If we don't have any embedded SCTs, then we might be dealing
267278
# with a Fulcio instance that provides detached SCTs.
268279

269-
# The SCT header is a base64-encoded payload, which in turn
280+
# The detached SCT is a base64-encoded payload, which in turn
270281
# is a JSON representation of the SignedCertificateTimestamp
271282
# in RFC 6962 (subsec. 3.2).
272-
sct_b64 = resp.headers.get("SCT")
273-
if sct_b64 is None:
274-
raise FulcioClientError("Fulcio response did not include a SCT header")
283+
try:
284+
sct_b64 = resp.json()["signedCertificateDetachedSct"]["signedCertificateTimestamp"]
285+
except KeyError:
286+
raise FulcioClientError("Fulcio response did not include a detached SCT")
275287

276288
try:
277289
sct_json = json.loads(base64.b64decode(sct_b64).decode())
@@ -291,16 +303,24 @@ def _opaque16(value: bytes) -> bytes:
291303
return FulcioCertificateSigningResponse(cert, chain, sct, raw_sct)
292304

293305

294-
class FulcioRootCert(Endpoint):
295-
def get(self) -> FulcioRootResponse:
296-
"""Get the root certificate"""
306+
class FulcioTrustBundle(Endpoint):
307+
def get(self) -> FulcioTrustBundleResponse:
308+
"""Get the certificate chains from Fulcio"""
297309
resp: requests.Response = self.session.get(self.url)
298310
try:
299311
resp.raise_for_status()
300312
except requests.HTTPError as http_error:
301313
raise FulcioClientError from http_error
302-
root_cert: Certificate = load_pem_x509_certificate(resp.content)
303-
return FulcioRootResponse(root_cert)
314+
315+
trust_bundle_json = resp.json()
316+
chains: List[List[Certificate]] = []
317+
for certificate_chain in trust_bundle_json["chains"]:
318+
chain: List[Certificate] = []
319+
for certificate in certificate_chain["certificates"]:
320+
cert: Certificate = load_pem_x509_certificate(certificate.encode())
321+
chain.append(cert)
322+
chains.append(chain)
323+
return FulcioTrustBundleResponse(chains)
304324

305325

306326
class FulcioClient:
@@ -327,7 +347,7 @@ def signing_cert(self) -> FulcioSigningCert:
327347
)
328348

329349
@property
330-
def root_cert(self) -> FulcioRootCert:
331-
return FulcioRootCert(
332-
urljoin(self.url, ROOT_CERT_ENDPOINT), session=self.session
350+
def trust_bundle(self) -> FulcioTrustBundle:
351+
return FulcioTrustBundle(
352+
urljoin(self.url, TRUST_BUNDLE_ENDPOINT), session=self.session
333353
)

0 commit comments

Comments
 (0)