Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,16 @@ All versions prior to 0.9.0 are untracked.
stable version of `sigstore-python`
([#379](https://github.com/sigstore/sigstore-python/pull/379))

* `sigstore` now has a public, importable Python API! You can find its
documentation [here](https://sigstore.github.io/sigstore-python/)
([#383](https://github.com/sigstore/sigstore-python/pull/383))

* `sigstore --staging` is now the intended way to request Sigstore's staging
instance, rather than per-subcommand options like `sigstore sign --staging`.
The latter is unchanged, but will be marked deprecated in a future stable
version of `sigstore-python`
([#383](https://github.com/sigstore/sigstore-python/pull/383))

### Changed

* The default behavior of `SIGSTORE_LOGLEVEL` has changed; the logger
Expand Down
15 changes: 12 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ Top-level:

<!-- @begin-sigstore-help@ -->
```
usage: sigstore [-h] [-V] [-v] {sign,verify,get-identity-token} ...
usage: sigstore [-h] [-V] [-v] [--staging]
{sign,verify,get-identity-token} ...

a tool for signing and verifying Python package distributions

Expand All @@ -83,6 +84,10 @@ optional arguments:
-V, --version show program's version number and exit
-v, --verbose run with additional debug logging; supply multiple
times to increase verbosity (default: 0)

Sigstore instance options:
--staging Use sigstore's staging instances, instead of the
default production instances (default: False)
```
<!-- @end-sigstore-help@ -->

Expand Down Expand Up @@ -138,7 +143,9 @@ Output options:

Sigstore instance options:
--staging Use sigstore's staging instances, instead of the
default production instances (default: False)
default production instances. This option will be
deprecated in favor of the global `--staging` option
in a future release. (default: False)
--rekor-url URL The Rekor instance to use (conflicts with --staging)
(default: https://rekor.sigstore.dev)
--rekor-root-pubkey FILE
Expand Down Expand Up @@ -205,7 +212,9 @@ Extended verification options:

Sigstore instance options:
--staging Use sigstore's staging instances, instead of the
default production instances (default: False)
default production instances. This option will be
deprecated in favor of the global `--staging` option
in a future release. (default: False)
--rekor-url URL The Rekor instance to use (conflicts with --staging)
(default: https://rekor.sigstore.dev)
--rekor-root-pubkey FILE
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ disallow_untyped_defs = true
ignore_missing_imports = true
no_implicit_optional = true
show_error_codes = true
sqlite_cache = true
strict_equality = true
warn_no_return = true
warn_redundant_casts = true
Expand Down
68 changes: 46 additions & 22 deletions sigstore/_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,25 +26,22 @@
from sigstore import __version__
from sigstore._internal.ctfe import CTKeyring
from sigstore._internal.fulcio.client import DEFAULT_FULCIO_URL, FulcioClient
from sigstore._internal.oidc.ambient import (
GitHubOidcPermissionCredentialError,
detect_credential,
)
from sigstore._internal.oidc.issuer import Issuer
from sigstore._internal.oidc.oauth import (
DEFAULT_OAUTH_ISSUER,
STAGING_OAUTH_ISSUER,
get_identity_token,
)
from sigstore._internal.rekor.client import (
DEFAULT_REKOR_URL,
RekorBundle,
RekorClient,
RekorEntry,
)
from sigstore._internal.tuf import TrustUpdater
from sigstore._sign import Signer
from sigstore._verify import (
from sigstore.oidc import (
DEFAULT_OAUTH_ISSUER_URL,
STAGING_OAUTH_ISSUER_URL,
GitHubOidcPermissionCredentialError,
Issuer,
detect_credential,
)
from sigstore.rekor import RekorEntry
from sigstore.sign import Signer
from sigstore.verify import (
CertificateVerificationFailure,
RekorEntryMissing,
VerificationFailure,
Expand Down Expand Up @@ -122,9 +119,14 @@ def _set_default_verify_subparser(parser: argparse.ArgumentParser, name: str) ->
def _add_shared_instance_options(group: argparse._ArgumentGroup) -> None:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

N.B.: Conceptually these common options belong at the global sigstore level, rather than embedded into each subcommand. I've moved one of them in this PR (--staging) because I needed to fix its behavior with get-identity-token, but I'll move the rest in a follow-up.

group.add_argument(
"--staging",
dest="__deprecated_staging",
action="store_true",
default=_boolify_env("SIGSTORE_STAGING"),
help="Use sigstore's staging instances, instead of the default production instances",
help=(
"Use sigstore's staging instances, instead of the default production instances. "
"This option will be deprecated in favor of the global `--staging` option "
"in a future release."
),
)
group.add_argument(
"--rekor-url",
Expand Down Expand Up @@ -169,7 +171,7 @@ def _add_shared_oidc_options(
"--oidc-issuer",
metavar="URL",
type=str,
default=os.getenv("SIGSTORE_OIDC_ISSUER", DEFAULT_OAUTH_ISSUER),
default=os.getenv("SIGSTORE_OIDC_ISSUER", DEFAULT_OAUTH_ISSUER_URL),
help="The OpenID Connect issuer to use (conflicts with --staging)",
)

Expand All @@ -190,6 +192,15 @@ def _parser() -> argparse.ArgumentParser:
default=0,
help="run with additional debug logging; supply multiple times to increase verbosity",
)

global_instance_options = parser.add_argument_group("Sigstore instance options")
global_instance_options.add_argument(
"--staging",
action="store_true",
default=_boolify_env("SIGSTORE_STAGING"),
help="Use sigstore's staging instances, instead of the default production instances",
)

subcommands = parser.add_subparsers(required=True, dest="subcommand")

# `sigstore sign`
Expand Down Expand Up @@ -390,6 +401,15 @@ def main() -> None:

logger.debug(f"parsed arguments {args}")

# `sigstore --staging some-cmd` is now the preferred form, rather than
# `sigstore some-cmd --staging`.
if getattr(args, "__deprecated_staging", False):
logger.warning(
"`--staging` should be used as a global option, rather than a subcommand option. "
"Passing `--staging` as a subcommand option will be deprecated in a future release."
)
args.staging = args.__deprecated_staging

# Stuff the parser back into our namespace, so that we can use it for
# error handling later.
args._parser = parser
Expand Down Expand Up @@ -472,7 +492,7 @@ def _sign(args: argparse.Namespace) -> None:
if args.staging:
logger.debug("sign: staging instances requested")
signer = Signer.staging()
args.oidc_issuer = STAGING_OAUTH_ISSUER
args.oidc_issuer = STAGING_OAUTH_ISSUER_URL
elif args.fulcio_url == DEFAULT_FULCIO_URL and args.rekor_url == DEFAULT_REKOR_URL:
signer = Signer.production()
else:
Expand Down Expand Up @@ -533,7 +553,7 @@ def _sign(args: argparse.Namespace) -> None:

if outputs["bundle"] is not None:
with outputs["bundle"].open(mode="w") as io:
bundle = result.log_entry.to_bundle()
bundle = RekorBundle.from_entry(result.log_entry)
print(bundle.json(by_alias=True), file=io)
print(f"Rekor bundle written to {outputs['bundle']}")

Expand Down Expand Up @@ -776,14 +796,18 @@ def _get_identity_token(args: argparse.Namespace) -> Optional[str]:
sys.exit(1)

if not token:
issuer = Issuer(args.oidc_issuer)
if args.staging:
issuer = Issuer.staging()
elif args.oidc_issuer == DEFAULT_OAUTH_ISSUER_URL:
issuer = Issuer.production()
else:
issuer = Issuer(args.oidc_issuer)

if args.oidc_client_secret is None:
args.oidc_client_secret = "" # nosec: B105

token = get_identity_token(
args.oidc_client_id,
args.oidc_client_secret,
issuer,
token = issuer.identity_token(
client_id=args.oidc_client_id, client_secret=args.oidc_client_secret
)

return token
2 changes: 1 addition & 1 deletion 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
from sigstore.rekor import RekorEntry


class InvalidInclusionProofError(Exception):
Expand Down
10 changes: 2 additions & 8 deletions sigstore/_internal/oidc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

import jwt

from sigstore.oidc import IdentityError

# See: https://github.com/sigstore/fulcio/blob/b2186c0/pkg/config/config.go#L182-L201
_KNOWN_OIDC_ISSUERS = {
"https://accounts.google.com": "email",
Expand All @@ -28,14 +30,6 @@
DEFAULT_AUDIENCE = "sigstore"


class IdentityError(Exception):
"""
Raised on any OIDC token format or claim error.
"""

pass


class Identity:
"""
A wrapper for an OIDC "identity", as extracted from an OIDC token.
Expand Down
44 changes: 7 additions & 37 deletions sigstore/_internal/oidc/ambient.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,55 +18,25 @@

import logging
import os
from typing import Callable, List, Optional
from typing import Optional

import requests
from pydantic import BaseModel, StrictStr

from sigstore._internal.oidc import DEFAULT_AUDIENCE, IdentityError
from sigstore._internal.oidc import DEFAULT_AUDIENCE
from sigstore.oidc import (
AmbientCredentialError,
GitHubOidcPermissionCredentialError,
)

logger = logging.getLogger(__name__)

_GCP_PRODUCT_NAME_FILE = "/sys/class/dmi/id/product_name"
_GCP_TOKEN_REQUEST_URL = "http://metadata/computeMetadata/v1/instance/service-accounts/default/token" # noqa # nosec B105
_GCP_IDENTITY_REQUEST_URL = "http://metadata/computeMetadata/v1/instance/service-accounts/default/identity" # noqa # nosec B105
_GCP_IDENTITY_REQUEST_URL = "http://metadata/computeMetadata/v1/instance/service-accounts/default/identity" # noqa
_GCP_GENERATEIDTOKEN_REQUEST_URL = "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/{}:generateIdToken" # noqa


class AmbientCredentialError(IdentityError):
"""
Raised when an ambient credential should be present, but
can't be retrieved (e.g. network failure).
"""

pass


class GitHubOidcPermissionCredentialError(AmbientCredentialError):
"""
Raised when the current GitHub Actions environment doesn't have permission
to retrieve an OIDC token.
"""

pass


def detect_credential() -> Optional[str]:
"""
Try each ambient credential detector, returning the first one to succeed
or `None` if all fail.

Raises `AmbientCredentialError` if any detector fails internally (i.e.
detects a credential, but cannot retrieve it).
"""
detectors: List[Callable[..., Optional[str]]] = [detect_github, detect_gcp]
for detector in detectors:
credential = detector()
if credential is not None:
return credential
return None


class _GitHubTokenPayload(BaseModel):
"""
A trivial model for GitHub's OIDC token endpoint payload.
Expand Down
69 changes: 0 additions & 69 deletions sigstore/_internal/oidc/issuer.py

This file was deleted.

Loading