Signed envelopes and routing records (#656)
This commit is contained in:
parent
73168b6eae
commit
0dfac6fce7
|
@ -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]
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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)
|
|
@ -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)
|
|
@ -15,7 +15,9 @@ import testmultibase,
|
|||
testmultihash,
|
||||
testmultiaddress,
|
||||
testcid,
|
||||
testpeerid
|
||||
testpeerid,
|
||||
testsigned_envelope,
|
||||
testrouting_record
|
||||
|
||||
import testtcptransport,
|
||||
testnameresolve,
|
||||
|
|
|
@ -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"
|
||||
|
|
@ -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
|
Loading…
Reference in New Issue