Skip to content

Commit e17d946

Browse files
author
Lukas Pühringer
authored
Merge pull request #522 from lukpueh/sigstore-signer
signer: add basic sigstore signer and verifier
2 parents 7252521 + 345d428 commit e17d946

File tree

8 files changed

+302
-0
lines changed

8 files changed

+302
-0
lines changed
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
name: Run Sigstore Signer tests
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
workflow_dispatch:
9+
10+
permissions: {}
11+
12+
jobs:
13+
test-sigstore:
14+
runs-on: ubuntu-latest
15+
16+
permissions:
17+
id-token: 'write' # ambient credential is used to sign
18+
19+
steps:
20+
- name: Checkout securesystemslib
21+
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c
22+
23+
- name: Set up Python
24+
uses: actions/setup-python@d27e3f3d7c64b4bbf8e4abfb9b63b83e846e0435
25+
with:
26+
python-version: '3.x'
27+
cache: 'pip'
28+
cache-dependency-path: 'requirements*.txt'
29+
30+
- name: Install dependencies
31+
run: |
32+
python -m pip install --upgrade pip
33+
pip install --upgrade tox
34+
35+
- run: |
36+
export CERT_ID=${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/.github/workflows/test-sigstore.yml@${GITHUB_REF}
37+
export CERT_ISSUER=https://token.actions.githubusercontent.com
38+
tox -e sigstore

mypy.ini

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,9 @@ ignore_missing_imports = True
2020

2121
[mypy-asn1crypto.*]
2222
ignore_missing_imports = True
23+
24+
[mypy-sigstore.*]
25+
ignore_missing_imports = True
26+
27+
[mypy-sigstore_protobuf_specs.*]
28+
ignore_missing_imports = True

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ gcpkms = ["google-cloud-kms", "cryptography>=37.0.0"]
4848
hsm = ["asn1crypto", "cryptography>=37.0.0", "PyKCS11"]
4949
pynacl = ["pynacl>1.2.0"]
5050
PySPX = ["PySPX>=0.5.0"]
51+
sigstore = ["sigstore"]
5152

5253
[tool.hatch.version]
5354
path = "securesystemslib/__init__.py"

requirements-sigstore.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
sigstore==1.1.1

securesystemslib/signer/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
Signer,
1616
SSlibSigner,
1717
)
18+
from securesystemslib.signer._sigstore_signer import SigstoreKey, SigstoreSigner
1819

