Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Sigstore sign/verify to CLI #310

Merged
merged 8 commits into from
Oct 11, 2024
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
69 changes: 48 additions & 21 deletions README.model_signing.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ are supported:

* Bring your own key pair
* Bring your own PKI
- Keyless signing using Sigstore with Fulcio root
* 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)
Expand Down Expand Up @@ -68,13 +69,13 @@ $ source .venv/bin/activate
## Sign

```bash
(.venv) $ python3 sign.py --model_path ${MODEL_PATH} --sig_out ${OUTPUT_PATH} --method {private-key, pki} {additional parameters depending on method}
(.venv) $ python3 sign.py --model_path ${MODEL_PATH} --sig_out ${SIG_PATH} {private-key, pki, sigstore} {additional parameters depending on method}
```

## Verify

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

### Examples
Expand All @@ -83,14 +84,15 @@ $ source .venv/bin/activate

```bash
$ MODEL_PATH='/path/to/your/model'
$ SIG_PATH='./model.sig'
$ 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
(.venv) $ python3 sign.py --model_path ${MODEL_PATH} --sig_out ${SIG_PATH} 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
(.venv) $ python3 verify.py --model_path ${MODEL_PATH} --sig_path ${SIG_PATH} private-key --public-key ec-secp256k1-pub-key.pem
...
```

Expand All @@ -104,27 +106,42 @@ In order to sign a model with your own PKI you need to create the following info

```bash
$ MODEL_PATH='/path/to/your/model'
$ SIG_PATH='./model.sig'
$ 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 \
(.venv) $ python3 sign.py --model_path ${MODEL_PATH} \
--sig_path ${SIG_PATH} \
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}
(.venv) $ python3 verify.py --model_path ${MODEL_PATH} \
--sig_path ${SIG_PATH} \
pki \
--root_certs ${ROOT_CERTS}
...
```

## Sigstore ID providers
#### Keyless signing using Sigstore

For developers signing models, there are three identity providers that can
```bash
$ MODEL_PATH='/path/to/your/model'
# SIGN
(.venv) $ python3 sign.py --model_path ${MODEL_PATH} sigstore
...
#VERIFY
(.venv) $ python3 verify.py --model_path ${MODEL_PATH} --sig_path ./model.sig sigstore --identity name@example.com --identity-provider https://accounts.example.com
...
```

### Sigstore ID providers

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

* Google's provider is `https://accounts.google.com`.
Expand All @@ -151,11 +168,13 @@ stored in TFHub, run the following commands:

