Skip to content

Commit bfeda3c

Browse files
committed
Signer: Add Key class
This is copied from python-tuf with following changes * verify_signature() is left in python-tuf as Metadata-related * is_verified() is added instead * type of keyval is changed from Dict[str, str] to Dict[str, Any] * Some docstrings made more reasonable for SSLib Signer.from_priv_key_uri() now takes a Key as argument. SSlibSigner constructor still takes a classic keydict as constructor argument and not a Key (in order to not break API) but that is now documented as an implementation detail.
1 parent c2cfd3b commit bfeda3c

File tree

2 files changed

+159
-31
lines changed

2 files changed

+159
-31
lines changed

securesystemslib/signer.py

Lines changed: 144 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,16 @@
66
"""
77

88
import abc
9-
import copy
9+
import logging
1010
import os
1111
from typing import Any, Callable, Dict, Mapping, Optional
1212
from urllib import parse
1313

1414
import securesystemslib.gpg.functions as gpg
1515
import securesystemslib.keys as sslib_keys
16-
from securesystemslib import formats
16+
from securesystemslib.exceptions import FormatError
17+
18+
logger = logging.getLogger(__name__)
1719

1820
# NOTE This dictionary is initialized here so it's available to Signer, but
1921
# filled at end of file when Signer subclass definitions are available.
@@ -146,6 +148,136 @@ def to_dict(self) -> Dict:
146148
}
147149

148150

151+
class Key:
152+
"""A container class representing the public portion of a key.
153+
154+
*All parameters named below are not just constructor arguments but also
155+
instance attributes.*
156+
157+
Args:
158+
keyid: Key identifier that is unique within the metadata it is used in.
159+
Keyid is not verified to be the hash of a specific representation
160+
of the key.
161+
keytype: Key type, e.g. "rsa", "ed25519" or "ecdsa-sha2-nistp256".
162+
scheme: Signature scheme. For example:
163+
"rsassa-pss-sha256", "ed25519", and "ecdsa-sha2-nistp256".
164+
keyval: Opaque key content
165+
unrecognized_fields: Dictionary of all attributes that are not managed
166+
by Securesystemslib
167+
168+
Raises:
169+
TypeError: Invalid type for an argument.
170+
"""
171+
172+
def __init__(
173+
self,
174+
keyid: str,
175+
keytype: str,
176+
scheme: str,
177+
keyval: Dict[str, Any],
178+
unrecognized_fields: Optional[Dict[str, Any]] = None,
179+
):
180+
if not all(
181+
isinstance(at, str) for at in [keyid, keytype, scheme]
182+
) or not isinstance(keyval, dict):
183+
raise TypeError("Unexpected Key attributes types!")
184+
self.keyid = keyid
185+
self.keytype = keytype
186+
self.scheme = scheme
187+
self.keyval = keyval
188+
if unrecognized_fields is None:
189+
unrecognized_fields = {}
190+
191+
self.unrecognized_fields = unrecognized_fields
192+
193+
def __eq__(self, other: Any) -> bool:
194+
if not isinstance(other, Key):
195+
return False
196+
197+
return (
198+
self.keyid == other.keyid
199+
and self.keytype == other.keytype
200+
and self.scheme == other.scheme
201+
and self.keyval == other.keyval
202+
and self.unrecognized_fields == other.unrecognized_fields
203+
)
204+
205+
@classmethod
206+
def from_dict(cls, keyid: str, key_dict: Dict[str, Any]) -> "Key":
207+
"""Creates ``Key`` object from TUF serialization dict.
208+
209+
Raises:
210+
KeyError, TypeError: Invalid arguments.
211+
"""
212+
keytype = key_dict.pop("keytype")
213+
scheme = key_dict.pop("scheme")
214+
keyval = key_dict.pop("keyval")
215+
# All fields left in the key_dict are unrecognized.
216+
return cls(keyid, keytype, scheme, keyval, key_dict)
217+
218+
def to_dict(self) -> Dict[str, Any]:
219+
"""Returns a dict for TUF serialization."""
220+
return {
221+
"keytype": self.keytype,
222+
"scheme": self.scheme,
223+
"keyval": self.keyval,
224+
**self.unrecognized_fields,
225+
}
226+
227+
def to_securesystemslib_key(self) -> Dict[str, Any]:
228+
"""Returns a classic Securesystemslib keydict"""
229+
return {
230+
"keyid": self.keyid,
231+
"keytype": self.keytype,
232+
"scheme": self.scheme,
233+
"keyval": self.keyval,
234+
}
235+
236+
@classmethod
237+
def from_securesystemslib_key(cls, key_dict: Dict[str, Any]) -> "Key":
238+
"""Creates a ``Key`` object from a classic securesystemlib keydict.
239+
240+
Args:
241+
key_dict: Key in securesystemlib dict representation.
242+
243+
Raises:
244+
ValueError: ``key_dict`` value is not in securesystemslib format.
245+
"""
246+
try:
247+
key_meta = sslib_keys.format_keyval_to_metadata(
248+
key_dict["keytype"],
249+
key_dict["scheme"],
250+
key_dict["keyval"],
251+
)
252+
except FormatError as e:
253+
raise ValueError("keydict not in securesystemslib format") from e
254+
255+
return cls(
256+
key_dict["keyid"],
257+
key_meta["keytype"],
258+
key_meta["scheme"],
259+
key_meta["keyval"],
260+
)
261+
262+
def is_verified(self, signature: Signature, data: bytes) -> bool:
263+
"""Verifies the signature over data.
264+
265+
Args:
266+
signature: Signature object.
267+
data: Payload bytes.
268+
269+
Raises:
270+
CryptoError, FormatError, UnsupportedAlgorithmError.
271+
272+
Returns True if signature is valid for this key for given data.
273+
"""
274+
return sslib_keys.verify_signature(
275+
self.to_securesystemslib_key(),
276+
signature.to_dict(),
277+
data,
278+
)
279+
280+
149281
# SecretsHandler is a function the calling code can provide to Signer:
150282
# If Signer needs secrets from user, the function will be called
151283
SecretsHandler = Callable[[str], str]
@@ -184,7 +316,7 @@ def sign(self, payload: bytes) -> Signature:
184316
def new_from_uri(
185317
cls,
186318
priv_key_uri: str,
187-
public_key: Dict[str, Any],
319+
public_key: Key,
188320
secrets_handler: SecretsHandler,
189321
) -> "Signer":
190322
"""Constructor for given private key URI
@@ -195,7 +327,7 @@ def new_from_uri(
195327
196328
Arguments:
197329
priv_key_uri: URI that identifies the private key and signer
198-
public_key: Public key metadata conforming to PUBLIC_KEY_SCHEMA
330+
public_key: Key object
199331
secrets_handler: Optional function that may be called if the
200332
signer needs additional secrets (like a PIN or passphrase)
201333
"""
@@ -204,17 +336,16 @@ def new_from_uri(
204336
@staticmethod
205337
def from_priv_key_uri(
206338
priv_key_uri: str,
207-
public_key: Dict[str, Any],
339+
public_key: Key,
208340
secrets_handler: Optional[SecretsHandler] = None,
209341
):
210342
"""Returns a concrete Signer implementation based on private key URI
211343
212344
Args:
213345
priv_key_uri: URI that identifies the private key location and signer
214-
public_key: Public key metadata conforming to PUBLIC_KEY_SCHEMA
346+
public_key: Key object
215347
"""
216348

217-
formats.PUBLIC_KEY_SCHEMA.check_match(public_key)
218349
scheme, _, _ = priv_key_uri.partition(":")
219350
if scheme not in SIGNER_FOR_URI_SCHEME:
220351
raise ValueError(f"Unsupported private key scheme {scheme}")
@@ -246,8 +377,8 @@ class SSlibSigner(Signer):
246377
247378
Attributes:
248379
key_dict:
249-
A securesystemslib-style key dictionary, which includes a keyid,
250-
key type, scheme, and keyval with both private and public parts.
380+
A securesystemslib-style key dictionary. This is an implementation
381+
detail, not part of public API
251382
"""
252383

253384
ENVVAR_URI_SCHEME = "envvar"
@@ -261,15 +392,14 @@ def __init__(self, key_dict: Dict):
261392
def new_from_uri(
262393
cls,
263394
priv_key_uri: str,
264-
public_key: Dict[str, Any],
395+
public_key: Key,
265396
secrets_handler: SecretsHandler,
266397
) -> "SSlibSigner":
267398
"""Semi-private Constructor for Signer to call
268399
269400
Arguments:
270401
priv_key_uri: private key URI described in class doc
271-
public_key: securesystemslib-style key dict, which includes keyid,
272-
type, scheme, and keyval the public key.
402+
public_key: Key object.
273403
274404
Raises:
275405
OSError: Reading the file failed with "file:" URI
@@ -279,7 +409,6 @@ def new_from_uri(
279409
Returns:
280410
SSlibSigner for the given private key URI.
281411
"""
282-
keydict = copy.deepcopy(public_key)
283412
uri = parse.urlparse(priv_key_uri)
284413

285414
if uri.scheme == cls.ENVVAR_URI_SCHEME:
@@ -308,6 +437,7 @@ def new_from_uri(
308437
f"SSlibSigner does not support priv key uri {priv_key_uri}"
309438
)
310439

440+
keydict = public_key.to_securesystemslib_key()
311441
keydict["keyval"]["private"] = private
312442
return cls(keydict)
313443

@@ -355,7 +485,7 @@ def __init__(
355485
def new_from_uri(
356486
cls,
357487
priv_key_uri: str,
358-
public_key: Dict[str, Any],
488+
public_key: Key,
359489
secrets_handler: SecretsHandler,
360490
) -> Signer:
361491
# GPGSigner uses keys and produces signature dicts that are not

tests/test_signer.py

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from securesystemslib.signer import (
2121
GPGSignature,
2222
GPGSigner,
23+
Key,
2324
Signature,
2425
Signer,
2526
SSlibSigner,
@@ -49,40 +50,37 @@ def tearDownClass(cls):
4950
def test_signer_sign_with_envvar_uri(self):
5051
for key in self.keys:
5152
# setup
52-
pubkey = copy.deepcopy(key)
53-
privkey = pubkey["keyval"].pop("private")
54-
os.environ["PRIVKEY"] = privkey
53+
pubkey = Key.from_securesystemslib_key(key)
54+
os.environ["PRIVKEY"] = key["keyval"]["private"]
5555

5656
# test signing
5757
signer = Signer.from_priv_key_uri("envvar:PRIVKEY", pubkey)
58-
sig = signer.sign(self.DATA).to_dict()
58+
sig = signer.sign(self.DATA)
5959

60-
self.assertTrue(KEYS.verify_signature(pubkey, sig, self.DATA))
61-
self.assertFalse(KEYS.verify_signature(pubkey, sig, b"NOT DATA"))
60+
self.assertTrue(pubkey.is_verified(sig, self.DATA))
61+
self.assertFalse(pubkey.is_verified(sig, b"NOT DATA"))
6262

6363
def test_signer_sign_with_file_uri(self):
6464
for key in self.keys:
6565
# setup
66-
pubkey = copy.deepcopy(key)
67-
privkey = pubkey["keyval"].pop("private")
66+
pubkey = Key.from_securesystemslib_key(key)
6867
# let teardownclass handle the file removal
6968
with tempfile.NamedTemporaryFile(
7069
dir=self.testdir.name, delete=False
7170
) as f:
72-
f.write(privkey.encode())
71+
f.write(key["keyval"]["private"].encode())
7372

7473
# test signing
7574
signer = Signer.from_priv_key_uri(f"file:{f.name}", pubkey)
76-
sig = signer.sign(self.DATA).to_dict()
75+
sig = signer.sign(self.DATA)
7776

78-
self.assertTrue(KEYS.verify_signature(pubkey, sig, self.DATA))
79-
self.assertFalse(KEYS.verify_signature(pubkey, sig, b"NOT DATA"))
77+
self.assertTrue(pubkey.is_verified(sig, self.DATA))
78+
self.assertFalse(pubkey.is_verified(sig, b"NOT DATA"))
8079

8180
def test_signer_sign_with_enc_file_uri(self):
8281
for key in self.keys:
8382
# setup
84-
pubkey = copy.deepcopy(key)
85-
pubkey["keyval"].pop("private")
83+
pubkey = Key.from_securesystemslib_key(key)
8684
privkey = KEYS.encrypt_key(key, "hunter2")
8785
# let teardownclass handle the file removal
8886
with tempfile.NamedTemporaryFile(
@@ -96,10 +94,10 @@ def secrets_handler(secret: str) -> str:
9694

9795
uri = f"encfile:{f.name}"
9896
signer = Signer.from_priv_key_uri(uri, pubkey, secrets_handler)
99-
sig = signer.sign(self.DATA).to_dict()
97+
sig = signer.sign(self.DATA)
10098

101-
self.assertTrue(KEYS.verify_signature(pubkey, sig, self.DATA))
102-
self.assertFalse(KEYS.verify_signature(pubkey, sig, b"NOT DATA"))
99+
self.assertTrue(pubkey.is_verified(sig, self.DATA))
100+
self.assertFalse(pubkey.is_verified(sig, b"NOT DATA"))
103101

104102
# test wrong passphrase
105103
def fake_handler(_) -> str:

0 commit comments

Comments
 (0)