8000 sigstore: add a CT keyring, use it for SCT verification (#267) · tetsuo-cpp/sigstore-python@b5dd9ee · GitHub
[go: up one dir, main page]

Skip to content

Commit b5dd9ee

Browse files
authored
sigstore: add a CT keyring, use it for SCT verification (sigstore#267)
1 parent 392e17a commit b5dd9ee

File tree

10 files changed

+364
-106
lines changed

10 files changed

+364
-106
lines changed

sigstore/_cli.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from typing import Optional, TextIO, Union, cast
2323

2424
from sigstore import __version__
25+
from sigstore._internal.ctfe import CTKeyring
2526
from sigstore._internal.fulcio.client import DEFAULT_FULCIO_URL, FulcioClient
2627
from sigstore._internal.oidc.ambient import (
2728
GitHubOidcPermissionCredentialError,
@@ -35,6 +36,7 @@
3536
)
3637
from sigstore._internal.rekor.client import DEFAULT_REKOR_URL, RekorClient
3738
from sigstore._sign import Signer
39+
from sigstore._utils import load_pem_public_key
3840
from sigstore._verify import (
3941
CertificateVerificationFailure,
4042
RekorEntryMissing,
@@ -357,10 +359,11 @@ def _sign(args: argparse.Namespace) -> None:
357359
elif args.fulcio_url == DEFAULT_FULCIO_URL and args.rekor_url == DEFAULT_REKOR_URL:
358360
signer = Signer.production()
359361
else:
362+
ct_keyring = CTKeyring([load_pem_public_key(args.ctfe_pem.read())])
360363
signer = Signer(
361364
fulcio=FulcioClient(args.fulcio_url),
362365
rekor=RekorClient(
363-
args.rekor_url, args.rekor_root_pubkey.read(), args.ctfe_pem.read()
366+
args.rekor_url, args.rekor_root_pubkey.read(), ct_keyring
364367
),
365368
)
366369

sigstore/_internal/ctfe.py

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
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+
"""
16+
Functionality for interacting with CT ("CTFE") signing keys.
17+
"""
18+
19+
from __future__ import annotations
20+
21+
from importlib import resources
22+
from typing import List
23+
24+
import cryptography.hazmat.primitives.asymmetric.padding as padding
25+
from cryptography.exceptions import InvalidSignature
26+
from cryptography.hazmat.primitives import hashes
27+
from cryptography.hazmat.primitives.asymmetric import ec, rsa
28+
29+
from sigstore._utils import PublicKey, key_id, load_pem_public_key
30+
31+
32+
class CTKeyringError(Exception):
33+
"""
34+
Raised on failure by `CTKeyring.verify()`.
35+
"""
36+
37+
pass
38+
39+
40+
class CTKeyringLookupError(CTKeyringError):
41+
"""
42+
A specialization of `CTKeyringError`, indicating that the specified
43+
key ID wasn't found in the keyring.
44+
"""
45+
46+
pass
47+
48+
49+
class CTKeyring:
50+
"""
51+
Represents a set of CT signing keys, each of which is a potentially
52+
valid signer for a Signed Certificate Timestamp (SCT).
53+
54+
This structure exists to facilitate key rotation in a CT log.
55+
"""
56+
57+
def __init__(self, keys: List[PublicKey] = []):
58+
self._keyring = {}
59+
for key in keys:
60+
self._keyring[key_id(key)] = key
61+
62+
@classmethod
63+
def staging(cls) -> CTKeyring:
64+
"""
65+
Returns a `CTKeyring` instance capable of verifying SCTs from
66+
Sigstore's staging deployment.
67+
"""
68+
keyring = cls()
69+
keyring._add_resource("ctfe.staging.pub")
70+
keyring._add_resource("ctfe_2022.staging.pub")
71+
keyring._add_resource("ctfe_2022.2.staging.pub")
72+
73+
return keyring
74+
75+
@classmethod
76+
def production(cls) -> CTKeyring:
77+
"""
78+
Returns a `CTKeyring` instance capable of verifying SCTs from
79+
Sigstore's production deployment.
80+
"""
81+
keyring = cls()
82+
keyring._add_resource("ctfe.pub")
83+
keyring._add_resource("ctfe_2022.pub")
84+
85+
return keyring
86+
87+
def _add_resource(self, name: str) -> None:
88+
"""
89+
Adds a key to the current keyring, as identified by its
90+
resource name under `sigstore._store`.
91+
"""
92+
key_pem = resources.read_binary("sigstore._store", name)
93+
self.add(key_pem)
94+
95+
def add(self, key_pem: bytes) -> None:
96+
"""
97+
Adds a PEM-encoded key to the current keyring.
98+
"""
99+
key = load_pem_public_key(key_pem)
100+
self._keyring[key_id(key)] = key
101+
102+
def verify(self, *, key_id: bytes, signature: bytes, data: bytes) -> None:
103+
key = self._keyring.get(key_id)
104+
if key is None:
105+
# If we don't have a key corresponding to this key ID, we can't
106+
# possibly verify the signature.
107+
raise CTKeyringLookupError(f"no known key for key ID {key_id.hex()}")
108+
109+
try:
110+
if isinstance(key, rsa.RSAPublicKey):
111+
key.verify(
112+
signature=signature,
113+
data=data,
114+
padding=padding.PKCS1v15(),
115+
algorithm=hashes.SHA256(),
116+
)
117+
elif isinstance(key, ec.EllipticCurvePublicKey):
118+
key.verify(
119+
signature=signature,
120+
data=data,
121+
signature_algorithm=ec.ECDSA(hashes.SHA256()),
122+
)
123+
else:
124+
# NOTE(ww): Unreachable without API misuse.
125+
raise CTKeyringError(f"unsupported key type: {key}")
126+
except InvalidSignature as exc:
127+
raise CTKeyringError("invalid signature") from exc

sigstore/_internal/rekor/client.py

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,11 @@
2727

2828
import requests
2929
from cryptography.hazmat.primitives import serialization
30-
from cryptography.hazmat.primitives.asymmetric import ec, rsa
30+
from cryptography.hazmat.primitives.asymmetric import ec
3131
from pydantic import BaseModel, Field, StrictInt, StrictStr, validator
3232

33+
from sigstore._internal.ctfe import CTKeyring
34+
3335
logger = logging.getLogger(__name__)
3436

3537
DEFAULT_REKOR_URL = "https://rekor.sigstore.dev"
@@ -272,7 +274,7 @@ def post(
272274
class RekorClient:
273275
"""The internal Rekor client"""
274276

275-
def __init__(self, url: str, pubkey: bytes, ctfe_pubkey: bytes) -> None:
277+
def __init__(self, url: str, pubkey: bytes, ct_keyring: CTKeyring) -> None:
276278
self.url = urljoin(url, "api/v1/")
277279
self.session = requests.Session()
278280
self.session.headers.update(
@@ -287,31 +289,20 @@ def __init__(self, url: str, pubkey: bytes, ctfe_pubkey: bytes) -> None:
287289
raise RekorClientError(f"Invalid public key type: {pubkey}")
288290
self._pubkey = pubkey
289291

290-
ctfe_pubkey = serialization.load_pem_public_key(ctfe_pubkey)
291-
if not isinstance(
292-
ctfe_pubkey,
293-
(
294-
rsa.RSAPublicKey,
295-
ec.EllipticCurvePublicKey,
296-
),
297-
):
298-
raise RekorClientError(f"Invalid CTFE public key type: {ctfe_pubkey}")
299-
self._ctfe_pubkey = ctfe_pubkey
292+
self._ct_keyring = ct_keyring
300293

301294
def __del__(self) -> None:
302295
self.session.close()
303296

304297
@classmethod
305298
def production(cls) -> RekorClient:
306299
return cls(
307-
DEFAULT_REKOR_URL, _DEFAULT_REKOR_ROOT_PUBKEY, _DEFAULT_REKOR_CTFE_PUBKEY
300+
DEFAULT_REKOR_URL, _DEFAULT_REKOR_ROOT_PUBKEY, CTKeyring.production()
308301
)
309302

310303
@classmethod
311304
def staging(cls) -> RekorClient:
312-
return cls(
313-
STAGING_REKOR_URL, _STAGING_REKOR_ROOT_PUBKEY, _STAGING_REKOR_CTFE_PUBKEY
314-
)
305+
return cls(STAGING_REKOR_URL, _STAGING_REKOR_ROOT_PUBKEY, CTKeyring.staging())
315306

316307
@property
317308
def log(self) -> RekorLog:

0 commit comments

Comments
 (0)
0