diff --git a/src/nacl/signing.py b/src/nacl/signing.py index 158c7845..14f07bdf 100644 --- a/src/nacl/signing.py +++ b/src/nacl/signing.py @@ -143,33 +143,64 @@ class SigningKey(encoding.Encodable, StringFixer): value can be passed into the :class:`~nacl.signing.SigningKey` as a :func:`bytes` whose length is 32. + Alternatively, you can construct :class:`~nacl.signing.SigningKey` directly + from a 64-byte Ed25519 private key. You must provide either a seed or a key, + but not both. + .. warning:: This **must** be protected and remain secret. Anyone who knows the value of your :class:`~nacl.signing.SigningKey` or it's seed can masquerade as you. - :param seed: [:class:`bytes`] Random 32-byte value (i.e. private key) + :param seed: [:class:`bytes`] Random 32-byte value for generating Ed25519 private key :param encoder: A class that is able to decode the seed + :param secret_key: [:class:`bytes`] A previously generated 64-byte Ed25519 private key :ivar: verify_key: [:class:`~nacl.signing.VerifyKey`] The verify (i.e. public) key that corresponds with this signing key. """ - def __init__(self, seed, encoder=encoding.RawEncoder): - # Decode the seed - seed = encoder.decode(seed) - if not isinstance(seed, bytes): - raise exc.TypeError( - "SigningKey must be created from a 32 byte seed" - ) + def __init__( + self, seed=None, encoder=encoding.RawEncoder, secret_key=None + ): + exc.ensure( + (secret_key is not None and seed is None) + or (secret_key is None and seed is not None), + "SigningKey must be created either from a 32 byte seed or a 64 byte key.", + ) + if seed is not None: + # Decode the seed + seed = encoder.decode(seed) + if not isinstance(seed, bytes): + raise exc.TypeError( + "SigningKey must be created from a 32 byte seed" + ) - # Verify that our seed is the proper size - if len(seed) != nacl.bindings.crypto_sign_SEEDBYTES: - raise exc.ValueError( - "The seed must be exactly %d bytes long" - % nacl.bindings.crypto_sign_SEEDBYTES + # Verify that our seed is the proper size + if len(seed) != nacl.bindings.crypto_sign_SEEDBYTES: + raise exc.ValueError( + "The seed must be exactly %d bytes long" + % nacl.bindings.crypto_sign_SEEDBYTES + ) + + public_key, secret_key = nacl.bindings.crypto_sign_seed_keypair( + seed ) + else: + # Decode the key + secret_key = encoder.decode(secret_key) + if not isinstance(secret_key, bytes): + raise exc.TypeError( + "SigningKey must be created from a 64 byte key" + ) - public_key, secret_key = nacl.bindings.crypto_sign_seed_keypair(seed) + # Verify that our key is the proper size + if len(secret_key) != nacl.bindings.crypto_sign_SECRETKEYBYTES: + raise exc.ValueError( + "The key must be exactly %d bytes long" + % nacl.bindings.crypto_sign_SECRETKEYBYTES + ) + public_key = nacl.bindings.crypto_sign_ed25519_sk_to_pk(secret_key) + seed = nacl.bindings.crypto_sign_ed25519_sk_to_seed(secret_key) self._seed = seed self._signing_key = secret_key diff --git a/tests/test_signing.py b/tests/test_signing.py index 6d10c51c..2e399edc 100644 --- a/tests/test_signing.py +++ b/tests/test_signing.py @@ -17,7 +17,11 @@ import pytest -from nacl.bindings import crypto_sign_PUBLICKEYBYTES, crypto_sign_SEEDBYTES +from nacl.bindings import ( + crypto_sign_PUBLICKEYBYTES, + crypto_sign_SECRETKEYBYTES, + crypto_sign_SEEDBYTES, +) from nacl.encoding import Base64Encoder, HexEncoder from nacl.exceptions import BadSignatureError from nacl.signing import SignedMessage, SigningKey, VerifyKey @@ -90,6 +94,37 @@ def test_different_keys_are_not_equal(self, k2): def test_initialization_with_seed(self, seed): SigningKey(seed, encoder=HexEncoder) + def test_initialize_with_secret_key(self): + k = SigningKey.generate() + seed_bytes = k._seed + secret_key_bytes = k._signing_key + k_from_seed = SigningKey(seed=seed_bytes) + k_from_secret_key = SigningKey(secret_key=secret_key_bytes) + assert_equal(k, k_from_seed) + assert_equal(k, k_from_secret_key) + assert id(k) != id(k_from_seed) + assert id(k) != id(k_from_secret_key) + assert id(k_from_secret_key) != id(k_from_seed) + + def test_initialization_with_secret_key_and_seed(self): + with pytest.raises(AssertionError): + SigningKey( + seed=b"\x00" * crypto_sign_SEEDBYTES, + secret_key=b"\x00" * crypto_sign_SECRETKEYBYTES, + ) + + def test_initialization_without_arguments(self): + with pytest.raises(AssertionError): + SigningKey() + + def test_wrong_key_length(self): + with pytest.raises(ValueError): + SigningKey(secret_key=b"\x00" * 5) + + def test_wrong_secret_key_type(self): + with pytest.raises(TypeError): + SigningKey(secret_key=12) + @pytest.mark.parametrize( ("seed", "_public_key", "message", "signature", "expected"), ed25519_known_answers(),