From 0dfac6fce744d19060988d9e50c9e86d230b447f Mon Sep 17 00:00:00 2001 From: Tanguy Date: Wed, 24 Nov 2021 21:03:40 +0100 Subject: [PATCH] Signed envelopes and routing records (#656) --- libp2p/multiaddress.nim | 2 +- libp2p/multicodec.nim | 1 + libp2p/routing_record.nim | 125 ++++++++++++++++++++++++++++++++++ libp2p/signed_envelope.nim | 118 ++++++++++++++++++++++++++++++++ tests/testnative.nim | 4 +- tests/testrouting_record.nim | 38 +++++++++++ tests/testsigned_envelope.nim | 30 ++++++++ 7 files changed, 316 insertions(+), 2 deletions(-) create mode 100644 libp2p/routing_record.nim create mode 100644 libp2p/signed_envelope.nim create mode 100644 tests/testrouting_record.nim create mode 100644 tests/testsigned_envelope.nim diff --git a/libp2p/multiaddress.nim b/libp2p/multiaddress.nim index 096143a..df410b6 100644 --- a/libp2p/multiaddress.nim +++ b/libp2p/multiaddress.nim @@ -1005,7 +1005,7 @@ proc `$`*(pat: MaPattern): string = proc write*(pb: var ProtoBuffer, field: int, value: MultiAddress) {.inline.} = write(pb, field, value.data.buffer) -proc getField*(pb: var ProtoBuffer, field: int, +proc getField*(pb: ProtoBuffer, field: int, value: var MultiAddress): ProtoResult[bool] {. inline.} = var buffer: seq[byte] diff --git a/libp2p/multicodec.nim b/libp2p/multicodec.nim index 84cb3b8..e3fbe9d 100644 --- a/libp2p/multicodec.nim +++ b/libp2p/multicodec.nim @@ -201,6 +201,7 @@ const MultiCodecList = [ ("p2p-webrtc-direct", 0x0114), # not in multicodec list ("onion", 0x01BC), ("p2p-circuit", 0x0122), + ("libp2p-peer-record", 0x0301), ("dns", 0x35), ("dns4", 0x36), ("dns6", 0x37), diff --git a/libp2p/routing_record.nim b/libp2p/routing_record.nim new file mode 100644 index 0000000..da60a47 --- /dev/null +++ b/libp2p/routing_record.nim @@ -0,0 +1,125 @@ +## Nim-Libp2p +## Copyright (c) 2021 Status Research & Development GmbH +## Licensed under either of +## * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) +## * MIT license ([LICENSE-MIT](LICENSE-MIT)) +## at your option. +## This file may not be copied, modified, or distributed except according to +## those terms. + +## This module implements Routing Records. + +{.push raises: [Defect].} + +import std/[sequtils, times] +import pkg/stew/[results, byteutils] +import + multiaddress, + multicodec, + peerid, + protobuf/minprotobuf, + signed_envelope + +export peerid, multiaddress, signed_envelope + +## Constants relating to signed peer records +const + EnvelopeDomain = multiCodec("libp2p-peer-record") # envelope domain as per RFC0002 + EnvelopePayloadType= @[(byte) 0x03, (byte) 0x01] # payload_type for routing records as spec'ed in RFC0003 + +type + AddressInfo* = object + address*: MultiAddress + + PeerRecord* = object + peerId*: PeerId + seqNo*: uint64 + addresses*: seq[AddressInfo] + +proc decode*( + T: typedesc[PeerRecord], + buffer: seq[byte]): Result[PeerRecord, ProtoError] = + + let pb = initProtoBuffer(buffer) + var record = PeerRecord() + + ? pb.getRequiredField(1, record.peerId) + ? pb.getRequiredField(2, record.seqNo) + + var addressInfos: seq[seq[byte]] + let pb3 = ? pb.getRepeatedField(3, addressInfos) + + if pb3: + for address in addressInfos: + var addressInfo = AddressInfo() + let subProto = initProtoBuffer(address) + if ? subProto.getField(1, addressInfo.address) == false: + return err(ProtoError.RequiredFieldMissing) + + record.addresses &= addressInfo + + ok(record) + +proc encode*(record: PeerRecord): seq[byte] = + var pb = initProtoBuffer() + + pb.write(1, record.peerId) + pb.write(2, record.seqNo) + + for address in record.addresses: + var addrPb = initProtoBuffer() + addrPb.write(1, address.address) + pb.write(3, addrPb) + + pb.finish() + pb.buffer + +proc init*(T: typedesc[PeerRecord], + peerId: PeerId, + seqNo: uint64, + addresses: seq[MultiAddress]): T = + + PeerRecord( + peerId: peerId, + seqNo: seqNo, + addresses: addresses.mapIt(AddressInfo(address: it)) + ) + + +## Functions related to signed peer records + +proc init*(T: typedesc[Envelope], + privateKey: PrivateKey, + peerRecord: PeerRecord): Result[Envelope, CryptoError] = + + ## Init a signed envelope wrapping a peer record + + let envelope = ? Envelope.init(privateKey, + EnvelopePayloadType, + peerRecord.encode(), + $EnvelopeDomain) + + ok(envelope) + +proc init*(T: typedesc[Envelope], + peerId: PeerId, + addresses: seq[MultiAddress], + privateKey: PrivateKey): Result[Envelope, CryptoError] = + ## Creates a signed peer record for this peer: + ## a peer routing record according to https://github.com/libp2p/specs/blob/500a7906dd7dd8f64e0af38de010ef7551fd61b6/RFC/0003-routing-records.md + ## in a signed envelope according to https://github.com/libp2p/specs/blob/500a7906dd7dd8f64e0af38de010ef7551fd61b6/RFC/0002-signed-envelopes.md + + # First create a peer record from the peer info + let peerRecord = PeerRecord.init(peerId, + getTime().toUnix().uint64, # This currently follows the recommended implementation, using unix epoch as seq no. + addresses) + + let envelope = ? Envelope.init(privateKey, + peerRecord) + + ok(envelope) + +proc getSignedPeerRecord*(pb: ProtoBuffer, field: int, + value: var Envelope): ProtoResult[bool] {. + inline.} = + getField(pb, field, value, $EnvelopeDomain) diff --git a/libp2p/signed_envelope.nim b/libp2p/signed_envelope.nim new file mode 100644 index 0000000..6210ca4 --- /dev/null +++ b/libp2p/signed_envelope.nim @@ -0,0 +1,118 @@ +## Nim-Libp2p +## Copyright (c) 2021 Status Research & Development GmbH +## Licensed under either of +## * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) +## * MIT license ([LICENSE-MIT](LICENSE-MIT)) +## at your option. +## This file may not be copied, modified, or distributed except according to +## those terms. + +## This module implements Signed Envelope. + +{.push raises: [Defect].} + +import pkg/stew/[results, byteutils] +import multicodec, + crypto/crypto, + protobuf/minprotobuf, + vbuffer + +export crypto + +type + EnvelopeError* = enum + EnvelopeInvalidProtobuf, + EnvelopeFieldMissing, + EnvelopeInvalidSignature + + Envelope* = object + publicKey*: PublicKey + domain*: string + payloadType*: seq[byte] + payload: seq[byte] + signature*: Signature + +proc mapProtobufError(e: ProtoError): EnvelopeError = + case e: + of RequiredFieldMissing: + EnvelopeFieldMissing + else: + EnvelopeInvalidProtobuf + +proc getSignatureBuffer(e: Envelope): seq[byte] = + var buffer = initVBuffer() + + let domainBytes = e.domain.toBytes() + buffer.writeSeq(domainBytes) + buffer.writeSeq(e.payloadType) + buffer.writeSeq(e.payload) + + buffer.buffer + +proc decode*(T: typedesc[Envelope], + buf: seq[byte], + domain: string): Result[Envelope, EnvelopeError] = + + let pb = initProtoBuffer(buf) + var envelope = Envelope() + + envelope.domain = domain + ? pb.getRequiredField(1, envelope.publicKey).mapErr(mapProtobufError) + discard ? pb.getField(2, envelope.payloadType).mapErr(mapProtobufError) + ? pb.getRequiredField(3, envelope.payload).mapErr(mapProtobufError) + ? pb.getRequiredField(5, envelope.signature).mapErr(mapProtobufError) + + if envelope.signature.verify(envelope.getSignatureBuffer(), envelope.publicKey) == false: + err(EnvelopeInvalidSignature) + else: + ok(envelope) + +proc init*(T: typedesc[Envelope], + privateKey: PrivateKey, + payloadType: seq[byte], + payload: seq[byte], + domain: string): Result[Envelope, CryptoError] = + var envelope = Envelope( + publicKey: ? privateKey.getPublicKey(), + domain: domain, + payloadType: payloadType, + payload: payload, + ) + + envelope.signature = ? privateKey.sign(envelope.getSignatureBuffer()) + + ok(envelope) + +proc encode*(env: Envelope): Result[seq[byte], CryptoError] = + var pb = initProtoBuffer() + + try: + pb.write(1, env.publicKey) + pb.write(2, env.payloadType) + pb.write(3, env.payload) + pb.write(5, env.signature) + except ResultError[CryptoError] as exc: + return err(exc.error) + + pb.finish() + ok(pb.buffer) + +proc payload*(env: Envelope): seq[byte] = + # Payload is readonly + env.payload + +proc getField*(pb: ProtoBuffer, field: int, + value: var Envelope, + domain: string): ProtoResult[bool] {. + inline.} = + var buffer: seq[byte] + let res = ? pb.getField(field, buffer) + if not(res): + ok(false) + else: + let env = Envelope.decode(buffer, domain) + if env.isOk(): + value = env.get() + ok(true) + else: + err(ProtoError.IncorrectBlob) diff --git a/tests/testnative.nim b/tests/testnative.nim index 8bfbe88..519e898 100644 --- a/tests/testnative.nim +++ b/tests/testnative.nim @@ -15,7 +15,9 @@ import testmultibase, testmultihash, testmultiaddress, testcid, - testpeerid + testpeerid, + testsigned_envelope, + testrouting_record import testtcptransport, testnameresolve, diff --git a/tests/testrouting_record.nim b/tests/testrouting_record.nim new file mode 100644 index 0000000..d10500b --- /dev/null +++ b/tests/testrouting_record.nim @@ -0,0 +1,38 @@ +import unittest2 +import stew/byteutils +import ../libp2p/[routing_record, crypto/crypto] + +suite "Routing record": + test "Encode -> decode test": + let + rng = newRng() + privKey = PrivateKey.random(rng[]).tryGet() + peerId = PeerId.init(privKey).tryGet() + multiAddresses = @[MultiAddress.init("/ip4/0.0.0.0/tcp/24").tryGet(), MultiAddress.init("/ip4/0.0.0.0/tcp/25").tryGet()] + routingRecord = PeerRecord.init(peerId, 42, multiAddresses) + + buffer = routingRecord.encode() + + parsedRR = PeerRecord.decode(buffer).tryGet() + + check: + parsedRR.peerId == peerId + parsedRR.seqNo == 42 + parsedRR.addresses.len == 2 + parsedRR.addresses[0].address == multiAddresses[0] + parsedRR.addresses[1].address == multiAddresses[1] + + test "Interop decode": + let + # from https://github.com/libp2p/go-libp2p-core/blob/b18a4c9c5629870bde2cd85ab3b87a507600d411/peer/record_test.go#L33 + # (with only 2 addresses) + inputData = "0a2600240801122011bba3ed1721948cefb4e50b0a0bb5cad8a6b52dc7b1a40f4f6652105c91e2c4109bf59d8dd99d8ddb161a0a0a0804010203040600001a0a0a080401020304060001".hexToSeqByte() + decodedRecord = PeerRecord.decode(inputData).tryGet() + + check: + $decodedRecord.peerId == "12D3KooWB1b3qZxWJanuhtseF3DmPggHCtG36KZ9ixkqHtdKH9fh" + decodedRecord.seqNo == uint64 1636553709551319707 + decodedRecord.addresses.len == 2 + $decodedRecord.addresses[0].address == "/ip4/1.2.3.4/tcp/0" + $decodedRecord.addresses[1].address == "/ip4/1.2.3.4/tcp/1" + diff --git a/tests/testsigned_envelope.nim b/tests/testsigned_envelope.nim new file mode 100644 index 0000000..3c58822 --- /dev/null +++ b/tests/testsigned_envelope.nim @@ -0,0 +1,30 @@ +import unittest2 +import stew/byteutils +import ../libp2p/[signed_envelope] + +suite "Signed envelope": + test "Encode -> decode test": + let + rng = newRng() + privKey = PrivateKey.random(rng[]).tryGet() + envelope = Envelope.init(privKey, @[byte 12, 0], "payload".toBytes(), "domain").tryGet() + buffer = envelope.encode().tryGet() + decodedEnvelope = Envelope.decode(buffer, "domain").tryGet() + wrongDomain = Envelope.decode(buffer, "wdomain") + + check: + decodedEnvelope == envelope + wrongDomain.error == EnvelopeInvalidSignature + + test "Interop decode test": + # from https://github.com/libp2p/go-libp2p-core/blob/b18a4c9c5629870bde2cd85ab3b87a507600d411/record/envelope_test.go#L68 + let inputData = "0a24080112206f1581709bb7b1ef030d210db18e3b0ba1c776fba65d8cdaad05415142d189f812102f6c69627032702f74657374646174611a0c68656c6c6f20776f726c64212a401178673b51dfa842aad17e465e25d646ad16628916b964c3fb10c711fee87872bdd4e4646f58c277cdff09704913d8be1aec6322de8d3d0bb852120374aece08".hexToSeqByte() + let decodedEnvelope = Envelope.decode(inputData, "libp2p-testing").tryGet() + check: + decodedEnvelope.payloadType == "/libp2p/testdata".toBytes() + decodedEnvelope.payload == "hello world!".toBytes() + + test "Signature validation": + # same as above, but payload altered + let inputData = "0a24080112206f1581709bb7b1ef030d210db18e3b0ba1c776fba65d8cdaad05415142d189f812102f6c69627032702f74657374646174611a0c00006c6c6f20776f726c64212a401178673b51dfa842aad17e465e25d646ad16628916b964c3fb10c711fee87872bdd4e4646f58c277cdff09704913d8be1aec6322de8d3d0bb852120374aece08".hexToSeqByte() + check Envelope.decode(inputData, "libp2p-testing").error == EnvelopeInvalidSignature