Signed envelopes and routing records (#656)

This commit is contained in:
Tanguy 2021-11-24 21:03:40 +01:00 committed by GitHub
parent 73168b6eae
commit 0dfac6fce7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 316 additions and 2 deletions

View File

@ -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]

View File

@ -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),

125
libp2p/routing_record.nim Normal file
View File

@ -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)

118
libp2p/signed_envelope.nim Normal file
View File

@ -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)

View File

@ -15,7 +15,9 @@ import testmultibase,
testmultihash,
testmultiaddress,
testcid,
testpeerid
testpeerid,
testsigned_envelope,
testrouting_record
import testtcptransport,
testnameresolve,

View File

@ -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"

View File

@ -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