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

_cli: add sigstore verify identity #379

Merged
merged 12 commits into from
Jan 4, 2023
11 changes: 10 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,22 +57,31 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3.2.0

# NOTE: We intentionally lint against our minimum supported Python.
- uses: actions/setup-python@5ccb29d8773c3f3f653e1705f474dfaa8a06a912
with:
python-version: "3.7"

- name: deps
run: make dev SIGSTORE_EXTRA=lint

- name: lint
run: make lint

check-readme:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3.2.0

# NOTE: We intentional check `--help` rendering against our minimum Python,
# since it changes slightly between Python versions.
- uses: actions/setup-python@5ccb29d8773c3f3f653e1705f474dfaa8a06a912
with:
python-version: "3.x"
python-version: "3.7"

- name: deps
run: make dev

- name: check-readme
run: make check-readme
6 changes: 3 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -117,14 +117,14 @@ check-readme:
$(MAKE) -s run ARGS="sign --help" \
)

# sigstore verify --help
# sigstore verify identity --help
@diff \
<( \
awk '/@begin-sigstore-verify-help@/{f=1;next} /@end-sigstore-verify-help@/{f=0} f' \
awk '/@begin-sigstore-verify-identity-help@/{f=1;next} /@end-sigstore-verify-identity-help@/{f=0} f' \
< README.md | sed '1d;$$d' \
) \
<( \
$(MAKE) -s run ARGS="verify --help" \
$(MAKE) -s run ARGS="verify identity --help" \
)


Expand Down
79 changes: 36 additions & 43 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,15 +78,16 @@ a tool for signing and verifying Python package distributions
positional arguments:
{sign,verify,get-identity-token}

options:
optional arguments:
-h, --help show this help message and exit
-V, --version show program's version number and exit
-v, --verbose run with additional debug logging; supply multiple
times to increase verbosity (default: 0)
```
<!-- @end-sigstore-help@ -->

Signing:

### Signing

<!-- @begin-sigstore-sign-help@ -->
```
Expand All @@ -102,7 +103,7 @@ usage: sigstore sign [-h] [--identity-token TOKEN] [--oidc-client-id ID]
positional arguments:
FILE The file to sign

options:
optional arguments:
-h, --help show this help message and exit

OpenID Connect options:
Expand Down Expand Up @@ -150,22 +151,30 @@ Sigstore instance options:
```
<!-- @end-sigstore-sign-help@ -->

Verifying:
### Verifying

#### Identities

<!-- @begin-sigstore-verify-help@ -->
This is the most common verification done with `sigstore`, and therefore
the one you probably want: you can use it to verify that a signature was
produced by a particular identity (like `hamilcar@example.com`), as attested
to by a particular OIDC provider (like `https://github.com/login/oauth`).

<!-- @begin-sigstore-verify-identity-help@ -->
```
usage: sigstore verify [-h] [--certificate FILE] [--signature FILE]
[--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 ...]
usage: sigstore verify identity [-h] [--certificate FILE] [--signature FILE]
[--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:
FILE The file to verify

options:
optional arguments:
-h, --help show this help message and exit

Verification inputs:
Expand Down Expand Up @@ -203,7 +212,11 @@ Sigstore instance options:
A PEM-encoded root public key for Rekor itself
(conflicts with --staging) (default: None)
```
<!-- @end-sigstore-verify-help@ -->
<!-- @end-sigstore-verify-identity-help@ -->

For backwards compatibility, `sigstore verify [args ...]` is equivalent to
`sigstore verify identity [args ...]`, but the latter form is **strongly**
preferred.

## Example uses

Expand Down Expand Up @@ -270,51 +283,31 @@ same directory as the file being verified:

```console
# looks for foo.txt.sig and foo.txt.crt
$ python -m sigstore verify foo.txt
$ python -m sigstore verify identity foo.txt \
--cert-identity 'hamilcar@example.com' \
--cert-oidc-issuer 'https://github.com/login/oauth'
```

Multiple files can be verified at once:

