diff --git a/noise/README.org b/noise/README.org new file mode 100644 index 0000000..be05e1b --- /dev/null +++ b/noise/README.org @@ -0,0 +1,21 @@ +* Protocol +The protocol was heavily inspired by the [[https://github.com/joostjager/whatsat#protocol][WhatSat protocol]]: + +| record type | length (bytes) | value | +|-------------+----------------+-----------------------------------------------------------------| +| 5482373484 | 32 | key send preimage | +| 34349334 | variable | chat message | +| 34349335 | 65 | compressed signature + recovery id | +| 34349339 | 33 | sender pubkey | +| 34349343 | 8 | timestamp in nano seconds since unix epoch (big endian encoded) | + +The key differences are that we don't explicitly pass the sender pubkey, since +we can recover that from the signature itself, and we use the compressed 64 +byte signature, instead of the DER encoded signature. This saves us 33 bytes +for the pubkey and ~7 bytes for the signature, but requires that we change the +TLV type for the signature (from ~34349337~ to ~34349335~). + +The signature is computed by serializing all other TLV fields, hex-encoding +the resulting TLV payload, and signing it using ~lightning-cli signmessage~ +returning the ~zbase32~ encoded signature. The signature consists of a 1 byte +recovery ID and the 64 byte raw signature. diff --git a/noise/noise.py b/noise/noise.py index efd59e5..866d897 100755 --- a/noise/noise.py +++ b/noise/noise.py @@ -11,9 +11,13 @@ import logging from collections import namedtuple import shelve from pyln.proto.onion import OnionPayload +import zbase32 plugin = Plugin() +TLV_KEYSEND_PREIMAGE = 5482373484 +TLV_NOISE_MESSAGE = 34349334 +TLV_NOISE_SIGNATURE = 34349335 class Message(object): def __init__(self, sender, body, signature, payment=None, id=None): @@ -22,6 +26,7 @@ class Message(object): self.body = body self.signature = signature self.payment = payment + self.verified = None def to_dict(self): return { @@ -30,6 +35,7 @@ class Message(object): "body": self.body, "signature": hexlify(self.signature).decode('ASCII'), "payment": self.payment, + "verified": self.verified, } @@ -104,12 +110,16 @@ def deliver(node_id, payload, amt, max_attempts=5, payment_hash=None): @plugin.async_method('sendmsg') def sendmsg(node_id, msg, plugin, request, amt=1000, **kwargs): payload = TlvPayload() - payload.add_field(34349334, msg.encode('UTF-8')) + payload.add_field(TLV_NOISE_MESSAGE, msg.encode('UTF-8')) + + sigmsg = hexlify(payload.to_bytes()).decode('ASCII') # Sign the message: - sig = plugin.rpc.signmessage(msg)['signature'] - sig = unhexlify(sig) - payload.add_field(34349336, sig) + sig = plugin.rpc.signmessage(sigmsg) + + sigcheck = plugin.rpc.checkmessage(sigmsg, sig['zbase']) + sig = zbase32.decode(sig['zbase']) + payload.add_field(TLV_NOISE_SIGNATURE, sig) res = deliver(node_id, payload.to_bytes(), amt=amt) request.set_result(res) @@ -128,15 +138,23 @@ def recvmsg(plugin, request, last_id=None, **kwargs): def on_htlc_accepted(onion, htlc, plugin, **kwargs): payload = OnionPayload.from_hex(onion['payload']) - # TODO verify the signature to extract the sender - msg = Message( id=len(plugin.messages), - sender="AAA", + sender=None, body=payload.get(34349334).value, - signature=payload.get(34349336).value, + signature=payload.get(34349335).value, payment=None) + # Filter out the signature so we can check it against the rest of the payload + sigpayload = TlvPayload() + sigpayload.fields = filter(lambda x: x.typenum != TLV_NOISE_SIGNATURE, payload.fields) + sigmsg = hexlify(sigpayload.to_bytes()).decode('ASCII') + + zsig = zbase32.encode(msg.signature).decode('ASCII') + sigcheck = plugin.rpc.checkmessage(sigmsg, zsig) + msg.sender = sigcheck['pubkey'] + msg.verified = sigcheck['verified'] + plugin.messages.append(msg) for r in plugin.receive_waiters: r.set_result(msg.to_dict()) diff --git a/noise/test_chat.py b/noise/test_chat.py index ec8a8db..2ba3172 100644 --- a/noise/test_chat.py +++ b/noise/test_chat.py @@ -1,6 +1,8 @@ from pyln.testing.fixtures import * from pyln.testing.utils import wait_for from pprint import pprint +import zbase32 + plugin = os.path.join(os.path.dirname(__file__), 'noise.py') @@ -20,6 +22,9 @@ def test_sendmsg_success(node_factory, executor): # They should be the same :-) assert(m1 == m2) + assert(m2['sender'] == l1.info['id']) + assert(m2['verified'] == True) + def test_sendmsg_retry(node_factory, executor): opts = [{'plugin': plugin}, {}, {'fee-base': 10000}, {'plugin': plugin}] @@ -52,3 +57,10 @@ def test_sendmsg_retry(node_factory, executor): print(recv.result(10)) msg = l4.rpc.recvmsg(last_id=-1) + + +def test_zbase32(): + zb32 = b'd75qtmgijm79rpooshmgzjwji9gj7dsdat8remuskyjp9oq1ugkaoj6orbxzhuo4njtyh96e3aq84p1tiuz77nchgxa1s4ka4carnbiy' + b = zbase32.decode(zb32) + enc = zbase32.encode(b) + assert(enc == zb32) diff --git a/noise/zbase32.py b/noise/zbase32.py new file mode 100644 index 0000000..68c6bf9 --- /dev/null +++ b/noise/zbase32.py @@ -0,0 +1,51 @@ +import bitstring + +zbase32_chars = b'ybndrfg8ejkmcpqxot1uwisza345h769' +zbase32_revchars = [ + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 18, 255, 25, 26, 27, 30, 29, 7, 31, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 24, 1, 12, 3, 8, 5, 6, 28, 21, 9, 10, 255, 11, 2, + 16, 13, 14, 4, 22, 17, 19, 255, 20, 15, 0, 23, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255 +] + +def bitarray_to_u5(barr): + assert len(barr) % 5 == 0 + ret = [] + s = bitstring.ConstBitStream(barr) + while s.pos != s.len: + ret.append(s.read(5).uint) + return ret + +def u5_to_bitarray(arr): + ret = bitstring.BitArray() + for a in arr: + ret += bitstring.pack("uint:5", a) + return ret + +def encode(b): + uint5s = bitarray_to_u5(b) + res = [zbase32_chars[c] for c in uint5s] + return bytes(res) + +def decode(b): + if isinstance(b, str): + b = b.encode('ASCII') + + uint5s = [] + for c in b: + uint5s.append(zbase32_revchars[c]) + dec = u5_to_bitarray(uint5s) + return dec.bytes