1"""Signer implementation for project SPHINCS+ post-quantum signature support."""
2
3from __future__ import annotations
4
5import logging
6import os
7from typing import Any
8
9from securesystemslib.exceptions import (
10 UnsupportedLibraryError,
11 UnverifiedSignatureError,
12 VerificationError,
13)
14from securesystemslib.signer._key import Key
15from securesystemslib.signer._signature import Signature
16from securesystemslib.signer._signer import SecretsHandler, Signer
17from securesystemslib.signer._utils import compute_default_keyid
18
19SPX_IMPORT_ERROR = None
20try:
21 from pyspx import shake_128s
22except ImportError:
23 SPX_IMPORT_ERROR = "spinhcs+ key support requires the pyspx library"
24
25_SHAKE_SEED_LEN = 48
26
27logger = logging.getLogger(__name__)
28
29
30def generate_spx_key_pair() -> tuple[bytes, bytes]:
31 """Generate SPHINCS+ key pair and return public and private bytes."""
32 if SPX_IMPORT_ERROR:
33 raise UnsupportedLibraryError(SPX_IMPORT_ERROR)
34
35 seed = os.urandom(_SHAKE_SEED_LEN)
36 public, private = shake_128s.generate_keypair(seed)
37
38 return public, private
39
40
41class SpxKey(Key):
42 """SPHINCS+ verifier.
43
44 NOTE: The SPHINCS+ key and signature serialization formats are not yet
45 considered stable in securesystemslib. They may change in future releases
46 and may not be supported by other implementations.
47 """
48
49 DEFAULT_KEY_TYPE = "sphincs"
50 DEFAULT_SCHEME = "sphincs-shake-128s"
51
52 @classmethod
53 def from_dict(cls, keyid: str, key_dict: dict[str, Any]) -> SpxKey:
54 keytype, scheme, keyval = cls._from_dict(key_dict)
55 return cls(keyid, keytype, scheme, keyval, key_dict)
56
57 @classmethod
58 def from_bytes(cls, public: bytes) -> SpxKey:
59 """Create SpxKey instance from public key bytes."""
60 keytype = cls.DEFAULT_KEY_TYPE
61 scheme = cls.DEFAULT_SCHEME
62 keyval = {"public": public.hex()}
63
64 keyid = compute_default_keyid(keytype, scheme, keyval)
65 return cls(keyid, keytype, scheme, keyval)
66
67 def to_dict(self) -> dict[str, Any]:
68 return self._to_dict()
69
70 def verify_signature(self, signature: Signature, data: bytes) -> None:
71 valid = None
72 try:
73 if SPX_IMPORT_ERROR:
74 raise UnsupportedLibraryError(SPX_IMPORT_ERROR)
75
76 key = bytes.fromhex(self.keyval["public"])
77 sig = bytes.fromhex(signature.signature)
78
79 valid = shake_128s.verify(data, sig, key)
80
81 except Exception as e:
82 logger.info("Key %s failed to verify sig: %s", self.keyid, str(e))
83 raise VerificationError(
84 f"Unknown failure to verify signature by {self.keyid}"
85 ) from e
86
87 if not valid:
88 raise UnverifiedSignatureError(
89 f"Failed to verify signature by {self.keyid}"
90 )
91
92
93class SpxSigner(Signer):
94 """SPHINCS+ signer.
95
96 NOTE: The SPHINCS+ key and signature serialization formats are not yet
97 considered stable in securesystemslib. They may change in future releases
98 and may not be supported by other implementations.
99
100 Usage::
101
102 public_bytes, private_bytes = generate_spx_key_pair()
103 public_key = SpxKey.from_bytes(public_bytes)
104 signer = SpxSigner(private_bytes, public_key)
105 signature = signer.sign(b"payload")
106
107 # Use public_key.to_dict() / Key.from_dict() to transport public key data
108 public_key = signer.public_key
109 public_key.verify_signature(signature, b"payload")
110
111 """
112
113 def __init__(self, private: bytes, public: SpxKey):
114 self.private_key = private
115 self._public_key = public
116
117 @property
118 def public_key(self) -> Key:
119 return self._public_key
120
121 @classmethod
122 def from_priv_key_uri(
123 cls,
124 priv_key_uri: str,
125 public_key: Key,
126 secrets_handler: SecretsHandler | None = None,
127 ) -> SpxSigner:
128 raise NotImplementedError
129
130 def sign(self, payload: bytes) -> Signature:
131 """Signs payload with SPHINCS+ private key on the instance.
132
133 Arguments:
134 payload: bytes to be signed.
135
136 Raises:
137 UnsupportedLibraryError: PySPX is not available.
138
139 Returns:
140 Signature.
141
142 """
143 if SPX_IMPORT_ERROR:
144 raise UnsupportedLibraryError(SPX_IMPORT_ERROR)
145
146 raw = shake_128s.sign(payload, self.private_key)
147 return Signature(self.public_key.keyid, raw.hex())