-
Notifications
You must be signed in to change notification settings - Fork 50
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #522 from lukpueh/sigstore-signer
signer: add basic sigstore signer and verifier
- Loading branch information
Showing
8 changed files
with
302 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
name: Run Sigstore Signer tests | ||
|
||
on: | ||
push: | ||
branches: | ||
- main | ||
pull_request: | ||
workflow_dispatch: | ||
|
||
permissions: {} | ||
|
||
jobs: | ||
test-sigstore: | ||
runs-on: ubuntu-latest | ||
|
||
permissions: | ||
id-token: 'write' # ambient credential is used to sign | ||
|
||
steps: | ||
- name: Checkout securesystemslib | ||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c | ||
|
||
- name: Set up Python | ||
uses: actions/setup-python@d27e3f3d7c64b4bbf8e4abfb9b63b83e846e0435 | ||
with: | ||
python-version: '3.x' | ||
cache: 'pip' | ||
cache-dependency-path: 'requirements*.txt' | ||
|
||
- name: Install dependencies | ||
run: | | ||
python -m pip install --upgrade pip | ||
pip install --upgrade tox | ||
- run: | | ||
export CERT_ID=${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/.github/workflows/test-sigstore.yml@${GITHUB_REF} | ||
export CERT_ISSUER=https://token.actions.githubusercontent.com | ||
tox -e sigstore |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
sigstore==1.1.1 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,175 @@ | ||
"""Signer implementation for project sigstore. | ||
Example: | ||
```python | ||
from sigstore.oidc import Issuer | ||
from securesystemslib.signer import SigstoreKey, SigstoreSigner | ||
# Create public key | ||
identity = "luk.puehringer@gmail.com" # change, unless you know my password | ||
issuer = "https://github.com/login/oauth" | ||
public_key = SigstoreKey.from_dict( | ||
"abcdefg", | ||
{ | ||
"keytype": "sigstore-oidc", | ||
"scheme": "Fulcio", | ||
"keyval": { | ||
"issuer": issuer, | ||
"identity": identity, | ||
}, | ||
}, | ||
) | ||
# Create signer | ||
issuer = Issuer.production() | ||
token = issuer.identity_token() # requires sign in with GitHub in a browser | ||
signer = SigstoreSigner(token, public_key) | ||
# Sign | ||
signature = signer.sign(b"data") | ||
# Verify | ||
public_key.verify_signature(signature, b"data") | ||
``` | ||
""" | ||
|
||
import io | ||
import logging | ||
from typing import Any, Dict, Optional | ||
|
||
from securesystemslib.exceptions import ( | ||
UnsupportedLibraryError, | ||
UnverifiedSignatureError, | ||
VerificationError, | ||
) | ||
from securesystemslib.signer._signer import ( | ||
Key, | ||
SecretsHandler, | ||
Signature, | ||
Signer, | ||
) | ||
|
||
IMPORT_ERROR = "sigstore library required to use 'sigstore-oidc' keys" | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
class SigstoreKey(Key): | ||
"""Sigstore verifier. | ||
NOTE: unstable API - routines and metadata formats may change! | ||
""" | ||
|
||
@classmethod | ||
def from_dict(cls, keyid: str, key_dict: Dict[str, Any]) -> "SigstoreKey": | ||
keytype = key_dict.pop("keytype") | ||
scheme = key_dict.pop("scheme") | ||
keyval = key_dict.pop("keyval") | ||
|
||
for content in ["identity", "issuer"]: | ||
if content not in keyval or not isinstance(keyval[content], str): | ||
raise ValueError( | ||
f"{content} string required for scheme {scheme}" | ||
) | ||
|
||
return cls(keyid, keytype, scheme, keyval, key_dict) | ||
|
||
def to_dict(self) -> Dict: | ||
return { | ||
"keytype": self.keytype, | ||
"scheme": self.scheme, | ||
"keyval": self.keyval, | ||
**self.unrecognized_fields, | ||
} | ||
|
||
def verify_signature(self, signature: Signature, data: bytes) -> None: | ||
# pylint: disable=import-outside-toplevel,import-error | ||
result = None | ||
try: | ||
from sigstore.verify import VerificationMaterials, Verifier | ||
from sigstore.verify.policy import Identity | ||
from sigstore_protobuf_specs.dev.sigstore.bundle.v1 import Bundle | ||
|
||
verifier = Verifier.production() | ||
identity = Identity( | ||
identity=self.keyval["identity"], issuer=self.keyval["issuer"] | ||
) | ||
bundle = Bundle().from_dict(signature.unrecognized_fields["bundle"]) | ||
materials = VerificationMaterials.from_bundle( | ||
input_=io.BytesIO(data), bundle=bundle, offline=True | ||
) | ||
result = verifier.verify(materials, identity) | ||
|
||
except Exception as e: | ||
logger.info("Key %s failed to verify sig: %s", self.keyid, str(e)) | ||
raise VerificationError( | ||
f"Unknown failure to verify signature by {self.keyid}" | ||
) from e | ||
|
||
if not result: | ||
logger.info( | ||
"Key %s failed to verify sig: %s", | ||
self.keyid, | ||
getattr(result, "reason", ""), | ||
) | ||
raise UnverifiedSignatureError( | ||
f"Failed to verify signature by {self.keyid}" | ||
) | ||
|
||
|
||
class SigstoreSigner(Signer): | ||
"""Sigstore signer. | ||
NOTE: unstable API - routines and metadata formats may change! | ||
""" | ||
|
||
def __init__(self, token: str, public_key: Key): | ||
# TODO: Vet public key | ||
# - signer eligible for keytype/scheme? | ||
# - token matches identity/issuer? | ||
self.public_key = public_key | ||
self._token = token | ||
|
||
@classmethod | ||
def from_priv_key_uri( | ||
cls, | ||
priv_key_uri: str, | ||
public_key: Key, | ||
secrets_handler: Optional[SecretsHandler] = None, | ||
) -> "SigstoreSigner": | ||
raise NotImplementedError() | ||
|
||
def sign(self, payload: bytes) -> Signature: | ||
"""Signs payload using the OIDC token on the signer instance. | ||
Arguments: | ||
payload: bytes to be signed. | ||
Raises: | ||
Various errors from sigstore-python. | ||
Returns: | ||
Signature. | ||
NOTE: The relevant data is in `unrecognized_fields["bundle"]`. | ||
""" | ||
# pylint: disable=import-outside-toplevel | ||
try: | ||
from sigstore.sign import Signer as _Signer | ||
except ImportError as e: | ||
raise UnsupportedLibraryError(IMPORT_ERROR) from e | ||
|
||
signer = _Signer.production() | ||
result = signer.sign(io.BytesIO(payload), self._token) | ||
# TODO: Ask upstream if they can make this public | ||
bundle = result._to_bundle() # pylint: disable=protected-access | ||
|
||
return Signature( | ||
self.public_key.keyid, | ||
bundle.message_signature.signature.hex(), | ||
{"bundle": bundle.to_dict()}, | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
""" | ||
Test SigstoreSigner API. | ||
NOTE: The filename prefix is check_ instead of test_ so that tests are | ||
only run when explicitly invoked in a suited environment. | ||
""" | ||
import os | ||
import unittest | ||
|
||
from sigstore.oidc import detect_credential # pylint: disable=import-error | ||
|
||
from securesystemslib.signer import ( | ||
KEY_FOR_TYPE_AND_SCHEME, | ||
Key, | ||
SigstoreKey, | ||
SigstoreSigner, | ||
) | ||
|
||
KEY_FOR_TYPE_AND_SCHEME.update( | ||
{ | ||
("sigstore-oidc", "Fulcio"): SigstoreKey, | ||
} | ||
) | ||
|
||
|
||
class TestSigstoreSigner(unittest.TestCase): | ||
"""Test public key parsing, signature creation and verification. | ||
Requires ambient credentials for signing (e.g. from GitHub Action). | ||
See sigstore-python docs for more infos about ambient credentials: | ||
https://github.com/sigstore/sigstore-python#signing-with-ambient-credentials | ||
See securesystemslib SigstoreSigner docs for how to test locally. | ||
""" | ||
|
||
def test_sign(self): | ||
token = detect_credential() | ||
self.assertIsNotNone(token, "ambient credentials required") | ||
|
||
identity = os.getenv("CERT_ID") | ||
self.assertIsNotNone(token, "certificate identity required") | ||
|
||
issuer = os.getenv("CERT_ISSUER") | ||
self.assertIsNotNone(token, "OIDC issuer required") | ||
|
||
public_key = Key.from_dict( | ||
"abcdef", | ||
{ | ||
"keytype": "sigstore-oidc", | ||
"scheme": "Fulcio", | ||
"keyval": { | ||
"issuer": issuer, | ||
"identity": identity, | ||
}, | ||
}, | ||
) | ||
|
||
signer = SigstoreSigner(token, public_key) | ||
sig = signer.sign(b"data") | ||
public_key.verify_signature(sig, b"data") | ||
|
||
|
||
if __name__ == "__main__": | ||
unittest.main(verbosity=4, buffer=False) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters