diff --git a/nacl/nacl.py b/nacl/nacl.py index 51cd239e4ac493dccc0228b3043d133224cdbff3..9b48c145af618ac8f56a11bc6aee60f9db74f544 100644 --- a/nacl/nacl.py +++ b/nacl/nacl.py @@ -21,6 +21,7 @@ ffi.cdef( int crypto_sign_seed_keypair(unsigned char *pk, unsigned char *sk, unsigned char *seed); int crypto_sign(unsigned char *sm, unsigned long long *smlen, const unsigned char *m, unsigned long long mlen, const unsigned char *sk); + int crypto_sign_open(unsigned char *m, unsigned long long *mlen, const unsigned char *sm, unsigned long long smlen, const unsigned char *pk); """ # Hashing @@ -58,6 +59,7 @@ def wrap_nacl_function(func): lib.crypto_sign_seed_keypair = wrap_nacl_function(lib.crypto_sign_seed_keypair) lib.crypto_sign = wrap_nacl_function(lib.crypto_sign) +lib.crypto_sign_open = wrap_nacl_function(lib.crypto_sign_open) lib.crypto_hash = wrap_nacl_function(lib.crypto_hash) lib.crypto_hash_sha256 = wrap_nacl_function(lib.crypto_hash_sha256) diff --git a/nacl/signing.py b/nacl/signing.py index 1c8dd4657a9a4cb87421d2983d4279f2c2d910d3..f97fa426183c04da5d4a65a2fa307e8660ce888a 100644 --- a/nacl/signing.py +++ b/nacl/signing.py @@ -7,6 +7,12 @@ from .exceptions import CryptoError from .random import random +class BadSignatureError(CryptoError): + """ + Raised when the signature was forged or otherwise corrupt. + """ + + class SignedMessage(six.binary_type): @property @@ -18,6 +24,30 @@ class SignedMessage(six.binary_type): return self[nacl.lib.crypto_sign_BYTES:] +class VerifyKey(object): + + def __init__(self, key): + if len(key) != nacl.lib.crypto_sign_PUBLICKEYBYTES: + raise ValueError("The key must be exactly %s bytes long" % + nacl.lib.crypto_sign_PUBLICKEYBYTES) + + self._key = key + + def verify(self, smessage, signature=None): + if signature is not None: + # If we were given the message and signature separately, combine + # them. + smessage = signature + smessage + + message = nacl.ffi.new("unsigned char[]", len(smessage)) + message_len = nacl.ffi.new("unsigned long long *") + + if not nacl.lib.crypto_sign_open(message, message_len, smessage, len(smessage), self._key): + raise BadSignatureError("Signature was forged or corrupt") + + return nacl.ffi.buffer(message, message_len[0])[:] + + class SigningKey(object): def __init__(self, seed): @@ -37,6 +67,9 @@ class SigningKey(object): self._seed = seed self._signing_key = nacl.ffi.buffer(sk, nacl.lib.crypto_sign_SECRETKEYBYTES)[:] + # Public values + self.verify_key = VerifyKey(nacl.ffi.buffer(pk, nacl.lib.crypto_sign_PUBLICKEYBYTES)[:]) + @classmethod def generate(cls): return cls(random(nacl.lib.crypto_sign_SECRETKEYBYTES // 2)) diff --git a/tests/test_signing.py b/tests/test_signing.py index 0d6295d3b8b8188be19bd6c2225fa8d0773a8759..b8c50ac9e38bbfadc9005a472b32f2fd7d113d67 100644 --- a/tests/test_signing.py +++ b/tests/test_signing.py @@ -50,3 +50,33 @@ class TestSigningKey: assert binascii.hexlify(signed) == expected assert binascii.hexlify(signed.message) == message assert binascii.hexlify(signed.signature) == signature + + +class TestVerifyKey: + + @pytest.mark.parametrize(("public_key", "signed", "message", "signature"), + [(x["public_key"], x["signed"], x["message"], x["signature"]) for x in ed25519_known_answers()] + ) + def test_valid_signed_message(self, public_key, signed, message, signature): + key = nacl.signing.VerifyKey(binascii.unhexlify(public_key)) + signedb = binascii.unhexlify(signed) + messageb = binascii.unhexlify(message) + signatureb = binascii.unhexlify(signature) + + assert binascii.hexlify(key.verify(signedb)) == message + assert binascii.hexlify(key.verify(messageb, signatureb)) == message + + def test_invalid_signed_message(self): + skey = nacl.signing.SigningKey.generate() + smessage = skey.sign("A Test Message!") + signature, message = smessage.signature, "A Forged Test Message!" + + # Small sanity check + assert skey.verify_key.verify(smessage) + + with pytest.raises(nacl.signing.BadSignatureError): + skey.verify_key.verify(message, signature) + + with pytest.raises(nacl.signing.BadSignatureError): + forged = nacl.signing.SignedMessage(signature + message) + skey.verify_key.verify(forged)