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

signer: add SigstoreSigner from_priv_key_uri and import_ methods #535

Merged
merged 5 commits into from
Mar 16, 2023
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
1 change: 0 additions & 1 deletion .github/workflows/test-sigstore.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ on:
push:
branches:
- main
pull_request:
workflow_dispatch:

permissions: {}
Expand Down
149 changes: 113 additions & 36 deletions securesystemslib/signer/_sigstore_signer.py
Original file line number Diff line number Diff line change
@@ -1,50 +1,19 @@
"""Signer implementation for project sigstore.

Example:
```python
from sigstore.oidc import Issuer

from securesystemslib.signer import SigstoreKey, SigstoreSigner

# Create public key
identity = "[email protected]" # 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 typing import Any, Dict, Optional, Tuple
from urllib import parse

from securesystemslib.exceptions import (
UnsupportedLibraryError,
UnverifiedSignatureError,
VerificationError,
)
from securesystemslib.formats import encode_canonical
from securesystemslib.hash import digest
from securesystemslib.signer._signer import (
Key,
SecretsHandler,
Expand All @@ -63,6 +32,9 @@ class SigstoreKey(Key):
NOTE: unstable API - routines and metadata formats may change!
"""

DEFAULT_KEY_TYPE = "sigstore-oidc"
DEFAULT_SCHEME = "Fulcio"

@classmethod
def from_dict(cls, keyid: str, key_dict: Dict[str, Any]) -> "SigstoreKey":
keytype = key_dict.pop("keytype")
Expand Down Expand Up @@ -124,8 +96,44 @@ class SigstoreSigner(Signer):
"""Sigstore signer.

NOTE: unstable API - routines and metadata formats may change!

All signers should be instantiated with ``Signer.from_priv_key_uri()``.
Unstable ``SigstoreSigner`` currently requires opt-in via
``securesystemslib.signer.SIGNER_FOR_URI_SCHEME``.

Usage::

identity = "[email protected]" # change, unless you know pw
issuer = "https://github.com/login/oauth"

# Create signer URI and public key for identity and issuer
uri, public_key = SigstoreSigner.import_(identity, issuer, ambient=False)

# Load signer from URI -- requires browser login with GitHub
signer = SigstoreSigner.from_priv_key_uri(uri, public_key)

# Sign with signer and verify public key
signature = signer.sign(b"data")
public_key.verify_signature(signature, b"data")

The private key URI scheme is "sigstore:?<PARAMS>", where PARAMS is
optional and toggles ambient credential usage. Example URIs:

* "sigstore:":
Sign with ambient credentials.
* "sigstore:?ambient=false":
Sign with OAuth2 + OpenID via browser login.

Arguments:
token: The OIDC identity token used for signing.
public_key: The related public key instance.

Raises:
UnsupportedLibraryError: sigstore library not found.
"""

SCHEME = "sigstore"

def __init__(self, token: str, public_key: Key):
# TODO: Vet public key
# - signer eligible for keytype/scheme?
Expand All @@ -140,7 +148,76 @@ def from_priv_key_uri(
public_key: Key,
secrets_handler: Optional[SecretsHandler] = None,
) -> "SigstoreSigner":
raise NotImplementedError()
# pylint: disable=import-outside-toplevel
try:
from sigstore.oidc import Issuer, detect_credential
except ImportError as e:
raise UnsupportedLibraryError(IMPORT_ERROR) from e

if not isinstance(public_key, SigstoreKey):
raise ValueError(f"expected SigstoreKey for {priv_key_uri}")

uri = parse.urlparse(priv_key_uri)

if uri.scheme != cls.SCHEME:
raise ValueError(f"SigstoreSigner does not support {priv_key_uri}")

params = dict(parse.parse_qsl(uri.query))

if params.get("ambient") == "false":
# TODO: Restrict oauth flow to use identity/issuer from public_key
# TODO: Use secrets_handler for identity_token() secret arg
issuer = Issuer.production()
token = issuer.identity_token()
else:
token = detect_credential()

return cls(token, public_key)

@classmethod
def _get_uri(cls, ambient: bool) -> str:
return f"{cls.SCHEME}:{'' if ambient else '?ambient=false'}"

@classmethod
def _get_keyid(cls, keytype: str, scheme, keyval: Dict[str, Any]) -> str:
"""Compute keyid as hexdigest over canonical json representation of key.

NOTE: Not compatible with ``securesystemslib.keys._get_keyid()``
"""
data = encode_canonical(
{
"keytype": keytype,
"scheme": scheme,
"keyval": keyval,
}
).encode("utf-8")
hasher = digest()
hasher.update(data)
return hasher.hexdigest()

@classmethod
def import_(
cls, identity: str, issuer: str, ambient: bool = True
) -> Tuple[str, SigstoreKey]:
"""Create public key and signer URI.

Returns a private key URI (for Signer.from_priv_key_uri()) and a public
key. import_() should be called once and the returned URI and public
key should be stored for later use.

Arguments:
identity: The OIDC identity used to create a signing token.
issuer: The OIDC issuer URL used to create a signing token.
ambient: Toggle usage of ambient credentials in returned URI.
"""
keytype = SigstoreKey.DEFAULT_KEY_TYPE
scheme = SigstoreKey.DEFAULT_SCHEME
keyval = {"identity": identity, "issuer": issuer}
keyid = cls._get_keyid(keytype, scheme, keyval)
key = SigstoreKey(keyid, keytype, scheme, keyval)
uri = cls._get_uri(ambient)

return uri, key

def sign(self, payload: bytes) -> Signature:
"""Signs payload using the OIDC token on the signer instance.
Expand Down
35 changes: 7 additions & 28 deletions tests/check_sigstore_signer.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,13 @@
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,
SIGNER_FOR_URI_SCHEME,
Signer,
SigstoreSigner,
)

KEY_FOR_TYPE_AND_SCHEME.update(
{
("sigstore-oidc", "Fulcio"): SigstoreKey,
}
)
SIGNER_FOR_URI_SCHEME[SigstoreSigner.SCHEME] = SigstoreSigner


class TestSigstoreSigner(unittest.TestCase):
Expand All @@ -36,28 +29,14 @@ class TestSigstoreSigner(unittest.TestCase):
"""

def test_sign(self):
token = detect_credential()
self.assertIsNotNone(token, "ambient credentials required")

identity = os.getenv("CERT_ID")
self.assertIsNotNone(token, "certificate identity required")

self.assertIsNotNone(identity, "certificate identity required")
issuer = os.getenv("CERT_ISSUER")
self.assertIsNotNone(token, "OIDC issuer required")
self.assertIsNotNone(issuer, "OIDC issuer required")

public_key = Key.from_dict(
"abcdef",
{
"keytype": "sigstore-oidc",
"scheme": "Fulcio",
"keyval": {
"issuer": issuer,
"identity": identity,
},
},
)
uri, public_key = SigstoreSigner.import_(identity, issuer)
signer = Signer.from_priv_key_uri(uri, public_key)

signer = SigstoreSigner(token, public_key)
sig = signer.sign(b"data")
public_key.verify_signature(sig, b"data")

Expand Down