Skip to content

Commit 9833f41

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 9833f41

File tree

2 files changed

+160
-31
lines changed

2 files changed

+160
-31
lines changed

securesystemslib/signer.py

Lines changed: 145 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,137 @@ 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+
206+
@classmethod
207+
def from_dict(cls, keyid: str, key_dict: Dict[str, Any]) -> "Key":
208+
"""Creates ``Key`` object from TUF serialization dict.
209+
210+
Raises:
211+
KeyError, TypeError: Invalid arguments.
212+
"""
213+
keytype = key_dict.pop("keytype")
214+
scheme = key_dict.pop("scheme")
215+
keyval = key_dict.pop("keyval")
216+
# All fields left in the key_dict are unrecognized.
217+
return cls(keyid, keytype, scheme, keyval, key_dict)
218+
219+
def to_dict(self) -> Dict[str, Any]:
220+
"""Returns a dict for TUF serialization."""
221+
return {
222+
"keytype": self.keytype,
223+
"scheme": self.scheme,
224+
"keyval": self.keyval,
225+
**self.unrecognized_fields,
226+
}
227+
228+
def to_securesystemslib_key(self) -> Dict[str, Any]:
229+
"""Returns a classic Securesystemslib keydict"""
230+
return {
231+
"keyid": self.keyid,
232+
"keytype": self.keytype,
233+
"scheme": self.scheme,
234+
"keyval": self.keyval,
235+
}
236+
237+
@classmethod
238+
def from_securesystemslib_key(cls, key_dict: Dict[str, Any]) -> "Key":
239+
"""Creates a ``Key`` object from a classic securesystemlib keydict.
240+
241+
Args:
242+
key_dict: Key in securesystemlib dict representation.
243+
244+
Raises:
245+
ValueError: ``key_dict`` value is not in securesystemslib format.
246+
"""
247+
try:
248+
key_meta = sslib_keys.format_keyval_to_metadata(
249+
key_dict["keytype"],
250+
key_dict["scheme"],
251+
key_dict["keyval"],
252+
)
253+
except FormatError as e:
254+
raise ValueError("keydict not in securesystemslib format") from e
255+
256+
return cls(
257+
key_dict["keyid"],
258+
key_meta["keytype"],
259+
key_meta["scheme"],
260+
key_meta["keyval"],
261+
)
262+
263+
def is_verified(self, signature: Signature, data: bytes) -> bool:
264+
"""Verifies the signature over data.
265+
266+
Args:
267+
signature: Signature object.
268+
data: Payload bytes.
269+
270+
Raises:
271+
CryptoError, FormatError, UnsupportedAlgorithmError.
272+
273+
Returns True if signature is valid for this key for given data.
274+
"""
275+
return sslib_keys.verify_signature(
276+
self.to_securesystemslib_key(),
277+
signature.to_dict(),
278+
data,
279+
)
280+
281+
149282
# SecretsHandler is a function the calling code can provide to Signer:
150283
# If Signer needs secrets from user, the function will be called
151284
SecretsHandler = Callable[[str], str]
@@ -184,7 +317,7 @@ def sign(self, payload: bytes) -> Signature:
184317
def new_from_uri(
185318
cls,
186319
priv_key_uri: str,
187-
public_key: Dict[str, Any],
320+
public_key: Key,
188321
secrets_handler: SecretsHandler,
189322
) -> "Signer":
190323
"""Constructor for given private key URI
@@ -195,7 +328,7 @@ def new_from_uri(
195328
196329
Arguments:
197330
priv_key_uri: URI that identifies the private key and signer
198-
public_key: Public key metadata conforming to PUBLIC_KEY_SCHEMA
331+
public_key: Key object
199332
secrets_handler: Optional function that may be called if the
200333
signer needs additional secrets (like a PIN or passphrase)
201334
"""
@@ -204,17 +337,16 @@ def new_from_uri(
204337
@staticmethod
205338
def from_priv_key_uri(
206339
priv_key_uri: str,
207-
public_key: Dict[str, Any],
340+
public_key: Key,
208341
secrets_handler: Optional[SecretsHandler] = None,
209342
):
210343
"""Returns a concrete Signer implementation based on private key URI
211344
212345
Args:
213346
priv_key_uri: URI that identifies the private key location and signer
214-
public_key: Public key metadata conforming to PUBLIC_KEY_SCHEMA
347+
public_key: Key object
215348
"""
216349

217-
formats.PUBLIC_KEY_SCHEMA.check_match(public_key)
218350
scheme, _, _ = priv_key_uri.partition(":")
219351
if scheme not in SIGNER_FOR_URI_SCHEME:
220352
raise ValueError(f"Unsupported private key scheme {scheme}")
@@ -246,8 +378,8 @@ class SSlibSigner(Signer):
246378
247379
Attributes:
248380
key_dict:
249-
A securesystemslib-style key dictionary, which includes a keyid,
250-
key type, scheme, and keyval with both private and public parts.
381+
A securesystemslib-style key dictionary. This is an implementation
382+
detail, not part of public API
251383
"""
252384

253385
ENVVAR_URI_SCHEME = "envvar"
@@ -261,15 +393,14 @@ def __init__(self, key_dict: Dict):
261393
def new_from_uri(
262394
cls,
263395
priv_key_uri: str,
264-
public_key: Dict[str, Any],
396+
public_key: Key,
265397
secrets_handler: SecretsHandler,
266398
) -> "SSlibSigner":
267399
"""Semi-private Constructor for Signer to call
268400
269401
Arguments:
270402
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.
403+
public_key: Key object.
273404
274405
Raises:
275406
OSError: Reading the file failed with "file:" URI
@@ -279,7 +410,6 @@ def new_from_uri(
279410
Returns:
280411
SSlibSigner for the given private key URI.
281412
"""
282-
keydict = copy.deepcopy(public_key)
283413
uri = parse.urlparse(priv_key_uri)
284414

285415
if uri.scheme == cls.ENVVAR_URI_SCHEME:
@@ -308,6 +438,7 @@ def new_from_uri(
308438
f"SSlibSigner does not support priv key uri {priv_key_uri}"
309439
)
310440

441+
keydict = public_key.to_securesystemslib_key()
311442
keydict["keyval"]["private"] = private
312443
return cls(keydict)
313444

@@ -355,7 +486,7 @@ def __init__(
355486
def new_from_uri(
356487
cls,
357488
priv_key_uri: str,
358-
public_key: Dict[str, Any],
489+
public_key: Key,
359490
secrets_handler: SecretsHandler,
360491
) -> Signer:
361492
# 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)