diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 6279f505..72f3fb1e 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -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. @@ -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 @@ -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}}" diff --git a/.github/workflows/cross_os.yml b/.github/workflows/cross_os.yml index 4587346f..820533fe 100644 --- a/.github/workflows/cross_os.yml +++ b/.github/workflows/cross_os.yml @@ -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 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 5e50e30b..3504fef8 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -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 diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index ca308390..eb88e0d9 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 21a0ed22..9160be91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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`). @@ -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 diff --git a/README.md b/README.md index f1010556..78966380 100644 --- a/README.md +++ b/README.md @@ -116,12 +116,21 @@ 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: @@ -129,9 +138,9 @@ 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: @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/src/model_signing/_cli.py b/src/model_signing/_cli.py index 0f387989..f311b33e 100644 --- a/src/model_signing/_cli.py +++ b/src/model_signing/_cli.py @@ -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", @@ -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.", @@ -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, @@ -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, @@ -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, @@ -197,7 +197,7 @@ 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.", @@ -205,7 +205,7 @@ def exists(self) -> bool: # 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, @@ -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.", ) @@ -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." @@ -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. @@ -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=( @@ -405,7 +453,7 @@ def _sign() -> None: ), ) @click.option( - "--oauth_force_oob", + "--oauth-force-oob", is_flag=True, default=False, help=( @@ -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", @@ -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 @@ -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. @@ -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, @@ -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, @@ -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, diff --git a/src/model_signing/signing.py b/src/model_signing/signing.py index 914febfb..f33f50c2 100644 --- a/src/model_signing/signing.py +++ b/src/model_signing/signing.py @@ -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) diff --git a/tests/_signing/sigstore_test.py b/tests/_signing/sigstore_test.py index 68dbd26f..40619bd4 100644 --- a/tests/_signing/sigstore_test.py +++ b/tests/_signing/sigstore_test.py @@ -332,3 +332,91 @@ def test_verify_not_intoto_statement( with pytest.raises(ValueError, match="Expected in-toto .* payload"): self._verify_dsse_signature(signature_path) + + def test_sign_with_custom_trust_config( + self, + sample_model_folder, + mocked_oidc_provider, + mocked_sigstore_signer, + mocked_sigstore_models, + tmp_path, + ): + trust_config_path = ( + pathlib.Path(__file__).parent + / "testdata" + / "custom_trust_config.json" + ) + + serializer = file.Serializer( + self._file_hasher_factory, allow_symlinks=True + ) + manifest = serializer.serialize(sample_model_folder) + signature_path = tmp_path / "model.sig" + + mocked_client_trust_config = mocked_sigstore_models["ClientTrustConfig"] + mocked_custom_config = mock.MagicMock() + mocked_client_trust_config.from_json.return_value = mocked_custom_config + + signer = sigstore.Signer( + use_staging=False, trust_config=trust_config_path + ) + payload = signing.Payload(manifest) + signature = signer.sign(payload) + signature.write(signature_path) + + assert mocked_client_trust_config.from_json.called + call_args = mocked_client_trust_config.from_json.call_args + assert call_args is not None + assert isinstance(call_args[0][0], str) + trust_config_content = json.loads(call_args[0][0]) + assert trust_config_content["mediaType"] == ( + "application/vnd.dev.sigstore.clienttrustconfig.v0.1+json" + ) + assert "signing_config" in trust_config_content + assert "trustedRoot" in trust_config_content + + def test_verify_with_custom_trust_config( + self, + sample_model_folder, + mocked_oidc_provider, + mocked_sigstore_signer, + mocked_sigstore_models, + mocked_sigstore_verifier, + tmp_path, + ): + trust_config_path = ( + pathlib.Path(__file__).parent + / "testdata" + / "custom_trust_config.json" + ) + + serializer = file.Serializer( + self._file_hasher_factory, allow_symlinks=True + ) + manifest = serializer.serialize(sample_model_folder) + signature_path = tmp_path / "model.sig" + self._sign_manifest(manifest, signature_path, sigstore.Signer) + + mocked_client_trust_config = mocked_sigstore_models["ClientTrustConfig"] + mocked_custom_config = mock.MagicMock() + mocked_client_trust_config.from_json.return_value = mocked_custom_config + + verifier = sigstore.Verifier( + identity="test", + oidc_issuer="test", + use_staging=False, + trust_config=trust_config_path, + ) + signature = sigstore.Signature.read(signature_path) + verifier.verify(signature) + + assert mocked_client_trust_config.from_json.called + call_args = mocked_client_trust_config.from_json.call_args + assert call_args is not None + assert isinstance(call_args[0][0], str) + trust_config_content = json.loads(call_args[0][0]) + assert trust_config_content["mediaType"] == ( + "application/vnd.dev.sigstore.clienttrustconfig.v0.1+json" + ) + assert "signing_config" in trust_config_content + assert "trustedRoot" in trust_config_content diff --git a/tests/_signing/testdata/custom_trust_config.json b/tests/_signing/testdata/custom_trust_config.json new file mode 100644 index 00000000..c021d799 --- /dev/null +++ b/tests/_signing/testdata/custom_trust_config.json @@ -0,0 +1,175 @@ +{ + "mediaType": "application/vnd.dev.sigstore.clienttrustconfig.v0.1+json", + "trustedRoot": { + "mediaType": "application/vnd.dev.sigstore.trustedroot+json;version=0.1", + "tlogs": [ + { + "baseUrl": "https://rekor.sigstore.dev", + "hashAlgorithm": "SHA2_256", + "publicKey": { + "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2G2Y+2tabdTV5BcGiBIx0a9fAFwrkBbmLSGtks4L3qX6yYY0zufBnhC8Ur/iy55GhWP/9A/bY2LhC30M9+RYtw==", + "keyDetails": "PKIX_ECDSA_P256_SHA_256", + "validFor": { + "start": "2021-01-12T11:53:27.000Z" + } + }, + "logId": { + "keyId": "wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0=" + } + } + ], + "certificateAuthorities": [ + { + "subject": { + "organization": "sigstore.dev", + "commonName": "sigstore" + }, + "uri": "https://fulcio.sigstore.dev", + "certChain": { + "certificates": [ + { + "rawBytes": "MIIB+DCCAX6gAwIBAgITNVkDZoCiofPDsy7dfm6geLbuhzAKBggqhkjOPQQDAzAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIxMDMwNzAzMjAyOVoXDTMxMDIyMzAzMjAyOVowKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABLSyA7Ii5k+pNO8ZEWY0ylemWDowOkNa3kL+GZE5Z5GWehL9/A9bRNA3RbrsZ5i0JcastaRL7Sp5fp/jD5dxqc/UdTVnlvS16an+2Yfswe/QuLolRUCrcOE2+2iA5+tzd6NmMGQwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYEFMjFHQBBmiQpMlEk6w2uSu1KBtPsMB8GA1UdIwQYMBaAFMjFHQBBmiQpMlEk6w2uSu1KBtPsMAoGCCqGSM49BAMDA2gAMGUCMH8liWJfMui6vXXBhjDgY4MwslmN/TJxVe/83WrFomwmNf056y1X48F9c4m3a3ozXAIxAKjRay5/aj/jsKKGIkmQatjI8uupHr/+CxFvaJWmpYqNkLDGRU+9orzh5hI2RrcuaQ==" + } + ] + }, + "validFor": { + "start": "2021-03-07T03:20:29.000Z", + "end": "2022-12-31T23:59:59.999Z" + } + }, + { + "subject": { + "organization": "sigstore.dev", + "commonName": "sigstore" + }, + "uri": "https://fulcio.sigstore.dev", + "certChain": { + "certificates": [ + { + "rawBytes": "MIICGjCCAaGgAwIBAgIUALnViVfnU0brJasmRkHrn/UnfaQwCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMjA0MTMyMDA2MTVaFw0zMTEwMDUxMzU2NThaMDcxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEeMBwGA1UEAxMVc2lnc3RvcmUtaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE8RVS/ysH+NOvuDZyPIZtilgUF9NlarYpAd9HP1vBBH1U5CV77LSS7s0ZiH4nE7Hv7ptS6LvvR/STk798LVgMzLlJ4HeIfF3tHSaexLcYpSASr1kS0N/RgBJz/9jWCiXno3sweTAOBgNVHQ8BAf8EBAMCAQYwEwYDVR0lBAwwCgYIKwYBBQUHAwMwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU39Ppz1YkEZb5qNjpKFWixi4YZD8wHwYDVR0jBBgwFoAUWMAeX5FFpWapesyQoZMi0CrFxfowCgYIKoZIzj0EAwMDZwAwZAIwPCsQK4DYiZYDPIaDi5HFKnfxXx6ASSVmERfsynYBiX2X6SJRnZU84/9DZdnFvvxmAjBOt6QpBlc4J/0DxvkTCqpclvziL6BCCPnjdlIB3Pu3BxsPmygUY7Ii2zbdCdliiow=" + }, + { + "rawBytes": "MIIB9zCCAXygAwIBAgIUALZNAPFdxHPwjeDloDwyYChAO/4wCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMTEwMDcxMzU2NTlaFw0zMTEwMDUxMzU2NThaMCoxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjERMA8GA1UEAxMIc2lnc3RvcmUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT7XeFT4rb3PQGwS4IajtLk3/OlnpgangaBclYpsYBr5i+4ynB07ceb3LP0OIOZdxexX69c5iVuyJRQ+Hz05yi+UF3uBWAlHpiS5sh0+H2GHE7SXrk1EC5m1Tr19L9gg92jYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRYwB5fkUWlZql6zJChkyLQKsXF+jAfBgNVHSMEGDAWgBRYwB5fkUWlZql6zJChkyLQKsXF+jAKBggqhkjOPQQDAwNpADBmAjEAj1nHeXZp+13NWBNa+EDsDP8G1WWg1tCMWP/WHPqpaVo0jhsweNFZgSs0eE7wYI4qAjEA2WB9ot98sIkoF3vZYdd3/VtWB5b9TNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ" + } + ] + }, + "validFor": { + "start": "2022-04-13T20:06:15.000Z" + } + } + ], + "ctlogs": [ + { + "baseUrl": "https://ctfe.sigstore.dev/test", + "hashAlgorithm": "SHA2_256", + "publicKey": { + "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEbfwR+RJudXscgRBRpKX1XFDy3PyudDxz/SfnRi1fT8ekpfBd2O1uoz7jr3Z8nKzxA69EUQ+eFCFI3zeubPWU7w==", + "keyDetails": "PKIX_ECDSA_P256_SHA_256", + "validFor": { + "start": "2021-03-14T00:00:00.000Z", + "end": "2022-10-31T23:59:59.999Z" + } + }, + "logId": { + "keyId": "CGCS8ChS/2hF0dFrJ4ScRWcYrBY9wzjSbea8IgY2b3I=" + } + }, + { + "baseUrl": "https://ctfe.sigstore.dev/2022", + "hashAlgorithm": "SHA2_256", + "publicKey": { + "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEiPSlFi0CmFTfEjCUqF9HuCEcYXNKAaYalIJmBZ8yyezPjTqhxrKBpMnaocVtLJBI1eM3uXnQzQGAJdJ4gs9Fyw==", + "keyDetails": "PKIX_ECDSA_P256_SHA_256", + "validFor": { + "start": "2022-10-20T00:00:00.000Z" + } + }, + "logId": { + "keyId": "3T0wasbHETJjGR4cmWc3AqJKXrjePK3/h4pygC8p7o4=" + } + } + ], + "timestampAuthorities": [ + { + "subject": { + "organization": "GitHub, Inc.", + "commonName": "Internal Services Root" + }, + "certChain": { + "certificates": [ + { + "rawBytes": "MIIB3DCCAWKgAwIBAgIUchkNsH36Xa04b1LqIc+qr9DVecMwCgYIKoZIzj0EAwMwMjEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRkwFwYDVQQDExBUU0EgaW50ZXJtZWRpYXRlMB4XDTIzMDQxNDAwMDAwMFoXDTI0MDQxMzAwMDAwMFowMjEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRkwFwYDVQQDExBUU0EgVGltZXN0YW1waW5nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUD5ZNbSqYMd6r8qpOOEX9ibGnZT9GsuXOhr/f8U9FJugBGExKYp40OULS0erjZW7xV9xV52NnJf5OeDq4e5ZKqNWMFQwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMIMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUaW1RudOgVt0leqY0WKYbuPr47wAwCgYIKoZIzj0EAwMDaAAwZQIwbUH9HvD4ejCZJOWQnqAlkqURllvu9M8+VqLbiRK+zSfZCZwsiljRn8MQQRSkXEE5AjEAg+VxqtojfVfu8DhzzhCx9GKETbJHb19iV72mMKUbDAFmzZ6bQ8b54Zb8tidy5aWe" + }, + { + "rawBytes": "MIICEDCCAZWgAwIBAgIUX8ZO5QXP7vN4dMQ5e9sU3nub8OgwCgYIKoZIzj0EAwMwODEVMBMGA1UEChMMR2l0SHViLCBJbmMuMR8wHQYDVQQDExZJbnRlcm5hbCBTZXJ2aWNlcyBSb290MB4XDTIzMDQxNDAwMDAwMFoXDTI4MDQxMjAwMDAwMFowMjEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRkwFwYDVQQDExBUU0EgaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEvMLY/dTVbvIJYANAuszEwJnQE1llftynyMKIMhh48HmqbVr5ygybzsLRLVKbBWOdZ21aeJz+gZiytZetqcyF9WlER5NEMf6JV7ZNojQpxHq4RHGoGSceQv/qvTiZxEDKo2YwZDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUaW1RudOgVt0leqY0WKYbuPr47wAwHwYDVR0jBBgwFoAU9NYYlobnAG4c0/qjxyH/lq/wz+QwCgYIKoZIzj0EAwMDaQAwZgIxAK1B185ygCrIYFlIs3GjswjnwSMG6LY8woLVdakKDZxVa8f8cqMs1DhcxJ0+09w95QIxAO+tBzZk7vjUJ9iJgD4R6ZWTxQWKqNm74jO99o+o9sv4FI/SZTZTFyMn0IJEHdNmyA==" + }, + { + "rawBytes": "MIIB9DCCAXqgAwIBAgIUa/JAkdUjK4JUwsqtaiRJGWhqLSowCgYIKoZIzj0EAwMwODEVMBMGA1UEChMMR2l0SHViLCBJbmMuMR8wHQYDVQQDExZJbnRlcm5hbCBTZXJ2aWNlcyBSb290MB4XDTIzMDQxNDAwMDAwMFoXDTMzMDQxMTAwMDAwMFowODEVMBMGA1UEChMMR2l0SHViLCBJbmMuMR8wHQYDVQQDExZJbnRlcm5hbCBTZXJ2aWNlcyBSb290MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEf9jFAXxz4kx68AHRMOkFBhflDcMTvzaXz4x/FCcXjJ/1qEKon/qPIGnaURskDtyNbNDOpeJTDDFqt48iMPrnzpx6IZwqemfUJN4xBEZfza+pYt/iyod+9tZr20RRWSv/o0UwQzAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBAjAdBgNVHQ4EFgQU9NYYlobnAG4c0/qjxyH/lq/wz+QwCgYIKoZIzj0EAwMDaAAwZQIxALZLZ8BgRXzKxLMMN9VIlO+e4hrBnNBgF7tz7Hnrowv2NetZErIACKFymBlvWDvtMAIwZO+ki6ssQ1bsZo98O8mEAf2NZ7iiCgDDU0Vwjeco6zyeh0zBTs9/7gV6AHNQ53xD" + } + ] + }, + "validFor": { + "start": "2023-04-14T00:00:00.000Z" + } + } + ] + }, + "signing_config": { + "mediaType": "application/vnd.dev.sigstore.signingconfig.v0.2+json", + "caUrls": [ + { + "url": "https://fulcio.sigstore.dev", + "majorApiVersion": 1, + "validFor": { + "start": "2023-04-14T21:38:40Z" + }, + "operator": "example.com" + }, + { + "url": "https://fulcio-old.example.com", + "majorApiVersion": 1, + "validFor": { + "start": "2022-04-14T21:38:40Z", + "end": "2023-04-14T21:38:40Z" + }, + "operator": "example.com" + } + ], + "oidcUrls": [ + { + "url": "https://oauth2.sigstore.dev/auth", + "majorApiVersion": 1, + "validFor": { + "start": "2025-04-16T00:00:00Z" + }, + "operator": "example.com" + } + ], + "rekorTlogUrls": [ + { + "url": "https://rekor.sigstore.dev", + "majorApiVersion": 1, + "validFor": { + "start": "2021-01-12T11:53:27Z" + }, + "operator": "example.com" + } + ], + "tsaUrls": [ + { + "url": "https://timestamp.sigstore.dev/api/v1/timestamp", + "majorApiVersion": 1, + "validFor": { + "start": "2025-04-09T00:00:00Z" + }, + "operator": "example.com" + } + ], + "rekorTlogConfig": { + "selector": "ANY" + }, + "tsaConfig": { + "selector": "ANY" + } + } +} diff --git a/tests/api_test.py b/tests/api_test.py index 77069604..280c5567 100644 --- a/tests/api_test.py +++ b/tests/api_test.py @@ -183,6 +183,52 @@ def test_sign_and_verify( sample_model_folder ) + @pytest.mark.integration + def test_sign_and_verify_with_custom_trust_config( + self, sigstore_oidc_beacon_token, sample_model_folder, tmp_path + ): + trust_config_path = ( + Path(__file__).parent + / "_signing" + / "testdata" + / "custom_trust_config.json" + ) + + sc = signing.Config() + sc.use_sigstore_signer( + use_staging=False, + identity_token=sigstore_oidc_beacon_token, + trust_config=trust_config_path, + ) + signature_path = tmp_path / "model.sig" + sc.sign(sample_model_folder, signature_path) + + expected_identity = "https://github.com/sigstore-conformance/extremely-dangerous-public-oidc-beacon/.github/workflows/extremely-dangerous-oidc-beacon.yml@refs/heads/main" + expected_oidc_issuer = "https://token.actions.githubusercontent.com" + verifying.Config().use_sigstore_verifier( + identity=expected_identity, + oidc_issuer=expected_oidc_issuer, + use_staging=False, + trust_config=trust_config_path, + ).verify(sample_model_folder, signature_path) + + assert get_signed_files(signature_path) == [ + "d0/f00", + "d0/f01", + "d0/f02", + "d1/f10", + "d1/f11", + "d1/f12", + "f0", + "f1", + "f2", + "f3", + ] + check_ignore_paths(signature_path, True, []) + assert get_model_name(signature_path) == os.path.basename( + sample_model_folder + ) + class TestKeySigning: def test_sign_and_verify(self, base_path, populate_tmpdir):