Skip to content
Open
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
71 changes: 58 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,9 @@ repository can do the same using [Hatch](https://hatch.pypa.io/latest/) via

For the remainder of the section, we would use `model_signing <args>` method.

The CLI has two subcommands: `sign` for signing and `verify` for verification.
The CLI has three subcommands: `sign` for signing, `verify` for verification,
and `trust-instance` for bootstrapping trust to a Sigstore instance (see
[Using Private Sigstore Instances](#using-private-sigstore-instances)).
Each subcommand has another level of subcommands to select the signing method
(`sigstore` -- the default, can be skipped --, `key`, `certificate`). Then, each
of these subcommands has several flags to configure parameters for
Expand Down Expand Up @@ -127,29 +129,57 @@ The digest subcommand follows the same ignore rules used when signing.

## Using Private Sigstore Instances

To use a private Sigstore setup (e.g. custom Rekor/Fulcio), use the `--trust-config` flag:
> **Note:** If you are signing and verifying with the default public
> [Sigstore](https://www.sigstore.dev/) instance, you do not need any of the
> options below — the CLI uses the public goods instance out of the box. This
> section is only relevant when operating your own private Sigstore deployment.

The recommended way to use a private Sigstore instance is via `--instance`,
which resolves trust configuration automatically through
[TUF](https://theupdateframework.io/).

**1. Bootstrap trust** (one-time setup):

Fetch the instance's initial `root.json` and register it locally:

```bash
[...]$ model_signing sign bert-base-uncased --trust-config client_trust_config.json
[...]$ curl -o root.json https://tuf-repo-cdn.sigstore.dev/1.root.json
[...]$ model_signing trust-instance --instance https://tuf-repo-cdn.sigstore.dev root.json
```

For verification:
**2. Sign and verify** using the instance URL:

```bash
[...]$ model_signing verify bert-base-uncased \
[...]$ model_signing sign --instance https://tuf-repo-cdn.sigstore.dev bert-base-uncased
[...]$ model_signing verify --instance https://tuf-repo-cdn.sigstore.dev \
--signature model.sig \
--trust-config client_trust_config.json
--identity "$identity"
--identity-provider "$oidc_provider"
--identity "$identity" \
--identity-provider "$oidc_provider" \
bert-base-uncased
```

The `client_trust_config.json` file should include:
After bootstrapping, only the URL is needed — TUF handles metadata updates and
key rotation transparently.

- A signed target trust root
- A `signingConfig` section with your private Rekor, Fulcio, and CT log endpoints
- Public keys for verification (if applicable)
#### Using a manual ClientTrustConfig

You can find an example `client_trust_config.json` that references the public Sigstore production services in the Sigstore Python repository [here](https://github.com/sigstore/sigstore-python/blob/main/test/assets/trust_config/config.v1.json).
If you need full control over the trust root (e.g. pinning specific keys or
endpoints), you can provide a `ClientTrustConfig` JSON file directly via
`--trust-config`. This takes precedence over `--instance` when both are given.

```bash
[...]$ model_signing sign --trust-config client_trust_config.json bert-base-uncased
[...]$ model_signing verify \
--trust-config client_trust_config.json \
--signature model.sig \
--identity "$identity" \
--identity-provider "$oidc_provider" \
bert-base-uncased
```

An example `client_trust_config.json` referencing the public Sigstore production
services can be found in the sigstore-python repository
[here](https://github.com/sigstore/sigstore-python/blob/main/test/assets/trust_config/config.v1.json).

As another example, here is how we can sign with private keys. First, we
generate the key pair:
Expand Down Expand Up @@ -382,6 +412,21 @@ model_signing.verifying.Config().use_sigstore_verifier(
).verify("finbert", "finbert.sig")
```

To sign or verify against a specific Sigstore instance, pass its TUF URL:

```python
import model_signing

model_signing.signing.Config().use_sigstore_signer(
instance="https://tuf-repo-cdn.sigstore.dev"
).sign("finbert", "finbert.sig")

model_signing.verifying.Config().use_sigstore_verifier(
identity=identity, oidc_issuer=oidc_provider,
instance="https://tuf-repo-cdn.sigstore.dev"
).verify("finbert", "finbert.sig")
```

The same verification configuration can be used to verify multiple models:

```python
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ dependencies = [
"click",
"cryptography",
"in-toto-attestation",
"sigstore>=4.0",
"sigstore>=4.2",
"sigstore-models>=0.0.5",
"typing_extensions",
]
Expand Down
54 changes: 54 additions & 0 deletions src/model_signing/_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,17 @@ def set_attribute(self, key, value):
help="The client trust configuration to use",
)

# Decorator for the commonly used option to specify a Sigstore instance URL.
_instance_option = click.option(
"--instance",
type=str,
metavar="URL",
help=(
"Use the Sigstore instance at the given TUF repository URL. "
"Must be bootstrapped first via `trust-instance`."
),
)

# Decorator for the commonly used option to ignore certain paths
_ignore_paths_option = click.option(
"--ignore-paths",
Expand Down Expand Up @@ -311,6 +322,43 @@ def _digest(
sys.exit(1)


@main.command(name="trust-instance")
@click.argument("root", type=pathlib.Path, metavar="ROOT_JSON")
@click.option(
"--instance",
type=str,
metavar="URL",
required=True,
help="The TUF repository URL of the Sigstore instance.",
)
def _trust_instance(root: pathlib.Path, instance: str) -> None:
r"""Bootstrap trust for a Sigstore instance.

Seeds the local TUF metadata cache with ROOT_JSON so that
subsequent sign/verify commands can use --instance URL without
needing the root file again.

\b
Example:
model_signing trust-instance \
--instance https://tuf-repo-cdn.sigstore.dev \
root.json
"""
from model_signing._signing.sign_sigstore import bootstrap_instance

if not root.is_file():
click.echo(f"Error: ROOT_JSON must be a file: {root}", err=True)
sys.exit(1)

try:
bootstrap_instance(instance, root)
except Exception as err:
click.echo(f"Bootstrapping instance failed: {err}", err=True)
sys.exit(1)

click.echo(f"Trust bootstrapped for {instance}")


@main.group(name="sign", subcommand_metavar="PKI_METHOD", cls=_PKICmdGroup)
def _sign() -> None:
"""Sign models.
Expand All @@ -334,6 +382,7 @@ def _sign() -> None:
@_write_signature_option
@_sigstore_staging_option
@_trust_config_option
@_instance_option
@click.option(
"--use-ambient-credentials",
type=bool,
Expand Down Expand Up @@ -383,6 +432,7 @@ def _sign_sigstore(
client_id: str | None = None,
client_secret: str | None = None,
trust_config: pathlib.Path | None = None,
instance: str | None = None,
) -> None:
"""Sign using Sigstore (DEFAULT signing method).

Expand Down Expand Up @@ -433,6 +483,7 @@ def _sign_sigstore(
client_id=client_id,
client_secret=client_secret,
trust_config=trust_config,
instance=instance,
).set_hashing_config(
model_signing.hashing.Config()
.set_ignored_paths(
Expand Down Expand Up @@ -688,6 +739,7 @@ def _verify() -> None:
@_allow_symlinks_option
@_sigstore_staging_option
@_trust_config_option
@_instance_option
@click.option(
"--identity",
type=str,
Expand All @@ -714,6 +766,7 @@ def _verify_sigstore(
use_staging: bool,
ignore_unsigned_files: bool,
trust_config: pathlib.Path | None = None,
instance: str | None = None,
) -> None:
"""Verify using Sigstore (DEFAULT verification method).

Expand Down Expand Up @@ -741,6 +794,7 @@ def _verify_sigstore(
oidc_issuer=identity_provider,
use_staging=use_staging,
trust_config=trust_config,
instance=instance,
).set_hashing_config(
model_signing.hashing.Config()
.set_ignored_paths(
Expand Down
70 changes: 54 additions & 16 deletions src/model_signing/_signing/sign_sigstore.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,40 @@
_DEFAULT_CLIENT_SECRET = ""


def _resolve_trust_config(
*,
use_staging: bool = False,
trust_config: pathlib.Path | None = None,
instance: str | None = None,
) -> sigstore_models.ClientTrustConfig:
"""Resolve trust configuration from the provided options.

Precedence: trust_config file > instance URL > staging > production.
"""
if trust_config:
return sigstore_models.ClientTrustConfig.from_json(
trust_config.read_text()
)
if instance:
return sigstore_models.ClientTrustConfig.from_tuf(instance)
if use_staging:
return sigstore_models.ClientTrustConfig.staging()
return sigstore_models.ClientTrustConfig.production()


def bootstrap_instance(instance: str, root: pathlib.Path) -> None:
"""Bootstrap trust for a Sigstore instance.

Seeds the local TUF cache with the provided root metadata so that
subsequent signing/verification can use ``--instance`` with just the URL.

Args:
instance: The TUF repository URL of the Sigstore instance.
root: Path to the initial TUF ``root.json`` for the instance.
"""
sigstore_models.ClientTrustConfig.from_tuf(instance, bootstrap_root=root)


class Signature(signing.Signature):
"""Sigstore signature support, wrapping around `sigstore_models.Bundle`."""

Expand Down Expand Up @@ -74,6 +108,7 @@ def __init__(
client_id: str | None = None,
client_secret: str | None = None,
trust_config: pathlib.Path | None = None,
instance: str | None = None,
):
"""Initializes Sigstore signers.

Expand Down Expand Up @@ -113,15 +148,16 @@ def __init__(
supplied PKI and trust configurations, instead of the default
Sigstore setup. If not specified, the default Sigstore
configuration is used.
instance: A Sigstore TUF repository URL. When provided, trust
configuration is fetched via TUF from this URL instead of using
the default production instance or a manual config file. Must
have been bootstrapped first via `bootstrap_instance()`.
"""
if use_staging:
trust_config = sigstore_models.ClientTrustConfig.staging()
elif trust_config:
trust_config = sigstore_models.ClientTrustConfig.from_json(
trust_config.read_text()
)
else:
trust_config = sigstore_models.ClientTrustConfig.production()
trust_config = _resolve_trust_config(
use_staging=use_staging,
trust_config=trust_config,
instance=instance,
)

if not oidc_issuer:
oidc_issuer = trust_config.signing_config.get_oidc_url()
Expand Down Expand Up @@ -190,6 +226,7 @@ def __init__(
oidc_issuer: str,
use_staging: bool = False,
trust_config: pathlib.Path | None = None,
instance: str | None = None,
):
"""Initializes Sigstore verifiers.

Expand All @@ -208,15 +245,16 @@ def __init__(
PKI and trust configurations, instead of the default Sigstore
setup. If not specified, the default Sigstore configuration
is used.
instance: A Sigstore TUF repository URL. When provided, trust
configuration is fetched via TUF from this URL instead of using
the default production instance or a manual config file. Must
have been bootstrapped first via `bootstrap_instance()`.
"""
if trust_config:
trust_config = sigstore_models.ClientTrustConfig.from_json(
trust_config.read_text()
)
elif use_staging:
trust_config = sigstore_models.ClientTrustConfig.staging()
else:
trust_config = sigstore_models.ClientTrustConfig.production()
trust_config = _resolve_trust_config(
use_staging=use_staging,
trust_config=trust_config,
instance=instance,
)

self._verifier = sigstore_verifier.Verifier(
trusted_root=trust_config.trusted_root
Expand Down
4 changes: 4 additions & 0 deletions src/model_signing/signing.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ def use_sigstore_signer(
client_id: str | None = None,
client_secret: str | None = None,
trust_config: pathlib.Path | None = None,
instance: str | None = None,
) -> Self:
"""Configures the signing to be performed with Sigstore.

Expand Down Expand Up @@ -170,6 +171,8 @@ def use_sigstore_signer(
PKI and trust configurations, instead of the default Sigstore
setup. If not specified, the default Sigstore configuration
is used.
instance: A Sigstore TUF repository URL. Trust configuration is
fetched via TUF from this URL. Must have been bootstrapped first.

Return:
The new signing configuration.
Expand All @@ -183,6 +186,7 @@ def use_sigstore_signer(
client_id=client_id,
client_secret=client_secret,
trust_config=trust_config,
instance=instance,
)
return self

Expand Down
4 changes: 4 additions & 0 deletions src/model_signing/verifying.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ def use_sigstore_verifier(
oidc_issuer: str,
use_staging: bool = False,
trust_config: pathlib.Path | None = None,
instance: str | None = None,
) -> Self:
"""Configures the verification of signatures produced by Sigstore.

Expand All @@ -235,6 +236,8 @@ def use_sigstore_verifier(
PKI and trust configurations, instead of the default Sigstore
setup. If not specified, the default Sigstore configuration
is used.
instance: A Sigstore TUF repository URL. Trust configuration is
fetched via TUF from this URL. Must have been bootstrapped first.

Return:
The new verification configuration.
Expand All @@ -245,6 +248,7 @@ def use_sigstore_verifier(
oidc_issuer=oidc_issuer,
use_staging=use_staging,
trust_config=trust_config,
instance=instance,
)
return self

Expand Down
Loading
Loading