diff --git a/src/utils.ts b/src/utils.ts index 191a7a5ff1..ed3abee0cb 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -3,7 +3,7 @@ import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' import { toString as uint8ArrayToString } from 'uint8arrays/to-string' import { sha256 } from 'multiformats/hashes/sha2' import type { Message, PubSubRPCMessage } from '@libp2p/interface-pubsub' -import { peerIdFromBytes } from '@libp2p/peer-id' +import { peerIdFromBytes, peerIdFromKeys } from '@libp2p/peer-id' import { codes } from './errors.js' import { CodeError } from '@libp2p/interfaces/errors' @@ -66,12 +66,30 @@ export const ensureArray = function (maybeArray: T | T[]) { return maybeArray } -export const toMessage = (message: PubSubRPCMessage): Message => { +const isSigned = async (message: PubSubRPCMessage): Promise => { + if ((message.sequenceNumber == null) || (message.from == null) || (message.signature == null)) { + return false + } + // if a public key is present in the `from` field, the message should be signed + const fromID = peerIdFromBytes(message.from) + if (fromID.publicKey != null) { + return true + } + + if (message.key != null) { + const signingID = await peerIdFromKeys(message.key) + return signingID.equals(fromID) + } + + return false +} + +export const toMessage = async (message: PubSubRPCMessage): Promise => { if (message.from == null) { throw new CodeError('RPC message was missing from', codes.ERR_MISSING_FROM) } - if (message.sequenceNumber == null || message.from == null || message.signature == null || message.key == null) { + if (!await isSigned(message)) { return { type: 'unsigned', topic: message.topic ?? '', @@ -83,9 +101,10 @@ export const toMessage = (message: PubSubRPCMessage): Message => { type: 'signed', from: peerIdFromBytes(message.from), topic: message.topic ?? '', - sequenceNumber: bigIntFromBytes(message.sequenceNumber), + sequenceNumber: bigIntFromBytes(message.sequenceNumber ?? new Uint8Array(0)), data: message.data ?? new Uint8Array(0), - signature: message.signature, + signature: message.signature ?? new Uint8Array(0), + // @ts-expect-error key need not be defined key: message.key } } diff --git a/test/utils.spec.ts b/test/utils.spec.ts index 85b0895abf..93b5bcde22 100644 --- a/test/utils.spec.ts +++ b/test/utils.spec.ts @@ -3,6 +3,7 @@ import * as utils from '../src/utils.js' import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' import type { Message, PubSubRPCMessage } from '@libp2p/interface-pubsub' import { peerIdFromBytes, peerIdFromString } from '@libp2p/peer-id' +import * as PeerIdFactory from '@libp2p/peer-id-factory' describe('utils', () => { it('randomSeqno', () => { @@ -94,4 +95,38 @@ describe('utils', () => { expect(utils.bigIntFromBytes(utils.bigIntToBytes(val))).to.equal(val) }) }) + + it('ensures message is signed if public key is extractable', async () => { + const dummyPeerID = await PeerIdFactory.createRSAPeerId() + + const cases: PubSubRPCMessage[] = [ + { + from: (await PeerIdFactory.createSecp256k1PeerId()).toBytes(), + topic: 'test', + data: new Uint8Array(0), + sequenceNumber: utils.bigIntToBytes(1n), + signature: new Uint8Array(0) + }, + { + from: peerIdFromString('QmPNdSYk5Rfpo5euNqwtyizzmKXMNHdXeLjTQhcN4yfX22').toBytes(), + topic: 'test', + data: new Uint8Array(0), + sequenceNumber: utils.bigIntToBytes(1n), + signature: new Uint8Array(0) + }, + { + from: dummyPeerID.toBytes(), + topic: 'test', + data: new Uint8Array(0), + sequenceNumber: utils.bigIntToBytes(1n), + signature: new Uint8Array(0), + key: dummyPeerID.publicKey + } + ] + const expected = ['signed', 'unsigned', 'signed'] + + const actual = (await Promise.all(cases.map(utils.toMessage))).map(m => m.type) + + expect(actual).to.deep.equal(expected) + }) })