diff --git a/CHANGELOG.md b/CHANGELOG.md index 3dc1d9631..0b1c12671 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,12 @@ All versions prior to 0.9.0 are untracked. ## [Unreleased] +### Changed + +* CLI: When verifying, the `--offline` flag now fully disables all online + operations, including routine local TUF repository refreshes + ([#1143](https://github.com/sigstore/sigstore-python/pull/1143)) + ## [3.3.0] ### Added diff --git a/README.md b/README.md index 5cbd426d7..beeff520a 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,9 @@ else! * [Signing with ambient credentials](#signing-with-ambient-credentials) * [Signing with an email identity](#signing-with-an-email-identity) * [Signing with an explicit identity token](#signing-with-an-explicit-identity-token) - * [Verifying against a signature and certificate](#verifying-against-a-signature-and-certificate) + * [Verifying against a bundle](#verifying-against-a-bundle) + * [Offline verification](#offline-verification) + * [Verifying a digest instead of a file](#verifying-a-digest-instead-of-a-file) * [Verifying signatures from GitHub Actions](#verifying-signatures-from-github-actions) * [Licensing](#licensing) * [Community](#community) @@ -402,7 +404,7 @@ $ python -m sigstore sign --identity-token YOUR-LONG-JWT-HERE foo.txt Note that passing a custom identity token does not circumvent Fulcio's requirements, namely the Fulcio's supported identity providers and the claims expected within the token. -### Verifying against a signature and certificate +### Verifying against a bundle By default, `sigstore verify identity` will attempt to find a `.sigstore.json` or `.sigstore` in the same directory as the file being verified: @@ -423,6 +425,50 @@ $ python -m sigstore verify identity foo.txt bar.txt \ --cert-oidc-issuer 'https://github.com/login/oauth' ``` +### Offline verification + +> [!IMPORTANT] +> Because `--offline` disables trust root updates, `sigstore-python` falls back +> to the latest cached trust root or, if none exists, the trust root baked +> into `sigstore-python` itself. Like with any other offline verification, +> this means that users may miss trust root changes (such as new root keys, +> or revocations) unless they separately keep the trust root up-to-date. +> +> Users who need to operationalize offline verification may wish to do this +> by distributing their own trust configuration; see +> [Configuring a custom root of trust](#configuring-a-custom-root-of-trust-byo-pki). + +During verification, there are two kinds of network access that `sigstore-python` +*can* perform: + +1. When verifying against "detached" materials (e.g. separate `.crt` and `.sig` + files), `sigstore-python` can perform an online transparency log lookup. +2. By default, during all verifications, `sigstore-python` will attempt to + refresh the locally cached root of trust via a TUF update. + +When performing bundle verification (i.e. `.sigstore` or `.sigstore.json`), +(1) does not apply. However, (2) can still result in online accesses. + +To perform **fully** offline verification, pass `--offline` to your +`sigstore verify` subcommand: + +```bash +$ python -m sigstore verify identity foo.txt \ + --offline \ + --cert-identity 'hamilcar@example.com' \ + --cert-oidc-issuer 'https://github.com/login/oauth' +``` + +Alternatively, users may choose to bypass TUF entirely by passing +an entire trust configuration to `sigstore-python` via `--trust-config`: + +```bash +$ python -m sigstore --trust-config public.trustconfig.json verify identity ... +``` + +This will similarly result in fully offline operation, as the trust +configuration contains a full trust root. + ### Verifying a digest instead of a file `sigstore-python` supports verifying digests directly, without requiring the artifact to be diff --git a/sigstore/_cli.py b/sigstore/_cli.py index a98f2b99f..11be20317 100644 --- a/sigstore/_cli.py +++ b/sigstore/_cli.py @@ -996,12 +996,12 @@ def _collect_verification_state( if args.staging: _logger.debug("verify: staging instances requested") - verifier = Verifier.staging() + verifier = Verifier.staging(offline=args.offline) elif args.trust_config: trust_config = ClientTrustConfig.from_json(args.trust_config.read_text()) verifier = Verifier._from_trust_config(trust_config) else: - verifier = Verifier.production() + verifier = Verifier.production(offline=args.offline) all_materials = [] for file_or_hashed, materials in input_map.items(): diff --git a/sigstore/_internal/tuf.py b/sigstore/_internal/tuf.py index a0abdfd47..7116be700 100644 --- a/sigstore/_internal/tuf.py +++ b/sigstore/_internal/tuf.py @@ -115,7 +115,11 @@ def __init__(self, url: str, offline: bool = False) -> None: _logger.debug(f"TUF targets cache: {self._targets_dir}") self._updater: None | Updater = None - if not offline: + if offline: + _logger.warning( + "TUF repository is loaded in offline mode; updates will not be performed" + ) + else: # Initialize and update the toplevel TUF metadata self._updater = Updater( metadata_dir=str(self._metadata_dir), diff --git a/sigstore/verify/verifier.py b/sigstore/verify/verifier.py index 8f46df117..9e0b4e4ff 100644 --- a/sigstore/verify/verifier.py +++ b/sigstore/verify/verifier.py @@ -77,23 +77,23 @@ def __init__(self, *, rekor: RekorClient, trusted_root: TrustedRoot): self._trusted_root = trusted_root @classmethod - def production(cls) -> Verifier: + def production(cls, *, offline: bool = False) -> Verifier: """ Return a `Verifier` instance configured against Sigstore's production-level services. """ return cls( rekor=RekorClient.production(), - trusted_root=TrustedRoot.production(), + trusted_root=TrustedRoot.production(offline=offline), ) @classmethod - def staging(cls) -> Verifier: + def staging(cls, *, offline: bool = False) -> Verifier: """ Return a `Verifier` instance configured against Sigstore's staging-level services. """ return cls( rekor=RekorClient.staging(), - trusted_root=TrustedRoot.staging(), + trusted_root=TrustedRoot.staging(offline=offline), ) @classmethod diff --git a/test/unit/verify/test_verifier.py b/test/unit/verify/test_verifier.py index eb6c7c428..ad703c351 100644 --- a/test/unit/verify/test_verifier.py +++ b/test/unit/verify/test_verifier.py @@ -69,16 +69,27 @@ def test_verifier_multiple_verifications(signing_materials, null_policy): verifier.verify_artifact(file.read_bytes(), bundle, null_policy) +@pytest.mark.online @pytest.mark.parametrize( "filename", ("bundle.txt", "bundle_v3.txt", "bundle_v3_alt.txt") ) -def test_verifier_bundle(signing_bundle, null_policy, mock_staging_tuf, filename): +def test_verifier_bundle(signing_bundle, null_policy, filename): (file, bundle) = signing_bundle(filename) verifier = Verifier.staging() verifier.verify_artifact(file.read_bytes(), bundle, null_policy) +@pytest.mark.parametrize( + "filename", ("bundle.txt", "bundle_v3.txt", "bundle_v3_alt.txt") +) +def test_verifier_bundle_offline(signing_bundle, null_policy, filename): + (file, bundle) = signing_bundle(filename) + + verifier = Verifier.staging(offline=True) + verifier.verify_artifact(file.read_bytes(), bundle, null_policy) + + @pytest.mark.staging def test_verifier_email_identity(signing_materials): verifier = Verifier.staging()