From 11209d00fc871d70020b6cecd4433fe77e292a8a Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Wed, 4 Jan 2023 11:10:45 -0500 Subject: [PATCH 01/14] _cli: skeleton for `sigstore verify github` Signed-off-by: William Woodruff --- sigstore/_cli.py | 56 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/sigstore/_cli.py b/sigstore/_cli.py index 216aea6f..234822b7 100644 --- a/sigstore/_cli.py +++ b/sigstore/_cli.py @@ -367,6 +367,49 @@ def _parser() -> argparse.ArgumentParser: help="The file to verify", ) + # `sigstore verify github` + verify_github = verify_subcommand.add_parser( + "github", + help="verify against GitHub Actions-specific claims", + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + + verify_github.add_argument( + "--workflow-trigger", + metavar="EVENT", + type=str, + default=os.getenv("SIGSTORE_VERIFY_GITHUB_WORKFLOW_TRIGGER"), + help="The GitHub Actions event name that triggered the workflow", + ) + verify_github.add_argument( + "--workflow-sha", + metavar="SHA", + type=str, + default=os.getenv("SIGSTORE_VERIFY_GITHUB_WORKFLOW_SHA"), + help="The `git` commit SHA that the workflow run was invoked with", + ) + verify_github.add_argument( + "--workflow-name", + metavar="NAME", + type=str, + default=os.getenv("SIGSTORE_VERIFY_GITHUB_WORKFLOW_NAME"), + help="The name of the workflow that was triggered", + ) + verify_github.add_argument( + "--workflow-repository", + metavar="REPO", + type=str, + default=os.getenv("SIGSTORE_VERIFY_GITHUB_WORKFLOW_REPOSITORY"), + help="The repository slug that the workflow was triggered under", + ) + verify_github.add_argument( + "--workflow-ref", + metavar="REF", + type=str, + default=os.getenv("SIGSTORE_VERIFY_GITHUB_WORKFLOW_REF"), + help="The `git` ref that the workflow was invoked with", + ) + # `sigstore verify` defaults to `sigstore verify identity`, for backwards # compatibility. _set_default_verify_subparser(verify, "identity") @@ -397,7 +440,12 @@ def main() -> None: if args.subcommand == "sign": _sign(args) elif args.subcommand == "verify": - _verify(args) + if args.verify_subcommand == "identity": + _verify_identity(args) + elif args.verify_subcommand == "github": + _verify_github(args) + else: + parser.error(f"Unknown verify subcommand: {args.verify_subcommand}") elif args.subcommand == "get-identity-token": token = _get_identity_token(args) if token: @@ -538,7 +586,7 @@ def _sign(args: argparse.Namespace) -> None: print(f"Rekor bundle written to {outputs['bundle']}") -def _verify(args: argparse.Namespace) -> None: +def _verify_identity(args: argparse.Namespace) -> None: # `--cert-email` has been functionally removed, but we check for it # explicitly to provide a nicer error message than just a missing # option. @@ -734,6 +782,10 @@ def _verify(args: argparse.Namespace) -> None: sys.exit(1) +def _verify_github(args: argparse.Namespace) -> None: + pass + + def _get_identity_token(args: argparse.Namespace) -> Optional[str]: token = None if not args.oidc_disable_ambient_providers: From 7da7c0bbacf45d2d83cb3d93ba1ba6979641a5c3 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Wed, 4 Jan 2023 14:05:05 -0500 Subject: [PATCH 02/14] _cli: reorg options, devolve into more groups Signed-off-by: William Woodruff --- sigstore/_cli.py | 149 ++++++++++++++++++++++++++++------------------- 1 file changed, 90 insertions(+), 59 deletions(-) diff --git a/sigstore/_cli.py b/sigstore/_cli.py index 234822b7..a71d47c8 100644 --- a/sigstore/_cli.py +++ b/sigstore/_cli.py @@ -120,6 +120,9 @@ def _set_default_verify_subparser(parser: argparse.ArgumentParser, name: str) -> def _add_shared_instance_options(group: argparse._ArgumentGroup) -> None: + """ + Common Sigstore instance options, shared between all `sigstore` subcommands. + """ group.add_argument( "--staging", action="store_true", @@ -142,9 +145,64 @@ def _add_shared_instance_options(group: argparse._ArgumentGroup) -> None: ) +def _add_shared_input_options(group: argparse._ArgumentGroup) -> None: + """ + Common input options, shared between all `sigstore verify` subcommands. + """ + group.add_argument( + "--certificate", + "--cert", + metavar="FILE", + type=Path, + default=os.getenv("SIGSTORE_CERTIFICATE"), + help="The PEM-encoded certificate to verify against; not used with multiple inputs", + ) + group.add_argument( + "--signature", + metavar="FILE", + type=Path, + default=os.getenv("SIGSTORE_SIGNATURE"), + help="The signature to verify against; not used with multiple inputs", + ) + group.add_argument( + "--rekor-bundle", + metavar="FILE", + type=Path, + default=os.getenv("SIGSTORE_REKOR_BUNDLE"), + help="The offline Rekor bundle to verify with; not used with multiple inputs", + ) + group.add_argument( + "files", + metavar="FILE", + type=Path, + nargs="+", + help="The file to verify", + ) + + +def _add_shared_verification_options(group: argparse._ArgumentGroup) -> None: + group.add_argument( + "--cert-identity", + metavar="IDENTITY", + type=str, + default=os.getenv("SIGSTORE_CERT_IDENTITY"), + help="The identity to check for in the certificate's Subject Alternative Name", + required=True, + ) + group.add_argument( + "--require-rekor-offline", + action="store_true", + default=_boolify_env("SIGSTORE_REQUIRE_REKOR_OFFLINE"), + help="Require offline Rekor verification with a bundle; implied by --rekor-bundle", + ) + + def _add_shared_oidc_options( group: Union[argparse._ArgumentGroup, argparse.ArgumentParser] ) -> None: + """ + Common OIDC options, shared between `sigstore sign` and `sigstore get-identity-token`. + """ group.add_argument( "--oidc-client-id", metavar="ID", @@ -292,55 +350,16 @@ def _parser() -> argparse.ArgumentParser: formatter_class=argparse.ArgumentDefaultsHelpFormatter, ) input_options = verify_identity.add_argument_group("Verification inputs") - input_options.add_argument( - "--certificate", - "--cert", - metavar="FILE", - type=Path, - default=os.getenv("SIGSTORE_CERTIFICATE"), - help="The PEM-encoded certificate to verify against; not used with multiple inputs", - ) - input_options.add_argument( - "--signature", - metavar="FILE", - type=Path, - default=os.getenv("SIGSTORE_SIGNATURE"), - help="The signature to verify against; not used with multiple inputs", - ) - input_options.add_argument( - "--rekor-bundle", - metavar="FILE", - type=Path, - default=os.getenv("SIGSTORE_REKOR_BUNDLE"), - help="The offline Rekor bundle to verify with; not used with multiple inputs", - ) + _add_shared_input_options(input_options) - verification_options = verify_identity.add_argument_group( - "Extended verification options" - ) - verification_options.add_argument( - "--certificate-chain", - metavar="FILE", - type=argparse.FileType("r"), - help=( - "Path to a list of CA certificates in PEM format which will be needed when building " - "the certificate chain for the signing certificate" - ), - ) + verification_options = verify_identity.add_argument_group("Verification options") + _add_shared_verification_options(verification_options) verification_options.add_argument( "--cert-email", metavar="EMAIL", type=str, help="Deprecated; causes an error. Use --cert-identity instead", ) - verification_options.add_argument( - "--cert-identity", - metavar="IDENTITY", - type=str, - default=os.getenv("SIGSTORE_CERT_IDENTITY"), - help="The identity to check for in the certificate's Subject Alternative Name", - required=True, - ) verification_options.add_argument( "--cert-oidc-issuer", metavar="URL", @@ -349,22 +368,17 @@ def _parser() -> argparse.ArgumentParser: help="The OIDC issuer URL to check for in the certificate's OIDC issuer extension", required=True, ) - verification_options.add_argument( - "--require-rekor-offline", - action="store_true", - default=_boolify_env("SIGSTORE_REQUIRE_REKOR_OFFLINE"), - help="Require offline Rekor verification with a bundle; implied by --rekor-bundle", - ) instance_options = verify_identity.add_argument_group("Sigstore instance options") _add_shared_instance_options(instance_options) - - verify_identity.add_argument( - "files", + instance_options.add_argument( + "--certificate-chain", metavar="FILE", - type=Path, - nargs="+", - help="The file to verify", + type=argparse.FileType("r"), + help=( + "Path to a list of CA certificates in PEM format which will be needed when building " + "the certificate chain for the Fulcio signing certificate" + ), ) # `sigstore verify github` @@ -374,35 +388,40 @@ def _parser() -> argparse.ArgumentParser: formatter_class=argparse.ArgumentDefaultsHelpFormatter, ) - verify_github.add_argument( + input_options = verify_github.add_argument_group("Verification inputs") + _add_shared_input_options(input_options) + + verification_options = verify_github.add_argument_group("Verification options") + _add_shared_verification_options(verification_options) + verification_options.add_argument( "--workflow-trigger", metavar="EVENT", type=str, default=os.getenv("SIGSTORE_VERIFY_GITHUB_WORKFLOW_TRIGGER"), help="The GitHub Actions event name that triggered the workflow", ) - verify_github.add_argument( + verification_options.add_argument( "--workflow-sha", metavar="SHA", type=str, default=os.getenv("SIGSTORE_VERIFY_GITHUB_WORKFLOW_SHA"), help="The `git` commit SHA that the workflow run was invoked with", ) - verify_github.add_argument( + verification_options.add_argument( "--workflow-name", metavar="NAME", type=str, default=os.getenv("SIGSTORE_VERIFY_GITHUB_WORKFLOW_NAME"), help="The name of the workflow that was triggered", ) - verify_github.add_argument( + verification_options.add_argument( "--workflow-repository", metavar="REPO", type=str, default=os.getenv("SIGSTORE_VERIFY_GITHUB_WORKFLOW_REPOSITORY"), help="The repository slug that the workflow was triggered under", ) - verify_github.add_argument( + verification_options.add_argument( "--workflow-ref", metavar="REF", type=str, @@ -410,6 +429,18 @@ def _parser() -> argparse.ArgumentParser: help="The `git` ref that the workflow was invoked with", ) + instance_options = verify_github.add_argument_group("Sigstore instance options") + _add_shared_instance_options(instance_options) + instance_options.add_argument( + "--certificate-chain", + metavar="FILE", + type=argparse.FileType("r"), + help=( + "Path to a list of CA certificates in PEM format which will be needed when building " + "the certificate chain for the Fulcio signing certificate" + ), + ) + # `sigstore verify` defaults to `sigstore verify identity`, for backwards # compatibility. _set_default_verify_subparser(verify, "identity") From 60bb89ec7999b229a77aae7fc4feb44408a1b4b8 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Wed, 4 Jan 2023 16:54:51 -0500 Subject: [PATCH 03/14] _cli: more factoring out, basic GH functionality Signed-off-by: William Woodruff --- sigstore/_cli.py | 142 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 127 insertions(+), 15 deletions(-) diff --git a/sigstore/_cli.py b/sigstore/_cli.py index a71d47c8..f5626148 100644 --- a/sigstore/_cli.py +++ b/sigstore/_cli.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations + import argparse import base64 import logging @@ -617,15 +619,16 @@ def _sign(args: argparse.Namespace) -> None: print(f"Rekor bundle written to {outputs['bundle']}") -def _verify_identity(args: argparse.Namespace) -> None: - # `--cert-email` has been functionally removed, but we check for it - # explicitly to provide a nicer error message than just a missing - # option. - if args.cert_email: - args._parser.error( - "--cert-email is a disabled alias for --cert-identity; " - "use --cert-identity instead" - ) +def _collect_verification_state( + args: argparse.Namespace, +) -> tuple[Verifier, list[tuple[Path, VerificationMaterials]]]: + """ + Performs CLI functionality common across all `sigstore verify` subcommands. + + Returns a tuple of the active verifier instance and a list of `(file, materials)` + tuples, where `file` is the path to the file being verified (for display + purposes) and `materials` is the `VerificationMaterials` to verify with. + """ # `--rekor-bundle` is a temporary option, pending stabilization of the # Sigstore bundle format. @@ -715,6 +718,7 @@ def _verify_identity(args: argparse.Namespace) -> None: fulcio_certificate_chain=certificate_chain, ) + all_materials = [] for file, inputs in input_map.items(): # Load the signing certificate logger.debug(f"Using certificate from: {inputs['cert']}") @@ -733,13 +737,34 @@ def _verify_identity(args: argparse.Namespace) -> None: logger.debug(f"Verifying contents from: {file}") with file.open(mode="rb", buffering=0) as io: - materials = VerificationMaterials( - input_=io, - cert_pem=cert_pem, - signature=base64.b64decode(b64_signature), - offline_rekor_entry=entry, + all_materials.append( + ( + file, + VerificationMaterials( + input_=io, + cert_pem=cert_pem, + signature=base64.b64decode(b64_signature), + offline_rekor_entry=entry, + ), + ) ) + return (verifier, all_materials) + + +def _verify_identity(args: argparse.Namespace) -> None: + # `--cert-email` has been functionally removed, but we check for it + # explicitly to provide a nicer error message than just a missing + # option. + if args.cert_email: + args._parser.error( + "--cert-email is a disabled alias for --cert-identity; " + "use --cert-identity instead" + ) + + verifier, files_with_materials = _collect_verification_state(args) + + for (file, materials) in files_with_materials: policy_ = policy.Identity( identity=args.cert_identity, issuer=args.cert_oidc_issuer, @@ -814,7 +839,94 @@ def _verify_identity(args: argparse.Namespace) -> None: def _verify_github(args: argparse.Namespace) -> None: - pass + # Every GitHub verification begins with an identity policy, + # for which we know the issuer URL ahead of time. + # We then add more policies, as configured by the user's passed-in options. + inner_policies: list[policy.VerificationPolicy] = [ + policy.Identity( + identity=args.cert_identity, + issuer="https://token.actions.githubusercontent.com", + ) + ] + + if args.workflow_trigger: + inner_policies.append(policy.GitHubWorkflowTrigger(args.workflow_trigger)) + if args.workflow_sha: + inner_policies.append(policy.GitHubWorkflowSHA(args.workflow_sha)) + if args.workflow_name: + inner_policies.append(policy.GitHubWorkflowName(args.workflow_name)) + if args.workflow_repository: + inner_policies.append(policy.GitHubWorkflowRepository(args.workflow_repository)) + if args.workflow_ref: + inner_policies.append(policy.GitHubWorkflowRef(args.workflow_ref)) + + policy_ = policy.AllOf(inner_policies) + + verifier, files_with_materials = _collect_verification_state(args) + for (file, materials) in files_with_materials: + result = verifier.verify(materials=materials, policy=policy_) + + if result: + print(f"OK: {file}") + else: + result = cast(VerificationFailure, result) + print(f"FAIL: {file}") + print(f"Failure reason: {result.reason}", file=sys.stderr) + + if isinstance(result, CertificateVerificationFailure): + # If certificate verification failed, it's either because of + # a chain issue or some outdated state in sigstore itself. + # These might already be resolved in a newer version, so + # we suggest that users try to upgrade and retry before + # anything else. + print( + dedent( + f""" + This may be a result of an outdated `sigstore` installation. + + Consider upgrading with: + + python -m pip install --upgrade sigstore + + Additional context: + + {result.exception} + """ + ), + file=sys.stderr, + ) + elif isinstance(result, RekorEntryMissing): + # If Rekor lookup failed, it's because the certificate either + # wasn't logged after creation or because the user requested the + # wrong Rekor instance (e.g., staging instead of production). + # The latter is significantly more likely, so we add + # some additional context to the output indicating it. + # + # NOTE: Even though the latter is more likely, it's still extremely + # unlikely that we'd hit this -- we should always fail with + # `CertificateVerificationFailure` instead, as the cert store should + # fail to validate due to a mismatch between the leaf and the trusted + # root + intermediates. + print( + dedent( + f""" + These signing artifacts could not be matched to a entry + in the configured transparency log. + + This may be a result of connecting to the wrong Rekor instance + (for example, staging instead of production, or vice versa). + + Additional context: + + Signature: {result.signature} + + Artifact hash: {result.artifact_hash} + """ + ), + file=sys.stderr, + ) + + sys.exit(1) def _get_identity_token(args: argparse.Namespace) -> Optional[str]: From b8d759f6d494e7c19a309561dedacf0109876cb6 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Wed, 4 Jan 2023 17:10:09 -0500 Subject: [PATCH 04/14] README, Makefile: `sigstore verify github --help` Signed-off-by: William Woodruff --- Makefile | 10 ++++++ README.md | 98 ++++++++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 89 insertions(+), 19 deletions(-) diff --git a/Makefile b/Makefile index 44f3c4ca..1bdf78b6 100644 --- a/Makefile +++ b/Makefile @@ -127,6 +127,16 @@ check-readme: $(MAKE) -s run ARGS="verify identity --help" \ ) + # sigstore verify github --help + @diff \ + <( \ + awk '/@begin-sigstore-verify-github-help@/{f=1;next} /@end-sigstore-verify-github-help@/{f=0} f' \ + < README.md | sed '1d;$$d' \ + ) \ + <( \ + $(MAKE) -s run ARGS="verify github --help" \ + ) + .PHONY: edit edit: diff --git a/README.md b/README.md index 1f510413..705f9fa0 100644 --- a/README.md +++ b/README.md @@ -153,7 +153,7 @@ Sigstore instance options: ### Verifying -#### Identities +#### Generic identities This is the most common verification done with `sigstore`, and therefore the one you probably want: you can use it to verify that a signature was @@ -163,17 +163,13 @@ to by a particular OIDC provider (like `https://github.com/login/oauth`). ``` usage: sigstore verify identity [-h] [--certificate FILE] [--signature FILE] - [--rekor-bundle FILE] - [--certificate-chain FILE] - [--cert-email EMAIL] --cert-identity IDENTITY - --cert-oidc-issuer URL - [--require-rekor-offline] [--staging] + [--rekor-bundle FILE] --cert-identity IDENTITY + [--require-rekor-offline] [--cert-email EMAIL] + --cert-oidc-issuer URL [--staging] [--rekor-url URL] [--rekor-root-pubkey FILE] + [--certificate-chain FILE] FILE [FILE ...] -positional arguments: - FILE The file to verify - optional arguments: -h, --help show this help message and exit @@ -185,23 +181,20 @@ Verification inputs: multiple inputs (default: None) --rekor-bundle FILE The offline Rekor bundle to verify with; not used with multiple inputs (default: None) + FILE The file to verify -Extended verification options: - --certificate-chain FILE - Path to a list of CA certificates in PEM format which - will be needed when building the certificate chain for - the signing certificate (default: None) - --cert-email EMAIL Deprecated; causes an error. Use --cert-identity - instead (default: None) +Verification options: --cert-identity IDENTITY The identity to check for in the certificate's Subject Alternative Name (default: None) - --cert-oidc-issuer URL - The OIDC issuer URL to check for in the certificate's - OIDC issuer extension (default: None) --require-rekor-offline Require offline Rekor verification with a bundle; implied by --rekor-bundle (default: False) + --cert-email EMAIL Deprecated; causes an error. Use --cert-identity + instead (default: None) + --cert-oidc-issuer URL + The OIDC issuer URL to check for in the certificate's + OIDC issuer extension (default: None) Sigstore instance options: --staging Use sigstore's staging instances, instead of the @@ -211,6 +204,10 @@ Sigstore instance options: --rekor-root-pubkey FILE A PEM-encoded root public key for Rekor itself (conflicts with --staging) (default: None) + --certificate-chain FILE + Path to a list of CA certificates in PEM format which + will be needed when building the certificate chain for + the Fulcio signing certificate (default: None) ``` @@ -218,6 +215,69 @@ For backwards compatibility, `sigstore verify [args ...]` is equivalent to `sigstore verify identity [args ...]`, but the latter form is **strongly** preferred. +#### Signatures from GitHub Actions + + +``` +usage: sigstore verify github [-h] [--certificate FILE] [--signature FILE] + [--rekor-bundle FILE] --cert-identity IDENTITY + [--require-rekor-offline] + [--workflow-trigger EVENT] [--workflow-sha SHA] + [--workflow-name NAME] + [--workflow-repository REPO] + [--workflow-ref REF] [--staging] + [--rekor-url URL] [--rekor-root-pubkey FILE] + [--certificate-chain FILE] + FILE [FILE ...] + +optional arguments: + -h, --help show this help message and exit + +Verification inputs: + --certificate FILE, --cert FILE + The PEM-encoded certificate to verify against; not + used with multiple inputs (default: None) + --signature FILE The signature to verify against; not used with + multiple inputs (default: None) + --rekor-bundle FILE The offline Rekor bundle to verify with; not used with + multiple inputs (default: None) + FILE The file to verify + +Verification options: + --cert-identity IDENTITY + The identity to check for in the certificate's Subject + Alternative Name (default: None) + --require-rekor-offline + Require offline Rekor verification with a bundle; + implied by --rekor-bundle (default: False) + --workflow-trigger EVENT + The GitHub Actions event name that triggered the + workflow (default: None) + --workflow-sha SHA The `git` commit SHA that the workflow run was invoked + with (default: None) + --workflow-name NAME The name of the workflow that was triggered (default: + None) + --workflow-repository REPO + The repository slug that the workflow was triggered + under (default: None) + --workflow-ref REF The `git` ref that the workflow was invoked with + (default: None) + +Sigstore instance options: + --staging Use sigstore's staging instances, instead of the + default production instances (default: False) + --rekor-url URL The Rekor instance to use (conflicts with --staging) + (default: https://rekor.sigstore.dev) + --rekor-root-pubkey FILE + A PEM-encoded root public key for Rekor itself + (conflicts with --staging) (default: None) + --certificate-chain FILE + Path to a list of CA certificates in PEM format which + will be needed when building the certificate chain for + the Fulcio signing certificate (default: None) +``` + + ## Example uses `sigstore` supports a wide variety of workflows and usages. Some common ones are From 6e9ae7f299771d365a5f8734dc82c365a73af0bd Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Wed, 4 Jan 2023 17:12:15 -0500 Subject: [PATCH 05/14] README: more docs Signed-off-by: William Woodruff --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 705f9fa0..57660dbf 100644 --- a/README.md +++ b/README.md @@ -217,6 +217,11 @@ preferred. #### Signatures from GitHub Actions +If your signatures are coming from GitHub Actions (e.g., a workflow +that uses its [ambient credentials](#signing-with-ambient-credentials)), +then you can use the `sigstore verify github` subcommand to verify +claims more precisely than `sigstore verify identity` allows: + ``` usage: sigstore verify github [-h] [--certificate FILE] [--signature FILE] From 92cec92a20b9c6fbb9c3bb387451a14f43d54543 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Wed, 4 Jan 2023 18:33:01 -0500 Subject: [PATCH 06/14] sigstore, README: remove `--cert-email` Long deprecated. Signed-off-by: William Woodruff --- README.md | 8 +++----- sigstore/_cli.py | 15 --------------- 2 files changed, 3 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 57660dbf..976483b5 100644 --- a/README.md +++ b/README.md @@ -164,9 +164,9 @@ to by a particular OIDC provider (like `https://github.com/login/oauth`). ``` usage: sigstore verify identity [-h] [--certificate FILE] [--signature FILE] [--rekor-bundle FILE] --cert-identity IDENTITY - [--require-rekor-offline] [--cert-email EMAIL] - --cert-oidc-issuer URL [--staging] - [--rekor-url URL] [--rekor-root-pubkey FILE] + [--require-rekor-offline] --cert-oidc-issuer + URL [--staging] [--rekor-url URL] + [--rekor-root-pubkey FILE] [--certificate-chain FILE] FILE [FILE ...] @@ -190,8 +190,6 @@ Verification options: --require-rekor-offline Require offline Rekor verification with a bundle; implied by --rekor-bundle (default: False) - --cert-email EMAIL Deprecated; causes an error. Use --cert-identity - instead (default: None) --cert-oidc-issuer URL The OIDC issuer URL to check for in the certificate's OIDC issuer extension (default: None) diff --git a/sigstore/_cli.py b/sigstore/_cli.py index f5626148..c74e27af 100644 --- a/sigstore/_cli.py +++ b/sigstore/_cli.py @@ -356,12 +356,6 @@ def _parser() -> argparse.ArgumentParser: verification_options = verify_identity.add_argument_group("Verification options") _add_shared_verification_options(verification_options) - verification_options.add_argument( - "--cert-email", - metavar="EMAIL", - type=str, - help="Deprecated; causes an error. Use --cert-identity instead", - ) verification_options.add_argument( "--cert-oidc-issuer", metavar="URL", @@ -753,15 +747,6 @@ def _collect_verification_state( def _verify_identity(args: argparse.Namespace) -> None: - # `--cert-email` has been functionally removed, but we check for it - # explicitly to provide a nicer error message than just a missing - # option. - if args.cert_email: - args._parser.error( - "--cert-email is a disabled alias for --cert-identity; " - "use --cert-identity instead" - ) - verifier, files_with_materials = _collect_verification_state(args) for (file, materials) in files_with_materials: From d95c093c5c84d4a7073ecd2687ddb0e73ad374d3 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Wed, 4 Jan 2023 18:46:13 -0500 Subject: [PATCH 07/14] _cli: tweak "helpful" error messages Now that we manage keys with TUF, the most likely error here is misconfiguration: someone asking us to verify a sig/cert that was issued against a different instance of Fulcio than we're verifying with. Signed-off-by: William Woodruff --- sigstore/_cli.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/sigstore/_cli.py b/sigstore/_cli.py index c74e27af..a2d28241 100644 --- a/sigstore/_cli.py +++ b/sigstore/_cli.py @@ -776,11 +776,11 @@ def _verify_identity(args: argparse.Namespace) -> None: print( dedent( f""" - This may be a result of an outdated `sigstore` installation. + The given certificate could not be verified against the + root of trust. - Consider upgrading with: - - python -m pip install --upgrade sigstore + This may be a result of connecting to the wrong Fulcio instance + (for example, staging instead of production, or vice versa). Additional context: @@ -867,11 +867,11 @@ def _verify_github(args: argparse.Namespace) -> None: print( dedent( f""" - This may be a result of an outdated `sigstore` installation. + The given certificate could not be verified against the + root of trust. - Consider upgrading with: - - python -m pip install --upgrade sigstore + This may be a result of connecting to the wrong Fulcio instance + (for example, staging instead of production, or vice versa). Additional context: From d523f9cf27df8d64fcee8115b15fa607dc5ee5c5 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Thu, 5 Jan 2023 11:08:58 -0500 Subject: [PATCH 08/14] _cli, README: tweak `sigstore verify github` flags Signed-off-by: William Woodruff --- README.md | 22 +++++++++------------- sigstore/_cli.py | 15 ++++++++++----- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 976483b5..48a0b398 100644 --- a/README.md +++ b/README.md @@ -224,12 +224,10 @@ claims more precisely than `sigstore verify identity` allows: ``` usage: sigstore verify github [-h] [--certificate FILE] [--signature FILE] [--rekor-bundle FILE] --cert-identity IDENTITY - [--require-rekor-offline] - [--workflow-trigger EVENT] [--workflow-sha SHA] - [--workflow-name NAME] - [--workflow-repository REPO] - [--workflow-ref REF] [--staging] - [--rekor-url URL] [--rekor-root-pubkey FILE] + [--require-rekor-offline] [--trigger EVENT] + [--sha SHA] [--name NAME] [--repository REPO] + [--ref REF] [--staging] [--rekor-url URL] + [--rekor-root-pubkey FILE] [--certificate-chain FILE] FILE [FILE ...] @@ -253,17 +251,15 @@ Verification options: --require-rekor-offline Require offline Rekor verification with a bundle; implied by --rekor-bundle (default: False) - --workflow-trigger EVENT - The GitHub Actions event name that triggered the + --trigger EVENT The GitHub Actions event name that triggered the workflow (default: None) - --workflow-sha SHA The `git` commit SHA that the workflow run was invoked + --sha SHA The `git` commit SHA that the workflow run was invoked with (default: None) - --workflow-name NAME The name of the workflow that was triggered (default: + --name NAME The name of the workflow that was triggered (default: None) - --workflow-repository REPO - The repository slug that the workflow was triggered + --repository REPO The repository slug that the workflow was triggered under (default: None) - --workflow-ref REF The `git` ref that the workflow was invoked with + --ref REF The `git` ref that the workflow was invoked with (default: None) Sigstore instance options: diff --git a/sigstore/_cli.py b/sigstore/_cli.py index a2d28241..c8004a7c 100644 --- a/sigstore/_cli.py +++ b/sigstore/_cli.py @@ -390,35 +390,40 @@ def _parser() -> argparse.ArgumentParser: verification_options = verify_github.add_argument_group("Verification options") _add_shared_verification_options(verification_options) verification_options.add_argument( - "--workflow-trigger", + "--trigger", + dest="workflow_trigger", metavar="EVENT", type=str, default=os.getenv("SIGSTORE_VERIFY_GITHUB_WORKFLOW_TRIGGER"), help="The GitHub Actions event name that triggered the workflow", ) verification_options.add_argument( - "--workflow-sha", + "--sha", + dest="workflow_sha", metavar="SHA", type=str, default=os.getenv("SIGSTORE_VERIFY_GITHUB_WORKFLOW_SHA"), help="The `git` commit SHA that the workflow run was invoked with", ) verification_options.add_argument( - "--workflow-name", + "--name", + dest="workflow_name", metavar="NAME", type=str, default=os.getenv("SIGSTORE_VERIFY_GITHUB_WORKFLOW_NAME"), help="The name of the workflow that was triggered", ) verification_options.add_argument( - "--workflow-repository", + "--repository", + dest="workflow_repository", metavar="REPO", type=str, default=os.getenv("SIGSTORE_VERIFY_GITHUB_WORKFLOW_REPOSITORY"), help="The repository slug that the workflow was triggered under", ) verification_options.add_argument( - "--workflow-ref", + "--ref", + dest="workflow_ref", metavar="REF", type=str, default=os.getenv("SIGSTORE_VERIFY_GITHUB_WORKFLOW_REF"), From 1cc40e0e46fa2d2bb3f3fab20a4ab2e893fc6989 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Mon, 9 Jan 2023 11:46:32 -0500 Subject: [PATCH 09/14] CHANGELOG: record changes Signed-off-by: William Woodruff --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e914b5e2..c85f18ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,10 @@ All versions prior to 0.9.0 are untracked. version of `sigstore-python` ([#383](https://github.com/sigstore/sigstore-python/pull/383)) +* `sigstore verify github` has been added, allowing for verification of + GitHub-specific claims within given certificate(s) + ([#381](https://github.com/sigstore/sigstore-python/pull/381)) + ### Changed * The default behavior of `SIGSTORE_LOGLEVEL` has changed; the logger From d9cf303984605d4acb6b6e15c3b1a4398ab14bbe Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Mon, 9 Jan 2023 11:54:17 -0500 Subject: [PATCH 10/14] CHANGELOG, cli: record more changes Signed-off-by: William Woodruff --- CHANGELOG.md | 6 ++++++ sigstore/_cli.py | 33 +++++++++++++++++++++++++++++++-- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c85f18ec..cf1d7b35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,12 @@ All versions prior to 0.9.0 are untracked. version of `sigstore-python` ([#383](https://github.com/sigstore/sigstore-python/pull/383)) +* The per-subcommand options `--rekor-url` and `--rekor-root-pubkey` have been + moved to the top-level `sigstore` command. Their subcommand forms are unchanged + and will continue to work, but will be marked deprecated in a future stable + version of `sigstore-python` + ([#381](https://github.com/sigstore/sigstore-python/pull/383)) + * `sigstore verify github` has been added, allowing for verification of GitHub-specific claims within given certificate(s) ([#381](https://github.com/sigstore/sigstore-python/pull/381)) diff --git a/sigstore/_cli.py b/sigstore/_cli.py index a1099e28..2023eb05 100644 --- a/sigstore/_cli.py +++ b/sigstore/_cli.py @@ -135,6 +135,7 @@ def _add_shared_instance_options(group: argparse._ArgumentGroup) -> None: ) group.add_argument( "--rekor-url", + dest="__deprecated_rekor_url", metavar="URL", type=str, default=os.getenv("SIGSTORE_REKOR_URL", DEFAULT_REKOR_URL), @@ -142,6 +143,7 @@ def _add_shared_instance_options(group: argparse._ArgumentGroup) -> None: ) group.add_argument( "--rekor-root-pubkey", + dest="__deprecated_rekor_root_pubkey", metavar="FILE", type=argparse.FileType("rb"), help="A PEM-encoded root public key for Rekor itself (conflicts with --staging)", @@ -260,6 +262,20 @@ def _parser() -> argparse.ArgumentParser: default=_boolify_env("SIGSTORE_STAGING"), help="Use sigstore's staging instances, instead of the default production instances", ) + global_instance_options.add_argument( + "--rekor-url", + metavar="URL", + type=str, + default=os.getenv("SIGSTORE_REKOR_URL", DEFAULT_REKOR_URL), + help="The Rekor instance to use (conflicts with --staging)", + ) + global_instance_options.add_argument( + "--rekor-root-pubkey", + metavar="FILE", + type=argparse.FileType("rb"), + help="A PEM-encoded root public key for Rekor itself (conflicts with --staging)", + default=os.getenv("SIGSTORE_REKOR_ROOT_PUBKEY"), + ) subcommands = parser.add_subparsers(required=True, dest="subcommand") @@ -476,14 +492,27 @@ def main() -> None: logger.debug(f"parsed arguments {args}") - # `sigstore --staging some-cmd` is now the preferred form, rather than - # `sigstore some-cmd --staging`. + # A few instance flags (like `--staging` and `--rekor-url`) are supported at both the + # top-level `sigstore` level and the subcommand level (e.g. `sigstore verify --staging`), + # but the former is preferred. if getattr(args, "__deprecated_staging", False): logger.warning( "`--staging` should be used as a global option, rather than a subcommand option. " "Passing `--staging` as a subcommand option will be deprecated in a future release." ) args.staging = args.__deprecated_staging + if hasattr(args, "__deprecated_rekor_url"): + logger.warning( + "`--rekor-url` should be used as a global option, rather than a subcommand option. " + "Passing `--rekor-url` as a subcommand option will be deprecated in a future release." + ) + args.rekor_url = args.__deprecated_rekor_url + if hasattr(args, "__deprecated_rekor_root_pubkey"): + logger.warning( + "`--rekor-root-pubkey` should be used as a global option, rather than a subcommand option. " + "Passing `--rekor-root-pubkey` as a subcommand option will be deprecated in a future release." + ) + args.rekor_root_pubkey = args.__deprecated_rekor_root_pubkey # Stuff the parser back into our namespace, so that we can use it for # error handling later. From a5ec46cb9ddf02a7a36990f3d63e15f92ab2a574 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Mon, 9 Jan 2023 12:42:47 -0500 Subject: [PATCH 11/14] _cli: add notes to help text Signed-off-by: William Woodruff --- sigstore/_cli.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/sigstore/_cli.py b/sigstore/_cli.py index 2023eb05..49410b3a 100644 --- a/sigstore/_cli.py +++ b/sigstore/_cli.py @@ -139,15 +139,23 @@ def _add_shared_instance_options(group: argparse._ArgumentGroup) -> None: metavar="URL", type=str, default=os.getenv("SIGSTORE_REKOR_URL", DEFAULT_REKOR_URL), - help="The Rekor instance to use (conflicts with --staging)", + help=( + "The Rekor instance to use (conflicts with --staging). " + "This option will be deprecated in favor of the global `--rekor-url` option " + "in a future release." + ), ) group.add_argument( "--rekor-root-pubkey", dest="__deprecated_rekor_root_pubkey", metavar="FILE", type=argparse.FileType("rb"), - help="A PEM-encoded root public key for Rekor itself (conflicts with --staging)", default=os.getenv("SIGSTORE_REKOR_ROOT_PUBKEY"), + help=( + "A PEM-encoded root public key for Rekor itself (conflicts with --staging). " + "This option will be deprecated in favor of the global `--rekor-root-pubkey` option " + "in a future release." + ), ) From 45c29934354c8b3a96bc52d422dab62ccae54bfa Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Mon, 9 Jan 2023 12:44:35 -0500 Subject: [PATCH 12/14] README: update `--help` text Signed-off-by: William Woodruff --- README.md | 42 +++++++++++++++++++++++++++++++----------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index b2478628..f5971946 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,8 @@ Top-level: ``` -usage: sigstore [-h] [-V] [-v] [--staging] +usage: sigstore [-h] [-V] [-v] [--staging] [--rekor-url URL] + [--rekor-root-pubkey FILE] {sign,verify,get-identity-token} ... a tool for signing and verifying Python package distributions @@ -88,6 +89,11 @@ optional arguments: Sigstore instance options: --staging Use sigstore's staging instances, instead of the default production instances (default: False) + --rekor-url URL The Rekor instance to use (conflicts with --staging) + (default: https://rekor.sigstore.dev) + --rekor-root-pubkey FILE + A PEM-encoded root public key for Rekor itself + (conflicts with --staging) (default: None) ``` @@ -146,11 +152,15 @@ Sigstore instance options: default production instances. This option will be deprecated in favor of the global `--staging` option in a future release. (default: False) - --rekor-url URL The Rekor instance to use (conflicts with --staging) - (default: https://rekor.sigstore.dev) + --rekor-url URL The Rekor instance to use (conflicts with --staging). + This option will be deprecated in favor of the global + `--rekor-url` option in a future release. (default: + https://rekor.sigstore.dev) --rekor-root-pubkey FILE A PEM-encoded root public key for Rekor itself - (conflicts with --staging) (default: None) + (conflicts with --staging). This option will be + deprecated in favor of the global `--rekor-root- + pubkey` option in a future release. (default: None) --fulcio-url URL The Fulcio instance to use (conflicts with --staging) (default: https://fulcio.sigstore.dev) --ctfe FILE A PEM-encoded public key for the CT log (conflicts @@ -206,11 +216,15 @@ Sigstore instance options: default production instances. This option will be deprecated in favor of the global `--staging` option in a future release. (default: False) - --rekor-url URL The Rekor instance to use (conflicts with --staging) - (default: https://rekor.sigstore.dev) + --rekor-url URL The Rekor instance to use (conflicts with --staging). + This option will be deprecated in favor of the global + `--rekor-url` option in a future release. (default: + https://rekor.sigstore.dev) --rekor-root-pubkey FILE A PEM-encoded root public key for Rekor itself - (conflicts with --staging) (default: None) + (conflicts with --staging). This option will be + deprecated in favor of the global `--rekor-root- + pubkey` option in a future release. (default: None) --certificate-chain FILE Path to a list of CA certificates in PEM format which will be needed when building the certificate chain for @@ -273,12 +287,18 @@ Verification options: Sigstore instance options: --staging Use sigstore's staging instances, instead of the - default production instances (default: False) - --rekor-url URL The Rekor instance to use (conflicts with --staging) - (default: https://rekor.sigstore.dev) + default production instances. This option will be + deprecated in favor of the global `--staging` option + in a future release. (default: False) + --rekor-url URL The Rekor instance to use (conflicts with --staging). + This option will be deprecated in favor of the global + `--rekor-url` option in a future release. (default: + https://rekor.sigstore.dev) --rekor-root-pubkey FILE A PEM-encoded root public key for Rekor itself - (conflicts with --staging) (default: None) + (conflicts with --staging). This option will be + deprecated in favor of the global `--rekor-root- + pubkey` option in a future release. (default: None) --certificate-chain FILE Path to a list of CA certificates in PEM format which will be needed when building the certificate chain for From afe23fb035f7e1a0a5da6fe8d5a90a4623695d4b Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Mon, 9 Jan 2023 12:54:07 -0500 Subject: [PATCH 13/14] _cli: fix fallback behavior Signed-off-by: William Woodruff --- README.md | 6 +++--- sigstore/_cli.py | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index f5971946..474d719c 100644 --- a/README.md +++ b/README.md @@ -155,7 +155,7 @@ Sigstore instance options: --rekor-url URL The Rekor instance to use (conflicts with --staging). This option will be deprecated in favor of the global `--rekor-url` option in a future release. (default: - https://rekor.sigstore.dev) + None) --rekor-root-pubkey FILE A PEM-encoded root public key for Rekor itself (conflicts with --staging). This option will be @@ -219,7 +219,7 @@ Sigstore instance options: --rekor-url URL The Rekor instance to use (conflicts with --staging). This option will be deprecated in favor of the global `--rekor-url` option in a future release. (default: - https://rekor.sigstore.dev) + None) --rekor-root-pubkey FILE A PEM-encoded root public key for Rekor itself (conflicts with --staging). This option will be @@ -293,7 +293,7 @@ Sigstore instance options: --rekor-url URL The Rekor instance to use (conflicts with --staging). This option will be deprecated in favor of the global `--rekor-url` option in a future release. (default: - https://rekor.sigstore.dev) + None) --rekor-root-pubkey FILE A PEM-encoded root public key for Rekor itself (conflicts with --staging). This option will be diff --git a/sigstore/_cli.py b/sigstore/_cli.py index 49410b3a..925f1ccc 100644 --- a/sigstore/_cli.py +++ b/sigstore/_cli.py @@ -126,7 +126,7 @@ def _add_shared_instance_options(group: argparse._ArgumentGroup) -> None: "--staging", dest="__deprecated_staging", action="store_true", - default=_boolify_env("SIGSTORE_STAGING"), + default=False, help=( "Use sigstore's staging instances, instead of the default production instances. " "This option will be deprecated in favor of the global `--staging` option " @@ -138,7 +138,7 @@ def _add_shared_instance_options(group: argparse._ArgumentGroup) -> None: dest="__deprecated_rekor_url", metavar="URL", type=str, - default=os.getenv("SIGSTORE_REKOR_URL", DEFAULT_REKOR_URL), + default=None, help=( "The Rekor instance to use (conflicts with --staging). " "This option will be deprecated in favor of the global `--rekor-url` option " @@ -150,7 +150,7 @@ def _add_shared_instance_options(group: argparse._ArgumentGroup) -> None: dest="__deprecated_rekor_root_pubkey", metavar="FILE", type=argparse.FileType("rb"), - default=os.getenv("SIGSTORE_REKOR_ROOT_PUBKEY"), + default=None, help=( "A PEM-encoded root public key for Rekor itself (conflicts with --staging). " "This option will be deprecated in favor of the global `--rekor-root-pubkey` option " @@ -509,13 +509,13 @@ def main() -> None: "Passing `--staging` as a subcommand option will be deprecated in a future release." ) args.staging = args.__deprecated_staging - if hasattr(args, "__deprecated_rekor_url"): + if getattr(args, "__deprecated_rekor_url", None): logger.warning( "`--rekor-url` should be used as a global option, rather than a subcommand option. " "Passing `--rekor-url` as a subcommand option will be deprecated in a future release." ) args.rekor_url = args.__deprecated_rekor_url - if hasattr(args, "__deprecated_rekor_root_pubkey"): + if getattr(args, "__deprecated_rekor_root_pubkey", None): logger.warning( "`--rekor-root-pubkey` should be used as a global option, rather than a subcommand option. " "Passing `--rekor-root-pubkey` as a subcommand option will be deprecated in a future release." From 38cbbaf47e4e14f03216aafd8e47a12b368f7eac Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Mon, 9 Jan 2023 13:04:15 -0500 Subject: [PATCH 14/14] _cli: lintage Signed-off-by: William Woodruff --- sigstore/_cli.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sigstore/_cli.py b/sigstore/_cli.py index 925f1ccc..c8fb4a2e 100644 --- a/sigstore/_cli.py +++ b/sigstore/_cli.py @@ -517,8 +517,9 @@ def main() -> None: args.rekor_url = args.__deprecated_rekor_url if getattr(args, "__deprecated_rekor_root_pubkey", None): logger.warning( - "`--rekor-root-pubkey` should be used as a global option, rather than a subcommand option. " - "Passing `--rekor-root-pubkey` as a subcommand option will be deprecated in a future release." + "`--rekor-root-pubkey` should be used as a global option, rather than a " + "subcommand option. Passing `--rekor-root-pubkey` as a subcommand option will be " + "deprecated in a future release." ) args.rekor_root_pubkey = args.__deprecated_rekor_root_pubkey