Skip to content

Commit

Permalink
Add signing and verification CLI scripts. (sigstore#240)
Browse files Browse the repository at this point in the history
* <3 git

Signed-off-by: Martin Sablotny <msablotny@nvidia.com>

* linter

Signed-off-by: Martin Sablotny <msablotny@nvidia.com>

---------

Signed-off-by: Martin Sablotny <msablotny@nvidia.com>
  • Loading branch information
susperius authored Aug 21, 2024
1 parent 5a88752 commit e18b1d5
Show file tree
Hide file tree
Showing 6 changed files with 639 additions and 34 deletions.
114 changes: 80 additions & 34 deletions README.model_signing.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,63 +21,109 @@ monitor](https://github.com/sigstore/rekor-monitor) that runs on GitHub Actions.

![Signing models with Sigstore](docs/images/sigstore-model-diagram.png)

## Usage
## Model Signing CLI

You will need to install a few prerequisites to be able to run all of the
examples below:
The `sign.py` and `verify.py` scripts aim to provide the necessary functionality
to sign and verify ML models. For signing and verification the following methods
are supported:

* Bring your own key pair
* Bring your own PKI
* Skip signing (only hash and create a bundle)

The signing part creates a [sigstore bundle](https://github.com/sigstore/protobuf-specs/blob/main/protos/sigstore_bundle.proto)
protobuf that is stored as in JSON format. The bundle contains the verification
material necessary to check the payload and a payload as a [DSSE envelope](https://github.com/sigstore/protobuf-specs/blob/main/protos/envelope.proto).
Further the DSSE envelope contains an in-toto statment and the signature over
that statement. The signature format and how the the signature is computed can
be seen [here](https://github.com/secure-systems-lab/dsse/blob/v1.0.0/protocol.md).

Finally, the statement itself contains subjects which are a list of (file path,
digest) pairs a predicate type set to `model_signing/v1/model`and a dictionary
f predicates. The idea is to use the predicates to store (and therefor sign) model
card information in the future.

The verification part reads the sigstore bundle file and firstly verifies that the
signature is valid and secondly compute the model's file hashes again to compare
against the signed ones.

**Note**: The signature is stored as `./model.sig` by default and can be adjusted
by setting the `--sig_out` flag.

### Usage

There are two scripts one can be used to create and sign a bundle and the other to
verify a bundle. Furthermore, the functionality can be used directly from other
Python tools. The `sign.py` and `verify.py` scripts can be used as canonical
how-to examples.

The easiest way to use the scripts directly is from a virtual environment:

```bash
sudo apt install git git-lfs python3-venv python3-pip unzip
git lfs install
$ python3 -m venv .venv
$ source .venv/bin/activate
(.venv) $ pip install -r install/requirements.in
```

After this, you can clone the repository, create a Python virtual environment
and install the dependencies needed by the project:
## Sign

```bash
git clone git@github.com:sigstore/model-transparency.git
cd model-transparency/model_signing
python3 -m venv test_env
source test_env/bin/activate
os=Linux # Supported: Linux, Windows, Darwin.
python3 -m pip install --require-hashes -r "install/requirements_${os}".txt
(.venv) $ python3 sign.py --model_path ${MODEL_PATH} --sig_out ${OUTPUT_PATH} --method {private-key, pki} {additional parameters depending on method}
```

After this point, you can use the project to sign and verify models and
checkpoints. A help message with all arguments can be obtained by passing `-h`
argument, either to the main driver or to the two subcommands:
## Verify

```bash
python3 main.py -h
python3 main.py sign -h
python3 main.py verify -h
(.venv) $ python3 verify.py --model_path ${MODEL_PATH} --method {private-key, pki} {additional parameters depending on method}
```

Signing a model requires passing an argument for the path to the model. This can
be a path to a file or a directory (for large models, or model formats such as
`SavedModel` which are stored as a directory of related files):
### Examples

#### Bring Your Own Key

```bash
path=path/to/model
python3 main.py sign --path "${path}"
$ MODEL_PATH='/path/to/your/model'
$ openssl ecparam -name secp256k1 -genkey -noout -out ec-secp256k1-priv-key.pem
$ openssl ec -in ec-secp256k1-priv-key.pem -pubout > ec-secp256k1-pub-key.pem
$ source .venv/bin/activate
# SIGN
(.venv) $ python3 sign_model.py --model_path ${MODEL_PATH} --method private-key --private-key ec-secp256k1-priv-key.pem
...
#VERIFY
(.venv) $ python3 verify_model.py --model_path ${MODEL_PATH} --method private-key --public-key ec-secp256k1-pub-key.pem
...
```

The sign process will start an OIDC workflow to generate a short lived
certificate based on an identity provider. This will be relevant when verifying
the signature, as shown below.
#### Bring your own PKI
In order to sign a model with your own PKI you need to create the following information:

**Note**: The signature is stored as `<file>.sig` for a model serialized as a
single file, and `<dir>/model.sig` for a model in a folder-based format.
- The signing certificate
- The elliptic curve private key matching the signing certificate's public key
- Optionally, the certificate chain used for verification.

For verification, we need to pass both the path to the model and identity
related arguments:

```bash
python3 main.py verify --path "${path}" \
--identity-provider https://accounts.google.com \
--identity myemail@gmail.com
$ MODEL_PATH='/path/to/your/model'
$ CERT_CHAIN='/path/to/cert_chain'
$ SIGNING_CERT='/path/to/signing_certificate'
$ PRIVATE_KEY='/path/to/private_key'
# SIGN
(.venv) $ python3 sign_model.py --model_path ${MODEL_PATH} \
--method pki \
--private-key ${PRIVATE_KEY} \
--signing_cert ${SIGNING_CERT} \
[--cert_chain ${CERT_CHAIN}]
...
#VERIFY
$ ROOT_CERTS='/path/to/root/certs'
(.venv) $ python3 verify_model.py --model_path ${MODEL_PATH} \
--method pki \
--root_certs ${ROOT_CERTS}
...
```

## Sigstore ID providers

For developers signing models, there are three identity providers that can
be used at the moment:

Expand Down
79 changes: 79 additions & 0 deletions src/model_signing/model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Copyright 2024 The Sigstore Authors
# Copyright (c) 2024, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import pathlib
from typing import Callable, Iterable, TypeAlias

from model_signing.manifest import manifest
from model_signing.serialization import serialization
from model_signing.signature import verifying
from model_signing.signing import signing


PayloadGeneratorFunc: TypeAlias = Callable[
[manifest.Manifest], signing.SigningPayload
]


def sign(
model_path: pathlib.Path,
signer: signing.Signer,
payload_generator: PayloadGeneratorFunc,
serializer: serialization.Serializer,
ignore_paths: Iterable[pathlib.Path] = frozenset(),
) -> signing.Signature:
"""Provides a wrapper function for the steps necessary to sign a model.
Args:
model_path: the model to be signed.
signer: the signer to be used.
payload_generator: funtion to generate the manifest.
serializer: the serializer to be used for the model.
ignore_paths: paths that should be ignored during serialization.
Defaults to an empty set.
Returns:
The model's signature.
"""
manifest = serializer.serialize(model_path, ignore_paths=ignore_paths)
payload = payload_generator(manifest)
sig = signer.sign(payload)
return sig


def verify(
sig: signing.Signature,
verifier: signing.Verifier,
model_path: pathlib.Path,
serializer: serialization.Serializer,
ignore_paths: Iterable[pathlib.Path] = frozenset(),
):
"""Provides a simple wrapper to verify models.
Args:
sig: the signature to be verified.
verifier: the verifier to verify the signature.
model_path: the path to the model to compare manifests.
serializer: the serializer used to generate the local manifest.
ignore_paths: paths that should be ignored during serialization.
Defaults to an empty set.
Raises:
verifying.VerificationError: on any verification error.
"""
peer_manifest = verifier.verify(sig)
local_manifest = serializer.serialize(model_path, ignore_paths=ignore_paths)
if peer_manifest != local_manifest:
raise verifying.VerificationError("the manifests do not match")
71 changes: 71 additions & 0 deletions src/model_signing/signing/in_toto_signature.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Copyright (c) 2024, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Support for signing intoto payloads into sigstore bundles."""

import json
import pathlib
from typing import Self

from sigstore_protobuf_specs.dev.sigstore.bundle import v1 as bundle_pb
from typing_extensions import override

from model_signing.manifest import manifest as manifest_module
from model_signing.signature import signing as signature_signing
from model_signing.signature import verifying as signature_verifying
from model_signing.signing import in_toto
from model_signing.signing import signing


class IntotoSignature(signing.Signature):
def __init__(self, bundle: bundle_pb.Bundle):
self._bundle = bundle

@override
def write(self, path: pathlib.Path) -> None:
path.write_text(self._bundle.to_json())

@classmethod
@override
def read(cls, path: pathlib.Path) -> Self:
bundle = bundle_pb.Bundle().from_json(path.read_text())
return cls(bundle)

def to_manifest(self) -> manifest_module.Manifest:
payload = json.loads(self._bundle.dsse_envelope.payload)
return in_toto.IntotoPayload.manifest_from_payload(payload)


class IntotoSigner(signing.Signer):
def __init__(self, sig_signer: signature_signing.Signer):
self._sig_signer = sig_signer

@override
def sign(self, payload: signing.SigningPayload) -> IntotoSignature:
if not isinstance(payload, in_toto.IntotoPayload):
raise TypeError("only IntotoPayloads are supported")
bundle = self._sig_signer.sign(payload.statement)
return IntotoSignature(bundle)


class IntotoVerifier(signing.Verifier):
def __init__(self, sig_verifier: signature_verifying.Verifier):
self._sig_verifier = sig_verifier

@override
def verify(self, signature: signing.Signature) -> manifest_module.Manifest:
if not isinstance(signature, IntotoSignature):
raise TypeError("only IntotoSignature is supported")
self._sig_verifier.verify(signature._bundle)
return signature.to_manifest()
Loading

0 comments on commit e18b1d5

Please sign in to comment.