Skip to content

Commit

Permalink
Merge branch 'main' into ww/dsse
Browse files Browse the repository at this point in the history
Signed-off-by: William Woodruff <william@trailofbits.com>
  • Loading branch information
woodruffw committed Jan 9, 2024
2 parents 0b23bc2 + e548d43 commit ffcfa5b
Show file tree
Hide file tree
Showing 15 changed files with 305 additions and 229 deletions.
8 changes: 8 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,18 @@ updates:
interval: daily
open-pull-requests-limit: 99
rebase-strategy: "disabled"
groups:
actions:
patterns:
- "*"

- package-ecosystem: github-actions
directory: .github/actions/upload-coverage/
schedule:
interval: daily
open-pull-requests-limit: 99
rebase-strategy: "disabled"
groups:
actions:
patterns:
- "*"
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ jobs:
- run: pip install coverage[toml]

- name: download coverage data
uses: actions/download-artifact@7a1cd3216ca9260cd8022db641d960b1db4d1be4 # v4.0.0
uses: actions/download-artifact@f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110 # v4.1.0
with:
path: all-artifacts/

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/conformance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
- name: install sigstore-python
run: python -m pip install .

- uses: sigstore/sigstore-conformance@c8d17eb7ee884cf86b93a3a3f471648fb0a83819 # v0.0.9
- uses: sigstore/sigstore-conformance@7375951316d6b28d07f7406c01e1dc7de2a75ce7 # v0.0.10
with:
entrypoint: ${{ github.workspace }}/test/integration/sigstore-python-conformance
xfail: "test_verify_with_trust_root" # see issue 821
xfail: "test_verify_with_trust_root test_verify_dsse_bundle_with_trust_root" # see issue 821
4 changes: 2 additions & 2 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
make doc
- name: upload docs artifact
uses: actions/upload-pages-artifact@a753861a5debcf57bf8b404356158c8e1e33150c # v2.0.0
uses: actions/upload-pages-artifact@0252fc4ba7626f0298f0cf00902a25c6afc77fa8 # v3.0.0
with:
path: ./html/

Expand All @@ -49,4 +49,4 @@ jobs:
url: ${{ steps.deployment.outputs.page_url }}
steps:
- id: deployment
uses: actions/deploy-pages@13b55b33dd8996121833dbc1db458c793a334630 # v3.0.1
uses: actions/deploy-pages@7a9bd943aa5e5175aeb8502edcc6c1c02d398e10 # v4.0.2
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ jobs:
id-token: write
steps:
- name: Download artifacts directories # goes to current working directory
uses: actions/download-artifact@7a1cd3216ca9260cd8022db641d960b1db4d1be4 # v4.0.0
uses: actions/download-artifact@f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110 # v4.1.0

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

