Skip to content

Commit

Permalink
Add Sigstore sign/verify to CLI (#310)
Browse files Browse the repository at this point in the history
* Update to support Sigstore sign via CLI

Signed-off-by: Ivan Font <[email protected]>

* Update to support Sigstore verify via CLI

Signed-off-by: Ivan Font <[email protected]>

* Update docs for Sigstore sign/verify CLI

Signed-off-by: Ivan Font <[email protected]>

* Update doc to reflect latest CLI changes

Signed-off-by: Ivan Font <[email protected]>

* Fix sign.py lint errors

Signed-off-by: Ivan Font <[email protected]>

* Fix verify.py lint errors

Signed-off-by: Ivan Font <[email protected]>

* Remvoe --method option from doc for required arg

Signed-off-by: Ivan Font <[email protected]>

* Fix Python type errors

Signed-off-by: Ivan Font <[email protected]>

---------

Signed-off-by: Ivan Font <[email protected]>
  • Loading branch information
font authored Oct 11, 2024
1 parent 9e133c4 commit 1ebab2f
Show file tree
Hide file tree
Showing 3 changed files with 131 additions and 49 deletions.
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 [email protected] --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 [email protected]
```
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 [email protected]
```
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 [email protected]
```
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 [email protected]
```
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 [email protected]
```
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":
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. [email protected]",
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":
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

0 comments on commit 1ebab2f

Please sign in to comment.