Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: add private key to libp2p components #2303

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/interface-compliance-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@
"test:electron-main": "aegir test -t electron-main"
},
"dependencies": {
"@libp2p/crypto": "^3.0.1",
"@libp2p/interface": "^1.0.1",
"@libp2p/interface-internal": "^1.0.2",
"@libp2p/logger": "^4.0.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { CodeError } from '@libp2p/interface'
import { isPeerId, type PeerId, type ComponentLogger, type Libp2pEvents, type PendingDial, type Connection, type TypedEventTarget, type PubSub, type Startable } from '@libp2p/interface'
import { isPeerId, type PeerId, type ComponentLogger, type Libp2pEvents, type PendingDial, type Connection, type TypedEventTarget, type PrivateKey, type PubSub, type Startable } from '@libp2p/interface'
import { PeerMap } from '@libp2p/peer-collections'
import { peerIdFromString } from '@libp2p/peer-id'
import { isMultiaddr, type Multiaddr } from '@multiformats/multiaddr'
Expand All @@ -8,6 +8,7 @@ import type { ConnectionManager, Registrar } from '@libp2p/interface-internal'

export interface MockNetworkComponents {
peerId: PeerId
privateKey: PrivateKey
registrar: Registrar
connectionManager: ConnectionManager
events: TypedEventTarget<Libp2pEvents>
Expand Down
3 changes: 2 additions & 1 deletion packages/interface-compliance-tests/src/pubsub/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ import messagesTest from './messages.js'
import multipleNodesTest from './multiple-nodes.js'
import twoNodesTest from './two-nodes.js'
import type { TestSetup } from '../index.js'
import type { ComponentLogger, PeerId, PubSub, PubSubInit } from '@libp2p/interface'
import type { ComponentLogger, PeerId, PrivateKey, PubSub, PubSubInit } from '@libp2p/interface'
import type { ConnectionManager, Registrar } from '@libp2p/interface-internal'

export interface PubSubComponents {
peerId: PeerId
privateKey: PrivateKey
registrar: Registrar
connectionManager: ConnectionManager
pubsub?: PubSub
Expand Down
5 changes: 4 additions & 1 deletion packages/interface-compliance-tests/src/pubsub/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { unmarshalPrivateKey } from '@libp2p/crypto/keys'
import { TypedEventEmitter } from '@libp2p/interface'
import { defaultLogger } from '@libp2p/logger'
import { createEd25519PeerId } from '@libp2p/peer-id-factory'
Expand All @@ -16,8 +17,10 @@ export async function waitForSubscriptionUpdate (a: PubSub, b: PeerId): Promise<
}

export async function createComponents (): Promise<MockNetworkComponents> {
const peerId = await createEd25519PeerId()
const components: any = {
peerId: await createEd25519PeerId(),
peerId,
privateKey: await unmarshalPrivateKey(peerId.privateKey as Uint8Array),
registrar: mockRegistrar(),
events: new TypedEventEmitter(),
logger: defaultLogger()
Expand Down
6 changes: 5 additions & 1 deletion packages/kad-dht/test/utils/test-dht.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { unmarshalPrivateKey } from '@libp2p/crypto/keys'
import { TypedEventEmitter, start, stop } from '@libp2p/interface'
import { mockRegistrar, mockConnectionManager, mockNetwork } from '@libp2p/interface-compliance-tests/mocks'
import { defaultLogger } from '@libp2p/logger'
Expand All @@ -23,8 +24,10 @@ export class TestDHT {

async spawn (options: Partial<KadDHTInit> = {}, autoStart = true): Promise<DefaultDualKadDHT> {
const events = new TypedEventEmitter<Libp2pEvents>()
const peerId = await createPeerId()
const privateKey = await unmarshalPrivateKey(peerId.privateKey as Uint8Array)
const components: KadDHTComponents = {
peerId: await createPeerId(),
peerId,
datastore: new MemoryDatastore(),
registrar: mockRegistrar(),
// connectionGater: mockConnectionGater(),
Expand All @@ -47,6 +50,7 @@ export class TestDHT {

mockNetwork.addNode({
...components,
privateKey,
events
})

Expand Down
4 changes: 3 additions & 1 deletion packages/libp2p/src/components.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { CodeError } from '@libp2p/interface'
import { isStartable, type Startable, type Libp2pEvents, type ComponentLogger, type NodeInfo, type ConnectionProtector, type ConnectionGater, type ContentRouting, type TypedEventTarget, type Metrics, type PeerId, type PeerRouting, type PeerStore, type Upgrader } from '@libp2p/interface'
import { isStartable, type Startable, type Libp2pEvents, type ComponentLogger, type NodeInfo, type ConnectionProtector, type ConnectionGater, type ContentRouting, type TypedEventTarget, type Metrics, type PeerId, type PeerRouting, type PeerStore, type PrivateKey, type Upgrader } from '@libp2p/interface'
import { defaultLogger } from '@libp2p/logger'
import type { AddressManager, ConnectionManager, Registrar, TransportManager } from '@libp2p/interface-internal'
import type { Datastore } from 'interface-datastore'

export interface Components extends Record<string, any>, Startable {
peerId: PeerId
privateKey: PrivateKey
nodeInfo: NodeInfo
logger: ComponentLogger
events: TypedEventTarget<Libp2pEvents>
Expand All @@ -25,6 +26,7 @@ export interface Components extends Record<string, any>, Startable {

export interface ComponentsInit {
peerId?: PeerId
privateKey?: PrivateKey
nodeInfo?: NodeInfo
logger?: ComponentLogger
events?: TypedEventTarget<Libp2pEvents>
Expand Down
7 changes: 6 additions & 1 deletion packages/libp2p/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { CodeError, FaultTolerance } from '@libp2p/interface'
import { peerIdFromKeys } from '@libp2p/peer-id'
import { defaultAddressSort } from '@libp2p/utils/address-sort'
import { dnsaddrResolver } from '@multiformats/multiaddr/resolvers'
import mergeOptions from 'merge-options'
Expand All @@ -25,12 +26,16 @@
}
}

export function validateConfig <T extends ServiceMap = Record<string, unknown>> (opts: RecursivePartial<Libp2pInit<T>>): Libp2pInit<T> {
export async function validateConfig <T extends ServiceMap = Record<string, unknown>> (opts: RecursivePartial<Libp2pInit<T>>): Promise<Libp2pInit<T>> {
const resultingOptions: Libp2pInit<T> = mergeOptions(DefaultConfig, opts)

if (resultingOptions.connectionProtector === null && globalThis.process?.env?.LIBP2P_FORCE_PNET != null) { // eslint-disable-line no-undef
throw new CodeError(messages.ERR_PROTECTOR_REQUIRED, codes.ERR_PROTECTOR_REQUIRED)
}

if (!(await peerIdFromKeys(resultingOptions.privateKey.public.bytes, resultingOptions.privateKey.bytes)).equals(resultingOptions.peerId)) {
throw new CodeError('Private key doesn\'t match peer id', codes.ERR_INVALID_KEY)
}

Check warning on line 38 in packages/libp2p/src/config.ts

View check run for this annotation

Codecov / codecov/patch

packages/libp2p/src/config.ts#L37-L38

Added lines #L37 - L38 were not covered by tests

return resultingOptions
}
7 changes: 6 additions & 1 deletion packages/libp2p/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import type { AddressManagerInit } from './address-manager/index.js'
import type { Components } from './components.js'
import type { ConnectionManagerInit } from './connection-manager/index.js'
import type { TransportManagerInit } from './transport-manager.js'
import type { Libp2p, ServiceMap, RecursivePartial, ComponentLogger, NodeInfo, ConnectionProtector, ConnectionEncrypter, ConnectionGater, ContentRouting, Metrics, PeerDiscovery, PeerId, PeerRouting, StreamMuxerFactory, Transport } from '@libp2p/interface'
import type { Libp2p, ServiceMap, RecursivePartial, ComponentLogger, NodeInfo, ConnectionProtector, ConnectionEncrypter, ConnectionGater, ContentRouting, Metrics, PeerDiscovery, PeerId, PeerRouting, StreamMuxerFactory, Transport, PrivateKey } from '@libp2p/interface'
import type { PersistentPeerStoreInit } from '@libp2p/peer-store'
import type { Datastore } from 'interface-datastore'

Expand All @@ -36,6 +36,11 @@ export interface Libp2pInit<T extends ServiceMap = { x: Record<string, unknown>
*/
peerId: PeerId

/**
* Private key associated with the peerId
*/
privateKey: PrivateKey

/**
* Metadata about the node - implementation name, version number, etc
*/
Expand Down
6 changes: 4 additions & 2 deletions packages/libp2p/src/libp2p.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { unmarshalPublicKey } from '@libp2p/crypto/keys'
import { unmarshalPrivateKey, unmarshalPublicKey } from '@libp2p/crypto/keys'
import { contentRoutingSymbol, CodeError, TypedEventEmitter, CustomEvent, setMaxListeners, peerDiscoverySymbol, peerRoutingSymbol } from '@libp2p/interface'
import { defaultLogger } from '@libp2p/logger'
import { PeerSet } from '@libp2p/peer-collections'
Expand Down Expand Up @@ -67,6 +67,7 @@ export class Libp2pNode<T extends ServiceMap = Record<string, unknown>> extends
this.services = {}
const components = this.components = defaultComponents({
peerId: init.peerId,
privateKey: init.privateKey,
nodeInfo: init.nodeInfo ?? {
name: pkg.name,
version: pkg.version
Expand Down Expand Up @@ -387,6 +388,7 @@ export class Libp2pNode<T extends ServiceMap = Record<string, unknown>> extends
*/
export async function createLibp2pNode <T extends ServiceMap = Record<string, unknown>> (options: Libp2pOptions<T> = {}): Promise<Libp2pNode<T>> {
options.peerId ??= await createEd25519PeerId()
options.privateKey ??= await unmarshalPrivateKey(options.peerId.privateKey as Uint8Array)

return new Libp2pNode(validateConfig(options))
return new Libp2pNode(await validateConfig(options))
}
39 changes: 15 additions & 24 deletions packages/peer-record/src/envelope/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { unmarshalPrivateKey, unmarshalPublicKey } from '@libp2p/crypto/keys'
import { unmarshalPublicKey } from '@libp2p/crypto/keys'
import { CodeError } from '@libp2p/interface'
import { peerIdFromKeys } from '@libp2p/peer-id'
import * as varint from 'uint8-varint'
Expand All @@ -7,10 +7,11 @@ import { equals as uint8ArrayEquals } from 'uint8arrays/equals'
import { fromString as uint8arraysFromString } from 'uint8arrays/from-string'
import { codes } from '../errors.js'
import { Envelope as Protobuf } from './envelope.js'
import type { PeerId, Record, Envelope } from '@libp2p/interface'
import type { Record, Envelope, PrivateKey, PublicKey, PeerId } from '@libp2p/interface'

export interface RecordEnvelopeInit {
peerId: PeerId
publicKey: PublicKey
payloadType: Uint8Array
payload: Uint8Array
signature: Uint8Array
Expand All @@ -23,9 +24,11 @@ export class RecordEnvelope implements Envelope {
static createFromProtobuf = async (data: Uint8Array | Uint8ArrayList): Promise<RecordEnvelope> => {
const envelopeData = Protobuf.decode(data)
const peerId = await peerIdFromKeys(envelopeData.publicKey)
const publicKey = unmarshalPublicKey(envelopeData.publicKey)

return new RecordEnvelope({
peerId,
publicKey,
payloadType: envelopeData.payloadType,
payload: envelopeData.payload,
signature: envelopeData.signature
Expand All @@ -34,22 +37,18 @@ export class RecordEnvelope implements Envelope {

/**
* Seal marshals the given Record, places the marshaled bytes inside an Envelope
* and signs it with the given peerId's private key
* and signs it with the given private key
*/
static seal = async (record: Record, peerId: PeerId): Promise<RecordEnvelope> => {
if (peerId.privateKey == null) {
throw new Error('Missing private key')
}

static seal = async (record: Record, privateKey: PrivateKey): Promise<RecordEnvelope> => {
const domain = record.domain
const payloadType = record.codec
const payload = record.marshal()
const signData = formatSignaturePayload(domain, payloadType, payload)
const key = await unmarshalPrivateKey(peerId.privateKey)
const signature = await key.sign(signData.subarray())
const signature = await privateKey.sign(signData.subarray())

return new RecordEnvelope({
peerId,
peerId: await peerIdFromKeys(privateKey.public.bytes),
publicKey: privateKey.public,
payloadType,
payload,
signature
Expand All @@ -72,6 +71,7 @@ export class RecordEnvelope implements Envelope {
}

public peerId: PeerId
public publicKey: PublicKey
public payloadType: Uint8Array
public payload: Uint8Array
public signature: Uint8Array
Expand All @@ -82,9 +82,10 @@ export class RecordEnvelope implements Envelope {
* by a libp2p peer.
*/
constructor (init: RecordEnvelopeInit) {
const { peerId, payloadType, payload, signature } = init
const { peerId, publicKey, payloadType, payload, signature } = init

this.peerId = peerId
this.publicKey = publicKey
this.payloadType = payloadType
this.payload = payload
this.signature = signature
Expand All @@ -94,13 +95,9 @@ export class RecordEnvelope implements Envelope {
* Marshal the envelope content
*/
marshal (): Uint8Array {
if (this.peerId.publicKey == null) {
throw new Error('Missing public key')
}

if (this.marshaled == null) {
this.marshaled = Protobuf.encode({
publicKey: this.peerId.publicKey,
publicKey: this.publicKey.bytes,
payloadType: this.payloadType,
payload: this.payload.subarray(),
signature: this.signature
Expand All @@ -123,13 +120,7 @@ export class RecordEnvelope implements Envelope {
async validate (domain: string): Promise<boolean> {
const signData = formatSignaturePayload(domain, this.payloadType, this.payload)

if (this.peerId.publicKey == null) {
throw new Error('Missing public key')
}

const key = unmarshalPublicKey(this.peerId.publicKey)

return key.verify(signData.subarray(), this.signature)
return this.publicKey.verify(signData.subarray(), this.signature)
}
}

Expand Down
12 changes: 8 additions & 4 deletions packages/peer-record/test/envelope.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { unmarshalPrivateKey } from '@libp2p/crypto/keys'
import { createEd25519PeerId } from '@libp2p/peer-id-factory'
import { expect } from 'aegir/chai'
import { equals as uint8ArrayEquals } from 'uint8arrays/equals'
import { fromString as uint8arrayFromString } from 'uint8arrays/from-string'
import { RecordEnvelope } from '../src/envelope/index.js'
import { codes as ErrorCodes } from '../src/errors.js'
import type { PeerId, Record } from '@libp2p/interface'
import type { PeerId, PrivateKey, Record } from '@libp2p/interface'

const domain = 'libp2p-testing'
const codec = uint8arrayFromString('/libp2p/testdata')
Expand Down Expand Up @@ -32,10 +33,12 @@ class TestRecord implements Record {
describe('Envelope', () => {
const payloadType = codec
let peerId: PeerId
let privateKey: PrivateKey
let testRecord: TestRecord

before(async () => {
peerId = await createEd25519PeerId()
privateKey = await unmarshalPrivateKey(peerId.privateKey as Uint8Array)
testRecord = new TestRecord('test-data')
})

Expand All @@ -45,6 +48,7 @@ describe('Envelope', () => {

const envelope = new RecordEnvelope({
peerId,
publicKey: privateKey.public,
payloadType,
payload,
signature
Expand All @@ -58,7 +62,7 @@ describe('Envelope', () => {
})

it('can seal a record', async () => {
const envelope = await RecordEnvelope.seal(testRecord, peerId)
const envelope = await RecordEnvelope.seal(testRecord, privateKey)
expect(envelope).to.exist()
expect(envelope.peerId.equals(peerId)).to.eql(true)
expect(envelope.payloadType).to.eql(payloadType)
Expand All @@ -67,7 +71,7 @@ describe('Envelope', () => {
})

it('can open and verify a sealed record', async () => {
const envelope = await RecordEnvelope.seal(testRecord, peerId)
const envelope = await RecordEnvelope.seal(testRecord, privateKey)
const rawEnvelope = envelope.marshal()

const unmarshalledEnvelope = await RecordEnvelope.openAndCertify(rawEnvelope, testRecord.domain)
Expand All @@ -78,7 +82,7 @@ describe('Envelope', () => {
})

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

await expect(RecordEnvelope.openAndCertify(rawEnvelope, '/bad-domain'))
Expand Down
8 changes: 5 additions & 3 deletions packages/peer-record/test/peer-record.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { multiaddr } from '@multiformats/multiaddr'
import { expect } from 'aegir/chai'
import { RecordEnvelope } from '../src/envelope/index.js'
import { PeerRecord } from '../src/peer-record/index.js'
import type { PeerId } from '@libp2p/interface'
import type { PeerId, PrivateKey } from '@libp2p/interface'

describe('PeerRecord', () => {
let peerId: PeerId
Expand All @@ -30,7 +30,7 @@ describe('PeerRecord', () => {
// The payload isn't going to match because of how the protobuf encodes uint64 values
// They are marshalled correctly on both sides, but will be off by 1 value
// Signatures will still be validated
const jsEnv = await RecordEnvelope.seal(record, peerId)
const jsEnv = await RecordEnvelope.seal(record, key)
expect(env.payloadType).to.eql(jsEnv.payloadType)
})

Expand Down Expand Up @@ -117,10 +117,12 @@ describe('PeerRecord', () => {

describe('PeerRecord inside Envelope', () => {
let peerId: PeerId
let privateKey: PrivateKey
let peerRecord: PeerRecord

before(async () => {
peerId = await createEd25519PeerId()
privateKey = await unmarshalPrivateKey(peerId.privateKey as Uint8Array)
const multiaddrs = [
multiaddr('/ip4/127.0.0.1/tcp/2000')
]
Expand All @@ -129,7 +131,7 @@ describe('PeerRecord inside Envelope', () => {
})

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

const decodedE = await RecordEnvelope.openAndCertify(byteE, PeerRecord.DOMAIN)
Expand Down
1 change: 1 addition & 0 deletions packages/peer-store/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
"uint8arrays": "^4.0.6"
},
"devDependencies": {
"@libp2p/crypto": "^3.0.1",
"@libp2p/logger": "^4.0.1",
"@types/sinon": "^17.0.0",
"aegir": "^41.0.2",
Expand Down
Loading
Loading