Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Offline Rekor bundle generation and verification #247

Merged
merged 40 commits into from
Nov 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
fd95140
_cli: flag scaffolding for offline rekor verification
woodruffw Oct 7, 2022
51d611d
_cli: more scaffolding
woodruffw Oct 7, 2022
bcf6616
Merge branch 'main' into ww/offline-rekor-bundle-verification
woodruffw Oct 11, 2022
e85b5b9
Merge branch 'main' into ww/offline-rekor-bundle-verification
woodruffw Oct 11, 2022
5aaeaf3
sigstore: refactor RekorEntry/SET verification for offline bundles
woodruffw Oct 11, 2022
6139a99
Merge branch 'main' into ww/offline-rekor-bundle-verification
woodruffw Oct 11, 2022
8c96fcd
_cli: add envvar defaults for new flags
woodruffw Oct 11, 2022
96f3af9
README: update `sigstore verify --help`
woodruffw Oct 11, 2022
98529de
_cli: handle `verify --offline` correctly
woodruffw Oct 11, 2022
d2c52c8
Merge branch 'main' into ww/offline-rekor-bundle-verification
woodruffw Oct 11, 2022
5d2e6ae
rekor/client: fix docstring
woodruffw Oct 11, 2022
988e75a
_cli: Add `rekor` suffix to offline bundle flags/options
woodruffw Oct 11, 2022
446cf31
README: update `sigstore verify`
woodruffw Oct 11, 2022
94db410
_verify: elaborate on the properties of a non-inclusion-proof verific…
woodruffw Oct 11, 2022
57d93e2
_verify: fix comment typos, reflow comments
woodruffw Oct 11, 2022
5986ade
Apply suggestions from code review
woodruffw Oct 12, 2022
286a5a4
_cli: lint
woodruffw Oct 12, 2022
b851afe
rekor/client: fix capitalization on Payload key
woodruffw Oct 13, 2022
081caad
rekor/client: fix keys
woodruffw Oct 13, 2022
34a1e4a
_cli: --rekor-bundle implies --rekor-offline
woodruffw Oct 13, 2022
caafd8d
sigstore, test: create and use a separate RekorBundle model
woodruffw Oct 13, 2022
0b0d036
sigstore, test: add offline bundle generation
woodruffw Oct 13, 2022
3abef33
sigstore: blacken
woodruffw Oct 13, 2022
8873b75
test: add an offline rekor test
woodruffw Oct 13, 2022
d689c03
_cli: tweak `--rekor-offline` language slightly
woodruffw Oct 13, 2022
64f4354
README: update `--help` blocks
woodruffw Oct 13, 2022
76b7f4c
test: unused import
woodruffw Oct 13, 2022
64370f5
sigstore: test Rekor entry's consistency against signing artifacts
woodruffw Oct 13, 2022
96bec0f
conftest: strip trailing whitespace from cert and sig
woodruffw Oct 13, 2022
76e2700
treewide: use .rekor for offline rekor bundle files
woodruffw Oct 13, 2022
df005fe
_verify: lint fixes
woodruffw Oct 13, 2022
b5eb560
_verify: more lint fixes
woodruffw Oct 13, 2022
1c3788c
README, _cli: `--rekor-offline` -> `--require-rekor-offline`
woodruffw Oct 13, 2022
44f6546
Apply suggestions from code review
woodruffw Oct 14, 2022
d1a8157
_verify: clarify comments, add a long comment explaining process
woodruffw Oct 14, 2022
e30dd3a
_verify: blacken
woodruffw Oct 14, 2022
d7c7d8e
Merge branch 'main' into ww/offline-rekor-bundle-verification
woodruffw Oct 24, 2022
4c2d4a9
Merge branch 'main' into ww/offline-rekor-bundle-verification
woodruffw Oct 26, 2022
592ec32
Merge branch 'main' into ww/offline-rekor-bundle-verification
woodruffw Oct 26, 2022
ea45d3e
_cli: add warnings when `--rekor-bundle` is used
woodruffw Nov 1, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ build
*.pem
*.sh
*.pub
*.rekor

