mirror of
https://github.com/aljazceru/plugins.git
synced 2025-12-22 15:44:20 +01:00
noise: Implement signature verification of chat messages
Since we don't pass the public key we just rely on the pubkey recovery and the `checkmessage` interface to tell us whether it is a publicly known `node_id` or not.
This commit is contained in:
21
noise/README.org
Normal file
21
noise/README.org
Normal file
@@ -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.
|
||||
@@ -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())
|
||||
|
||||
@@ -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)
|
||||
|
||||
51
noise/zbase32.py
Normal file
51
noise/zbase32.py
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user