```console
# looks for {foo,bar}.txt.{sig,crt}
$ python -m sigstore verify foo.txt bar.txt
$ python -m sigstore verify identity foo.txt bar.txt \
--cert-identity 'hamilcar@example.com' \
--cert-oidc-issuer 'https://github.com/login/oauth'
```

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 \
$ python -m sigstore verify identity foo.txt \
--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
--cert-identity 'hamilcar@example.com' \
--cert-oidc-issuer 'https://github.com/login/oauth'
```

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
60 changes: 55 additions & 5 deletions sigstore/_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,42 @@ def _boolify_env(envvar: str) -> bool:
raise ValueError(f"can't coerce '{val}' to a boolean")


def _set_default_verify_subparser(parser: argparse.ArgumentParser, name: str) -> None:
"""
An argparse patch for configuring a default subparser for `sigstore verify`.

Adapted from <https://stackoverflow.com/a/26379693>
"""
subparser_found = False
for arg in sys.argv[1:]:
if arg in ["-h", "--help"]: # global help if no subparser
break
else:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoa... how did I not know that this exists.

for x in parser._subparsers._actions: # type: ignore[union-attr]
if not isinstance(x, argparse._SubParsersAction):
continue
for sp_name in x._name_parser_map.keys():
if sp_name in sys.argv[1:]:
subparser_found = True
if not subparser_found:
try:
# If `sigstore verify identity` wasn't passed explicitly, we need
# to insert the `identity` subcommand into the correct position
# within `sys.argv`. To do that, we get the index of the `verify`
# subcommand, and insert it directly after it.
verify_idx = sys.argv.index("verify")
sys.argv.insert(verify_idx + 1, name)
Copy link
Collaborator

@tetsuo-cpp tetsuo-cpp Jan 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we maybe log a warning here? I realise we don't want to make a breaking change but perhaps we can at least mark it as deprecated and slate it for future removal.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, a log sounds good. I'll do that in a moment.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logger.warning(
"`sigstore verify` without a subcommand will be treated as "
"`sigstore verify identity`, but this behavior will be deprecated "
"in a future release"
)
except ValueError:
# This happens when we invoke `sigstore sign`, since there's no
# `verify` subcommand to insert under. We do nothing in this case.
pass


def _add_shared_instance_options(group: argparse._ArgumentGroup) -> None:
group.add_argument(
"--staging",
Expand Down Expand Up @@ -243,10 +279,18 @@ def _parser() -> argparse.ArgumentParser:

# `sigstore verify`
verify = subcommands.add_parser(
"verify", formatter_class=argparse.ArgumentDefaultsHelpFormatter
"verify",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
verify_subcommand = verify.add_subparsers(dest="verify_subcommand")

input_options = verify.add_argument_group("Verification inputs")
# `sigstore verify identity`
verify_identity = verify_subcommand.add_parser(
"identity",
help="verify against a known identity and identity provider",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
input_options = verify_identity.add_argument_group("Verification inputs")
input_options.add_argument(
"--certificate",
"--cert",
Expand All @@ -270,7 +314,9 @@ def _parser() -> argparse.ArgumentParser:
help="The offline Rekor bundle to verify with; not used with multiple inputs",
)

verification_options = verify.add_argument_group("Extended verification options")
verification_options = verify_identity.add_argument_group(
"Extended verification options"
)
verification_options.add_argument(
"--certificate-chain",
metavar="FILE",
Expand Down Expand Up @@ -309,17 +355,21 @@ def _parser() -> argparse.ArgumentParser:
help="Require offline Rekor verification with a bundle; implied by --rekor-bundle",
)

instance_options = verify.add_argument_group("Sigstore instance options")
instance_options = verify_identity.add_argument_group("Sigstore instance options")
_add_shared_instance_options(instance_options)

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

# `sigstore verify` defaults to `sigstore verify identity`, for backwards
# compatibility.
_set_default_verify_subparser(verify, "identity")

# `sigstore get-identity-token`
get_identity_token = subcommands.add_parser("get-identity-token")
_add_shared_oidc_options(get_identity_token)
Expand Down