```bash
model_path=bertseq2seq
sig_path=model.sig
wget "https://tfhub.dev/google/bertseq2seq/bert24_en_de/1?tf-hub-format=compressed" -O "${model_path}".tgz
mkdir -p "${model_path}"
cd "${model_path}" && tar xvzf ../"${model_path}".tgz && rm ../"${model_path}".tgz && cd -
python3 main.py sign --path "${model_path}"
python3 main.py verify --path "${model_path}" \
python3 sign.py --model_path "${model_path}" sigstore
python3 verify.py --model_path "${model_path}" --sig_path ${sig_path} \
sigstore \
--identity-provider https://accounts.google.com \
--identity myemail@gmail.com
```
Expand All @@ -173,9 +192,11 @@ After this, we can sign and verify a Bert base model:
```bash
model_name=bert-base-uncased
model_path="${model_name}"
sig_path=model.sig
git clone --depth=1 "https://huggingface.co/${model_name}" && rm -rf "${model_name}"/.git
python3 main.py sign --path "${model_path}"
python3 main.py verify --path "${model_path}" \
python3 sign.py --model_path "${model_path}"
python3 verify.py --model_path "${model_path}" --sig_path ${sig_path} \
sigstore \
--identity-provider https://accounts.google.com \
--identity myemail@gmail.com
```
Expand All @@ -185,9 +206,11 @@ Similarly, we can sign and verify a Falcon model:
```bash
model_name=tiiuae/falcon-7b
model_path=$(echo "${model_name}" | cut -d/ -f2)
sig_path=model.sig
git clone --depth=1 "https://huggingface.co/${model_name}" && rm -rf "${model_name}"/.git
python3 main.py sign --path "${model_path}"
python3 main.py verify --path "${model_path}" \
python3 sign.py --model_path "${model_path}"
python3 verify.py --model_path "${model_path}" --sig_path ${sig_path} \
sigstore \
--identity-provider https://accounts.google.com \
--identity myemail@gmail.com
```
Expand All @@ -197,11 +220,13 @@ We can also support models from the PyTorch Hub:
```bash
model_name=hustvl/YOLOP
model_path=$(echo "${model_name}" | cut -d/ -f2)
sig_path=model.sig
wget "https://github.com/${model_name}/archive/main.zip" -O "${model_path}".zip
mkdir -p "${model_path}"
cd "${model_path}" && unzip ../"${model_path}".zip && rm ../"${model_path}".zip && shopt -s dotglob && mv YOLOP-main/* . && shopt -u dotglob && rmdir YOLOP-main/ && cd -
python3 main.py sign --path "${model_path}"
python3 main.py verify --path "${model_path}" \
python3 sign.py --model_path "${model_path}"
python3 verify.py --model_path "${model_path}" --sig_path ${sig_path} \
sigstore \
--identity-provider https://accounts.google.com \
--identity myemail@gmail.com
```
Expand All @@ -211,9 +236,11 @@ We also support ONNX models, for example Roberta:
```bash
model_name=roberta-base-11
model_path="${model_name}.onnx"
sig_path=model.sig
wget "https://github.com/onnx/models/raw/main/text/machine_comprehension/roberta/model/${model_name}.onnx"
python3 main.py sign --path "${model_path}"
python3 main.py verify --path "${model_path}" \
python3 sign.py --model_path "${model_path}"
python3 verify.py --model_path "${model_path}" --sig_path ${sig_path} \
sigstore \
--identity-provider https://accounts.google.com \
--identity myemail@gmail.com
```
Expand Down
37 changes: 29 additions & 8 deletions src/sign.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@
from model_signing.signature import fake
from model_signing.signature import key
from model_signing.signature import pki
from model_signing.signature import signing
from model_signing.signing import in_toto
from model_signing.signing import in_toto_signature
from model_signing.signing import signing
from model_signing.signing import sigstore


log = logging.getLogger(__name__)
Expand All @@ -54,7 +55,7 @@ def _arguments() -> argparse.Namespace:
method_cmd = parser.add_subparsers(
required=True,
dest="method",
help="method to sign the model: [pki, private-key, skip]",
help="method to sign the model: [pki, private-key, sigstore, skip]",
)
# PKI
pki = method_cmd.add_parser("pki")
Expand Down Expand Up @@ -91,6 +92,17 @@ def _arguments() -> argparse.Namespace:
type=pathlib.Path,
dest="key_path",
)
# sigstore
sigstore = method_cmd.add_parser("sigstore")
sigstore.add_argument(
"--use_ambient_credentials",
help="use ambient credentials (also known as Workload Identity,"
+ "default is true)",
required=False,
type=bool,
default=True,
dest="use_ambient_credentials",
)
# skip
method_cmd.add_parser("skip")

Expand All @@ -100,17 +112,27 @@ def _arguments() -> argparse.Namespace:
def _get_payload_signer(args: argparse.Namespace) -> signing.Signer:
if args.method == "private-key":
_check_private_key_options(args)
return key.ECKeySigner.from_path(private_key_path=args.key_path)
payload_signer = key.ECKeySigner.from_path(
private_key_path=args.key_path
)
return in_toto_signature.IntotoSigner(payload_signer)
elif args.method == "pki":
_check_pki_options(args)
return pki.PKISigner.from_path(
payload_signer = pki.PKISigner.from_path(
args.key_path, args.signing_cert_path, args.cert_chain_path
)
return in_toto_signature.IntotoSigner(payload_signer)
elif args.method == "sigstore":
return sigstore.SigstoreDSSESigner(
use_ambient_credentials=args.use_ambient_credentials
)
elif args.method == "skip":
mihaimaruseac marked this conversation as resolved.
Show resolved Hide resolved
return fake.FakeSigner()
return in_toto_signature.IntotoSigner(fake.FakeSigner())
else:
log.error(f"unsupported signing method {args.method}")
log.error('supported methods: ["pki", "private-key", "skip"]')
log.error(
'supported methods: ["pki", "private-key", "sigstore", "skip"]'
)
exit(-1)


Expand Down Expand Up @@ -151,10 +173,9 @@ def hasher_factory(file_path: pathlib.Path) -> file.FileHasher:
file_hasher_factory=hasher_factory
)

