Skip to content

Commit ffcfa5b

Browse files
committed
Merge branch 'main' into ww/dsse
Signed-off-by: William Woodruff <william@trailofbits.com>
2 parents 0b23bc2 + e548d43 commit ffcfa5b

File tree

15 files changed

+305
-229
lines changed

15 files changed

+305
-229
lines changed

.github/dependabot.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,18 @@ updates:
1212
interval: daily
1313
open-pull-requests-limit: 99
1414
rebase-strategy: "disabled"
15+
groups:
16+
actions:
17+
patterns:
18+
- "*"
1519

1620
- package-ecosystem: github-actions
1721
directory: .github/actions/upload-coverage/
1822
schedule:
1923
interval: daily
2024
open-pull-requests-limit: 99
2125
rebase-strategy: "disabled"
26+
groups:
27+
actions:
28+
patterns:
29+
- "*"

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ jobs:
9191
- run: pip install coverage[toml]
9292

9393
- name: download coverage data
94-
uses: actions/download-artifact@7a1cd3216ca9260cd8022db641d960b1db4d1be4 # v4.0.0
94+
uses: actions/download-artifact@f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110 # v4.1.0
9595
with:
9696
path: all-artifacts/
9797

.github/workflows/conformance.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222
- name: install sigstore-python
2323
run: python -m pip install .
2424

25-
- uses: sigstore/sigstore-conformance@c8d17eb7ee884cf86b93a3a3f471648fb0a83819 # v0.0.9
25+
- uses: sigstore/sigstore-conformance@7375951316d6b28d07f7406c01e1dc7de2a75ce7 # v0.0.10
2626
with:
2727
entrypoint: ${{ github.workspace }}/test/integration/sigstore-python-conformance
28-
xfail: "test_verify_with_trust_root" # see issue 821
28+
xfail: "test_verify_with_trust_root test_verify_dsse_bundle_with_trust_root" # see issue 821

.github/workflows/docs.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ jobs:
2828
make doc
2929
3030
- name: upload docs artifact
31-
uses: actions/upload-pages-artifact@a753861a5debcf57bf8b404356158c8e1e33150c # v2.0.0
31+
uses: actions/upload-pages-artifact@0252fc4ba7626f0298f0cf00902a25c6afc77fa8 # v3.0.0
3232
with:
3333
path: ./html/
3434

@@ -49,4 +49,4 @@ jobs:
4949
url: ${{ steps.deployment.outputs.page_url }}
5050
steps:
5151
- id: deployment
52-
uses: actions/deploy-pages@13b55b33dd8996121833dbc1db458c793a334630 # v3.0.1
52+
uses: actions/deploy-pages@7a9bd943aa5e5175aeb8502edcc6c1c02d398e10 # v4.0.2

.github/workflows/release.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ jobs:
119119
id-token: write
120120
steps:
121121
- name: Download artifacts directories # goes to current working directory
122-
uses: actions/download-artifact@7a1cd3216ca9260cd8022db641d960b1db4d1be4 # v4.0.0
122+
uses: actions/download-artifact@f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110 # v4.1.0
123123

124124
- name: publish
125125
uses: pypa/gh-action-pypi-publish@2f6f737ca5f74c637829c0f5c3acd0e29ea5e8bf # v1.8.11
@@ -134,7 +134,7 @@ jobs:
134134
contents: write
135135
steps:
136136
- name: Download artifacts directories # goes to current working directory
137-
uses: actions/download-artifact@7a1cd3216ca9260cd8022db641d960b1db4d1be4 # v4.0.0
137+
uses: actions/download-artifact@f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110 # v4.1.0
138138

139139
- name: Upload artifacts to github
140140
# Confusingly, this action also supports updating releases, not

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ lint = [
6060
"mypy ~= 1.1",
6161
# NOTE(ww): ruff is under active development, so we pin conservatively here
6262
# and let Dependabot periodically perform this update.
63-
"ruff < 0.1.9",
63+
"ruff < 0.1.11",
6464
"types-requests",
6565
"types-protobuf",
6666
"types-pyOpenSSL",

sigstore/_cli.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
RekorClient,
4141
RekorKeyring,
4242
)
43-
from sigstore._internal.tuf import TrustUpdater
43+
from sigstore._internal.trustroot import TrustedRoot
4444
from sigstore._utils import PEMCert
4545
from sigstore.errors import Error
4646
from sigstore.oidc import (
@@ -650,16 +650,16 @@ def _sign(args: argparse.Namespace) -> None:
650650
elif args.fulcio_url == DEFAULT_FULCIO_URL and args.rekor_url == DEFAULT_REKOR_URL:
651651
signing_ctx = SigningContext.production()
652652
else:
653-
# Assume "production" keys if none are given as arguments
654-
updater = TrustUpdater.production()
653+
# Assume "production" trust root if no keys are given as arguments
654+
trusted_root = TrustedRoot.production()
655655
if args.ctfe_pem is not None:
656656
ctfe_keys = [args.ctfe_pem.read()]
657657
else:
658-
ctfe_keys = updater.get_ctfe_keys()
658+
ctfe_keys = trusted_root.get_ctfe_keys()
659659
if args.rekor_root_pubkey is not None:
660660
rekor_keys = [args.rekor_root_pubkey.read()]
661661
else:
662-
rekor_keys = updater.get_rekor_keys()
662+
rekor_keys = trusted_root.get_rekor_keys()
663663

664664
ct_keyring = CTKeyring(Keyring(ctfe_keys))
665665
rekor_keyring = RekorKeyring(Keyring(rekor_keys))
@@ -828,8 +828,8 @@ def _collect_verification_state(
828828
if args.rekor_root_pubkey is not None:
829829
rekor_keys = [args.rekor_root_pubkey.read()]
830830
else:
831-
updater = TrustUpdater.production()
832-
rekor_keys = updater.get_rekor_keys()
831+
trusted_root = TrustedRoot.production()
832+
rekor_keys = trusted_root.get_rekor_keys()
833833

834834
verifier = Verifier(
835835
rekor=RekorClient(

sigstore/_internal/rekor/client.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030

3131
from sigstore._internal.ctfe import CTKeyring
3232
from sigstore._internal.keyring import Keyring
33-
from sigstore._internal.tuf import TrustUpdater
33+
from sigstore._internal.trustroot import TrustedRoot
3434
from sigstore.transparency import LogEntry
3535

3636
logger = logging.getLogger(__name__)
@@ -249,14 +249,14 @@ def __del__(self) -> None:
249249
self.session.close()
250250

251251
@classmethod
252-
def production(cls, updater: TrustUpdater) -> RekorClient:
252+
def production(cls, trust_root: TrustedRoot) -> RekorClient:
253253
"""
254254
Returns a `RekorClient` populated with the default Rekor production instance.
255255
256-
updater must be a `TrustUpdater` for the production TUF repository.
256+
trust_root must be a `TrustedRoot` for the production TUF repository.
257257
"""
258-
rekor_keys = updater.get_rekor_keys()
259-
ctfe_keys = updater.get_ctfe_keys()
258+
rekor_keys = trust_root.get_rekor_keys()
259+
ctfe_keys = trust_root.get_ctfe_keys()
260260

261261
return cls(
262262
DEFAULT_REKOR_URL,
@@ -265,14 +265,14 @@ def production(cls, updater: TrustUpdater) -> RekorClient:
265265
)
266266

267267
@classmethod
268-
def staging(cls, updater: TrustUpdater) -> RekorClient:
268+
def staging(cls, trust_root: TrustedRoot) -> RekorClient:
269269
"""
270270
Returns a `RekorClient` populated with the default Rekor staging instance.
271271
272-
updater must be a `TrustUpdater` for the staging TUF repository.
272+
trust_root must be a `TrustedRoot` for the staging TUF repository.
273273
"""
274-
rekor_keys = updater.get_rekor_keys()
275-
ctfe_keys = updater.get_ctfe_keys()
274+
rekor_keys = trust_root.get_rekor_keys()
275+
ctfe_keys = trust_root.get_ctfe_keys()
276276

277277
return cls(
278278
STAGING_REKOR_URL,

sigstore/_internal/trustroot.py

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
# Copyright 2023 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+
Trust root management for sigstore-python.
17+
"""
18+
19+
from __future__ import annotations
20+
21+
from datetime import datetime, timezone
22+
from pathlib import Path
23+
from typing import Iterable
24+
25+
from cryptography.x509 import Certificate, load_der_x509_certificate
26+
from sigstore_protobuf_specs.dev.sigstore.common.v1 import TimeRange
27+
from sigstore_protobuf_specs.dev.sigstore.trustroot.v1 import (
28+
CertificateAuthority,
29+
TransparencyLogInstance,
30+
)
31+
from sigstore_protobuf_specs.dev.sigstore.trustroot.v1 import (
32+
TrustedRoot as _TrustedRoot,
33+
)
34+
35+
from sigstore._internal.tuf import DEFAULT_TUF_URL, STAGING_TUF_URL, TrustUpdater
36+
from sigstore.errors import MetadataError
37+
38+
39+
def _is_timerange_valid(period: TimeRange | None, *, allow_expired: bool) -> bool:
40+
"""
41+
Given a `period`, checks that the the current time is not before `start`. If
42+
`allow_expired` is `False`, also checks that the current time is not after
43+
`end`.
44+
"""
45+
now = datetime.now(timezone.utc)
46+
47+
# If there was no validity period specified, the key is always valid.
48+
if not period:
49+
return True
50+
51+
# Active: if the current time is before the starting period, we are not yet
52+
# valid.
53+
if now < period.start:
54+
return False
55+
56+
# If we want Expired keys, the key is valid at this point. Otherwise, check
57+
# that we are within range.
58+
return allow_expired or (period.end is None or now <= period.end)
59+
60+
61+
class TrustedRoot(_TrustedRoot):
62+
"""Complete set of trusted entities for a Sigstore client"""
63+
64+
@classmethod
65+
def from_file(cls, path: str) -> "TrustedRoot":
66+
"""Create a new trust root from file"""
67+
tr: TrustedRoot = cls().from_json(Path(path).read_bytes())
68+
return tr
69+
70+
@classmethod
71+
def from_tuf(cls, url: str, offline: bool = False) -> "TrustedRoot":
72+
"""Create a new trust root from a TUF repository.
73+
74+
If `offline`, will use trust root in local TUF cache. Otherwise will
75+
update the trust root from remote TUF repository.
76+
"""
77+
path = TrustUpdater(url, offline).get_trusted_root_path()
78+
return cls.from_file(path)
79+
80+
@classmethod
81+
def production(cls, offline: bool = False) -> "TrustedRoot":
82+
"""Create new trust root from Sigstore production TUF repository.
83+
84+
If `offline`, will use trust root in local TUF cache. Otherwise will
85+
update the trust root from remote TUF repository.
86+
"""
87+
return cls.from_tuf(DEFAULT_TUF_URL, offline)
88+
89+
@classmethod
90+
def staging(cls, offline: bool = False) -> "TrustedRoot":
91+
"""Create new trust root from Sigstore staging TUF repository.
92+
93+
If `offline`, will use trust root in local TUF cache. Otherwise will
94+
update the trust root from remote TUF repository.
95+
"""
96+
return cls.from_tuf(STAGING_TUF_URL, offline)
97+
98+
@staticmethod
99+
def _get_tlog_keys(tlogs: list[TransparencyLogInstance]) -> Iterable[bytes]:
100+
"""Return public key contents given transparency log instances."""
101+
102+
for key in tlogs:
103+
if not _is_timerange_valid(key.public_key.valid_for, allow_expired=False):
104+
continue
105+
key_bytes = key.public_key.raw_bytes
106+
if key_bytes:
107+
yield key_bytes
108+
109+
@staticmethod
110+
def _get_ca_keys(
111+
cas: list[CertificateAuthority], *, allow_expired: bool
112+
) -> Iterable[bytes]:
113+
"""Return public key contents given certificate authorities."""
114+
115+
for ca in cas:
116+
if not _is_timerange_valid(ca.valid_for, allow_expired=allow_expired):
117+
continue
118+
for cert in ca.cert_chain.certificates:
119+
yield cert.raw_bytes
120+
121+
def get_ctfe_keys(self) -> list[bytes]:
122+
"""Return the active CTFE public keys contents."""
123+
ctfes: list[bytes] = list(self._get_tlog_keys(self.ctlogs))
124+
if not ctfes:
125+
raise MetadataError("Active CTFE keys not found in trusted root")
126+
return ctfes
127+
128+
def get_rekor_keys(self) -> list[bytes]:
129+
"""Return the rekor public key content."""
130+
keys: list[bytes] = list(self._get_tlog_keys(self.tlogs))
131+
132+
if len(keys) != 1:
133+
raise MetadataError("Did not find one active Rekor key in trusted root")
134+
return keys
135+
136+
def get_fulcio_certs(self) -> list[Certificate]:
137+
"""Return the Fulcio certificates."""
138+
139+
certs: list[Certificate]
140+
141+
# Return expired certificates too: they are expired now but may have
142+
# been active when the certificate was used to sign.
143+
certs = [
144+
load_der_x509_certificate(c)
145+
for c in self._get_ca_keys(self.certificate_authorities, allow_expired=True)
146+
]
147+
148+
if not certs:
149+
raise MetadataError("Fulcio certificates not found in trusted root")
150+
return certs

0 commit comments

Comments
 (0)