Skip to content

Commit

Permalink
sigstore verify: support multiple inputs (#130)
Browse files Browse the repository at this point in the history
* cli, README: support multiple inputs to `sigstore verify`

Signed-off-by: William Woodruff <william@trailofbits.com>

* staging-tests: remove unneeded flags

Signed-off-by: William Woodruff <william@trailofbits.com>

* Apply suggestions from code review

Co-authored-by: Dustin Ingram <di@users.noreply.github.com>

* README: add some `sigstore verify` examples

Signed-off-by: William Woodruff <william@trailofbits.com>

* Apply suggestions from code review

Co-authored-by: Dustin Ingram <di@users.noreply.github.com>

Co-authored-by: Dustin Ingram <di@users.noreply.github.com>
  • Loading branch information
woodruffw and di authored Jun 10, 2022
1 parent 5d1f7e2 commit 4ecd7c0
Show file tree
Hide file tree
Showing 3 changed files with 152 additions and 62 deletions.
6 changes: 2 additions & 4 deletions .github/workflows/staging-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,5 @@ jobs:
# Verification also requires a different Rekor instance, so we
# also test it.
./staging-env/bin/python -m sigstore verify --staging \
README.md \
--cert README.md.crt \
--signature README.md.sig \
--cert-oidc-issuer https://token.actions.githubusercontent.com
--cert-oidc-issuer https://token.actions.githubusercontent.com \
README.md
63 changes: 58 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,10 +125,10 @@ Verifying:

<!-- @begin-sigstore-verify-help@ -->
```
usage: sigstore verify [-h] --certificate FILE --signature FILE
usage: sigstore verify [-h] [--certificate FILE] [--signature FILE]
[--cert-email EMAIL] [--cert-oidc-issuer URL]
[--rekor-url URL] [--staging]
FILE
FILE [FILE ...]
positional arguments:
FILE The file to verify
Expand All @@ -138,9 +138,10 @@ options:
Verification inputs:
--certificate FILE, --cert FILE
The PEM-encoded certificate to verify against
(default: None)
--signature FILE The signature to verify against (default: None)
The PEM-encoded certificate to verify against; not
used with multiple inputs (default: None)
--signature FILE The signature to verify against; not used with
multiple inputs (default: None)
Extended verification options:
--cert-email EMAIL The email address to check for in the certificate's
Expand Down Expand Up @@ -215,6 +216,58 @@ $ python -m sigstore sign --identity-token YOUR-LONG-JWT-HERE foo.txt
Note that passing a custom identity token does not circumvent Fulcio's requirements,
namely the Fulcio's supported identity providers and the claims expected within the token.

### Verifying against a signature and certificate

By default, `sigstore verify` will attempt to find a `<filename>.sig` and `<filename>.crt` in the
same directory as the file being verified:

```console
# looks for foo.txt.sig and foo.txt.crt
$ python -m sigstore verify foo.txt
```

Multiple files can be verified at once:

```console
# looks for {foo,bar}.txt.{sig,crt}
$ python -m sigstore verify foo.txt bar.txt
```

If your signature and certificate are at different paths, you can specify them
explicitly (but only for one file at a time):

```console
$ python -m sigstore verify \
--certificate some/other/path/foo.crt \
--signature some/other/path/foo.sig \
foo.txt
```

### Extended verification against OpenID Connect claims

By default, `sigstore verify` only checks the validity of the certificate,
the correctness of the signature, and the consistency of both with the
certificate transparency log.

To assert further details about the signature (such as *who* or *what* signed for the artifact),
you can test against the OpenID Connect claims embedded within it.

For example, to accept the signature and certificate only if they correspond to a particular
email identity:

```console
$ python -m sigstore verify --cert-email developer@example.com foo.txt
```

Or to accept only if the OpenID Connect issuer is the expected one:

```console
$ python -m sigstore verify --cert-oidc-issuer https://github.com/login/oauth foo.txt
```

These options can be combined, and further extended validation options (e.g., for
signing results from GitHub Actions) are under development.

## Licensing

`sigstore` is licensed under the Apache 2.0 License.
Expand Down
145 changes: 92 additions & 53 deletions sigstore/_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,16 +191,14 @@ def _parser() -> argparse.ArgumentParser:
"--certificate",
"--cert",
metavar="FILE",
type=argparse.FileType("rb"),
required=True,
help="The PEM-encoded certificate to verify against",
type=Path,
help="The PEM-encoded certificate to verify against; not used with multiple inputs",
)
input_options.add_argument(
"--signature",
metavar="FILE",
type=argparse.FileType("rb"),
required=True,
help="The signature to verify against",
type=Path,
help="The signature to verify against; not used with multiple inputs",
)

verification_options = verify.add_argument_group("Extended verification options")
Expand Down Expand Up @@ -232,7 +230,11 @@ def _parser() -> argparse.ArgumentParser:
)

verify.add_argument(
"file", metavar="FILE", type=argparse.FileType("rb"), help="The file to verify"
"files",
metavar="FILE",
type=Path,
nargs="+",
help="The file to verify",
)

return parser
Expand Down Expand Up @@ -277,6 +279,9 @@ def _sign(args: argparse.Namespace) -> None:
# so that we can fail early if overwriting without `--overwrite`.
output_map = {}
for file in args.files:
if not file.is_file():
args._parser.error(f"Input must be a file: {file}")

sig, cert = args.output_signature, args.output_certificate
if not args.no_default_files:
sig = file.parent / f"{file.name}.sig"
Expand Down Expand Up @@ -362,6 +367,38 @@ def _sign(args: argparse.Namespace) -> None:


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:
args._parser.error(
"--certificate and --signature can only be used with a single input file"
)

# The converse of `sign`: we build up an expected input map and check
# that we have everything so that we can fail early.
input_map = {}
for file in args.files:
if not file.is_file():
args._parser.error(f"Input must be a file: {file}")

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

missing = []
if not sig.is_file():
missing.append(str(sig))
if not cert.is_file():
missing.append(str(cert))

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

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

if args.staging:
logger.debug("verify: staging instances requested")
verifier = Verifier.staging()
Expand All @@ -374,51 +411,53 @@ def _verify(args: argparse.Namespace) -> None:
"Custom Rekor and Fulcio configuration for verification isn't fully supported yet!",
)

# Load the signing certificate
logger.debug(f"Using certificate from: {args.certificate.name}")
certificate = args.certificate.read()

# Load the signature
logger.debug(f"Using signature from: {args.signature.name}")
signature = args.signature.read()

logger.debug(f"Verifying contents from: {args.file.name}")
result = verifier.verify(
input_=args.file.read(),
certificate=certificate,
signature=signature,
expected_cert_email=args.cert_email,
expected_cert_oidc_issuer=args.cert_oidc_issuer,
)
for file, inputs in input_map.items():
# Load the signing certificate
logger.debug(f"Using certificate from: {inputs['cert']}")
certificate = inputs["cert"].read_bytes()

if result:
print(f"OK: {args.file.name}")
else:
result = cast(VerificationFailure, result)
print(f"FAIL: {args.file.name}")
print(f"Failure reason: {result.reason}", file=sys.stderr)

if isinstance(result, CertificateVerificationFailure):
# If certificate verification failed, it's either because of
# a chain issue or some outdated state in sigstore itself.
# These might already be resolved in a newer version, so
# we suggest that users try to upgrade and retry before
# anything else.
print(
dedent(
f"""
This may be a result of an outdated `sigstore` installation.
Consider upgrading with:
python -m pip install --upgrade sigstore
Additional context:
{result.exception}
"""
),
file=sys.stderr,
)
# Load the signature
logger.debug(f"Using signature from: {inputs['sig']}")
signature = inputs["sig"].read_bytes()

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

result = verifier.verify(
input_=file.read_bytes(),
certificate=certificate,
signature=signature,
expected_cert_email=args.cert_email,
expected_cert_oidc_issuer=args.cert_oidc_issuer,
)

if result:
print(f"OK: {file}")
else:
result = cast(VerificationFailure, result)
print(f"FAIL: {file}")
print(f"Failure reason: {result.reason}", file=sys.stderr)

if isinstance(result, CertificateVerificationFailure):
# If certificate verification failed, it's either because of
# a chain issue or some outdated state in sigstore itself.
# These might already be resolved in a newer version, so
# we suggest that users try to upgrade and retry before
# anything else.
print(
dedent(
f"""
This may be a result of an outdated `sigstore` installation.
Consider upgrading with:
python -m pip install --upgrade sigstore
Additional context:
{result.exception}
"""
),
file=sys.stderr,
)

sys.exit(1)
sys.exit(1)

0 comments on commit 4ecd7c0

Please sign in to comment.