diff --git a/CHANGELOG.md b/CHANGELOG.md index 8519c1a3..49a5863e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ All versions prior to 1.0.0 are untracked. ## [Unreleased] ### Added -- ... +-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. ### Changed - ... diff --git a/README.md b/README.md index 35a9f3de..10233289 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,15 @@ 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: diff --git a/src/model_signing/_cli.py b/src/model_signing/_cli.py index 35a9013c..2e2190d4 100644 --- a/src/model_signing/_cli.py +++ b/src/model_signing/_cli.py @@ -264,6 +264,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.