From 4ac02ea9e774c40427f2eddfa4187dcef1d4dee7 Mon Sep 17 00:00:00 2001 From: Alex Cameron Date: Thu, 1 Dec 2022 03:07:32 +1100 Subject: [PATCH] _cli: Add `--certificate-chain` and support `--rekor-url` for verification (#323) * _cli: Add `--certificate-chain` and support `--rekor-url` for verification Signed-off-by: Alex Cameron * README: Update README with new flags Signed-off-by: Alex Cameron * README: Update usage Signed-off-by: Alex Cameron * README: Document the new `--certificate-chain` flag Signed-off-by: Alex Cameron * Update sigstore/_cli.py Co-authored-by: William Woodruff Signed-off-by: Alex Cameron * _cli: Amend `--certificate-chain` description Signed-off-by: Alex Cameron * _cli: Move check for empty PEM file Signed-off-by: Alex Cameron * _cli, _utils: Move split chain helper to utilities module Signed-off-by: Alex Cameron * _utils: appease the linter Signed-off-by: William Woodruff * _utils: lintage Signed-off-by: William Woodruff * sigstore: more linting, use intrinsic list for type Signed-off-by: William Woodruff Signed-off-by: Alex Cameron Signed-off-by: William Woodruff Co-authored-by: William Woodruff --- README.md | 28 +++++++++++++++++-------- sigstore/_cli.py | 51 +++++++++++++++++++++++++++++++++++----------- sigstore/_utils.py | 35 +++++++++++++++++++++++++++++++ 3 files changed, 93 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 28707948..ba403744 100644 --- a/README.md +++ b/README.md @@ -91,8 +91,8 @@ usage: sigstore sign [-h] [--identity-token TOKEN] [--oidc-client-id ID] [--oidc-disable-ambient-providers] [--oidc-issuer URL] [--no-default-files] [--signature FILE] [--certificate FILE] [--rekor-bundle FILE] [--overwrite] - [--staging] [--rekor-url URL] [--fulcio-url URL] - [--ctfe FILE] [--rekor-root-pubkey FILE] + [--staging] [--rekor-url URL] [--rekor-root-pubkey FILE] + [--fulcio-url URL] [--ctfe FILE] FILE [FILE ...] positional arguments: @@ -136,14 +136,14 @@ Sigstore instance options: default production instances (default: False) --rekor-url URL The Rekor instance to use (conflicts with --staging) (default: https://rekor.sigstore.dev) - --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 - with --staging) (default: ctfe.pub (embedded)) --rekor-root-pubkey FILE A PEM-encoded root public key for Rekor itself (conflicts with --staging) (default: rekor.pub (embedded)) + --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 + with --staging) (default: ctfe.pub (embedded)) ``` @@ -152,9 +152,11 @@ Verifying: ``` usage: sigstore verify [-h] [--certificate FILE] [--signature FILE] - [--rekor-bundle FILE] [--cert-email EMAIL] - --cert-identity IDENTITY --cert-oidc-issuer URL - [--require-rekor-offline] [--staging] [--rekor-url URL] + [--rekor-bundle FILE] [--certificate-chain FILE] + [--cert-email EMAIL] --cert-identity IDENTITY + --cert-oidc-issuer URL [--require-rekor-offline] + [--staging] [--rekor-url URL] + [--rekor-root-pubkey FILE] FILE [FILE ...] positional arguments: @@ -173,6 +175,10 @@ Verification inputs: multiple inputs (default: None) 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) --cert-identity IDENTITY @@ -190,6 +196,10 @@ Sigstore instance options: 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: rekor.pub + (embedded)) ``` diff --git a/sigstore/_cli.py b/sigstore/_cli.py index f303203c..c5ca84d2 100644 --- a/sigstore/_cli.py +++ b/sigstore/_cli.py @@ -42,7 +42,11 @@ RekorEntry, ) from sigstore._sign import Signer -from sigstore._utils import load_pem_public_key +from sigstore._utils import ( + SplitCertificateChainError, + load_pem_public_key, + split_certificate_chain, +) from sigstore._verify import ( CertificateVerificationFailure, RekorEntryMissing, @@ -107,6 +111,13 @@ def _add_shared_instance_options(group: argparse._ArgumentGroup) -> None: default=os.getenv("SIGSTORE_REKOR_URL", DEFAULT_REKOR_URL), help="The Rekor instance to use (conflicts with --staging)", ) + group.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", _Embedded("rekor.pub")), + ) def _add_shared_oidc_options( @@ -229,13 +240,6 @@ def _parser() -> argparse.ArgumentParser: help="A PEM-encoded public key for the CT log (conflicts with --staging)", default=os.getenv("SIGSTORE_CTFE", _Embedded("ctfe.pub")), ) - 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", _Embedded("rekor.pub")), - ) sign.add_argument( "files", @@ -275,6 +279,15 @@ def _parser() -> argparse.ArgumentParser: ) verification_options = verify.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.add_argument( "--cert-email", metavar="EMAIL", @@ -536,10 +549,24 @@ def _verify(args: argparse.Namespace) -> None: elif args.rekor_url == DEFAULT_REKOR_URL: verifier = Verifier.production() else: - # TODO: We need CLI flags that allow the user to figure the Fulcio cert chain - # for verification. - args._parser.error( - "Custom Rekor and Fulcio configuration for verification isn't fully supported yet!", + if not args.certificate_chain: + args._parser.error( + "Custom Rekor URL used without specifying --certificate-chain" + ) + + try: + certificate_chain = split_certificate_chain(args.certificate_chain.read()) + except SplitCertificateChainError as error: + args._parser.error(f"Failed to parse certificate chain: {error}") + + verifier = Verifier( + rekor=RekorClient( + url=args.rekor_url, + pubkey=args.rekor_root_pubkey.read(), + # We don't use the CT keyring in verification so we can supply an empty keyring + ct_keyring=CTKeyring(), + ), + fulcio_certificate_chain=certificate_chain, ) for file, inputs in input_map.items(): diff --git a/sigstore/_utils.py b/sigstore/_utils.py index 5287e9ef..086a7ebc 100644 --- a/sigstore/_utils.py +++ b/sigstore/_utils.py @@ -16,6 +16,8 @@ Shared utilities. """ +from __future__ import annotations + import base64 import hashlib from typing import Union @@ -69,3 +71,36 @@ def key_id(key: PublicKey) -> bytes: ) return hashlib.sha256(public_bytes).digest() + + +class SplitCertificateChainError(Exception): + pass + + +def split_certificate_chain(chain_pem: str) -> list[bytes]: + """ + Returns a list of PEM bytes for each individual certificate in the chain. + """ + pem_header = "-----BEGIN CERTIFICATE-----" + + # Check for no certificates + if not chain_pem: + raise SplitCertificateChainError("empty PEM file") + + # Use the "begin certificate" marker as a delimiter to split the chain + certificate_chain = chain_pem.split(pem_header) + + # The first entry in the list should be empty since we split by the "begin certificate" marker + # and there should be nothing before the first certificate + if certificate_chain[0]: + raise SplitCertificateChainError( + "encountered unrecognized content before first PEM entry" + ) + + # Remove the empty entry + certificate_chain = certificate_chain[1:] + + # Add the delimiters back into each entry since this is required for valid PEM + certificate_chain = [(pem_header + c).encode() for c in certificate_chain] + + return certificate_chain