Skip to content

Commit

Permalink
fix: signature compliant with spec
Browse files Browse the repository at this point in the history
  • Loading branch information
vasco-santos committed Jun 26, 2020
1 parent afa91d3 commit 44eb3be
Show file tree
Hide file tree
Showing 8 changed files with 79 additions and 57 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
"sanitize-filename": "^1.6.3",
"streaming-iterables": "^4.1.0",
"timeout-abort-controller": "^1.0.0",
"varint": "^5.0.0",
"xsalsa20": "^1.0.2"
},
"devDependencies": {
Expand Down
3 changes: 2 additions & 1 deletion src/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,6 @@ exports.codes = {
ERR_TRANSPORT_UNAVAILABLE: 'ERR_TRANSPORT_UNAVAILABLE',
ERR_TRANSPORT_DIAL_FAILED: 'ERR_TRANSPORT_DIAL_FAILED',
ERR_UNSUPPORTED_PROTOCOL: 'ERR_UNSUPPORTED_PROTOCOL',
ERR_INVALID_MULTIADDR: 'ERR_INVALID_MULTIADDR'
ERR_INVALID_MULTIADDR: 'ERR_INVALID_MULTIADDR',
ERR_SIGNATURE_NOT_VALID: 'ERR_SIGNATURE_NOT_VALID'
}
18 changes: 2 additions & 16 deletions src/record/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ All libp2p nodes keep a `PeerStore`, that among other information stores a set o

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.

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

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

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

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.

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

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.

Expand All @@ -112,17 +112,3 @@ When a subsystem wants to provide a record, it should get it from the AddressBoo
- Modular dialer? (taken from go PR notes)
- 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.
- Multiple parallel dials. We already have the issue where new addresses aren't added to existing dials.

### Notes:

- Possible design for AddressBook

```
addr_book_record
\_ peer_id: bytes
\_ signed_addrs: []AddrEntry
\_ unsigned_addrs: []AddrEntry
\_ certified_record
\_ seq: bytes
\_ raw: bytes
```
59 changes: 38 additions & 21 deletions src/record/envelope/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,18 @@ const log = debug('libp2p:envelope')
log.error = debug('libp2p:envelope:error')
const errCode = require('err-code')

const { Buffer } = require('buffer')

const crypto = require('libp2p-crypto')
const multicodec = require('multicodec')
const PeerId = require('peer-id')
const varint = require('varint')

const { codes } = require('../../errors')
const Protobuf = require('./envelope.proto')

/**
* The Envelope is responsible for keeping arbitrary signed by a libp2p peer.
* The Envelope is responsible for keeping an arbitrary signed record
* by a libp2p peer.
*/
class Envelope {
/**
Expand Down Expand Up @@ -41,7 +45,7 @@ class Envelope {
if (this._marshal) {
return this._marshal
}
// TODO: type for marshal (default: RSA)

const publicKey = crypto.keys.marshalPublicKey(this.peerId.pubKey)

this._marshal = Protobuf.encode({
Expand Down Expand Up @@ -69,34 +73,43 @@ class Envelope {
/**
* Validate envelope data signature for the given domain.
* @param {string} domain
* @return {Promise}
* @return {Promise<boolean>}
*/
async validate (domain) {
validate (domain) {
const signData = createSignData(domain, this.payloadType, this.payload)

try {
await this.peerId.pubKey.verify(signData, this.signature)
} catch (_) {
log.error('record signature verification failed')
// TODO
throw errCode(new Error('record signature verification failed'), 'ERRORS.ERR_SIGNATURE_VERIFICATION')
}
return this.peerId.pubKey.verify(signData, this.signature)
}
}

/**
* Helper function that prepares a buffer to sign or verify a signature.
* @param {string} domain
* @param {number} payloadType
* @param {Buffer} payloadType
* @param {Buffer} payload
* @return {Buffer}
*/
const createSignData = (domain, payloadType, payload) => {
// TODO: this should be compliant with the spec!
const domainBuffer = Buffer.from(domain)
const payloadTypeBuffer = Buffer.from(payloadType.toString())

return Buffer.concat([domainBuffer, payloadTypeBuffer, payload])
// When signing, a peer will prepare a buffer by concatenating the following:
// - The length of the domain separation string string in bytes
// - The domain separation string, encoded as UTF-8
// - The length of the payload_type field in bytes
// - The value of the payload_type field
// - The length of the payload field in bytes
// - The value of the payload field

const domainLength = varint.encode(Buffer.byteLength(domain))
const payloadTypeLength = varint.encode(payloadType.length)
const payloadLength = varint.encode(payload.length)

return Buffer.concat([
Buffer.from(domainLength),
Buffer.from(domain),
Buffer.from(payloadTypeLength),
payloadType,
Buffer.from(payloadLength),
payload
])
}

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

/**
* Seal marshals the given Record, places the marshaled bytes inside an Envelope
* and signs with the given private key.
* and signs it with the given peerId's private key.
* @async
* @param {Record} record
* @param {PeerId} peerId
* @return {Envelope}
*/
Envelope.seal = async (record, peerId) => {
const domain = record.domain
const payloadType = Buffer.from(`${multicodec.print[record.codec]}${domain}`)
const payloadType = Buffer.from(record.codec)
const payload = record.marshal()

const signData = createSignData(domain, payloadType, payload)
Expand All @@ -149,7 +162,11 @@ Envelope.seal = async (record, peerId) => {
*/
Envelope.openAndCertify = async (data, domain) => {
const envelope = await unmarshalEnvelope(data)
await envelope.validate(domain)
const valid = await envelope.validate(domain)

if (!valid) {
throw errCode(new Error('envelope signature is not valid for the given domain'), codes.ERR_SIGNATURE_NOT_VALID)
}

return envelope
}
Expand Down
9 changes: 1 addition & 8 deletions src/record/peer-record/consts.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
'use strict'

// const { Buffer } = require('buffer')
const multicodec = require('multicodec')

// The domain string used for peer records contained in a Envelope.
Expand All @@ -9,10 +8,4 @@ module.exports.ENVELOPE_DOMAIN_PEER_RECORD = 'libp2p-peer-record'
// The type hint used to identify peer records in a Envelope.
// Defined in https://github.com/multiformats/multicodec/blob/master/table.csv
// with name "libp2p-peer-record"
// TODO
// const b = Buffer.aloc(2)
// b.writeInt16BE(multicodec.LIBP2P_PEER_RECORD)
// module.exports.ENVELOPE_PAYLOAD_TYPE_PEER_RECORD = b

// const ENVELOPE_PAYLOAD_TYPE_PEER_RECORD = Buffer.aloc(2)
module.exports.ENVELOPE_PAYLOAD_TYPE_PEER_RECORD = multicodec.LIBP2P_PEER_RECORD
module.exports.ENVELOPE_PAYLOAD_TYPE_PEER_RECORD = multicodec.print[multicodec.LIBP2P_PEER_RECORD]
1 change: 0 additions & 1 deletion src/record/peer-record/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ class PeerRecord extends Record {
* @param {number} [params.seqNumber] monotonically-increasing sequence counter that's used to order PeerRecords in time.
*/
constructor ({ peerId, multiaddrs = [], seqNumber = Date.now() }) {
// TODO: verify domain/payload type
super(ENVELOPE_DOMAIN_PEER_RECORD, ENVELOPE_PAYLOAD_TYPE_PEER_RECORD)

this.peerId = peerId
Expand Down
17 changes: 9 additions & 8 deletions test/record/envelope.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,18 @@ chai.use(require('dirty-chai'))
chai.use(require('chai-bytes'))
const { expect } = chai

const multicodec = require('multicodec')

const Envelope = require('../../src/record/envelope')
const Record = require('libp2p-interfaces/src/record')
const { codes: ErrorCodes } = require('../../src/errors')

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

const domain = '/test-domain'
const domain = 'libp2p-testing'
const codec = '/libp2p/testdata'

class TestRecord extends Record {
constructor (data) {
super(domain, multicodec.LIBP2P_PEER_RECORD)
super(domain, codec)
this.data = data
}

Expand All @@ -31,7 +31,7 @@ class TestRecord extends Record {
}

describe('Envelope', () => {
const payloadType = Buffer.from(`${multicodec.print[multicodec.LIBP2P_PEER_RECORD]}${domain}`)
const payloadType = Buffer.from(codec)
let peerId
let testRecord

Expand Down Expand Up @@ -78,11 +78,12 @@ describe('Envelope', () => {
expect(isEqual).to.eql(true)
})

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

await expect(Envelope.openAndCertify(rawEnvelope, '/fake-domain'))
.to.eventually.rejected()
await expect(Envelope.openAndCertify(rawEnvelope, '/bad-domain'))
.to.eventually.be.rejected()
.and.to.have.property('code', ErrorCodes.ERR_SIGNATURE_NOT_VALID)
})
})
28 changes: 26 additions & 2 deletions test/record/peer-record.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ const chai = require('chai')
chai.use(require('dirty-chai'))
const { expect } = chai

const tests = require('libp2p-interfaces/src/record/tests')
const multiaddr = require('multiaddr')

const tests = require('libp2p-interfaces/src/record/tests')
const Envelope = require('../../src/record/envelope')
const PeerRecord = require('../../src/record/peer-record')

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

describe('PeerRecord inside Envelope', () => {
// TODO
let peerId
let peerRecord

before(async () => {
[peerId] = await peerUtils.createPeerId()
const multiaddrs = [
multiaddr('/ip4/127.0.0.1/tcp/2000')
]
const seqNumber = Date.now()
peerRecord = new PeerRecord({ peerId, multiaddrs, seqNumber })
})

it('creates an envelope with the PeerRecord and can unmarshal it', async () => {
const e = await Envelope.seal(peerRecord, peerId)
const byteE = e.marshal()

const decodedE = await Envelope.openAndCertify(byteE, peerRecord.domain)
expect(decodedE).to.exist()

const decodedPeerRecord = PeerRecord.createFromProtobuf(decodedE.payload)

const isEqual = peerRecord.isEqual(decodedPeerRecord)
expect(isEqual).to.eql(true)
})
})

0 comments on commit 44eb3be

Please sign in to comment.