- name: Upload artifacts to github
# Confusingly, this action also supports updating releases, not
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ lint = [
"mypy ~= 1.1",
# NOTE(ww): ruff is under active development, so we pin conservatively here
# and let Dependabot periodically perform this update.
"ruff < 0.1.9",
"ruff < 0.1.11",
"types-requests",
"types-protobuf",
"types-pyOpenSSL",
Expand Down
14 changes: 7 additions & 7 deletions sigstore/_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
RekorClient,
RekorKeyring,
)
from sigstore._internal.tuf import TrustUpdater
from sigstore._internal.trustroot import TrustedRoot
from sigstore._utils import PEMCert
from sigstore.errors import Error
from sigstore.oidc import (
Expand Down Expand Up @@ -650,16 +650,16 @@ def _sign(args: argparse.Namespace) -> None:
elif args.fulcio_url == DEFAULT_FULCIO_URL and args.rekor_url == DEFAULT_REKOR_URL:
signing_ctx = SigningContext.production()
else:
# Assume "production" keys if none are given as arguments
updater = TrustUpdater.production()
# Assume "production" trust root if no keys are given as arguments
trusted_root = TrustedRoot.production()
if args.ctfe_pem is not None:
ctfe_keys = [args.ctfe_pem.read()]
else:
ctfe_keys = updater.get_ctfe_keys()
ctfe_keys = trusted_root.get_ctfe_keys()
if args.rekor_root_pubkey is not None:
rekor_keys = [args.rekor_root_pubkey.read()]
else:
rekor_keys = updater.get_rekor_keys()
rekor_keys = trusted_root.get_rekor_keys()

ct_keyring = CTKeyring(Keyring(ctfe_keys))
rekor_keyring = RekorKeyring(Keyring(rekor_keys))
Expand Down Expand Up @@ -828,8 +828,8 @@ def _collect_verification_state(
if args.rekor_root_pubkey is not None:
rekor_keys = [args.rekor_root_pubkey.read()]
else:
updater = TrustUpdater.production()
rekor_keys = updater.get_rekor_keys()
trusted_root = TrustedRoot.production()
rekor_keys = trusted_root.get_rekor_keys()

verifier = Verifier(
rekor=RekorClient(
Expand Down
18 changes: 9 additions & 9 deletions sigstore/_internal/rekor/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@

from sigstore._internal.ctfe import CTKeyring
from sigstore._internal.keyring import Keyring
from sigstore._internal.tuf import TrustUpdater
from sigstore._internal.trustroot import TrustedRoot
from sigstore.transparency import LogEntry

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -249,14 +249,14 @@ def __del__(self) -> None:
self.session.close()

@classmethod
def production(cls, updater: TrustUpdater) -> RekorClient:
def production(cls, trust_root: TrustedRoot) -> RekorClient:
"""
Returns a `RekorClient` populated with the default Rekor production instance.
updater must be a `TrustUpdater` for the production TUF repository.
trust_root must be a `TrustedRoot` for the production TUF repository.
"""
rekor_keys = updater.get_rekor_keys()
ctfe_keys = updater.get_ctfe_keys()
rekor_keys = trust_root.get_rekor_keys()
ctfe_keys = trust_root.get_ctfe_keys()

return cls(
DEFAULT_REKOR_URL,
Expand All @@ -265,14 +265,14 @@ def production(cls, updater: TrustUpdater) -> RekorClient:
)

@classmethod
def staging(cls, updater: TrustUpdater) -> RekorClient:
def staging(cls, trust_root: TrustedRoot) -> RekorClient:
"""
Returns a `RekorClient` populated with the default Rekor staging instance.
updater must be a `TrustUpdater` for the staging TUF repository.
trust_root must be a `TrustedRoot` for the staging TUF repository.
"""
rekor_keys = updater.get_rekor_keys()
ctfe_keys = updater.get_ctfe_keys()
rekor_keys = trust_root.get_rekor_keys()
ctfe_keys = trust_root.get_ctfe_keys()

return cls(
STAGING_REKOR_URL,
Expand Down
150 changes: 150 additions & 0 deletions sigstore/_internal/trustroot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
# Copyright 2023 The Sigstore Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
Trust root management for sigstore-python.
"""

from __future__ import annotations

from datetime import datetime, timezone
from pathlib import Path
from typing import Iterable

from cryptography.x509 import Certificate, load_der_x509_certificate
from sigstore_protobuf_specs.dev.sigstore.common.v1 import TimeRange
from sigstore_protobuf_specs.dev.sigstore.trustroot.v1 import (
CertificateAuthority,
TransparencyLogInstance,
)
from sigstore_protobuf_specs.dev.sigstore.trustroot.v1 import (
TrustedRoot as _TrustedRoot,
)

from sigstore._internal.tuf import DEFAULT_TUF_URL, STAGING_TUF_URL, TrustUpdater
from sigstore.errors import MetadataError


def _is_timerange_valid(period: TimeRange | None, *, allow_expired: bool) -> bool:
"""
Given a `period`, checks that the the current time is not before `start`. If
`allow_expired` is `False`, also checks that the current time is not after
`end`.
"""
now = datetime.now(timezone.utc)

# If there was no validity period specified, the key is always valid.
if not period:
return True

# Active: if the current time is before the starting period, we are not yet
# valid.
if now < period.start:
return False

# If we want Expired keys, the key is valid at this point. Otherwise, check
# that we are within range.
return allow_expired or (period.end is None or now <= period.end)


class TrustedRoot(_TrustedRoot):
"""Complete set of trusted entities for a Sigstore client"""

@classmethod
def from_file(cls, path: str) -> "TrustedRoot":
"""Create a new trust root from file"""
tr: TrustedRoot = cls().from_json(Path(path).read_bytes())
return tr

@classmethod
def from_tuf(cls, url: str, offline: bool = False) -> "TrustedRoot":
"""Create a new trust root from a TUF repository.
If `offline`, will use trust root in local TUF cache. Otherwise will
update the trust root from remote TUF repository.
"""
path = TrustUpdater(url, offline).get_trusted_root_path()
return cls.from_file(path)

@classmethod
def production(cls, offline: bool = False) -> "TrustedRoot":
"""Create new trust root from Sigstore production TUF repository.
If `offline`, will use trust root in local TUF cache. Otherwise will
update the trust root from remote TUF repository.
"""
return cls.from_tuf(DEFAULT_TUF_URL, offline)

@classmethod
def staging(cls, offline: bool = False) -> "TrustedRoot":
"""Create new trust root from Sigstore staging TUF repository.
If `offline`, will use trust root in local TUF cache. Otherwise will
update the trust root from remote TUF repository.
"""
return cls.from_tuf(STAGING_TUF_URL, offline)

@staticmethod
def _get_tlog_keys(tlogs: list[TransparencyLogInstance]) -> Iterable[bytes]:
"""Return public key contents given transparency log instances."""

for key in tlogs:
if not _is_timerange_valid(key.public_key.valid_for, allow_expired=False):
continue
key_bytes = key.public_key.raw_bytes
if key_bytes:
yield key_bytes

@staticmethod
def _get_ca_keys(
cas: list[CertificateAuthority], *, allow_expired: bool
) -> Iterable[bytes]:
"""Return public key contents given certificate authorities."""

for ca in cas:
if not _is_timerange_valid(ca.valid_for, allow_expired=allow_expired):
continue
for cert in ca.cert_chain.certificates:
yield cert.raw_bytes

def get_ctfe_keys(self) -> list[bytes]:
"""Return the active CTFE public keys contents."""
ctfes: list[bytes] = list(self._get_tlog_keys(self.ctlogs))
if not ctfes:
raise MetadataError("Active CTFE keys not found in trusted root")
return ctfes

def get_rekor_keys(self) -> list[bytes]:
"""Return the rekor public key content."""
keys: list[bytes] = list(self._get_tlog_keys(self.tlogs))

if len(keys) != 1:
raise MetadataError("Did not find one active Rekor key in trusted root")
return keys

def get_fulcio_certs(self) -> list[Certificate]:
"""Return the Fulcio certificates."""

certs: list[Certificate]

# Return expired certificates too: they are expired now but may have
# been active when the certificate was used to sign.
certs = [
load_der_x509_certificate(c)
for c in self._get_ca_keys(self.certificate_authorities, allow_expired=True)
]

if not certs:
raise MetadataError("Fulcio certificates not found in trusted root")
return certs
Loading

0 comments on commit ffcfa5b

Please sign in to comment.