1920
# Register supported private key uri schemes and the Signers implementing them
2021
SIGNER_FOR_URI_SCHEME.update(
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
"""Signer implementation for project sigstore.
2+
3+
Example:
4+
```python
5+
from sigstore.oidc import Issuer
6+
7+
from securesystemslib.signer import SigstoreKey, SigstoreSigner
8+
9+
# Create public key
10+
identity = "[email protected]" # change, unless you know my password
11+
issuer = "https://github.com/login/oauth"
12+
public_key = SigstoreKey.from_dict(
13+
"abcdefg",
14+
{
15+
"keytype": "sigstore-oidc",
16+
"scheme": "Fulcio",
17+
"keyval": {
18+
"issuer": issuer,
19+
"identity": identity,
20+
},
21+
},
22+
)
23+
24+
# Create signer
25+
issuer = Issuer.production()
26+
token = issuer.identity_token() # requires sign in with GitHub in a browser
27+
signer = SigstoreSigner(token, public_key)
28+
29+
# Sign
30+
signature = signer.sign(b"data")
31+
32+
# Verify
33+
public_key.verify_signature(signature, b"data")
34+
35+
```
36+
37+
"""
38+
39+
import io
40+
import logging
41+
from typing import Any, Dict, Optional
42+
43+
from securesystemslib.exceptions import (
44+
UnsupportedLibraryError,
45+
UnverifiedSignatureError,
46+
VerificationError,
47+
)
48+
from securesystemslib.signer._signer import (
49+
Key,
50+
SecretsHandler,
51+
Signature,
52+
Signer,
53+
)
54+
55+
IMPORT_ERROR = "sigstore library required to use 'sigstore-oidc' keys"
56+
57+
logger = logging.getLogger(__name__)
58+
59+
60+
class SigstoreKey(Key):
61+
"""Sigstore verifier.
62+
63+
NOTE: unstable API - routines and metadata formats may change!
64+
"""
65+
66+
@classmethod
67+
def from_dict(cls, keyid: str, key_dict: Dict[str, Any]) -> "SigstoreKey":
68+
keytype = key_dict.pop("keytype")
69+
scheme = key_dict.pop("scheme")
70+
keyval = key_dict.pop("keyval")
71+
72+
for content in ["identity", "issuer"]:
73+
if content not in keyval or not isinstance(keyval[content], str):
74+
raise ValueError(
75+
f"{content} string required for scheme {scheme}"
76+
)
77+
78+
return cls(keyid, keytype, scheme, keyval, key_dict)
79+
80+
def to_dict(self) -> Dict:
81+
return {
82+
"keytype": self.keytype,
83+
"scheme": self.scheme,
84+
"keyval": self.keyval,
85+
**self.unrecognized_fields,
86+
}
87+
88+
def verify_signature(self, signature: Signature, data: bytes) -> None:
89+
# pylint: disable=import-outside-toplevel,import-error
90+
result = None
91+
try:
92+
from sigstore.verify import VerificationMaterials, Verifier
93+
from sigstore.verify.policy import Identity
94+
from sigstore_protobuf_specs.dev.sigstore.bundle.v1 import Bundle
95+
96+
verifier = Verifier.production()
97+
identity = Identity(
98+
identity=self.keyval["identity"], issuer=self.keyval["issuer"]
99+
)
100+
bundle = Bundle().from_dict(signature.unrecognized_fields["bundle"])
101+
materials = VerificationMaterials.from_bundle(
102+
input_=io.BytesIO(data), bundle=bundle, offline=True
103+
)
104+
result = verifier.verify(materials, identity)
105+
106+
except Exception as e:
107+
logger.info("Key %s failed to verify sig: %s", self.keyid, str(e))
108+
raise VerificationError(
109+
f"Unknown failure to verify signature by {self.keyid}"
110+
) from e
111+
112+
if not result:
113+
logger.info(
114+
"Key %s failed to verify sig: %s",
115+
self.keyid,
116+
getattr(result, "reason", ""),
117+
)
118+
raise UnverifiedSignatureError(
119+
f"Failed to verify signature by {self.keyid}"
120+
)
121+
122+
123+
class SigstoreSigner(Signer):
124+
"""Sigstore signer.
125+
126+
NOTE: unstable API - routines and metadata formats may change!
127+
"""
128+
129+
def __init__(self, token: str, public_key: Key):
130+
# TODO: Vet public key
131+
# - signer eligible for keytype/scheme?
132+
# - token matches identity/issuer?
133+
self.public_key = public_key
134+
self._token = token
135+
136+
@classmethod
137+
def from_priv_key_uri(
138+
cls,
139+
priv_key_uri: str,
140+
public_key: Key,
141+
secrets_handler: Optional[SecretsHandler] = None,
142+
) -> "SigstoreSigner":
143+
raise NotImplementedError()
144+
145+
def sign(self, payload: bytes) -> Signature:
146+
"""Signs payload using the OIDC token on the signer instance.
147+
148+
Arguments:
149+
payload: bytes to be signed.
150+
151+
Raises:
152+
Various errors from sigstore-python.
153+
154+
Returns:
155+
Signature.
156+
157+
NOTE: The relevant data is in `unrecognized_fields["bundle"]`.
158+
159+
"""
160+
# pylint: disable=import-outside-toplevel
161+
try:
162+
from sigstore.sign import Signer as _Signer
163+
except ImportError as e:
164+
raise UnsupportedLibraryError(IMPORT_ERROR) from e
165+
166+
signer = _Signer.production()
167+
result = signer.sign(io.BytesIO(payload), self._token)
168+
# TODO: Ask upstream if they can make this public
169+
bundle = result._to_bundle() # pylint: disable=protected-access
170+
171+
return Signature(
172+
self.public_key.keyid,
173+
bundle.message_signature.signature.hex(),
174+
{"bundle": bundle.to_dict()},
175+
)

tests/check_sigstore_signer.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
"""
2+
Test SigstoreSigner API.
3+
4+
NOTE: The filename prefix is check_ instead of test_ so that tests are
5+
only run when explicitly invoked in a suited environment.
6+
7+
"""
8+
import os
9+
import unittest
10+
11+
from sigstore.oidc import detect_credential # pylint: disable=import-error
12+
13+
from securesystemslib.signer import (
14+
KEY_FOR_TYPE_AND_SCHEME,
15+
Key,
16+
SigstoreKey,
17+
SigstoreSigner,
18+
)
19+
20+
KEY_FOR_TYPE_AND_SCHEME.update(
21+
{
22+
("sigstore-oidc", "Fulcio"): SigstoreKey,
23+
}
24+
)
25+
26+
27+
class TestSigstoreSigner(unittest.TestCase):
28+
"""Test public key parsing, signature creation and verification.
29+
30+
Requires ambient credentials for signing (e.g. from GitHub Action).
31+
32+
See sigstore-python docs for more infos about ambient credentials:
33+
https://github.com/sigstore/sigstore-python#signing-with-ambient-credentials
34+
35+
See securesystemslib SigstoreSigner docs for how to test locally.
36+
"""
37+
38+
def test_sign(self):
39+
token = detect_credential()
40+
self.assertIsNotNone(token, "ambient credentials required")
41+
42+
identity = os.getenv("CERT_ID")
43+
self.assertIsNotNone(token, "certificate identity required")
44+
45+
issuer = os.getenv("CERT_ISSUER")
46+
self.assertIsNotNone(token, "OIDC issuer required")
47+
48+
public_key = Key.from_dict(
49+
"abcdef",
50+
{
51+
"keytype": "sigstore-oidc",
52+
"scheme": "Fulcio",
53+
"keyval": {
54+
"issuer": issuer,
55+
"identity": identity,
56+
},
57+
},
58+
)
59+
60+
signer = SigstoreSigner(token, public_key)
61+
sig = signer.sign(b"data")
62+
public_key.verify_signature(sig, b"data")
63+
64+
65+
if __name__ == "__main__":
66+
unittest.main(verbosity=4, buffer=False)

tox.ini

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,20 @@ passenv =
4545
commands =
4646
python -m tests.check_kms_signers
4747

48+
[testenv:sigstore]
49+
deps =
50+
-r{toxinidir}/requirements-pinned.txt
51+
-r{toxinidir}/requirements-sigstore.txt
52+
passenv =
53+
# These are required to detect ambient credentials on GitHub
54+
GITHUB_ACTIONS
55+
ACTIONS_ID_TOKEN_REQUEST_TOKEN
56+
ACTIONS_ID_TOKEN_REQUEST_URL
57+
CERT_ID
58+
CERT_ISSUER
59+
commands =
60+
python -m tests.check_sigstore_signer
61+
4862
# This checks that importing securesystemslib.gpg.constants doesn't shell out on
4963
# import.
5064
[testenv:py311-test-gpg-fails]

0 commit comments

Comments
 (0)