intoto_signer = in_toto_signature.IntotoSigner(payload_signer)
sig = model.sign(
model_path=args.model_path,
signer=intoto_signer,
signer=payload_signer,
payload_generator=in_toto.DigestsIntotoPayload.from_manifest,
serializer=serializer,
ignore_paths=[args.sig_out],
Expand Down
74 changes: 54 additions & 20 deletions src/verify.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
from model_signing.signature import pki
from model_signing.signature import verifying
from model_signing.signing import in_toto_signature
from model_signing.signing import signing
from model_signing.signing import sigstore


log = logging.getLogger(__name__)
Expand All @@ -51,7 +53,7 @@ def _arguments() -> argparse.Namespace:
method_cmd = parser.add_subparsers(
required=True,
dest="method",
help="method to verify the model: [pki, private-key, skip]",
help="method to verify the model: [pki, private-key, sigstore, skip]",
)
# pki subcommand
pki = method_cmd.add_parser("pki")
Expand All @@ -73,12 +75,52 @@ def _arguments() -> argparse.Namespace:
type=pathlib.Path,
dest="key",
)

# sigstore subcommand
sigstore = method_cmd.add_parser("sigstore")
sigstore.add_argument(
"--identity",
help="the expected identity of the signer e.g. name@example.com",
required=True,
type=str,
dest="identity",
)
sigstore.add_argument(
"--identity-provider",
help="the identity provider expected e.g. https://accounts.example.com",
required=True,
type=str,
dest="identity_provider",
)
# skip subcommand
method_cmd.add_parser("skip")

return parser.parse_args()


def _get_verifier(args: argparse.Namespace) -> signing.Verifier:
verifier: verifying.Verifier
if args.method == "private-key":
_check_private_key_flags(args)
verifier = key.ECKeyVerifier.from_path(args.key)
return in_toto_signature.IntotoVerifier(verifier)
elif args.method == "pki":
_check_pki_flags(args)
verifier = pki.PKIVerifier.from_paths(args.root_certs)
return in_toto_signature.IntotoVerifier(verifier)
elif args.method == "sigstore":
return sigstore.SigstoreDSSEVerifier(
identity=args.identity, oidc_issuer=args.identity_provider
)
elif args.method == "skip":
font marked this conversation as resolved.
Show resolved Hide resolved
return in_toto_signature.IntotoVerifier(fake.FakeVerifier())
else:
log.error(f"unsupported verification method {args.method}")
log.error(
'supported methods: ["pki", "private-key", "sigstore", "skip"]'
)
exit(-1)


def _check_private_key_flags(args: argparse.Namespace):
if args.key == "":
log.error("--public_key must be defined")
Expand All @@ -90,28 +132,22 @@ def _check_pki_flags(args: argparse.Namespace):
log.warning("no root of trust is set using system default")


def _get_signature(args: argparse.Namespace) -> signing.Signature:
if args.method == "sigstore":
return sigstore.SigstoreSignature.read(args.sig_path)
else:
return in_toto_signature.IntotoSignature.read(args.sig_path)


def main():
logging.basicConfig(level=logging.INFO)
args = _arguments()

verifier: verifying.Verifier
log.info(f"Creating verifier for {args.method}")
if args.method == "private-key":
_check_private_key_flags(args)
verifier = key.ECKeyVerifier.from_path(args.key)
elif args.method == "pki":
_check_pki_flags(args)
verifier = pki.PKIVerifier.from_paths(args.root_certs)
elif args.method == "skip":
verifier = fake.FakeVerifier()
else:
log.error(f"unsupported verification method {args.method}")
log.error('supported methods: ["pki", "private-key", "skip"]')
exit(-1)

verifier = _get_verifier(args)
log.info(f"Verifying model signature from {args.sig_path}")

sig = in_toto_signature.IntotoSignature.read(args.sig_path)
sig = _get_signature(args)

def hasher_factory(file_path: pathlib.Path) -> file.FileHasher:
return file.SimpleFileHasher(
Expand All @@ -122,12 +158,10 @@ def hasher_factory(file_path: pathlib.Path) -> file.FileHasher:
file_hasher_factory=hasher_factory
)

intoto_verifier = in_toto_signature.IntotoVerifier(verifier)

try:
model.verify(
sig=sig,
verifier=intoto_verifier,
verifier=verifier,
model_path=args.model_path,
serializer=serializer,
ignore_paths=[args.sig_path],
Expand Down
Loading