From 3d9dc5400aeb3b43ecc7bd61049ce9aea39efec7 Mon Sep 17 00:00:00 2001 From: William Woodruff <william@trailofbits.com> Date: Fri, 20 Jan 2023 11:47:08 -0500 Subject: [PATCH 01/13] Initial Sigstore bundle support Signed-off-by: William Woodruff <william@trailofbits.com> --- .gitignore | 1 + pyproject.toml | 1 + sigstore/_cli.py | 106 ++++++++++++++++++++++++++++++++++------------- sigstore/sign.py | 83 +++++++++++++++++++++++++++++++++++++ 4 files changed, 163 insertions(+), 28 deletions(-) diff --git a/.gitignore b/.gitignore index 4533ac602..cb7d2edaa 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ build *.sh *.pub *.rekor +*.sigstore # Don't ignore these files when we intend to include them !sigstore/_store/*.crt diff --git a/pyproject.toml b/pyproject.toml index e803b343b..f20c079c4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,6 +34,7 @@ dependencies = [ "pyOpenSSL >= 23.0.0", "requests", "securesystemslib", + "sigstore-protobuf-specs ~= 0.1.0", "tuf >= 2.0.0", ] requires-python = ">=3.7" diff --git a/sigstore/_cli.py b/sigstore/_cli.py index 035f6617e..19dd04a81 100644 --- a/sigstore/_cli.py +++ b/sigstore/_cli.py @@ -159,7 +159,7 @@ def _add_shared_instance_options(group: argparse._ArgumentGroup) -> None: ) -def _add_shared_input_options(group: argparse._ArgumentGroup) -> None: +def _add_shared_verify_input_options(group: argparse._ArgumentGroup) -> None: """ Common input options, shared between all `sigstore verify` subcommands. """ @@ -185,6 +185,15 @@ def _add_shared_input_options(group: argparse._ArgumentGroup) -> None: default=os.getenv("SIGSTORE_REKOR_BUNDLE"), help="The offline Rekor bundle to verify with; not used with multiple inputs", ) + group.add_argument( + "--bundle", + action="store_true", + default=_boolify_env("SIGSTORE_BUNDLE"), + help=( + "Verify from {input}.sigstore for each input; this option is experimental " + "and may change between releases until stabilized" + ), + ) group.add_argument( "files", metavar="FILE", @@ -340,6 +349,15 @@ def _parser() -> argparse.ArgumentParser: "multiple input files" ), ) + output_options.add_argument( + "--bundle", + action="store_true", + default=_boolify_env("SIGSTORE_BUNDLE"), + help=( + "Emit a single {input}.sigstore file for each input; this option is experimental " + "and may change between releases until stabilized" + ), + ) output_options.add_argument( "--overwrite", action="store_true", @@ -387,7 +405,7 @@ def _parser() -> argparse.ArgumentParser: formatter_class=argparse.ArgumentDefaultsHelpFormatter, ) input_options = verify_identity.add_argument_group("Verification inputs") - _add_shared_input_options(input_options) + _add_shared_verify_input_options(input_options) verification_options = verify_identity.add_argument_group("Verification options") _add_shared_verification_options(verification_options) @@ -420,7 +438,7 @@ def _parser() -> argparse.ArgumentParser: ) input_options = verify_github.add_argument_group("Verification inputs") - _add_shared_input_options(input_options) + _add_shared_verify_input_options(input_options) verification_options = verify_github.add_argument_group("Verification options") _add_shared_verification_options(verification_options) @@ -556,16 +574,21 @@ def _sign(args: argparse.Namespace) -> None: "upcoming release of sigstore-python in favor of Sigstore-style bundles" ) - # `--no-default-files` has no effect on `--{signature,certificate,rekor-bundle}`, but we - # forbid it because it indicates user confusion. + # `--no-default-files` has no effect on `--{signature,certificate,rekor-bundle,bundle}`, + # but we forbid it because it indicates user confusion. if args.no_default_files and ( - args.signature or args.certificate or args.rekor_bundle + args.signature or args.certificate or args.rekor_bundle or args.bundle ): args._parser.error( "--no-default-files may not be combined with --signature, " - "--certificate, or --rekor-bundle", + "--certificate, --rekor-bundle, or --bundle", ) + # Similarly forbid `--rekor-bundle` with `--bundle`, since it again indicates + # user confusion around outputs. + if args.rekor_bundle and args.bundle: + args._parser.error("--rekor-bundle may not be combined with --bundle") + # Fail if `--signature` or `--certificate` is specified *and* we have more # than one input. if (args.signature or args.certificate or args.rekor_bundle) and len( @@ -579,33 +602,51 @@ def _sign(args: argparse.Namespace) -> None: # Build up the map of inputs -> outputs ahead of any signing operations, # so that we can fail early if overwriting without `--overwrite`. output_map = {} + extants = [] for file in args.files: if not file.is_file(): args._parser.error(f"Input must be a file: {file}") - sig, cert, bundle = args.signature, args.certificate, args.rekor_bundle - if not sig and not cert and not bundle and not args.no_default_files: - sig = file.parent / f"{file.name}.sig" - cert = file.parent / f"{file.name}.crt" - bundle = file.parent / f"{file.name}.rekor" + if args.bundle: + logger.warning( + "--bundle support is experimental; the behavior of this flag may change " + "between releases until stabilized." + ) + + bundle = file.parent / f"{file.name}.sigstore" - if not args.overwrite: - extants = [] - if sig and sig.exists(): - extants.append(str(sig)) - if cert and cert.exists(): - extants.append(str(cert)) - if bundle and bundle.exists(): + if bundle.exists(): extants.append(str(bundle)) + output_map[file] = {"bundle": bundle} + else: + sig, cert, rekor_bundle = ( + args.signature, + args.certificate, + args.rekor_bundle, + ) + + if not sig and not cert and not rekor_bundle and not args.no_default_files: + sig = file.parent / f"{file.name}.sig" + cert = file.parent / f"{file.name}.crt" + rekor_bundle = file.parent / f"{file.name}.rekor" + + if sig and sig.exists(): + extants.append(str(sig)) + if cert and cert.exists(): + extants.append(str(cert)) + if rekor_bundle and rekor_bundle.exists(): + extants.append(str(rekor_bundle)) + + output_map[file] = {"cert": cert, "sig": sig, "rekor_bundle": rekor_bundle} + + if not args.overwrite and len(extants) > 0: if extants: args._parser.error( "Refusing to overwrite outputs without --overwrite: " f"{', '.join(extants)}" ) - output_map[file] = {"cert": cert, "sig": sig, "bundle": bundle} - # Select the signer to use. if args.staging: logger.debug("sign: staging instances requested") @@ -655,25 +696,30 @@ def _sign(args: argparse.Namespace) -> None: print(f"Transparency log entry created at index: {result.log_entry.log_index}") sig_output: TextIO - if outputs["sig"]: + if "sig" in outputs: sig_output = outputs["sig"].open("w") else: sig_output = sys.stdout print(result.b64_signature, file=sig_output) - if outputs["sig"] is not None: + if "sig" in outputs: print(f"Signature written to {outputs['sig']}") - if outputs["cert"] is not None: + if "cert" in outputs: with outputs["cert"].open(mode="w") as io: print(result.cert_pem, file=io) print(f"Certificate written to {outputs['cert']}") - if outputs["bundle"] is not None: + if "rekor_bundle" in outputs: + with outputs["rekor_bundle"].open(mode="w") as io: + rekor_bundle = RekorBundle.from_entry(result.log_entry) + print(rekor_bundle.json(by_alias=True), file=io) + print(f"Rekor bundle written to {outputs['rekor_bundle']}") + + if "bundle" in outputs: with outputs["bundle"].open(mode="w") as io: - bundle = RekorBundle.from_entry(result.log_entry) - print(bundle.json(by_alias=True), file=io) - print(f"Rekor bundle written to {outputs['bundle']}") + print(result._to_bundle().to_json(), file=io) + print(f"Sigstore bundle written to {outputs['bundle']}") def _collect_verification_state( @@ -687,6 +733,10 @@ def _collect_verification_state( purposes) and `materials` is the `VerificationMaterials` to verify with. """ + # TODO: Allow --bundle during verification. Until then, error. + if args.bundle: + args._parser.error("--bundle is not supported during verification yet") + # `--rekor-bundle` is a temporary option, pending stabilization of the # Sigstore bundle format. if args.rekor_bundle: diff --git a/sigstore/sign.py b/sigstore/sign.py index 587b81dcd..f00b96253 100644 --- a/sigstore/sign.py +++ b/sigstore/sign.py @@ -47,6 +47,24 @@ from cryptography.hazmat.primitives.asymmetric.utils import Prehashed from cryptography.x509.oid import NameOID from pydantic import BaseModel +from sigstore_protobuf_specs.dev.sigstore.bundle.v1 import ( + Bundle, + VerificationMaterial, +) +from sigstore_protobuf_specs.dev.sigstore.common.v1 import ( + HashAlgorithm, + HashOutput, + LogId, + MessageSignature, + X509Certificate, + X509CertificateChain, +) +from sigstore_protobuf_specs.dev.sigstore.rekor.v1 import ( + InclusionPromise, + InclusionProof, + KindVersion, + TransparencyLogEntry, +) from sigstore._internal.fulcio import FulcioClient from sigstore._internal.oidc import Identity @@ -163,6 +181,7 @@ def sign( logger.debug(f"Transparency log entry created with index: {entry.log_index}") return SigningResult( + input_digest=input_digest.hex(), cert_pem=cert.public_bytes(encoding=serialization.Encoding.PEM).decode(), b64_signature=b64_artifact_signature, log_entry=entry, @@ -174,6 +193,11 @@ class SigningResult(BaseModel): Represents the artifacts of a signing operation. """ + input_digest: str + """ + The hex-encoded SHA256 digest of the input that was signed for. + """ + cert_pem: str """ The PEM-encoded public half of the certificate used for signing. @@ -188,3 +212,62 @@ class SigningResult(BaseModel): """ A record of the Rekor log entry for the signing operation. """ + + def _to_bundle(self) -> Bundle: + """ + Creates a Sigstore bundle (as defined by Sigstore's protobuf specs) + from this `SigningResult`. + """ + + # TODO: Should we include our Fulcio intermediate in the chain? + chain = X509CertificateChain( + certificates=[X509Certificate(raw_bytes=self.cert_pem.encode())] + ) + + inclusion_proof: InclusionProof | None = None + if self.log_entry.inclusion_proof is not None: + inclusion_proof = InclusionProof( + log_index=self.log_entry.inclusion_proof.log_index, + root_hash=bytes.fromhex(self.log_entry.inclusion_proof.root_hash), + tree_size=self.log_entry.inclusion_proof.tree_size, + hashes=[ + bytes.fromhex(h) for h in self.log_entry.inclusion_proof.hashes + ], + ) + + tlog_entry = TransparencyLogEntry( + log_index=self.log_entry.log_index, + log_id=LogId(key_id=bytes.fromhex(self.log_entry.log_id)), + # TODO: Maybe leave this field out? It appears to be optional + # and it's just hardcoded for us, since it's the only kind of Rekor + # entry we support. + kind_version=KindVersion(kind="hashedrekord", version="0.0.1"), + integrated_time=self.log_entry.integrated_time, + inclusion_promise=InclusionPromise( + signed_entry_timestamp=base64.b64decode( + self.log_entry.signed_entry_timestamp + ) + ), + inclusion_proof=inclusion_proof, + # TODO: Is this correct? + canonicalized_body=self.log_entry.encode_canonical(), + ) + + material = VerificationMaterial( + x509_certificate_chain=chain, + tlog_entries=[tlog_entry], + ) + + bundle = Bundle( + media_type="application/vnd.dev.sigstore.bundle+json;version=0.1", + verification_material=material, + message_signature=MessageSignature( + message_digest=HashOutput( + algorithm=HashAlgorithm.SHA2_256, + digest=bytes.fromhex(self.input_digest), + ), + signature=base64.b64decode(self.b64_signature), + ), + ) + + return bundle From a0377a4f4bfc04c96e49ed1950826b27cfdaca5e Mon Sep 17 00:00:00 2001 From: William Woodruff <william@trailofbits.com> Date: Fri, 20 Jan 2023 11:53:30 -0500 Subject: [PATCH 02/13] README: update `--help` texts Signed-off-by: William Woodruff <william@trailofbits.com> --- README.md | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 9ec3e6a4f..dcad5f763 100644 --- a/README.md +++ b/README.md @@ -131,9 +131,10 @@ usage: sigstore sign [-h] [--identity-token TOKEN] [--oidc-client-id ID] [--oidc-client-secret SECRET] [--oidc-disable-ambient-providers] [--oidc-issuer URL] [--no-default-files] [--signature FILE] - [--certificate FILE] [--rekor-bundle FILE] [--overwrite] - [--staging] [--rekor-url URL] [--rekor-root-pubkey FILE] - [--fulcio-url URL] [--ctfe FILE] + [--certificate FILE] [--rekor-bundle FILE] [--bundle] + [--overwrite] [--staging] [--rekor-url URL] + [--rekor-root-pubkey FILE] [--fulcio-url URL] + [--ctfe FILE] FILE [FILE ...] positional arguments: @@ -169,6 +170,9 @@ Output options: Write a single offline Rekor bundle to the given file; does not work with multiple input files (default: None) + --bundle Emit a single {input}.sigstore file for each input; + this option is experimental and may change between + releases until stabilized (default: False) --overwrite Overwrite preexisting signature and certificate outputs, if present (default: False) @@ -205,7 +209,8 @@ to by a particular OIDC provider (like `https://github.com/login/oauth`). <!-- @begin-sigstore-verify-identity-help@ --> ``` usage: sigstore verify identity [-h] [--certificate FILE] [--signature FILE] - [--rekor-bundle FILE] --cert-identity IDENTITY + [--rekor-bundle FILE] [--bundle] + --cert-identity IDENTITY [--require-rekor-offline] --cert-oidc-issuer URL [--staging] [--rekor-url URL] [--rekor-root-pubkey FILE] @@ -223,6 +228,9 @@ Verification inputs: multiple inputs (default: None) --rekor-bundle FILE The offline Rekor bundle to verify with; not used with multiple inputs (default: None) + --bundle Verify from {input}.sigstore for each input; this + option is experimental and may change between releases + until stabilized (default: False) FILE The file to verify Verification options: @@ -271,11 +279,11 @@ claims more precisely than `sigstore verify identity` allows: <!-- @begin-sigstore-verify-github-help@ --> ``` usage: sigstore verify github [-h] [--certificate FILE] [--signature FILE] - [--rekor-bundle FILE] --cert-identity IDENTITY - [--require-rekor-offline] [--trigger EVENT] - [--sha SHA] [--name NAME] [--repository REPO] - [--ref REF] [--staging] [--rekor-url URL] - [--rekor-root-pubkey FILE] + [--rekor-bundle FILE] [--bundle] --cert-identity + IDENTITY [--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 ...] @@ -290,6 +298,9 @@ Verification inputs: multiple inputs (default: None) --rekor-bundle FILE The offline Rekor bundle to verify with; not used with multiple inputs (default: None) + --bundle Verify from {input}.sigstore for each input; this + option is experimental and may change between releases + until stabilized (default: False) FILE The file to verify Verification options: From 63d2d8756655258d8ef6d607423f50e28f08ee33 Mon Sep 17 00:00:00 2001 From: William Woodruff <william@trailofbits.com> Date: Fri, 20 Jan 2023 13:14:00 -0500 Subject: [PATCH 03/13] sign: fix bundle generation Certs are base64'd DER, not PEM, and the canonicalized_body is the log entry body, not the canonicalized contents that the SET is signed over. Signed-off-by: William Woodruff <william@trailofbits.com> --- sigstore/sign.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/sigstore/sign.py b/sigstore/sign.py index f00b96253..48385e053 100644 --- a/sigstore/sign.py +++ b/sigstore/sign.py @@ -220,9 +220,9 @@ def _to_bundle(self) -> Bundle: """ # TODO: Should we include our Fulcio intermediate in the chain? - chain = X509CertificateChain( - certificates=[X509Certificate(raw_bytes=self.cert_pem.encode())] - ) + cert = x509.load_pem_x509_certificate(self.cert_pem.encode()) + cert_der = cert.public_bytes(encoding=serialization.Encoding.DER) + chain = X509CertificateChain(certificates=[X509Certificate(raw_bytes=cert_der)]) inclusion_proof: InclusionProof | None = None if self.log_entry.inclusion_proof is not None: @@ -249,8 +249,7 @@ def _to_bundle(self) -> Bundle: ) ), inclusion_proof=inclusion_proof, - # TODO: Is this correct? - canonicalized_body=self.log_entry.encode_canonical(), + canonicalized_body=base64.b64decode(self.log_entry.body), ) material = VerificationMaterial( From 56f063697f0af3c87e8dbbea8acd3a062ac12480 Mon Sep 17 00:00:00 2001 From: William Woodruff <william@trailofbits.com> Date: Mon, 23 Jan 2023 21:25:25 -0600 Subject: [PATCH 04/13] sign: remove TODO Signed-off-by: William Woodruff <william@trailofbits.com> --- sigstore/sign.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/sigstore/sign.py b/sigstore/sign.py index 48385e053..65ffde9de 100644 --- a/sigstore/sign.py +++ b/sigstore/sign.py @@ -238,9 +238,6 @@ def _to_bundle(self) -> Bundle: tlog_entry = TransparencyLogEntry( log_index=self.log_entry.log_index, log_id=LogId(key_id=bytes.fromhex(self.log_entry.log_id)), - # TODO: Maybe leave this field out? It appears to be optional - # and it's just hardcoded for us, since it's the only kind of Rekor - # entry we support. kind_version=KindVersion(kind="hashedrekord", version="0.0.1"), integrated_time=self.log_entry.integrated_time, inclusion_promise=InclusionPromise( From 8651f955bfa2b60f50d73d8cbf2f63696ab73910 Mon Sep 17 00:00:00 2001 From: William Woodruff <william@trailofbits.com> Date: Mon, 23 Jan 2023 21:29:01 -0600 Subject: [PATCH 05/13] sign: update TODO Signed-off-by: William Woodruff <william@trailofbits.com> --- sigstore/sign.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sigstore/sign.py b/sigstore/sign.py index 65ffde9de..a948a847e 100644 --- a/sigstore/sign.py +++ b/sigstore/sign.py @@ -219,7 +219,8 @@ def _to_bundle(self) -> Bundle: from this `SigningResult`. """ - # TODO: Should we include our Fulcio intermediate in the chain? + # TODO: Include the current Fulcio intermediate and root in the + # chain as well. cert = x509.load_pem_x509_certificate(self.cert_pem.encode()) cert_der = cert.public_bytes(encoding=serialization.Encoding.DER) chain = X509CertificateChain(certificates=[X509Certificate(raw_bytes=cert_der)]) From cd7d31cb4a6cb3de711bccc64fa76eb124c5117a Mon Sep 17 00:00:00 2001 From: Alex Cameron <asc@tetsuo.sh> Date: Thu, 26 Jan 2023 00:04:01 +1100 Subject: [PATCH 06/13] _cli: Make `--bundle` refer to a path and create a `--no-bundle` flag to control whether Sigstore bundles are emitted by default Signed-off-by: Alex Cameron <asc@tetsuo.sh> --- sigstore/_cli.py | 117 +++++++++++++++++++++++++++++------------------ 1 file changed, 73 insertions(+), 44 deletions(-) diff --git a/sigstore/_cli.py b/sigstore/_cli.py index 19dd04a81..a0dd42c9b 100644 --- a/sigstore/_cli.py +++ b/sigstore/_cli.py @@ -187,11 +187,12 @@ def _add_shared_verify_input_options(group: argparse._ArgumentGroup) -> None: ) group.add_argument( "--bundle", - action="store_true", - default=_boolify_env("SIGSTORE_BUNDLE"), + metavar="FILE", + type=Path, + default=os.getenv("SIGSTORE_BUNDLE"), help=( - "Verify from {input}.sigstore for each input; this option is experimental " - "and may change between releases until stabilized" + "The Sigstore bundle to verify with; not used with multiple inputs; this option is " + "experimental and may change between releases until stabilized" ), ) group.add_argument( @@ -351,10 +352,20 @@ def _parser() -> argparse.ArgumentParser: ) output_options.add_argument( "--bundle", + metavar="FILE", + type=Path, + default=os.getenv("SIGSTORE_BUNDLE"), + help=( + "Write a single Sigstore bundle to the given file; does not work with multiple input " + "files; this option is experimental and may change between releases until stabilized" + ), + ) + output_options.add_argument( + "--no-bundle", action="store_true", - default=_boolify_env("SIGSTORE_BUNDLE"), + default=False, help=( - "Emit a single {input}.sigstore file for each input; this option is experimental " + "Don't emit {input}.sigstore files for each input; this option is experimental " "and may change between releases until stabilized" ), ) @@ -574,6 +585,18 @@ def _sign(args: argparse.Namespace) -> None: "upcoming release of sigstore-python in favor of Sigstore-style bundles" ) + if args.bundle: + logger.warning( + "--bundle support is experimental; this flag may change behaviour " + "between releases until stabilized or may be removed." + ) + + if args.no_bundle: + logger.warning( + "--no-bundle support is experimental; this flag may change behaviour " + "between releases until stabilized or may be removed." + ) + # `--no-default-files` has no effect on `--{signature,certificate,rekor-bundle,bundle}`, # but we forbid it because it indicates user confusion. if args.no_default_files and ( @@ -589,6 +612,10 @@ def _sign(args: argparse.Namespace) -> None: if args.rekor_bundle and args.bundle: args._parser.error("--rekor-bundle may not be combined with --bundle") + # Fail if `--bundle` and `--no-bundle` are both specified. + if args.bundle and args.no_bundle: + args._parser.error("--bundle may not be combined with --no-bundle") + # Fail if `--signature` or `--certificate` is specified *and* we have more # than one input. if (args.signature or args.certificate or args.rekor_bundle) and len( @@ -602,51 +629,53 @@ def _sign(args: argparse.Namespace) -> None: # Build up the map of inputs -> outputs ahead of any signing operations, # so that we can fail early if overwriting without `--overwrite`. output_map = {} - extants = [] for file in args.files: if not file.is_file(): args._parser.error(f"Input must be a file: {file}") - if args.bundle: - logger.warning( - "--bundle support is experimental; the behavior of this flag may change " - "between releases until stabilized." - ) - - bundle = file.parent / f"{file.name}.sigstore" - - if bundle.exists(): + sig, cert, rekor_bundle, bundle = ( + args.signature, + args.certificate, + args.rekor_bundle, + args.bundle, + ) + if ( + not sig + and not cert + and not rekor_bundle + and not bundle + and not args.no_default_files + ): + sig = file.parent / f"{file.name}.sig" + cert = file.parent / f"{file.name}.crt" + rekor_bundle = file.parent / f"{file.name}.rekor" + if not args.no_bundle: + bundle = file.parent / f"{file.name}.sigstore" + + extants = [] + if not args.overwrite: + if sig and sig.exists(): + extants.append(str(sig)) + if cert and cert.exists(): + extants.append(str(cert)) + if rekor_bundle and rekor_bundle.exists(): + extants.append(str(rekor_bundle)) + if bundle and bundle.exists(): extants.append(str(bundle)) - output_map[file] = {"bundle": bundle} - else: - sig, cert, rekor_bundle = ( - args.signature, - args.certificate, - args.rekor_bundle, - ) - - if not sig and not cert and not rekor_bundle and not args.no_default_files: - sig = file.parent / f"{file.name}.sig" - cert = file.parent / f"{file.name}.crt" - rekor_bundle = file.parent / f"{file.name}.rekor" - - if sig and sig.exists(): - extants.append(str(sig)) - if cert and cert.exists(): - extants.append(str(cert)) - if rekor_bundle and rekor_bundle.exists(): - extants.append(str(rekor_bundle)) - - output_map[file] = {"cert": cert, "sig": sig, "rekor_bundle": rekor_bundle} - - if not args.overwrite and len(extants) > 0: if extants: args._parser.error( "Refusing to overwrite outputs without --overwrite: " f"{', '.join(extants)}" ) + output_map[file] = { + "cert": cert, + "sig": sig, + "rekor_bundle": rekor_bundle, + "bundle": bundle, + } + # Select the signer to use. if args.staging: logger.debug("sign: staging instances requested") @@ -696,27 +725,27 @@ def _sign(args: argparse.Namespace) -> None: print(f"Transparency log entry created at index: {result.log_entry.log_index}") sig_output: TextIO - if "sig" in outputs: + if outputs["sig"] in outputs: sig_output = outputs["sig"].open("w") else: sig_output = sys.stdout print(result.b64_signature, file=sig_output) - if "sig" in outputs: + if outputs["sig"] is not None: print(f"Signature written to {outputs['sig']}") - if "cert" in outputs: + if outputs["cert"] is not None: with outputs["cert"].open(mode="w") as io: print(result.cert_pem, file=io) print(f"Certificate written to {outputs['cert']}") - if "rekor_bundle" in outputs: + if outputs["rekor_bundle"] is not None: with outputs["rekor_bundle"].open(mode="w") as io: rekor_bundle = RekorBundle.from_entry(result.log_entry) print(rekor_bundle.json(by_alias=True), file=io) print(f"Rekor bundle written to {outputs['rekor_bundle']}") - if "bundle" in outputs: + if outputs["bundle"] is not None: with outputs["bundle"].open(mode="w") as io: print(result._to_bundle().to_json(), file=io) print(f"Sigstore bundle written to {outputs['bundle']}") From a25e546ca72a99b3c5f36ff3e146e08b65afcb91 Mon Sep 17 00:00:00 2001 From: Alex Cameron <asc@tetsuo.sh> Date: Thu, 26 Jan 2023 00:07:15 +1100 Subject: [PATCH 07/13] _cli: Move variable to correct scope Signed-off-by: Alex Cameron <asc@tetsuo.sh> --- sigstore/_cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sigstore/_cli.py b/sigstore/_cli.py index a0dd42c9b..94eb1e45f 100644 --- a/sigstore/_cli.py +++ b/sigstore/_cli.py @@ -652,8 +652,8 @@ def _sign(args: argparse.Namespace) -> None: if not args.no_bundle: bundle = file.parent / f"{file.name}.sigstore" - extants = [] if not args.overwrite: + extants = [] if sig and sig.exists(): extants.append(str(sig)) if cert and cert.exists(): From 93698cd0f76a7c52dd5114cda5ea05a1fe521222 Mon Sep 17 00:00:00 2001 From: Alex Cameron <asc@tetsuo.sh> Date: Thu, 26 Jan 2023 00:09:48 +1100 Subject: [PATCH 08/13] _cli: Reword warnings for bundle flags Signed-off-by: Alex Cameron <asc@tetsuo.sh> --- sigstore/_cli.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sigstore/_cli.py b/sigstore/_cli.py index 94eb1e45f..bf4f924f8 100644 --- a/sigstore/_cli.py +++ b/sigstore/_cli.py @@ -587,14 +587,14 @@ def _sign(args: argparse.Namespace) -> None: if args.bundle: logger.warning( - "--bundle support is experimental; this flag may change behaviour " - "between releases until stabilized or may be removed." + "--bundle support is experimental; the behaviour of this flag may change " + "between releases until stabilized." ) if args.no_bundle: logger.warning( - "--no-bundle support is experimental; this flag may change behaviour " - "between releases until stabilized or may be removed." + "--no-bundle support is experimental; the behaviour of this flag may change " + "between releases until stabilized." ) # `--no-default-files` has no effect on `--{signature,certificate,rekor-bundle,bundle}`, From ae7cc838331c981fda8c8e3d95511490da33c813 Mon Sep 17 00:00:00 2001 From: Alex Cameron <asc@tetsuo.sh> Date: Thu, 26 Jan 2023 00:13:14 +1100 Subject: [PATCH 09/13] README: Fix sign example Signed-off-by: Alex Cameron <asc@tetsuo.sh> --- README.md | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index dcad5f763..d7abb2ed5 100644 --- a/README.md +++ b/README.md @@ -131,10 +131,10 @@ usage: sigstore sign [-h] [--identity-token TOKEN] [--oidc-client-id ID] [--oidc-client-secret SECRET] [--oidc-disable-ambient-providers] [--oidc-issuer URL] [--no-default-files] [--signature FILE] - [--certificate FILE] [--rekor-bundle FILE] [--bundle] - [--overwrite] [--staging] [--rekor-url URL] - [--rekor-root-pubkey FILE] [--fulcio-url URL] - [--ctfe FILE] + [--certificate FILE] [--rekor-bundle FILE] + [--bundle FILE] [--no-bundle] [--overwrite] [--staging] + [--rekor-url URL] [--rekor-root-pubkey FILE] + [--fulcio-url URL] [--ctfe FILE] FILE [FILE ...] positional arguments: @@ -170,9 +170,13 @@ Output options: Write a single offline Rekor bundle to the given file; does not work with multiple input files (default: None) - --bundle Emit a single {input}.sigstore file for each input; - this option is experimental and may change between - releases until stabilized (default: False) + --bundle FILE Write a single Sigstore bundle to the given file; does + not work with multiple input files; this option is + experimental and may change between releases until + stabilized (default: None) + --no-bundle Don't emit {input}.sigstore files for each input; this + option is experimental and may change between releases + until stabilized (default: False) --overwrite Overwrite preexisting signature and certificate outputs, if present (default: False) From be7bf1bc2cd08e10d84b6bae14ba0c4b61fbe654 Mon Sep 17 00:00:00 2001 From: Alex Cameron <asc@tetsuo.sh> Date: Thu, 26 Jan 2023 00:16:25 +1100 Subject: [PATCH 10/13] README: Update verify invocations Signed-off-by: Alex Cameron <asc@tetsuo.sh> --- README.md | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index d7abb2ed5..3eb039a06 100644 --- a/README.md +++ b/README.md @@ -213,7 +213,7 @@ to by a particular OIDC provider (like `https://github.com/login/oauth`). <!-- @begin-sigstore-verify-identity-help@ --> ``` usage: sigstore verify identity [-h] [--certificate FILE] [--signature FILE] - [--rekor-bundle FILE] [--bundle] + [--rekor-bundle FILE] [--bundle FILE] --cert-identity IDENTITY [--require-rekor-offline] --cert-oidc-issuer URL [--staging] [--rekor-url URL] @@ -232,9 +232,10 @@ Verification inputs: multiple inputs (default: None) --rekor-bundle FILE The offline Rekor bundle to verify with; not used with multiple inputs (default: None) - --bundle Verify from {input}.sigstore for each input; this - option is experimental and may change between releases - until stabilized (default: False) + --bundle FILE The Sigstore bundle to verify with; not used with + multiple inputs; this option is experimental and may + change between releases until stabilized (default: + None) FILE The file to verify Verification options: @@ -283,8 +284,8 @@ claims more precisely than `sigstore verify identity` allows: <!-- @begin-sigstore-verify-github-help@ --> ``` usage: sigstore verify github [-h] [--certificate FILE] [--signature FILE] - [--rekor-bundle FILE] [--bundle] --cert-identity - IDENTITY [--require-rekor-offline] + [--rekor-bundle FILE] [--bundle FILE] + --cert-identity IDENTITY [--require-rekor-offline] [--trigger EVENT] [--sha SHA] [--name NAME] [--repository REPO] [--ref REF] [--staging] [--rekor-url URL] [--rekor-root-pubkey FILE] @@ -302,9 +303,10 @@ Verification inputs: multiple inputs (default: None) --rekor-bundle FILE The offline Rekor bundle to verify with; not used with multiple inputs (default: None) - --bundle Verify from {input}.sigstore for each input; this - option is experimental and may change between releases - until stabilized (default: False) + --bundle FILE The Sigstore bundle to verify with; not used with + multiple inputs; this option is experimental and may + change between releases until stabilized (default: + None) FILE The file to verify Verification options: From c2559dabfff981328872dccf045c6a3a3737a4fc Mon Sep 17 00:00:00 2001 From: Alex Cameron <asc@tetsuo.sh> Date: Thu, 26 Jan 2023 00:18:00 +1100 Subject: [PATCH 11/13] README: Fix line breaks Signed-off-by: Alex Cameron <asc@tetsuo.sh> --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 3eb039a06..ce3392599 100644 --- a/README.md +++ b/README.md @@ -285,10 +285,11 @@ claims more precisely than `sigstore verify identity` allows: ``` usage: sigstore verify github [-h] [--certificate FILE] [--signature FILE] [--rekor-bundle FILE] [--bundle FILE] - --cert-identity IDENTITY [--require-rekor-offline] - [--trigger EVENT] [--sha SHA] [--name NAME] - [--repository REPO] [--ref REF] [--staging] - [--rekor-url URL] [--rekor-root-pubkey FILE] + --cert-identity IDENTITY + [--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 ...] From d045d21ebd0e714ac5f52fefa7f2c7b4b0327268 Mon Sep 17 00:00:00 2001 From: William Woodruff <william@trailofbits.com> Date: Wed, 25 Jan 2023 09:40:19 -0600 Subject: [PATCH 12/13] _cli: fix sig output Signed-off-by: William Woodruff <william@trailofbits.com> --- sigstore/_cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sigstore/_cli.py b/sigstore/_cli.py index bf4f924f8..440018f3f 100644 --- a/sigstore/_cli.py +++ b/sigstore/_cli.py @@ -725,7 +725,7 @@ def _sign(args: argparse.Namespace) -> None: print(f"Transparency log entry created at index: {result.log_entry.log_index}") sig_output: TextIO - if outputs["sig"] in outputs: + if "sig" in outputs: sig_output = outputs["sig"].open("w") else: sig_output = sys.stdout From 24c247202060ff5f0f1b2fb1bbdc6fdcad18a523 Mon Sep 17 00:00:00 2001 From: William Woodruff <william@trailofbits.com> Date: Wed, 25 Jan 2023 09:45:49 -0600 Subject: [PATCH 13/13] _cli: fix sig check, take 2 Signed-off-by: William Woodruff <william@trailofbits.com> --- sigstore/_cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sigstore/_cli.py b/sigstore/_cli.py index 440018f3f..21fa28ff3 100644 --- a/sigstore/_cli.py +++ b/sigstore/_cli.py @@ -725,7 +725,7 @@ def _sign(args: argparse.Namespace) -> None: print(f"Transparency log entry created at index: {result.log_entry.log_index}") sig_output: TextIO - if "sig" in outputs: + if outputs["sig"] is not None: sig_output = outputs["sig"].open("w") else: sig_output = sys.stdout