Skip to content

Commit 4ac02ea

Browse files
_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 <asc@tetsuo.sh> * README: Update README with new flags Signed-off-by: Alex Cameron <asc@tetsuo.sh> * README: Update usage Signed-off-by: Alex Cameron <asc@tetsuo.sh> * README: Document the new `--certificate-chain` flag Signed-off-by: Alex Cameron <asc@tetsuo.sh> * Update sigstore/_cli.py Co-authored-by: William Woodruff <william@trailofbits.com> Signed-off-by: Alex Cameron <asc@tetsuo.sh> * _cli: Amend `--certificate-chain` description Signed-off-by: Alex Cameron <asc@tetsuo.sh> * _cli: Move check for empty PEM file Signed-off-by: Alex Cameron <asc@tetsuo.sh> * _cli, _utils: Move split chain helper to utilities module Signed-off-by: Alex Cameron <asc@tetsuo.sh> * _utils: appease the linter Signed-off-by: William Woodruff <william@trailofbits.com> * _utils: lintage Signed-off-by: William Woodruff <william@trailofbits.com> * sigstore: more linting, use intrinsic list for type Signed-off-by: William Woodruff <william@trailofbits.com> Signed-off-by: Alex Cameron <asc@tetsuo.sh> Signed-off-by: William Woodruff <william@trailofbits.com> Co-authored-by: William Woodruff <william@trailofbits.com>
1 parent 8a7767e commit 4ac02ea

File tree

3 files changed

+93
-21
lines changed

3 files changed

+93
-21
lines changed

README.md

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,8 @@ usage: sigstore sign [-h] [--identity-token TOKEN] [--oidc-client-id ID]
9191
[--oidc-disable-ambient-providers] [--oidc-issuer URL]
9292
[--no-default-files] [--signature FILE]
9393
[--certificate FILE] [--rekor-bundle FILE] [--overwrite]
94-
[--staging] [--rekor-url URL] [--fulcio-url URL]
95-
[--ctfe FILE] [--rekor-root-pubkey FILE]
94+
[--staging] [--rekor-url URL] [--rekor-root-pubkey FILE]
95+
[--fulcio-url URL] [--ctfe FILE]
9696
FILE [FILE ...]
9797

