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
|
from collections import namedtuple
|
||||||
import shelve
|
import shelve
|
||||||
from pyln.proto.onion import OnionPayload
|
from pyln.proto.onion import OnionPayload
|
||||||
|
import zbase32
|
||||||
|
|
||||||
plugin = Plugin()
|
plugin = Plugin()
|
||||||
|
|
||||||
|
TLV_KEYSEND_PREIMAGE = 5482373484
|
||||||
|
TLV_NOISE_MESSAGE = 34349334
|
||||||
|
TLV_NOISE_SIGNATURE = 34349335
|
||||||
|
|
||||||
class Message(object):
|
class Message(object):
|
||||||
def __init__(self, sender, body, signature, payment=None, id=None):
|
def __init__(self, sender, body, signature, payment=None, id=None):
|
||||||
@@ -22,6 +26,7 @@ class Message(object):
|
|||||||
self.body = body
|
self.body = body
|
||||||
self.signature = signature
|
self.signature = signature
|
||||||
self.payment = payment
|
self.payment = payment
|
||||||
|
self.verified = None
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
return {
|
return {
|
||||||
@@ -30,6 +35,7 @@ class Message(object):
|
|||||||
"body": self.body,
|
"body": self.body,
|
||||||
"signature": hexlify(self.signature).decode('ASCII'),
|
"signature": hexlify(self.signature).decode('ASCII'),
|
||||||
"payment": self.payment,
|
"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')
|
@plugin.async_method('sendmsg')
|
||||||
def sendmsg(node_id, msg, plugin, request, amt=1000, **kwargs):
|
def sendmsg(node_id, msg, plugin, request, amt=1000, **kwargs):
|
||||||
payload = TlvPayload()
|
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:
|
# Sign the message:
|
||||||
sig = plugin.rpc.signmessage(msg)['signature']
|
sig = plugin.rpc.signmessage(sigmsg)
|
||||||
sig = unhexlify(sig)
|
|
||||||
payload.add_field(34349336, sig)
|
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)
|
res = deliver(node_id, payload.to_bytes(), amt=amt)
|
||||||
request.set_result(res)
|
request.set_result(res)
|
||||||
@@ -128,15 +138,23 @@ def recvmsg(plugin, request, last_id=None, **kwargs):
|
|||||||
def on_htlc_accepted(onion, htlc, plugin, **kwargs):
|
def on_htlc_accepted(onion, htlc, plugin, **kwargs):
|
||||||
payload = OnionPayload.from_hex(onion['payload'])
|
payload = OnionPayload.from_hex(onion['payload'])
|
||||||
|
|
||||||
# TODO verify the signature to extract the sender
|
|
||||||
|
|
||||||
msg = Message(
|
msg = Message(
|
||||||
id=len(plugin.messages),
|
id=len(plugin.messages),
|
||||||
sender="AAA",
|
sender=None,
|
||||||
body=payload.get(34349334).value,
|
body=payload.get(34349334).value,
|
||||||
signature=payload.get(34349336).value,
|
signature=payload.get(34349335).value,
|
||||||
payment=None)
|
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)
|
plugin.messages.append(msg)
|
||||||
for r in plugin.receive_waiters:
|
for r in plugin.receive_waiters:
|
||||||
r.set_result(msg.to_dict())
|
r.set_result(msg.to_dict())
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
from pyln.testing.fixtures import *
|
from pyln.testing.fixtures import *
|
||||||
from pyln.testing.utils import wait_for
|
from pyln.testing.utils import wait_for
|
||||||
from pprint import pprint
|
from pprint import pprint
|
||||||
|
import zbase32
|
||||||
|
|
||||||
|
|
||||||
plugin = os.path.join(os.path.dirname(__file__), 'noise.py')
|
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 :-)
|
# They should be the same :-)
|
||||||
assert(m1 == m2)
|
assert(m1 == m2)
|
||||||
|
|
||||||
|
assert(m2['sender'] == l1.info['id'])
|
||||||
|
assert(m2['verified'] == True)
|
||||||
|
|
||||||
|
|
||||||
def test_sendmsg_retry(node_factory, executor):
|
def test_sendmsg_retry(node_factory, executor):
|
||||||
opts = [{'plugin': plugin}, {}, {'fee-base': 10000}, {'plugin': plugin}]
|
opts = [{'plugin': plugin}, {}, {'fee-base': 10000}, {'plugin': plugin}]
|
||||||
@@ -52,3 +57,10 @@ def test_sendmsg_retry(node_factory, executor):
|
|||||||
print(recv.result(10))
|
print(recv.result(10))
|
||||||
|
|
||||||
msg = l4.rpc.recvmsg(last_id=-1)
|
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