1"""Signer implementation for project sigstore.""" 
    2 
    3from __future__ import annotations 
    4 
    5import json 
    6import logging 
    7from typing import Any 
    8from urllib import parse 
    9 
    10from securesystemslib.exceptions import ( 
    11    UnsupportedLibraryError, 
    12    UnverifiedSignatureError, 
    13    VerificationError, 
    14) 
    15from securesystemslib.signer._signer import ( 
    16    Key, 
    17    SecretsHandler, 
    18    Signature, 
    19    Signer, 
    20) 
    21from securesystemslib.signer._utils import compute_default_keyid 
    22 
    23IMPORT_ERROR = "sigstore library required to use 'sigstore-oidc' keys" 
    24 
    25# ruff: noqa: PLC0415 
    26 
    27logger = logging.getLogger(__name__) 
    28 
    29 
    30class SigstoreKey(Key): 
    31    """Sigstore verifier. 
    32 
    33    NOTE: The Sigstore key and signature serialization formats are not yet 
    34    considered stable in securesystemslib. They may change in future releases 
    35    and may not be supported by other implementations. 
    36    """ 
    37 
    38    DEFAULT_KEY_TYPE = "sigstore-oidc" 
    39    DEFAULT_SCHEME = "Fulcio" 
    40 
    41    def __init__( 
    42        self, 
    43        keyid: str, 
    44        keytype: str, 
    45        scheme: str, 
    46        keyval: dict[str, Any], 
    47        unrecognized_fields: dict[str, Any] | None = None, 
    48    ): 
    49        for content in ["identity", "issuer"]: 
    50            if content not in keyval or not isinstance(keyval[content], str): 
    51                raise ValueError(f"{content} string required for scheme {scheme}") 
    52        super().__init__(keyid, keytype, scheme, keyval, unrecognized_fields) 
    53 
    54    @classmethod 
    55    def from_dict(cls, keyid: str, key_dict: dict[str, Any]) -> SigstoreKey: 
    56        keytype, scheme, keyval = cls._from_dict(key_dict) 
    57        return cls(keyid, keytype, scheme, keyval, key_dict) 
    58 
    59    def to_dict(self) -> dict: 
    60        return self._to_dict() 
    61 
    62    def verify_signature(self, signature: Signature, data: bytes) -> None: 
    63        try: 
    64            from sigstore.errors import VerificationError as SigstoreVerifyError 
    65            from sigstore.models import Bundle 
    66            from sigstore.verify import Verifier 
    67            from sigstore.verify.policy import Identity 
    68        except ImportError as e: 
    69            raise VerificationError(IMPORT_ERROR) from e 
    70 
    71        try: 
    72            verifier = Verifier.production() 
    73            identity = Identity( 
    74                identity=self.keyval["identity"], issuer=self.keyval["issuer"] 
    75            ) 
    76            bundle_data = signature.unrecognized_fields["bundle"] 
    77            bundle = Bundle.from_json(json.dumps(bundle_data)) 
    78 
    79            verifier.verify_artifact(data, bundle, identity) 
    80 
    81        except SigstoreVerifyError as e: 
    82            logger.info( 
    83                "Key %s failed to verify sig: %s", 
    84                self.keyid, 
    85                e, 
    86            ) 
    87            raise UnverifiedSignatureError( 
    88                f"Failed to verify signature by {self.keyid}" 
    89            ) from e 
    90        except Exception as e: 
    91            logger.info("Key %s failed to verify sig: %s", self.keyid, str(e)) 
    92            raise VerificationError( 
    93                f"Unknown failure to verify signature by {self.keyid}" 
    94            ) from e 
    95 
    96 
    97class SigstoreSigner(Signer): 
    98    """Sigstore signer. 
    99 
    100    NOTE: The Sigstore key and signature serialization formats are not yet 
    101    considered stable in securesystemslib. They may change in future releases 
    102    and may not be supported by other implementations. 
    103 
    104    All signers should be instantiated with ``Signer.from_priv_key_uri()``. 
    105    Unstable ``SigstoreSigner`` currently requires opt-in via 
    106    ``securesystemslib.signer.SIGNER_FOR_URI_SCHEME``. 
    107 
    108    Usage:: 
    109 
    110        identity = "luk.puehringer@gmail.com"  # change, unless you know pw 
    111        issuer = "https://github.com/login/oauth" 
    112 
    113        # Create signer URI and public key for identity and issuer 
    114        uri, public_key = SigstoreSigner.import_(identity, issuer, ambient=False) 
    115 
    116        # Load signer from URI -- requires browser login with GitHub 
    117        signer = SigstoreSigner.from_priv_key_uri(uri, public_key) 
    118 
    119        # Sign with signer and verify public key 
    120        signature = signer.sign(b"data") 
    121        public_key.verify_signature(signature, b"data") 
    122 
    123    The private key URI scheme is "sigstore:?<PARAMS>", where PARAMS is 
    124    optional and toggles ambient credential usage. Example URIs: 
    125 
    126    * "sigstore:": 
    127        Sign with ambient credentials. 
    128    * "sigstore:?ambient=false": 
    129        Sign with OAuth2 + OpenID via browser login. 
    130 
    131    Arguments: 
    132        token: The OIDC identity token used for signing. 
    133        public_key: The related public key instance. 
    134 
    135    Raises: 
    136        UnsupportedLibraryError: sigstore library not found. 
    137    """ 
    138 
    139    SCHEME = "sigstore" 
    140 
    141    def __init__(self, token: Any, public_key: Key): 
    142        self._public_key = public_key 
    143        # token is of type sigstore.oidc.IdentityToken but the module should be usable 
    144        # without sigstore so it's not annotated 
    145        self._token = token 
    146 
    147    @property 
    148    def public_key(self) -> Key: 
    149        return self._public_key 
    150 
    151    @classmethod 
    152    def from_priv_key_uri( 
    153        cls, 
    154        priv_key_uri: str, 
    155        public_key: Key, 
    156        secrets_handler: SecretsHandler | None = None, 
    157    ) -> SigstoreSigner: 
    158        try: 
    159            from sigstore.oidc import IdentityToken, Issuer, detect_credential 
    160        except ImportError as e: 
    161            raise UnsupportedLibraryError(IMPORT_ERROR) from e 
    162 
    163        if not isinstance(public_key, SigstoreKey): 
    164            raise ValueError(f"expected SigstoreKey for {priv_key_uri}") 
    165 
    166        uri = parse.urlparse(priv_key_uri) 
    167 
    168        if uri.scheme != cls.SCHEME: 
    169            raise ValueError(f"SigstoreSigner does not support {priv_key_uri}") 
    170 
    171        params = dict(parse.parse_qsl(uri.query)) 
    172        ambient = params.get("ambient", "true") == "true" 
    173 
    174        if not ambient: 
    175            # TODO: Restrict oauth flow to use identity/issuer from public_key 
    176            # TODO: Use secrets_handler for identity_token() secret arg 
    177            token = Issuer.production().identity_token() 
    178        else: 
    179            credential = detect_credential() 
    180            if not credential: 
    181                raise RuntimeError("Failed to detect Sigstore credentials") 
    182            token = IdentityToken(credential) 
    183 
    184        key_identity = public_key.keyval["identity"] 
    185        key_issuer = public_key.keyval["issuer"] 
    186        if key_issuer != token.federated_issuer: 
    187            raise ValueError( 
    188                f"Signer identity issuer {token.federated_issuer} " 
    189                f"did not match key: {key_issuer}" 
    190            ) 
    191        # TODO: should check ambient identity too: unfortunately IdentityToken does 
    192        # not provide access to the expected identity value (cert SAN) in ambient case 
    193        if not ambient and key_identity != token.identity: 
    194            raise ValueError( 
    195                f"Signer identity {token.identity} did not match key: {key_identity}" 
    196            ) 
    197 
    198        return cls(token, public_key) 
    199 
    200    @classmethod 
    201    def _get_uri(cls, ambient: bool) -> str: 
    202        return f"{cls.SCHEME}:{'' if ambient else '?ambient=false'}" 
    203 
    204    @classmethod 
    205    def import_( 
    206        cls, identity: str, issuer: str, ambient: bool = True 
    207    ) -> tuple[str, SigstoreKey]: 
    208        """Create public key and signer URI. 
    209 
    210        Returns a private key URI (for Signer.from_priv_key_uri()) and a public 
    211        key. import_() should be called once and the returned URI and public 
    212        key should be stored for later use. 
    213 
    214        Arguments: 
    215            identity: The OIDC identity to use when verifying a signature. 
    216            issuer: The OIDC issuer to use when verifying a signature. 
    217            ambient: Toggle usage of ambient credentials in returned URI. 
    218        """ 
    219        keytype = SigstoreKey.DEFAULT_KEY_TYPE 
    220        scheme = SigstoreKey.DEFAULT_SCHEME 
    221        keyval = {"identity": identity, "issuer": issuer} 
    222        keyid = compute_default_keyid(keytype, scheme, keyval) 
    223        key = SigstoreKey(keyid, keytype, scheme, keyval) 
    224        uri = cls._get_uri(ambient) 
    225 
    226        return uri, key 
    227 
    228    @classmethod 
    229    def import_via_auth(cls) -> tuple[str, SigstoreKey]: 
    230        """Create public key and signer URI by interactive authentication 
    231 
    232        Returns a private key URI (for Signer.from_priv_key_uri()) and a public 
    233        key. This method always uses the interactive authentication. 
    234        """ 
    235        try: 
    236            from sigstore.oidc import Issuer 
    237        except ImportError as e: 
    238            raise UnsupportedLibraryError(IMPORT_ERROR) from e 
    239 
    240        # authenticate to get the identity and issuer 
    241        token = Issuer.production().identity_token() 
    242        return cls.import_(token.identity, token.federated_issuer, False) 
    243 
    244    def sign(self, payload: bytes) -> Signature: 
    245        """Signs payload using the OIDC token on the signer instance. 
    246 
    247        Arguments: 
    248            payload: bytes to be signed. 
    249 
    250        Raises: 
    251            Various errors from sigstore-python. 
    252 
    253        Returns: 
    254            Signature. 
    255 
    256            NOTE: The relevant data is in `unrecognized_fields["bundle"]`. 
    257 
    258        """ 
    259        try: 
    260            from sigstore.sign import SigningContext 
    261        except ImportError as e: 
    262            raise UnsupportedLibraryError(IMPORT_ERROR) from e 
    263 
    264        context = SigningContext.production() 
    265        with context.signer(self._token) as sigstore_signer: 
    266            bundle = sigstore_signer.sign_artifact(payload) 
    267        # We want to access the actual signature, see 
    268        # https://github.com/sigstore/protobuf-specs/blob/main/protos/sigstore_bundle.proto 
    269        bundle_json = json.loads(bundle.to_json()) 
    270        return Signature( 
    271            self.public_key.keyid, 
    272            bundle_json["messageSignature"]["signature"], 
    273            {"bundle": bundle_json}, 
    274        ) 
    275 
    276    @classmethod 
    277    def import_github_actions( 
    278        cls, project: str, workflow_path: str, ref: str | None = "refs/heads/main" 
    279    ) -> tuple[str, SigstoreKey]: 
    280        """Convenience method to build identity and issuer string for import_() from 
    281        GitHub project and workflow path. 
    282 
    283        Args: 
    284            project: GitHub project name (example: 
    285               "secure-systems-lab/securesystemslib") 
    286            workflow_path: GitHub workflow path (example: 
    287               ".github/workflows/online-sign.yml") 
    288            ref: optional GitHub ref, defaults to refs/heads/main 
    289 
    290        Returns: 
    291            uri: string 
    292            key: SigstoreKey 
    293 
    294        """ 
    295        identity = f"https://github.com/{project}/{workflow_path}@{ref}" 
    296        issuer = "https://token.actions.githubusercontent.com" 
    297        uri, key = cls.import_(identity, issuer) 
    298 
    299        return uri, key