core-lightning/test/test_onion.py

346 lines
12 KiB
Python
Raw Normal View History

#!/usr/bin/env python
import argparse
import sys
import time
from hashlib import sha256
from binascii import hexlify, unhexlify
import hmac
import random
from cryptography.hazmat.primitives.ciphers import Cipher, modes, algorithms
from cryptography.hazmat.primitives.ciphers.algorithms import AES
from cryptography.hazmat.primitives.ciphers.modes import CTR
from cryptography.hazmat.backends import default_backend
# http://cryptography.io
from pyelliptic import ecc
class MyEx(Exception): pass
def hmac_sha256(k, m):
return hmac.new(k, m, sha256).digest()
## pyelliptic doesn't support compressed pubkey representations
## so we have to add some code...
from pyelliptic.openssl import OpenSSL
import ctypes
OpenSSL.EC_POINT_set_compressed_coordinates_GFp = \
OpenSSL._lib.EC_POINT_set_compressed_coordinates_GFp
OpenSSL.EC_POINT_set_compressed_coordinates_GFp.restype = ctypes.c_int
OpenSSL.EC_POINT_set_compressed_coordinates_GFp.argtypes = [
ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_int,
ctypes.c_void_p]
def ecc_ecdh_key(sec, pub):
assert isinstance(sec, ecc.ECC)
if isinstance(pub, ecc.ECC):
pub = pub.get_pubkey()
#return sec.get_ecdh_key(pub)
pubkey_x, pubkey_y = ecc.ECC._decode_pubkey(pub, 'binary')
other_key = other_pub_key_x = other_pub_key_y = other_pub_key = None
own_priv_key = res = res_x = res_y = None
try:
other_key = OpenSSL.EC_KEY_new_by_curve_name(sec.curve)
if other_key == 0:
raise Exception("[OpenSSL] EC_KEY_new_by_curve_name FAIL ... " + OpenSSL.get_error())
other_pub_key_x = OpenSSL.BN_bin2bn(pubkey_x, len(pubkey_x), 0)
other_pub_key_y = OpenSSL.BN_bin2bn(pubkey_y, len(pubkey_y), 0)
other_group = OpenSSL.EC_KEY_get0_group(other_key)
other_pub_key = OpenSSL.EC_POINT_new(other_group)
if (other_pub_key == None):
raise Exception("[OpenSSl] EC_POINT_new FAIL ... " + OpenSSL.get_error())
if (OpenSSL.EC_POINT_set_affine_coordinates_GFp(other_group,
other_pub_key,
other_pub_key_x,
other_pub_key_y,
0)) == 0:
raise Exception(
"[OpenSSL] EC_POINT_set_affine_coordinates_GFp FAIL ..." + OpenSSL.get_error())
own_priv_key = OpenSSL.BN_bin2bn(sec.privkey, len(sec.privkey), 0)
res = OpenSSL.EC_POINT_new(other_group)
if (OpenSSL.EC_POINT_mul(other_group, res, 0, other_pub_key, own_priv_key, 0)) == 0:
raise Exception(
"[OpenSSL] EC_POINT_mul FAIL ..." + OpenSSL.get_error())
res_x = OpenSSL.BN_new()
res_y = OpenSSL.BN_new()
if (OpenSSL.EC_POINT_get_affine_coordinates_GFp(other_group, res,
res_x,
res_y, 0
)) == 0:
raise Exception(
"[OpenSSL] EC_POINT_get_affine_coordinates_GFp FAIL ... " + OpenSSL.get_error())
resx = OpenSSL.malloc(0, OpenSSL.BN_num_bytes(res_x))
resy = OpenSSL.malloc(0, OpenSSL.BN_num_bytes(res_y))
OpenSSL.BN_bn2bin(res_x, resx)
resx = resx.raw
OpenSSL.BN_bn2bin(res_y, resy)
resy = resy.raw
return resx, resy
finally:
if other_key: OpenSSL.EC_KEY_free(other_key)
if other_pub_key_x: OpenSSL.BN_free(other_pub_key_x)
if other_pub_key_y: OpenSSL.BN_free(other_pub_key_y)
if other_pub_key: OpenSSL.EC_POINT_free(other_pub_key)
if own_priv_key: OpenSSL.BN_free(own_priv_key)
if res: OpenSSL.EC_POINT_free(res)
if res_x: OpenSSL.BN_free(res_x)
if res_y: OpenSSL.BN_free(res_y)
def get_pos_y_for_x(pubkey_x, yneg=0):
key = pub_key = pub_key_x = pub_key_y = None
try:
key = OpenSSL.EC_KEY_new_by_curve_name(OpenSSL.get_curve('secp256k1'))
group = OpenSSL.EC_KEY_get0_group(key)
pub_key_x = OpenSSL.BN_bin2bn(pubkey_x, len(pubkey_x), 0)
pub_key = OpenSSL.EC_POINT_new(group)
if OpenSSL.EC_POINT_set_compressed_coordinates_GFp(group, pub_key,
pub_key_x, yneg, 0) == 0:
raise Exception("[OpenSSL] EC_POINT_set_compressed_coordinates_GFp FAIL ... " + OpenSSL.get_error())
pub_key_y = OpenSSL.BN_new()
if (OpenSSL.EC_POINT_get_affine_coordinates_GFp(group, pub_key,
pub_key_x,
pub_key_y, 0
)) == 0:
raise Exception("[OpenSSL] EC_POINT_get_affine_coordinates_GFp FAIL ... " + OpenSSL.get_error())
pubkeyy = OpenSSL.malloc(0, OpenSSL.BN_num_bytes(pub_key_y))
OpenSSL.BN_bn2bin(pub_key_y, pubkeyy)
pubkeyy = pubkeyy.raw
field_size = OpenSSL.EC_GROUP_get_degree(OpenSSL.EC_KEY_get0_group(key))
secret_len = int((field_size + 7) / 8)
if len(pubkeyy) < secret_len:
pubkeyy = pubkeyy.rjust(secret_len, b'\0')
return pubkeyy
finally:
if key is not None: OpenSSL.EC_KEY_free(key)
if pub_key is not None: OpenSSL.EC_POINT_free(pub_key)
if pub_key_x is not None: OpenSSL.BN_free(pub_key_x)
if pub_key_y is not None: OpenSSL.BN_free(pub_key_y)
def ec_decompress(pubkey, curve='secp256k1'):
if pubkey[0] == '\x02' or pubkey[0] == '\x03':
yneg = ord(pubkey[0]) & 1
pubkey = "\x04" + pubkey[1:] + get_pos_y_for_x(pubkey[1:], yneg=yneg)
elif pubkey[0] == '\x04':
pass
else:
raise Exception("Unrecognised pubkey format: %s" % (pubkey,))
return pubkey
class Onion(object):
HMAC_LEN = 32
PKEY_LEN = 32
MSG_LEN = 128
ZEROES = b"\x00" * (HMAC_LEN + PKEY_LEN + MSG_LEN)
@staticmethod
def tweak_sha(sha, d):
sha = sha.copy()
sha.update(d)
return sha.digest()
@classmethod
def get_ecdh_secrets(cls, sec, pkey_x, pkey_y):
pkey = unhexlify('04') + pkey_x + pkey_y
tmp_key = ecc.ECC(curve='secp256k1', pubkey=pkey)
sec_x, sec_y = ecc_ecdh_key(sec, tmp_key)
b = '\x02' if ord(sec_y[-1]) % 2 == 0 else '\x03'
sec = sha256(sha256(b + sec_x).digest())
enckey = cls.tweak_sha(sec, b'\x00')[:16]
hmac = cls.tweak_sha(sec, b'\x01')
ivs = cls.tweak_sha(sec, b'\x02')
iv, pad_iv = ivs[:16], ivs[16:]
return enckey, hmac, iv, pad_iv
def enc_pad(self, enckey, pad_iv):
aes = Cipher(AES(enckey), CTR(pad_iv),
default_backend()).encryptor()
return aes.update(self.ZEROES)
class OnionDecrypt(Onion):
def __init__(self, onion, my_ecc):
self.my_ecc = my_ecc
hmac_end = len(onion)
pkey_end = hmac_end - self.HMAC_LEN
self.msg_end = pkey_end - self.PKEY_LEN
self.fwd_end = self.msg_end - self.MSG_LEN
self.onion = onion
self.pkey = onion[self.msg_end:pkey_end]
self.hmac = onion[pkey_end:hmac_end]
self.get_secrets()
def decrypt(self):
pad = self.enc_pad(self.enckey, self.pad_iv)
aes = Cipher(AES(self.enckey), CTR(self.iv),
default_backend()).decryptor()
self.fwd = pad + aes.update(self.onion[:self.fwd_end])
self.msg = aes.update(self.onion[self.fwd_end:self.msg_end])
def get_secrets(self):
pkey_x = self.pkey
pkey_y = get_pos_y_for_x(pkey_x) # always positive by design
enckey, hmac, iv, pad_iv = self.get_ecdh_secrets(self.my_ecc, pkey_x, pkey_y)
if not self.check_hmac(hmac):
raise Exception("HMAC did not verify")
self.enckey = enckey
self.iv = iv
self.pad_iv = pad_iv
def check_hmac(self, hmac_key):
calc = hmac_sha256(hmac_key, self.onion[:-self.HMAC_LEN])
return calc == self.hmac
class OnionEncrypt(Onion):
def __init__(self, msgs, pubkeys):
assert len(msgs) == len(pubkeys)
assert 0 < len(msgs) <= 20
assert all( len(m) <= self.MSG_LEN for m in msgs )
msgs = [m + "\0"*(self.MSG_LEN - len(m)) for m in msgs]
pubkeys = [ecc.ECC(pubkey=pk, curve='secp256k1') for pk in pubkeys]
n = len(msgs)
tmpkeys = []
tmppubkeys = []
for i in range(n):
while True:
t = ecc.ECC(curve='secp256k1')
if ord(t.pubkey_y[-1]) % 2 == 0:
break
# or do the math to "flip" the secret key and pub key
tmpkeys.append(t)
tmppubkeys.append(t.pubkey_x)
enckeys, hmacs, ivs, pad_ivs = zip(*[self.get_ecdh_secrets(tmpkey, pkey.pubkey_x, pkey.pubkey_y)
for tmpkey, pkey in zip(tmpkeys, pubkeys)])
# padding takes the form:
# E_(n-1)(0000s)
# D_(n-1)(
# E(n-2)(0000s)
# D(n-2)(
# ...
# )
# )
padding = ""
for i in range(n-1):
pad = self.enc_pad(enckeys[i], pad_ivs[i])
aes = Cipher(AES(enckeys[i]), CTR(ivs[i]),
default_backend()).decryptor()
padding = pad + aes.update(padding)
if n < 20:
padding += str(bytearray(random.getrandbits(8)
for _ in range(len(self.ZEROES) * (20-n))))
# to encrypt the message we need to bump the counter past all
# the padding, then just encrypt the final message
aes = Cipher(AES(enckeys[-1]), CTR(ivs[-1]),
default_backend()).encryptor()
aes.update(padding) # don't care about cyphertext
msgenc = aes.update(msgs[-1])
msgenc = padding + msgenc + tmppubkeys[-1]
del padding
msgenc += hmac_sha256(hmacs[-1], msgenc)
# *PHEW*
# now iterate
for i in reversed(range(n-1)):
# drop the padding this node will add
msgenc = msgenc[len(self.ZEROES):]
# adding the msg
msgenc += msgs[i]
# encrypt it
aes = Cipher(AES(enckeys[i]), CTR(ivs[i]),
default_backend()).encryptor()
msgenc = aes.update(msgenc)
# add the tmp key
msgenc += tmppubkeys[i]
# add the hmac
msgenc += hmac_sha256(hmacs[i], msgenc)
self.onion = msgenc
def generate(args):
server_keys = []
msgs = []
for k in args.pubkeys:
k = unhexlify(k)
msgs.append("Message for %s..." % (hexlify(k[1:21]),))
k = ec_decompress(k)
server_keys.append(k)
o = OnionEncrypt(msgs, server_keys)
sys.stdout.write(o.onion)
return
def decode(args):
msg = sys.stdin.read()
key = ecc.ECC(privkey=unhexlify(args.seckey),
pubkey=ec_decompress(unhexlify(args.pubkey)),
curve='secp256k1')
o = OnionDecrypt(msg, key)
o.decrypt()
#sys.stderr.write("Message: \"%s\"\n" % (o.msg,))
want_msg = "Message for %s..." % (args.pubkey[2:42])
if o.msg != want_msg + "\0"*(Onion.MSG_LEN - len(want_msg)):
raise Exception("Unexpected message: \"%s\" (wanted: %s)" % (o.msg, want_msg))
sys.stdout.write(o.fwd)
def main(argv):
parser = argparse.ArgumentParser(description="Process some integers.")
sp = parser.add_subparsers()
p = sp.add_parser("generate")
p.add_argument("pubkeys", nargs='+', help="public keys of recipients")
p.set_defaults(func=generate)
p = sp.add_parser("decode")
p.add_argument("seckey", help="secret key for router")
p.add_argument("pubkey", help="public key for router")
p.set_defaults(func=decode)
args = parser.parse_args(argv)
return args.func(args)
if __name__ == "__main__":
main(sys.argv[1:])
sys.exit(0)