| @@ -25,6 +25,7 @@ | |||||
| import os | import os | ||||
| import unittest | import unittest | ||||
| from binascii import a2b_hex | |||||
| from ctypes import Structure, POINTER, CFUNCTYPE, pointer, sizeof | from ctypes import Structure, POINTER, CFUNCTYPE, pointer, sizeof | ||||
| from ctypes import c_uint8, c_uint16, c_ssize_t, c_size_t, c_uint64, c_int | from ctypes import c_uint8, c_uint16, c_ssize_t, c_size_t, c_uint64, c_int | ||||
| from ctypes import CDLL | from ctypes import CDLL | ||||
| @@ -149,6 +150,55 @@ def x25519_base(scalar, clamp): | |||||
| return bytes(out) | return bytes(out) | ||||
| class X25519: | |||||
| '''Class to wrap the x25519 functions into something a bit more | |||||
| usable. This provides better key ingestion and better support | |||||
| for other key formats. | |||||
| Use either the gen method to generate a random key, or the frombytes | |||||
| method. | |||||
| a = X25519.gen() | |||||
| b = X25519.gen() | |||||
| a.dh(b.getpub()) == b.dh(a.getpub()) | |||||
| That is, each party generates a key, sends their public part to the | |||||
| other party, and then uses their received public part as an argument | |||||
| to the dh method. The resulting value will be shared between the | |||||
| two parties. | |||||
| ''' | |||||
| def __init__(self, key): | |||||
| self.privkey = key | |||||
| self.pubkey = x25519_base(key, 1) | |||||
| def dh(self, pub): | |||||
| '''Perform a DH operation using the public part pub.''' | |||||
| return x25519_wrap(self.pubkey, self.privkey, pub, 1) | |||||
| def getpub(self): | |||||
| '''Get the public part of the key. This is to be sent | |||||
| to the other party for key exchange.''' | |||||
| return self.pubkey | |||||
| def getpriv(self): | |||||
| return self.privkey | |||||
| @classmethod | |||||
| def gen(cls): | |||||
| '''Generate a random X25519 key.''' | |||||
| return cls(x25519_genkey()) | |||||
| @classmethod | |||||
| def frombytes(cls, key): | |||||
| '''Generate an X25519 key from 32 bytes.''' | |||||
| return cls(key) | |||||
| def comms_process_wrap(state, input): | def comms_process_wrap(state, input): | ||||
| '''A wrapper around comms_process that converts the argument | '''A wrapper around comms_process that converts the argument | ||||
| into the buffer, and the returns the message as a bytes string. | into the buffer, and the returns the message as a bytes string. | ||||
| @@ -164,7 +214,39 @@ def comms_process_wrap(state, input): | |||||
| return outbuf._from() | return outbuf._from() | ||||
| class TestX25519(unittest.TestCase): | class TestX25519(unittest.TestCase): | ||||
| def test_basic(self): | |||||
| PUBLIC_BYTES = EC_PUBLIC_BYTES | |||||
| PRIVATE_BYTES = EC_PRIVATE_BYTES | |||||
| def test_class(self): | |||||
| key = X25519.gen() | |||||
| pubkey = key.getpub() | |||||
| privkey = key.getpriv() | |||||
| apubkey = x25519_base(privkey, 1) | |||||
| self.assertEqual(apubkey, pubkey) | |||||
| self.assertEqual(X25519.frombytes(privkey).getpub(), pubkey) | |||||
| with self.assertRaises(ValueError): | |||||
| X25519(b'0'*31) | |||||
| def test_rfc7748_6_1(self): | |||||
| # KAT from https://datatracker.ietf.org/doc/html/rfc7748#section-6.1 | |||||
| apriv = a2b_hex('77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a') | |||||
| akey = X25519(apriv) | |||||
| self.assertEqual(akey.getpub(), a2b_hex('8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a')) | |||||
| bpriv = a2b_hex('5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb') | |||||
| bkey = X25519(bpriv) | |||||
| self.assertEqual(bkey.getpub(), a2b_hex('de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f')) | |||||
| ss = a2b_hex('4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742') | |||||
| self.assertEqual(akey.dh(bkey.getpub()), ss) | |||||
| self.assertEqual(bkey.dh(akey.getpub()), ss) | |||||
| def test_basic_ops(self): | |||||
| aprivkey = x25519_genkey() | aprivkey = x25519_genkey() | ||||
| apubkey = x25519_base(aprivkey, 1) | apubkey = x25519_base(aprivkey, 1) | ||||