Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow initializing SigningKey from a raw key #648

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
59 changes: 45 additions & 14 deletions src/nacl/signing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
37 changes: 36 additions & 1 deletion tests/test_signing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(),
Expand Down