9898
positional arguments:
@@ -136,14 +136,14 @@ Sigstore instance options:
136136
default production instances (default: False)
137137
--rekor-url URL The Rekor instance to use (conflicts with --staging)
138138
(default: https://rekor.sigstore.dev)
139-
--fulcio-url URL The Fulcio instance to use (conflicts with --staging)
140-
(default: https://fulcio.sigstore.dev)
141-
--ctfe FILE A PEM-encoded public key for the CT log (conflicts
142-
with --staging) (default: ctfe.pub (embedded))
143139
--rekor-root-pubkey FILE
144140
A PEM-encoded root public key for Rekor itself
145141
(conflicts with --staging) (default: rekor.pub
146142
(embedded))
143+
--fulcio-url URL The Fulcio instance to use (conflicts with --staging)
144+
(default: https://fulcio.sigstore.dev)
145+
--ctfe FILE A PEM-encoded public key for the CT log (conflicts
146+
with --staging) (default: ctfe.pub (embedded))
147147
```
148148
<!-- @end-sigstore-sign-help@ -->
149149
@@ -152,9 +152,11 @@ Verifying:
152152
<!-- @begin-sigstore-verify-help@ -->
153153
```
154154
usage: sigstore verify [-h] [--certificate FILE] [--signature FILE]
155-
[--rekor-bundle FILE] [--cert-email EMAIL]
156-
--cert-identity IDENTITY --cert-oidc-issuer URL
157-
[--require-rekor-offline] [--staging] [--rekor-url URL]
155+
[--rekor-bundle FILE] [--certificate-chain FILE]
156+
[--cert-email EMAIL] --cert-identity IDENTITY
157+
--cert-oidc-issuer URL [--require-rekor-offline]
158+
[--staging] [--rekor-url URL]
159+
[--rekor-root-pubkey FILE]
158160
FILE [FILE ...]
159161

160162
positional arguments:
@@ -173,6 +175,10 @@ Verification inputs:
173175
multiple inputs (default: None)
174176

175177
Extended verification options:
178+
--certificate-chain FILE
179+
Path to a list of CA certificates in PEM format which
180+
will be needed when building the certificate chain for
181+
the signing certificate (default: None)
176182
--cert-email EMAIL Deprecated; causes an error. Use --cert-identity
177183
instead (default: None)
178184
--cert-identity IDENTITY
@@ -190,6 +196,10 @@ Sigstore instance options:
190196
default production instances (default: False)
191197
--rekor-url URL The Rekor instance to use (conflicts with --staging)
192198
(default: https://rekor.sigstore.dev)
199+
--rekor-root-pubkey FILE
200+
A PEM-encoded root public key for Rekor itself
201+
(conflicts with --staging) (default: rekor.pub
202+
(embedded))
193203
```
194204
<!-- @end-sigstore-verify-help@ -->
195205

sigstore/_cli.py

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,11 @@
4242
RekorEntry,
4343
)
4444
from sigstore._sign import Signer
45-
from sigstore._utils import load_pem_public_key
45+
from sigstore._utils import (
46+
SplitCertificateChainError,
47+
load_pem_public_key,
48+
split_certificate_chain,
49+
)
4650
from sigstore._verify import (
4751
CertificateVerificationFailure,
4852
RekorEntryMissing,
@@ -107,6 +111,13 @@ def _add_shared_instance_options(group: argparse._ArgumentGroup) -> None:
107111
default=os.getenv("SIGSTORE_REKOR_URL", DEFAULT_REKOR_URL),
108112
help="The Rekor instance to use (conflicts with --staging)",
109113
)
114+
group.add_argument(
115+
"--rekor-root-pubkey",
116+
metavar="FILE",
117+
type=argparse.FileType("rb"),
118+
help="A PEM-encoded root public key for Rekor itself (conflicts with --staging)",
119+
default=os.getenv("SIGSTORE_REKOR_ROOT_PUBKEY", _Embedded("rekor.pub")),
120+
)
110121

111122

112123
def _add_shared_oidc_options(
@@ -229,13 +240,6 @@ def _parser() -> argparse.ArgumentParser:
229240
help="A PEM-encoded public key for the CT log (conflicts with --staging)",
230241
default=os.getenv("SIGSTORE_CTFE", _Embedded("ctfe.pub")),
231242
)
232-
instance_options.add_argument(
233-
"--rekor-root-pubkey",
234-
metavar="FILE",
235-
type=argparse.FileType("rb"),
236-
help="A PEM-encoded root public key for Rekor itself (conflicts with --staging)",
237-
default=os.getenv("SIGSTORE_REKOR_ROOT_PUBKEY", _Embedded("rekor.pub")),
238-
)
239243

240244
sign.add_argument(
241245
"files",
@@ -275,6 +279,15 @@ def _parser() -> argparse.ArgumentParser:
275279
)
276280

277281
verification_options = verify.add_argument_group("Extended verification options")
282+
verification_options.add_argument(
283+
"--certificate-chain",
284+
metavar="FILE",
285+
type=argparse.FileType("r"),
286+
help=(
287+
"Path to a list of CA certificates in PEM format which will be needed when building "
288+
"the certificate chain for the signing certificate"
289+
),
290+
)
278291
verification_options.add_argument(
279292
"--cert-email",
280293
metavar="EMAIL",
@@ -536,10 +549,24 @@ def _verify(args: argparse.Namespace) -> None:
536549
elif args.rekor_url == DEFAULT_REKOR_URL:
537550
verifier = Verifier.production()
538551
else:
539-
# TODO: We need CLI flags that allow the user to figure the Fulcio cert chain
540-
# for verification.
541-
args._parser.error(
542-
"Custom Rekor and Fulcio configuration for verification isn't fully supported yet!",
552+
if not args.certificate_chain:
553+
args._parser.error(
554+
"Custom Rekor URL used without specifying --certificate-chain"
555+
)
556+
557+
try:
558+
certificate_chain = split_certificate_chain(args.certificate_chain.read())
559+
except SplitCertificateChainError as error:
560+
args._parser.error(f"Failed to parse certificate chain: {error}")
561+
562+
verifier = Verifier(
563+
rekor=RekorClient(
564+
url=args.rekor_url,
565+
pubkey=args.rekor_root_pubkey.read(),
566+
# We don't use the CT keyring in verification so we can supply an empty keyring
567+
ct_keyring=CTKeyring(),
568+
),
569+
fulcio_certificate_chain=certificate_chain,
543570
)
544571

545572
for file, inputs in input_map.items():

sigstore/_utils.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
Shared utilities.
1717
"""
1818

19+
from __future__ import annotations
20+
1921
import base64
2022
import hashlib
2123
from typing import Union
@@ -69,3 +71,36 @@ def key_id(key: PublicKey) -> bytes:
6971
)
7072

7173
return hashlib.sha256(public_bytes).digest()
74+
75+
76+
class SplitCertificateChainError(Exception):
77+
pass
78+
79+
80+
def split_certificate_chain(chain_pem: str) -> list[bytes]:
81+
"""
82+
Returns a list of PEM bytes for each individual certificate in the chain.
83+
"""
84+
pem_header = "-----BEGIN CERTIFICATE-----"
85+
86+
# Check for no certificates
87+
if not chain_pem:
88+
raise SplitCertificateChainError("empty PEM file")
89+
90+
# Use the "begin certificate" marker as a delimiter to split the chain
91+
certificate_chain = chain_pem.split(pem_header)
92+
93+
# The first entry in the list should be empty since we split by the "begin certificate" marker
94+
# and there should be nothing before the first certificate
95+
if certificate_chain[0]:
96+
raise SplitCertificateChainError(
97+
"encountered unrecognized content before first PEM entry"
98+
)
99+
100+
# Remove the empty entry
101+
certificate_chain = certificate_chain[1:]
102+
103+
# Add the delimiters back into each entry since this is required for valid PEM
104+
certificate_chain = [(pem_header + c).encode() for c in certificate_chain]
105+
106+
return certificate_chain

0 commit comments

Comments
 (0)