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
6 changes: 3 additions & 3 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ jobs:

# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
uses: github/codeql-action/init@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
Expand All @@ -68,7 +68,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
uses: github/codeql-action/autobuild@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10

# ℹ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
Expand All @@ -81,6 +81,6 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
uses: github/codeql-action/analyze@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
with:
category: "/language:${{matrix.language}}"
2 changes: 1 addition & 1 deletion .github/workflows/cross_os.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ jobs:
- name: Set up Hatch
uses: pypa/hatch@257e27e51a6a5616ed08a39a408a21c35c9931bc # install
- name: store beacon token into oidc-token.txt
uses: sigstore-conformance/extremely-dangerous-public-oidc-beacon@9775b7374737339e046064d8e5a4bbf4b00565a4 # main
uses: sigstore-conformance/extremely-dangerous-public-oidc-beacon@1e3cabecd3790f48b79a795424e12fa3cb880dcb # main
- name: Sign the model
run: hatch run python -m model_signing sign sigstore model_root/ --use_staging --signature model.sig --identity_token $(cat oidc-token.txt)
- name: upload model signature
Expand Down
17 changes: 17 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,20 @@ jobs:
run: hatch fmt --check
env:
RUFF_OUTPUT_FORMAT: github

cli-flag-lint:
runs-on: ubuntu-latest
name: CLI Lint
permissions:
contents: read
steps:
- name: Check out source repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
- name: Check for CLI flags with underscores
run: |
if grep --recursive --line-number --extended-regexp --include="*.py" '"--[a-zA-Z0-9-]+_[a-zA-Z0-9-]+' src; then
echo "::error::Found CLI flags with underscores. Please use dashes."
exit 1
fi
2 changes: 1 addition & 1 deletion .github/workflows/scorecard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,6 @@ jobs:

# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
uses: github/codeql-action/upload-sarif@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
with:
sarif_file: results.sarif
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Red Hat Tech Preview release, based on upstream [sigstore/model-transparency](ht

### Added
- Added support for signing and verifying OCI model manifests directly without requiring model files on disk. OCI manifest JSON files can be detected and signed, or verified against. When verifying local files against signatures created from OCI manifests, the tool automatically matches files by path using `org.opencontainers.image.title` annotations (ORAS-style), enabling cross-verification between OCI images and local model directories.
- Added the `digest` subcommand to compute and print a model's digest. This enables other tools to easily pair the attestations with a model directory.
- Package renamed to `rh-model-signing` for Red Hat distribution.
- Added `rh_model_signing` CLI entry point (in addition to `model_signing`).

Expand All @@ -24,6 +25,7 @@ Red Hat Tech Preview release, based on upstream [sigstore/model-transparency](ht
- Support for `--allow_symlinks` and `--ignore_unsigned_files` options
- OpenTelemetry tracing support
- BLAKE3 hashing support
- Standardized CLI flags to use hyphens (e.g., `--trust-config` instead of `--trust_config`). Underscore variants are still accepted for backwards compatibility via token normalization.
- Removed Python 3.9 support (EOL 2025-10-31)
- Added Python 3.14 support

Expand Down
25 changes: 17 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,22 +116,31 @@ This will open an OIDC flow to obtain a short lived token for the certificate.
The identity used during signing and the provider must be reused during
verification.

To only compute and output the digest of the model, you can use the `digest`
subcommand, pointing it to the model directory:

```bash
[...]$ model_signing digest bert-base-uncased
```

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:
To use a private Sigstore setup (e.g. custom Rekor/Fulcio), use the `--trust-config` flag:

```bash
[...]$ model_signing sign bert-base-uncased --trust_config client_trust_config.json
[...]$ model_signing sign bert-base-uncased --trust-config client_trust_config.json
```

For verification:

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

The `client_trust_config.json` file should include:
Expand All @@ -153,7 +162,7 @@ generate the key pair:
And then we use the private key to sign.

```bash
[...]$ model_signing sign key bert-base-uncased --private_key key.priv
[...]$ model_signing sign key bert-base-uncased --private-key key.priv
```

All signing methods support changing the signature name and location via the
Expand All @@ -173,7 +182,7 @@ model we use
[...]$ model_signing verify bert-base-uncased \
--signature model.sig \
--identity "$identity" \
--identity_provider "$oidc_provider"
--identity-provider "$oidc_provider"
```

Where `$identity` and `$oidc_provider` are those set up during the signing flow
Expand Down Expand Up @@ -201,7 +210,7 @@ Similarly, for key verification, we can use

```bash
[...]$ model_signing verify key bert-base-uncased \
--signature resnet.sig --public_key key.pub
--signature resnet.sig --public-key key.pub
```

#### Signing and Verifying OCI Images
Expand Down Expand Up @@ -307,7 +316,7 @@ the PKCS #11 device and store it in a file in PEM format. With can then use:

```bash
[...]$ model_signing verify key --signature model.sig\
--public_key key.pub /path/to/your/model
--public-key key.pub /path/to/your/model
```

#### OpenTelemetry Support
Expand Down
94 changes: 71 additions & 23 deletions src/model_signing/_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ def exists(self) -> bool:

# Decorator for the commonly used option for the custom trust configuration.
_trust_config_option = click.option(
"--trust_config",
"--trust-config",
type=pathlib.Path,
metavar="TRUST_CONFIG_PATH",
help="The client trust configuration to use",
Expand All @@ -159,7 +159,7 @@ def exists(self) -> bool:

# Decorator for the commonly used option to ignore all unsigned files
_ignore_unsigned_files_option = click.option(
"--ignore_unsigned_files/--no-ignore_unsigned_files",
"--ignore-unsigned-files/--no-ignore-unsigned-files",
type=bool,
show_default=True,
help="Ignore all files that were not originally signed.",
Expand All @@ -168,7 +168,7 @@ def exists(self) -> bool:
# Decorator for the commonly used option to set the path to the private key
# (when using non-Sigstore PKI).
_private_key_option = click.option(
"--private_key",
"--private-key",
type=pathlib.Path,
metavar="PRIVATE_KEY",
required=True,
Expand All @@ -177,7 +177,7 @@ def exists(self) -> bool:

# Decorator for the commonly used option to set a PKCS #11 URI
_pkcs11_uri_option = click.option(
"--pkcs11_uri",
"--pkcs11-uri",
type=str,
metavar="PKCS11_URI",
required=True,
Expand All @@ -187,7 +187,7 @@ def exists(self) -> bool:
# Decorator for the commonly used option to pass a certificate chain to
# establish root of trust (when signing or verifying using certificates).
_certificate_root_of_trust_option = click.option(
"--certificate_chain",
"--certificate-chain",
type=pathlib.Path,
metavar="CERTIFICATE_PATH",
multiple=True,
Expand All @@ -197,15 +197,15 @@ def exists(self) -> bool:

# Decorator for the commonly used option to use Sigstore's staging instance.
_sigstore_staging_option = click.option(
"--use_staging",
"--use-staging",
type=bool,
is_flag=True,
help="Use Sigstore's staging instance.",
)

# Decorator for the commonly used option to pass the signing key's certificate
_signing_certificate_option = click.option(
"--signing_certificate",
"--signing-certificate",
type=pathlib.Path,
metavar="CERTIFICATE_PATH",
required=True,
Expand All @@ -214,7 +214,7 @@ def exists(self) -> bool:

# Decorator for the commonly used option to allow symlinks
_allow_symlinks_option = click.option(
"--allow_symlinks",
"--allow-symlinks",
is_flag=True,
help="Whether to allow following symlinks when signing or verifying files.",
)
Expand Down Expand Up @@ -314,7 +314,10 @@ def resolve_command(


@click.group(
context_settings=dict(help_option_names=["-h", "--help"]),
context_settings=dict(
help_option_names=["-h", "--help"],
token_normalize_func=lambda x: x.replace("_", "-"),
),
epilog=(
"Check https://sigstore.github.io/model-transparency for "
"documentation and more details."
Expand Down Expand Up @@ -361,6 +364,51 @@ def main(log_level: str) -> None:
sys.exit(1)


@main.command(name="digest")
@_model_path_argument
@_ignore_paths_option
@_ignore_git_paths_option
@_allow_symlinks_option
def _digest(
model_path: pathlib.Path,
ignore_paths: Iterable[pathlib.Path],
ignore_git_paths: bool,
allow_symlinks: bool,
) -> None:
"""Computes the digest of a model.

The digest subcommand serializes a model directory and computes the "root"
digest (hash), the same used when signing and as the attestation subject.

By default, git-related files are ignored (same behavior as the sign
command). Use --no-ignore-git-paths to include them. To ignore other
files from the directory serialization, use --ignore-paths.
"""
from model_signing._hashing import memory

try:
# First, generate the manifest of the model directory
ignored = _resolve_ignore_paths(model_path, list(ignore_paths))
manifest = (
model_signing.hashing.Config()
.set_ignored_paths(paths=ignored, ignore_git_paths=ignore_git_paths)
.set_allow_symlinks(allow_symlinks)
.hash(model_path)
)

# Then, hash the resource descriptors as done when signing
hasher = memory.SHA256()
for descriptor in manifest.resource_descriptors():
hasher.update(descriptor.digest.digest_value)
root_digest = hasher.compute()

click.echo(f"{root_digest.algorithm}:{root_digest.digest_hex}")

except Exception as err:
click.echo(f"Computing digest failed: {err}", err=True)
sys.exit(1)


@main.group(name="sign", subcommand_metavar="PKI_METHOD", cls=_PKICmdGroup)
def _sign() -> None:
"""Sign models.
Expand Down Expand Up @@ -390,13 +438,13 @@ def _sign() -> None:
@_sigstore_staging_option
@_trust_config_option
@click.option(
"--use_ambient_credentials",
"--use-ambient-credentials",
type=bool,
is_flag=True,
help="Use credentials from ambient environment.",
)
@click.option(
"--identity_token",
"--identity-token",
type=str,
metavar="TOKEN",
help=(
Expand All @@ -405,7 +453,7 @@ def _sign() -> None:
),
)
@click.option(
"--oauth_force_oob",
"--oauth-force-oob",
is_flag=True,
default=False,
help=(
Expand All @@ -414,13 +462,13 @@ def _sign() -> None:
),
)
@click.option(
"--client_id",
"--client-id",
type=str,
metavar="ID",
help="The custom OpenID Connect client ID to use during OAuth2",
)
@click.option(
"--client_secret",
"--client-secret",
type=str,
metavar="SECRET",
help="The custom OpenID Connect client secret to use during OAuth2",
Expand Down Expand Up @@ -449,20 +497,20 @@ def _sign_sigstore(
taken from an interactive OIDC flow, but ambient credentials could be used
to use workload identity tokens (e.g., when running in GitHub actions).
Alternatively, a constant identity token can be provided via
`--identity_token`.
`--identity-token`.

Sigstore allows users to use a staging instance for test-only signatures.
Passing the `--use_staging` flag would use that instance instead of the
Passing the `--use-staging` flag would use that instance instead of the
production one.

Additionally, you can specify a custom trust configuration JSON file using
the `--trust_config` flag. This allows you to fully customize the PKI
the `--trust-config` flag. This allows you to fully customize the PKI
(Private Key Infrastructure) used in the signing process. By providing a
`--trust_config`, you can define your own transparency logs, certificate
`--trust-config`, you can define your own transparency logs, certificate
authorities, and other trust settings, enabling full control over the
trust model, including which PKI to use for signature verification.

If `--trust_config` is not provided, the default Sigstore instance is
If `--trust-config` is not provided, the default Sigstore instance is
used, which is pre-configured with Sigstore’s own trusted transparency
logs and certificate authorities. This provides a ready-to-use default
trust model for most use cases but may not be suitable for custom or
Expand Down Expand Up @@ -880,7 +928,7 @@ def _verify() -> None:
subcommand).

To enable verification with custom PKI configurations, use the
`--trust_config` option. This allows you to specify your own set of trusted
`--trust-config` option. This allows you to specify your own set of trusted
public keys, transparency logs, and certificate authorities for verifying
the signature. If not provided, the default Sigstore instance and its
associated public keys, logs, and authorities are used.
Expand All @@ -905,7 +953,7 @@ def _verify() -> None:
help="The expected identity of the signer (e.g., name@example.com).",
)
@click.option(
"--identity_provider",
"--identity-provider",
type=str,
metavar="IDENTITY_PROVIDER",
required=True,
Expand Down Expand Up @@ -1001,7 +1049,7 @@ def _verify_sigstore(
@_ignore_git_paths_option
@_allow_symlinks_option
@click.option(
"--public_key",
"--public-key",
type=pathlib.Path,
metavar="PUBLIC_KEY",
required=True,
Expand Down Expand Up @@ -1079,7 +1127,7 @@ def _verify_private_key(
@_allow_symlinks_option
@_certificate_root_of_trust_option
@click.option(
"--log_fingerprints",
"--log-fingerprints",
type=bool,
is_flag=True,
default=False,
Expand Down
2 changes: 1 addition & 1 deletion src/model_signing/signing.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ def sign(
model_path: The path to the model to sign.
signature_path: The path of the resulting signature.
"""
if not self._signer:
if self._signer is None:
self.use_sigstore_signer()
manifest = self._hashing_config.hash(model_path)
payload = signing.Payload(manifest)
Expand Down
Loading
Loading