8000 Initial Sigstore bundle support (#465) · tetsuo-cpp/sigstore-python@e919f5e · GitHub
[go: up one dir, main page]

Skip to content

Commit e919f5e

Browse files
Initial Sigstore bundle support (sigstore#465)
* Initial Sigstore bundle support Signed-off-by: William Woodruff <william@trailofbits.com> * README: update `--help` texts Signed-off-by: William Woodruff <william@trailofbits.com> * 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> * sign: remove TODO Signed-off-by: William Woodruff <william@trailofbits.com> * sign: update TODO Signed-off-by: William Woodruff <william@trailofbits.com> * _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> * _cli: Move variable to correct scope Signed-off-by: Alex Cameron <asc@tetsuo.sh> * _cli: Reword warnings for bundle flags Signed-off-by: Alex Cameron <asc@tetsuo.sh> * README: Fix sign example Signed-off-by: Alex Cameron <asc@tetsuo.sh> * README: Update verify invocations Signed-off-by: Alex Cameron <asc@tetsuo.sh> * README: Fix line breaks Signed-off-by: Alex Cameron <asc@tetsuo.sh> * _cli: fix sig output Signed-off-by: William Woodruff <william@trailofbits.com> * _cli: fix sig check, take 2 Signed-off-by: William Woodruff <william@trailofbits.com> Signed-off-by: William Woodruff <william@trailofbits.com> Signed-off-by: Alex Cameron <asc@tetsuo.sh> Co-authored-by: Alex Cameron <asc@tetsuo.sh>
1 parent 6ae96b2 commit e919f5e

File tree

5 files changed

+198
-19
lines changed

5 files changed

+198
-19
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ build
1616
*.sh
1717
*.pub
1818
*.rekor
19+
*.sigstore
1920

2021
# Don't ignore these files when we intend to include them
2122
!sigstore/_store/*.crt

README.md

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,9 @@ usage: sigstore sign [-h] [--identity-token TOKEN] [--oidc-client-id ID]
131131
[--oidc-client-secret SECRET]
132132
8000 [--oidc-disable-ambient-providers] [--oidc-issuer URL]
133133
[--no-default-files] [--signature FILE]
134-
[--certificate FILE] [--rekor-bundle FILE] [--overwrite]
135-
[--staging] [--rekor-url URL] [--rekor-root-pubkey FILE]
134+
[--certificate FILE] [--rekor-bundle FILE]
135+
[--bundle FILE] [--no-bundle] [--overwrite] [--staging]
136+
[--rekor-url URL] [--rekor-root-pubkey FILE]
136137
[--fulcio-url URL] [--ctfe FILE]
137138
FILE [FILE ...]
138139

@@ -169,6 +170,13 @@ Output options:
169170
Write a single offline Rekor bundle to the given file;
170171
does not work with multiple input files (default:
171172
None)
173+
--bundle FILE Write a single Sigstore bundle to the given file; does
174+
not work with multiple input files; this option is
175+
experimental and may change between releases until
176+
stabilized (default: None)
177+
--no-bundle Don't emit {input}.sigstore files for each input; this
178+
option is experimental and may change between releases
179+
until stabilized (default: False)
172180
--overwrite Overwrite preexisting signature and certificate
173181
outputs, if present (default: False)
174182

@@ -205,7 +213,8 @@ to by a particular OIDC provider (like `https://github.com/login/oauth`).
205213
<!-- @begin-sigstore-verify-identity-help@ -->
206214
```
207215
usage: sigstore verify identity [-h] [--certificate FILE] [--signature FILE]
208-
[--rekor-bundle FILE] --cert-identity IDENTITY
216+
[--rekor-bundle FILE] [--bundle FILE]
217+
--cert-identity IDENTITY
209218
[--require-rekor-offline] --cert-oidc-issuer
210219
URL [--staging] [--rekor-url URL]
211220
[--rekor-root-pubkey FILE]
@@ -223,6 +232,10 @@ Verification inputs:
223232
multiple inputs (default: None)
224233
--rekor-bundle FILE The offline Rekor bundle to verify with; not used with
225234
multiple inputs (default: None)
235+
--bundle FILE The Sigstore bundle to verify with; not used with
236+
multiple inputs; this option is experimental and may
237+
change between releases until stabilized (default:
238+
None)
226239
FILE The file to verify
227240

228241
Verification options:
@@ -271,7 +284,8 @@ claims more precisely than `sigstore verify identity` allows:
271284
<!-- @begin-sigstore-verify-github-help@ -->
272285
```
273286
usage: sigstore verify github [-h] [--certificate FILE] [--signature FILE]
274-
[--rekor-bundle FILE] --cert-identity IDENTITY
287+
[--rekor-bundle FILE] [--bundle FILE]
288+
--cert-identity IDENTITY
275289
[--require-rekor-offline] [--trigger EVENT]
276290
[--sha SHA] [--name NAME] [--repository REPO]
277291
[--ref REF] [--staging] [--rekor-url URL]
@@ -290,6 +304,10 @@ Verification inputs:
290304
multiple inputs (default: None)
291305
--rekor-bundle FILE The offline Rekor bundle to verify with; not used with
292306
multiple inputs (default: None)
307+
--bundle FILE The Sigstore bundle to verify with; not used with
308+
multiple inputs; this option is experimental and may
309+
change between releases until stabilized (default:
310+
None)
293311
FILE The file to verify
294312

295313
Verification options:

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ dependencies = [
3434
"pyOpenSSL >= 23.0.0",
3535
"requests",
3636
"securesystemslib",
37+
"sigstore-protobuf-specs ~= 0.1.0",
3738
"tuf >= 2.0.0",
3839
]
3940
requires-python = ">=3.7"

sigstore/_cli.py

Lines changed: 94 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ def _add_shared_instance_options(group: argparse._ArgumentGroup) -> None:
159159
)
160160

161161

162-
def _add_shared_input_options(group: argparse._ArgumentGroup) -> None:
162+
def _add_shared_verify_input_options(group: argparse._ArgumentGroup) -> None:
163163
"""
164164
Common input options, shared between all `sigstore verify` subcommands.
165165
"""
@@ -185,6 +185,16 @@ def _add_shared_input_options(group: argparse._ArgumentGroup) -> None:
185185
default=os.getenv("SIGSTORE_REKOR_BUNDLE"),
186186
help="The offline Rekor bundle to verify with; not used with multiple inputs",
187187
)
188+
group.add_argument(
189+
"--bundle",
190+
metavar="FILE",
191+
type=Path,
192+
default=os.getenv("SIGSTORE_BUNDLE"),
193+
help=(
194+
"The Sigstore bundle to verify with; not used with multiple inputs; this option is "
195+
"experimental and may change between releases until stabilized"
196+
),
197+
)
188198
group.add_argument(
189199
"files",
190200
metavar="FILE",
@@ -340,6 +350,25 @@ def _parser() -> argparse.ArgumentParser:
340350
"multiple input files"
341351
),
342352
)
353+
output_options.add_argument(
354+
"--bundle",
355+
metavar="FILE",
356+
type=Path,
357+
default=os.getenv("SIGSTORE_BUNDLE"),
358+
help=(
359+
"Write a single Sigstore bundle to the given file; does not work with multiple input "
360+
"files; this option is experimental and may change between releases until stabilized"
361+
),
362+
)
363+
output_options.add_argument(
364+
"--no-bundle",
365+
action="store_true",
366+
default=False,
367+
help=(
368+
"Don't emit {input}.sigstore files for each input; this option is experimental "
369+
"and may change between releases until stabilized"
370+
),
371+
)
343372
output_options.add_argument(
344373
"--overwrite",
345374
action="store_true",
@@ -387,7 +416,7 @@ def _parser() -> argparse.ArgumentParser:
387416
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
388417
)
389418
input_options = verify_identity.add_argument_group("Verification inputs")
390-
_add_shared_input_options(input_options)
419+
_add_shared_verify_input_options(input_options)
391420

392421
verification_options = verify_identity.add_argument_group("Verification options")
393422
_add_shared_verification_options(verification_options)
@@ -420,7 +449,7 @@ def _parser() -> argparse.ArgumentParser:
420449
)
421450

422451
input_options = verify_github.add_argument_group("Verification inputs")
423-
_add_shared_input_options(input_options)
452+
_add_shared_verify_input_options(input_options)
424453

425454
verification_options = verify_github.add_argument_group("Verification options")
426455
_add_shared_verification_options(verification_options)
@@ -556,16 +585,37 @@ def _sign(args: argparse.Namespace) -> None:
556585
"upcoming release of sigstore-python in favor of Sigstore-style bundles"
557586
)
558587

559-
# `--no-default-files` has no effect on `--{signature,certificate,rekor-bundle}`, but we
560-
# forbid it because it indicates user confusion.
588+
if args.bundle:
589+
logger.warning(
590+
"--bundle support is experimental; the behaviour of this flag may change "
591+
"between releases until stabilized."
592+
)
593+
594+
if args.no_bundle:
595+
logger.warning(
596+
"--no-bundle support is experimental; the behaviour of this flag may change "
597+
"between releases until stabilized."
598+
)
599+
600+
# `--no-default-files` has no effect on `--{signature,certificate,rekor-bundle,bundle}`,
601+
# but we forbid it because it indicates user confusion.
561602
if args.no_default_files and (
562-
args.signature or args.certificate or args.rekor_bundle
603+
args.signature or args.certificate or args.rekor_bundle or args.bundle
563604
):
564605
args._parser.error(
565606
"--no-default-files may not be combined with --signature, "
566-
"--certificate, or --rekor-bundle",
607+
"--certificate, --rekor-bundle, or --bundle",
567608
)
568609

610+
# Similarly forbid `--rekor-bundle` with `--bundle`, since it again indicates
611+
# user confusion around outputs.
612+
if args.rekor_bundle and args.bundle:
613+
args._parser.error("--rekor-bundle may not be combined with --bundle")
614+
615+
# Fail if `--bundle` and `--no-bundle` are both specified.
616+
if args.bundle and args.no_bundle:
617+
args._parser.error("--bundle may not be combined with --no-bundle")
618+
569619
# Fail if `--signature` or `--certificate` is specified *and* we have more
570620
# than one input.
571621
if (args.signature or args.certificate or args.rekor_bundle) and len(
@@ -583,18 +633,33 @@ def _sign(args: argparse.Namespace) -> None:
583633
if not file.is_file():
584634
args._parser.error(f"Input must be a file: {file}" 1CF5 )
585635

586-
sig, cert, bundle = args.signature, args.certificate, args.rekor_bundle
587-
if not sig and not cert and not bundle and not args.no_default_files:
636+
sig, cert, rekor_bundle, bundle = (
637+
args.signature,
638+
args.certificate,
639+
args.rekor_bundle,
640+
args.bundle,
641+
)
642+
if (
643+
not sig
644+
and not cert
645+
and not rekor_bundle
646+
and not bundle
647+
and not args.no_default_files
648+
):
588649
sig = file.parent / f"{file.name}.sig"
589650
cert = file.parent / f"{file.name}.crt"
590-
bundle = file.parent / f"{file.name}.rekor"
651+
rekor_bundle = file.parent / f"{file.name}.rekor"
652+
if not args.no_bundle:
653+
bundle = file.parent / f"{file.name}.sigstore"
591654

592655
if not args.overwrite:
593656
extants = []
594657
if sig and sig.exists():
595658
extants.append(str(sig))
596659
if cert and cert.exists():
597660
extants.append(str(cert))
661+
if rekor_bundle and rekor_bundle.exists():
662+
extants.append(str(rekor_bundle))
598663
if bundle and bundle.exists():
599664
extants.append(str(bundle))
600665

@@ -604,7 +669,12 @@ def _sign(args: argparse.Namespace) -> None:
604669
f"{', '.join(extants)}"
605670
)
606671

607-
output_map[file] = {"cert": cert, "sig": sig, "bundle": bundle}
672+
output_map[file] = {
673+
"cert": cert,
674+
"sig": sig,
675+
"rekor_bundle": rekor_bundle,
676+
"bundle": bundle,
677+
}
608678

609679
# Select the signer to use.
610680
if args.staging:
@@ -655,7 +725,7 @@ def _sign(args: argparse.Namespace) -> None:
655725
print(f"Transparency log entry created at index: {result.log_entry.log_index}")
656726

657727
sig_output: TextIO
658-
if outputs["sig"]:
728+
if outputs["sig"] is not None:
659729
sig_output = outputs["sig"].open("w")
660730
else:
661731
sig_output = sys.stdout
@@ -669,11 +739,16 @@ def _sign(args: argparse.Namespace) -> None:
669739
print(result.cert_pem, file=io)
670740
print(f"Certificate written to {outputs['cert']}")
671741

742+
if outputs["rekor_bundle"] is not None:
743+
with outputs["rekor_bundle"].open(mode="w") as io:
744+
rekor_bundle = RekorBundle.from_entry(result.log_entry)
745+
print(rekor_bundle.json(by_alias=True), file=io)
746+
print(f"Rekor bundle written to {outputs['rekor_bundle']}")
747+
672748
if outputs["bundle"] is not None:
673749
with outputs["bundle"].open(mode="w") as io:
674-
bundle = RekorBundle.from_entry(result.log_entry)
675-
print(bundle.json(by_alias=True), file=io)
676-
print(f"Rekor bundle written to {outputs['bundle']}")
750+
print(result._to_bundle().to_json(), file=io)
751+
print(f"Sigstore bundle written to {outputs['bundle']}")
677752

678753

679754
def _collect_verification_state(
@@ -687,6 +762,10 @@ def _collect_verification_state(
687762
purposes) and `materials` is the `VerificationMaterials` to verify with.
688763
"""
689764

765+
# TODO: Allow --bundle during verification. Until then, error.
766+
if args.bundle:
767+
args._parser.error("--bundle is not supported during verification yet")
768+
690769
# `--rekor-bundle` is a temporary option, pending stabilization of the
691770
# Sigstore bundle format.
692771
if args.rekor_bundle:

sigstore/sign.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,24 @@
4747
from cryptography.hazmat.primitives.asymmetric.utils import Prehashed
4848
from cryptography.x509.oid import NameOID
4949
from pydantic import BaseModel
50+
from sigstore_protobuf_specs.dev.sigstore.bundle.v1 import (
51+
Bundle,
52+
VerificationMaterial,
53+
)
54+
from sigstore_protobuf_specs.dev.sigstore.common.v1 import (
55+
HashAlgorithm,
56+
HashOutput,
57+
LogId,
58+
MessageSignature,
59+
X509Certificate,
60+
X509CertificateChain,
61+
)
62+
from sigstore_protobuf_specs.dev.sigstore.rekor.v1 import (
63+
InclusionPromise,
64+
InclusionProof,
65+
KindVersion,
66+
TransparencyLogEntry,
67+
)
5068

5169
from sigstore._internal.fulcio import FulcioClient
5270
from sigstore._internal.oidc import Identity
@@ -163,6 +181,7 @@ def sign(
163181
logger.debug(f"Transparency log entry created with index: {entry.log_index}")
164182

165183
return SigningResult(
184+
input_digest=input_digest.hex(),
166185
cert_pem=cert.public_bytes(encoding=serialization.Encoding.PEM).decode(),
167186
b64_signature=b64_artifact_signature,
168187
log_entry=entry,
@@ -174,6 +193,11 @@ class SigningResult(BaseModel):
174193
Represents the artifacts of a signing operation.
175194
"""
176195

196+
input_digest: str
197+
"""
198+
The hex-encoded SHA256 digest of the input that was signed for.
199+
"""
200+
177201
cert_pem: str
178202
"""
179203
The PEM-encoded public half of the certificate used for signing.
@@ -188,3 +212,59 @@ class SigningResult(BaseModel):
188212
"""
189213
A record of the Rekor log entry for the signing operation.
190214
"""
215+
216+
def _to_bundle(self) -> Bundle:
217+
"""
218+
Creates a Sigstore bundle (as defined by Sigstore's protobuf specs)
219+
from this `SigningResult`.
220+
"""
221+
222+
# TODO: Include the current Fulcio intermediate and root in the
223+
# chain as well.
224+
cert = x509.load_pem_x509_certificate(self.cert_pem.encode())
225+
cert_der = cert.public_bytes(encoding=serialization.Encoding.DER)
226+
chain = X509CertificateChain(certificates=[X509Certificate(raw_bytes=cert_der)])
227+
228+
inclusion_proof: InclusionProof | None = None
229+
if self.log_entry.inclusion_proof is not None:
230+
inclusion_proof = InclusionProof(
231+
log_index=self.log_entry.inclusion_proof.log_index,
232+
root_hash=bytes.fromhex(self.log_entry.inclusion_proof.root_hash),
233+
tree_size=self.log_entry.inclusion_proof.tree_size,
234+
hashes=[
235+
bytes.fromhex(h) for h in self.log_entry.inclusion_proof.hashes
236+
],
237+
)
238+
239+
tlog_entry = TransparencyLogEntry(
240+
log_index=self.log_entry.log_index,
241+
log_id=LogId(key_id=bytes.fromhex(self.log_entry.log_id)),
242+
kind_version=KindVersion(kind="hashedrekord", version="0.0.1"),
243+
integrated_time=self.log_entry.integrated_time,
244+
inclusion_promise=InclusionPromise(
245+
signed_entry_timestamp=base64.b64decode(
246+
self.log_entry.signed_entry_timestamp
247+
)
248+
),
249+
inclusion_proof=inclusion_proof,
250+
canonicalized_body=base64.b64decode(self.log_entry.body),
251+
)
252+
253+
material = VerificationMaterial(
254+
x509_certificate_chain=chain,
255+
tlog_entries=[tlog_entry],
256+
)
257+
258+
bundle = Bundle(
259+
media_type="application/vnd.dev.sigstore.bundle+json;version=0.1",
260+
verification_material=material,
261+
message_signature=MessageSignature(
262+
message_digest=HashOutput(
263+
algorithm=HashAlgorithm.SHA2_256,
264+
digest=bytes.fromhex(self.input_digest),
265+
),
266+
signature=base64.b64decode(self.b64_signature),
267+
),
268+
)
269+
270+
return bundle

0 commit comments

Comments
 (0)
0