# Don't ignore these files when we intend to include them
!sigstore/_store/*.crt
Expand All @@ -23,3 +24,4 @@ build
!test/assets/*.txt
!test/assets/*.crt
!test/assets/*.sig
!test/assets/*.rekor
22 changes: 16 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,9 @@ 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] [--overwrite] [--staging]
[--rekor-url URL] [--fulcio-url URL] [--ctfe FILE]
[--rekor-root-pubkey FILE]
[--certificate FILE] [--rekor-bundle FILE] [--overwrite]
[--staging] [--rekor-url URL] [--fulcio-url URL]
[--ctfe FILE] [--rekor-root-pubkey FILE]
FILE [FILE ...]

positional arguments:
Expand All @@ -115,14 +115,18 @@ OpenID Connect options:
--staging) (default: https://oauth2.sigstore.dev/auth)

Output options:
--no-default-files Don't emit the default output files ({input}.sig and
{input}.crt) (default: False)
--no-default-files Don't emit the default output files ({input}.sig,
{input}.crt, {input}.rekor) (default: False)
--signature FILE, --output-signature FILE
Write a single signature to the given file; does not
work with multiple input files (default: None)
--certificate FILE, --output-certificate FILE
Write a single certificate to the given file; does not
work with multiple input files (default: None)
--rekor-bundle FILE, --output-rekor-bundle FILE
Write a single offline Rekor bundle to the given file;
does not work with multiple input files (default:
None)
--overwrite Overwrite preexisting signature and certificate
outputs, if present (default: False)

Expand All @@ -147,7 +151,8 @@ Verifying:
<!-- @begin-sigstore-verify-help@ -->
```
usage: sigstore verify [-h] [--certificate FILE] [--signature FILE]
[--cert-email EMAIL] [--cert-oidc-issuer URL]
[--rekor-bundle FILE] [--cert-email EMAIL]
[--cert-oidc-issuer URL] [--require-rekor-offline]
[--staging] [--rekor-url URL]
FILE [FILE ...]

Expand All @@ -163,13 +168,18 @@ Verification inputs:
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)

Extended verification options:
--cert-email EMAIL The email address 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)

Sigstore instance options:
--staging Use sigstore's staging instances, instead of the
Expand Down
123 changes: 101 additions & 22 deletions sigstore/_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,12 @@
STAGING_OAUTH_ISSUER,
get_identity_token,
)
from sigstore._internal.rekor.client import DEFAULT_REKOR_URL, RekorClient
from sigstore._internal.rekor.client import (
DEFAULT_REKOR_URL,
RekorBundle,
RekorClient,
RekorEntry,
)
from sigstore._sign import Signer
from sigstore._utils import load_pem_public_key
from sigstore._verify import (
Expand Down Expand Up @@ -164,7 +169,7 @@ def _parser() -> argparse.ArgumentParser:
"--no-default-files",
action="store_true",
default=_boolify_env("SIGSTORE_NO_DEFAULT_FILES"),
help="Don't emit the default output files ({input}.sig and {input}.crt)",
help="Don't emit the default output files ({input}.sig, {input}.crt, {input}.rekor)",
)
output_options.add_argument(
"--signature",
Expand All @@ -186,6 +191,17 @@ def _parser() -> argparse.ArgumentParser:
"Write a single certificate to the given file; does not work with multiple input files"
),
)
output_options.add_argument(
"--rekor-bundle",
"--output-rekor-bundle",
metavar="FILE",
type=Path,
default=os.getenv("SIGSTORE_OUTPUT_BUNDLE"),
help=(
"Write a single offline Rekor bundle to the given file; does not work with "
"multiple input files"
),
)
output_options.add_argument(
"--overwrite",
action="store_true",
Expand Down Expand Up @@ -247,6 +263,13 @@ def _parser() -> argparse.ArgumentParser:
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",
)

verification_options = verify.add_argument_group("Extended verification options")
verification_options.add_argument(
Expand All @@ -263,6 +286,12 @@ def _parser() -> argparse.ArgumentParser:
default=os.getenv("SIGSTORE_CERT_OIDC_ISSUER"),
help="The OIDC issuer URL to check for in the certificate's OIDC issuer extension",
)
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.add_argument_group("Sigstore instance options")
_add_shared_instance_options(instance_options)
Expand Down Expand Up @@ -308,20 +337,32 @@ def main() -> None:


def _sign(args: argparse.Namespace) -> None:
# `--no-default-files` has no effect on `--{signature,certificate}`, but we
# `--rekor-bundle` is a temporary option, pending stabilization of the
# Sigstore bundle format.
if args.rekor_bundle:
logger.warning(
"--rekor-bundle is a temporary format, and will be removed in an "
"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.
if args.no_default_files and (args.signature or args.certificate):
if args.no_default_files and (
args.signature or args.certificate or args.rekor_bundle
):
args._parser.error(
"--no-default-files may not be combined with --signature or "
"--certificate",
"--no-default-files may not be combined with --signature, "
"--certificate, or --rekor-bundle",
)

# Fail if `--signature` or `--certificate` is specified *and* we have more
# than one input.
if (args.signature or args.certificate) and len(args.files) > 1:
if (args.signature or args.certificate or args.rekor_bundle) and len(
args.files
) > 1:
args._parser.error(
"Error: --signature and --certificate can't be used with explicit "
"outputs for multiple inputs",
"Error: --signature, --certificate, and --rekor-bundle can't be used "
"with explicit outputs for multiple inputs",
)

# Build up the map of inputs -> outputs ahead of any signing operations,
Expand All @@ -331,25 +372,28 @@ def _sign(args: argparse.Namespace) -> None:
if not file.is_file():
args._parser.error(f"Input must be a file: {file}")

sig, cert = args.signature, args.certificate
if not sig and not cert and not args.no_default_files:
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 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():
extants.append(str(bundle))

if extants:
args._parser.error(
"Refusing to overwrite outputs without --overwrite: "
f"{', '.join(extants)}"
)

output_map[file] = {"cert": cert, "sig": sig}
output_map[file] = {"cert": cert, "sig": sig, "bundle": bundle}

# Select the signer to use.
if args.staging:
Expand Down Expand Up @@ -396,20 +440,41 @@ def _sign(args: argparse.Namespace) -> None:
sig_output = sys.stdout

print(result.b64_signature, file=sig_output)
if outputs["sig"]:
print(f"Signature written to file {outputs['sig']}")
if outputs["sig"] is not None:
print(f"Signature written to {outputs['sig']}")

if outputs["cert"] is not None:
cert_output = open(outputs["cert"], "w")
print(result.cert_pem, file=cert_output)
print(f"Certificate written to file {outputs['cert']}")
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:
with outputs["bundle"].open(mode="w") as io:
bundle = result.log_entry.to_bundle()
print(bundle.json(by_alias=True), file=io)
print(f"Rekor bundle written to {outputs['bundle']}")


def _verify(args: argparse.Namespace) -> None:
# Fail if `--certificate` or `--signature` is specified and we have more than one input.
if (args.certificate or args.signature) and len(args.files) > 1:
# `--rekor-bundle` is a temporary option, pending stabilization of the
# Sigstore bundle format.
if args.rekor_bundle:
logger.warning(
"--rekor-bundle is a temporary format, and will be removed in an "
"upcoming release of sigstore-python in favor of Sigstore-style bundles"
)

# The presence of --rekor-bundle implies --require-rekor-offline.
args.require_rekor_offline = args.require_rekor_offline or args.rekor_bundle

# Fail if --certificate, --signature, or --rekor-bundle is specified and we
# have more than one input.
if (args.certificate or args.signature or args.rekor_bundle) and len(
args.files
) > 1:
args._parser.error(
"--certificate and --signature can only be used with a single input file"
"--certificate, --signature, and --rekor-bundle can only be used "
"with a single input file"
)

# The converse of `sign`: we build up an expected input map and check
Expand All @@ -419,24 +484,31 @@ def _verify(args: argparse.Namespace) -> None:
if not file.is_file():
args._parser.error(f"Input must be a file: {file}")

sig, cert = args.signature, args.certificate
sig, cert, bundle = args.signature, args.certificate, args.rekor_bundle
if sig is None:
sig = file.parent / f"{file.name}.sig"
if cert is None:
cert = file.parent / f"{file.name}.crt"
if bundle is None:
bundle = file.parent / f"{file.name}.rekor"

missing = []
if not sig.is_file():
missing.append(str(sig))
if not cert.is_file():
missing.append(str(cert))
if not bundle.is_file() and args.require_rekor_offline:
woodruffw marked this conversation as resolved.
Show resolved Hide resolved
# NOTE: We only produce errors on missing bundle files
# if the user has explicitly requested offline-only verification.
# Otherwise, we fall back on online verification.
missing.append(str(bundle))

if missing:
args._parser.error(
f"Missing verification materials for {(file)}: {', '.join(missing)}"
)

input_map[file] = {"cert": cert, "sig": sig}
input_map[file] = {"cert": cert, "sig": sig, "bundle": bundle}

if args.staging:
logger.debug("verify: staging instances requested")
Expand All @@ -459,6 +531,12 @@ def _verify(args: argparse.Namespace) -> None:
logger.debug(f"Using signature from: {inputs['sig']}")
signature = inputs["sig"].read_bytes().rstrip()

entry: Optional[RekorEntry] = None
if inputs["bundle"].is_file():
logger.debug(f"Using offline Rekor bundle from: {inputs['bundle']}")
bundle = RekorBundle.parse_file(inputs["bundle"])
entry = bundle.to_entry()

logger.debug(f"Verifying contents from: {file}")

result = verifier.verify(
Expand All @@ -467,6 +545,7 @@ def _verify(args: argparse.Namespace) -> None:
signature=signature,
expected_cert_email=args.cert_email,
expected_cert_oidc_issuer=args.cert_oidc_issuer,
offline_rekor_entry=entry,
)

if result:
Expand Down
9 changes: 5 additions & 4 deletions sigstore/_internal/merkle.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
import struct
from typing import List, Tuple

from sigstore._internal.rekor import RekorEntry, RekorInclusionProof
from sigstore._internal.rekor import RekorEntry


class InvalidInclusionProofError(Exception):
Expand Down Expand Up @@ -91,10 +91,11 @@ def _hash_leaf(leaf: bytes) -> bytes:
return hashlib.sha256(data).digest()


def verify_merkle_inclusion(
inclusion_proof: RekorInclusionProof, entry: RekorEntry
) -> None:
def verify_merkle_inclusion(entry: RekorEntry) -> None:
woodruffw marked this conversation as resolved.
Show resolved Hide resolved
"""Verify the Merkle Inclusion Proof for a given Rekor entry"""
inclusion_proof = entry.inclusion_proof
if inclusion_proof is None:
raise InvalidInclusionProofError("Rekor entry has no inclusion proof")

# Figure out which subset of hashes corresponds to the inner and border nodes.
inner, border = _decomp_inclusion_proof(
Expand Down
Loading