Skip to content

Commit 4ab125e

Browse files
vasco-santosjacobheun
authored andcommitted
fix: signature compliant with spec
1 parent 71daac2 commit 4ab125e

File tree

8 files changed

+79
-57
lines changed

8 files changed

+79
-57
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
"sanitize-filename": "^1.6.3",
7979
"streaming-iterables": "^4.1.0",
8080
"timeout-abort-controller": "^1.0.0",
81+
"varint": "^5.0.0",
8182
"xsalsa20": "^1.0.2"
8283
},
8384
"devDependencies": {

src/errors.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,6 @@ exports.codes = {
2929
ERR_TRANSPORT_UNAVAILABLE: 'ERR_TRANSPORT_UNAVAILABLE',
3030
ERR_TRANSPORT_DIAL_FAILED: 'ERR_TRANSPORT_DIAL_FAILED',
3131
ERR_UNSUPPORTED_PROTOCOL: 'ERR_UNSUPPORTED_PROTOCOL',
32-
ERR_INVALID_MULTIADDR: 'ERR_INVALID_MULTIADDR'
32+
ERR_INVALID_MULTIADDR: 'ERR_INVALID_MULTIADDR',
33+
ERR_SIGNATURE_NOT_VALID: 'ERR_SIGNATURE_NOT_VALID'
3334
}

src/record/README.md

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ All libp2p nodes keep a `PeerStore`, that among other information stores a set o
4747

4848
Libp2p peer records were created to enable the distribution of verifiable address records, which we can prove originated from the addressed peer itself. With such guarantees, libp2p can prioritize addresses based on their authenticity, with the most strict strategy being to only dial certified addresses.
4949

50-
A peer record contains the peers' publicly reachable listen addresses, and may be extended in the future to contain additional metadata relevant to routing. It also contains a `seq` field, so that we can order peer records by time and identify if a received record is more recent than the stored one.
50+
A peer record contains the peers' publicly reachable listen addresses, and may be extended in the future to contain additional metadata relevant to routing. It also contains a `seqNumber` field, so that we can order peer records by time and identify if a received record is more recent than the stored one.
5151

5252
You can read further about the Peer Record in [libp2p/specs#217](https://github.com/libp2p/specs/pull/217).
5353

@@ -92,7 +92,7 @@ When a libp2p node changes its listen addresses, the identify service should be
9292

9393
Considering that a node can discover other peers' addresses from a variety of sources, Libp2p Peerstore should be able to differentiate the addresses that were obtained through a signed peer record.
9494

95-
Once a record is received and its signature properly validated, its envelope should be stored in the AddressBook on its byte representations. However, the `seq` number of the record must be compared with potentially stored records, so that we do not override correct data.
95+
Once a record is received and its signature properly validated, its envelope should be stored in the AddressBook on its byte representations. However, the `seqNumber` number of the record must be compared with potentially stored records, so that we do not override correct data.
9696

9797
The AddressBook Addresses must be updated with the content of the envelope with a certified property that allows other subsystems to identify that the known certified addresses of a peer.
9898

@@ -112,17 +112,3 @@ When a subsystem wants to provide a record, it should get it from the AddressBoo
112112
- Modular dialer? (taken from go PR notes)
113113
- With the modular dialer, users should easily be able to configure precedence. With dialer v1, anything we do to prioritise dials is gonna be spaghetti and adhoc. With the modular dialer, you’d be able to specify the order of dials when instantiating the pipeline.
114114
- Multiple parallel dials. We already have the issue where new addresses aren't added to existing dials.
115-
116-
### Notes:
117-
118-
- Possible design for AddressBook
119-
120-
```
121-
addr_book_record
122-
\_ peer_id: bytes
123-
\_ signed_addrs: []AddrEntry
124-
\_ unsigned_addrs: []AddrEntry
125-
\_ certified_record
126-
\_ seq: bytes
127-
\_ raw: bytes
128-
```

src/record/envelope/index.js

Lines changed: 38 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,18 @@ const log = debug('libp2p:envelope')
55
log.error = debug('libp2p:envelope:error')
66
const errCode = require('err-code')
77

8+
const { Buffer } = require('buffer')
9+
810
const crypto = require('libp2p-crypto')
9-
const multicodec = require('multicodec')
1011
const PeerId = require('peer-id')
12+
const varint = require('varint')
1113

14+
const { codes } = require('../../errors')
1215
const Protobuf = require('./envelope.proto')
1316

1417
/**
15-
* The Envelope is responsible for keeping arbitrary signed by a libp2p peer.
18+
* The Envelope is responsible for keeping an arbitrary signed record
19+
* by a libp2p peer.
1620
*/
1721
class Envelope {
1822
/**
@@ -41,7 +45,7 @@ class Envelope {
4145
if (this._marshal) {
4246
return this._marshal
4347
}
44-
// TODO: type for marshal (default: RSA)
48+
4549
const publicKey = crypto.keys.marshalPublicKey(this.peerId.pubKey)
4650

4751
this._marshal = Protobuf.encode({
@@ -69,34 +73,43 @@ class Envelope {
6973
/**
7074
* Validate envelope data signature for the given domain.
7175
* @param {string} domain
72-
* @return {Promise}
76+
* @return {Promise<boolean>}
7377
*/
74-
async validate (domain) {
78+
validate (domain) {
7579
const signData = createSignData(domain, this.payloadType, this.payload)
7680

77-
try {
78-
await this.peerId.pubKey.verify(signData, this.signature)
79-
} catch (_) {
80-
log.error('record signature verification failed')
81-
// TODO
82-
throw errCode(new Error('record signature verification failed'), 'ERRORS.ERR_SIGNATURE_VERIFICATION')
83-
}
81+
return this.peerId.pubKey.verify(signData, this.signature)
8482
}
8583
}
8684

8785
/**
8886
* Helper function that prepares a buffer to sign or verify a signature.
8987
* @param {string} domain
90-
* @param {number} payloadType
88+
* @param {Buffer} payloadType
9189
* @param {Buffer} payload
9290
* @return {Buffer}
9391
*/
9492
const createSignData = (domain, payloadType, payload) => {
95-
// TODO: this should be compliant with the spec!
96-
const domainBuffer = Buffer.from(domain)
97-
const payloadTypeBuffer = Buffer.from(payloadType.toString())
98-
99-
return Buffer.concat([domainBuffer, payloadTypeBuffer, payload])
93+
// When signing, a peer will prepare a buffer by concatenating the following:
94+
// - The length of the domain separation string string in bytes
95+
// - The domain separation string, encoded as UTF-8
96+
// - The length of the payload_type field in bytes
97+
// - The value of the payload_type field
98+
// - The length of the payload field in bytes
99+
// - The value of the payload field
100+
101+
const domainLength = varint.encode(Buffer.byteLength(domain))
102+
const payloadTypeLength = varint.encode(payloadType.length)
103+
const payloadLength = varint.encode(payload.length)
104+
105+
return Buffer.concat([
106+
Buffer.from(domainLength),
107+
Buffer.from(domain),
108+
Buffer.from(payloadTypeLength),
109+
payloadType,
110+
Buffer.from(payloadLength),
111+
payload
112+
])
100113
}
101114

102115
/**
@@ -118,15 +131,15 @@ const unmarshalEnvelope = async (data) => {
118131

119132
/**
120133
* Seal marshals the given Record, places the marshaled bytes inside an Envelope
121-
* and signs with the given private key.
134+
* and signs it with the given peerId's private key.
122135
* @async
123136
* @param {Record} record
124137
* @param {PeerId} peerId
125138
* @return {Envelope}
126139
*/
127140
Envelope.seal = async (record, peerId) => {
128141
const domain = record.domain
129-
const payloadType = Buffer.from(`${multicodec.print[record.codec]}${domain}`)
142+
const payloadType = Buffer.from(record.codec)
130143
const payload = record.marshal()
131144

132145
const signData = createSignData(domain, payloadType, payload)
@@ -149,7 +162,11 @@ Envelope.seal = async (record, peerId) => {
149162
*/
150163
Envelope.openAndCertify = async (data, domain) => {
151164
const envelope = await unmarshalEnvelope(data)
152-
await envelope.validate(domain)
165+
const valid = await envelope.validate(domain)
166+
167+
if (!valid) {
168+
throw errCode(new Error('envelope signature is not valid for the given domain'), codes.ERR_SIGNATURE_NOT_VALID)
169+
}
153170

154171
return envelope
155172
}

src/record/peer-record/consts.js

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
'use strict'
22

3-
// const { Buffer } = require('buffer')
43
const multicodec = require('multicodec')
54

65
// The domain string used for peer records contained in a Envelope.
@@ -9,10 +8,4 @@ module.exports.ENVELOPE_DOMAIN_PEER_RECORD = 'libp2p-peer-record'
98
// The type hint used to identify peer records in a Envelope.
109
// Defined in https://github.com/multiformats/multicodec/blob/master/table.csv
1110
// with name "libp2p-peer-record"
12-
// TODO
13-
// const b = Buffer.aloc(2)
14-
// b.writeInt16BE(multicodec.LIBP2P_PEER_RECORD)
15-
// module.exports.ENVELOPE_PAYLOAD_TYPE_PEER_RECORD = b
16-
17-
// const ENVELOPE_PAYLOAD_TYPE_PEER_RECORD = Buffer.aloc(2)
18-
module.exports.ENVELOPE_PAYLOAD_TYPE_PEER_RECORD = multicodec.LIBP2P_PEER_RECORD
11+
module.exports.ENVELOPE_PAYLOAD_TYPE_PEER_RECORD = multicodec.print[multicodec.LIBP2P_PEER_RECORD]

src/record/peer-record/index.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ class PeerRecord extends Record {
2424
* @param {number} [params.seqNumber] monotonically-increasing sequence counter that's used to order PeerRecords in time.
2525
*/
2626
constructor ({ peerId, multiaddrs = [], seqNumber = Date.now() }) {
27-
// TODO: verify domain/payload type
2827
super(ENVELOPE_DOMAIN_PEER_RECORD, ENVELOPE_PAYLOAD_TYPE_PEER_RECORD)
2928

3029
this.peerId = peerId

test/record/envelope.spec.js

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,18 @@ chai.use(require('dirty-chai'))
66
chai.use(require('chai-bytes'))
77
const { expect } = chai
88

9-
const multicodec = require('multicodec')
10-
119
const Envelope = require('../../src/record/envelope')
1210
const Record = require('libp2p-interfaces/src/record')
11+
const { codes: ErrorCodes } = require('../../src/errors')
1312

1413
const peerUtils = require('../utils/creators/peer')
1514

16-
const domain = '/test-domain'
15+
const domain = 'libp2p-testing'
16+
const codec = '/libp2p/testdata'
1717

1818
class TestRecord extends Record {
1919
constructor (data) {
20-
super(domain, multicodec.LIBP2P_PEER_RECORD)
20+
super(domain, codec)
2121
this.data = data
2222
}
2323

@@ -31,7 +31,7 @@ class TestRecord extends Record {
3131
}
3232

3333
describe('Envelope', () => {
34-
const payloadType = Buffer.from(`${multicodec.print[multicodec.LIBP2P_PEER_RECORD]}${domain}`)
34+
const payloadType = Buffer.from(codec)
3535
let peerId
3636
let testRecord
3737

@@ -78,11 +78,12 @@ describe('Envelope', () => {
7878
expect(isEqual).to.eql(true)
7979
})
8080

81-
it.skip('throw on open and verify when a different domain is used', async () => {
81+
it('throw on open and verify when a different domain is used', async () => {
8282
const envelope = await Envelope.seal(testRecord, peerId)
8383
const rawEnvelope = envelope.marshal()
8484

85-
await expect(Envelope.openAndCertify(rawEnvelope, '/fake-domain'))
86-
.to.eventually.rejected()
85+
await expect(Envelope.openAndCertify(rawEnvelope, '/bad-domain'))
86+
.to.eventually.be.rejected()
87+
.and.to.have.property('code', ErrorCodes.ERR_SIGNATURE_NOT_VALID)
8788
})
8889
})

test/record/peer-record.spec.js

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@ const chai = require('chai')
55
chai.use(require('dirty-chai'))
66
const { expect } = chai
77

8+
const tests = require('libp2p-interfaces/src/record/tests')
89
const multiaddr = require('multiaddr')
910

10-
const tests = require('libp2p-interfaces/src/record/tests')
11+
const Envelope = require('../../src/record/envelope')
1112
const PeerRecord = require('../../src/record/peer-record')
1213

1314
const peerUtils = require('../utils/creators/peer')
@@ -113,5 +114,28 @@ describe('PeerRecord', () => {
113114
})
114115

115116
describe('PeerRecord inside Envelope', () => {
116-
// TODO
117+
let peerId
118+
let peerRecord
119+
120+
before(async () => {
121+
[peerId] = await peerUtils.createPeerId()
122+
const multiaddrs = [
123+
multiaddr('/ip4/127.0.0.1/tcp/2000')
124+
]
125+
const seqNumber = Date.now()
126+
peerRecord = new PeerRecord({ peerId, multiaddrs, seqNumber })
127+
})
128+
129+
it('creates an envelope with the PeerRecord and can unmarshal it', async () => {
130+
const e = await Envelope.seal(peerRecord, peerId)
131+
const byteE = e.marshal()
132+
133+
const decodedE = await Envelope.openAndCertify(byteE, peerRecord.domain)
134+
expect(decodedE).to.exist()
135+
136+
const decodedPeerRecord = PeerRecord.createFromProtobuf(decodedE.payload)
137+
138+
const isEqual = peerRecord.isEqual(decodedPeerRecord)
139+
expect(isEqual).to.eql(true)
140+
})
117141
})

0 commit comments

Comments
 (0)