Skip to content

Commit

Permalink
fix!: remove private key field from peer id (#2660)
Browse files Browse the repository at this point in the history
The only time you should ever see a private key on a `PeerId` is for the id of the currently running node.  An exception to this is the keychain which used `PeerId`s to export private keys, but a nicer API there is to just deal with `PrivateKey` instances directly.

At runtime a service can obtain the unmarshaled private key by adding a `privateKey: PrivateKey` field to it's components map so there's no need to have the field on every `PeerId`.

The `publicKey` field of a `PeerId` was a `Uint8Array` which (if present) held the public key bytes in a protobuf wrapper along with a field saying what type of key it was.  This was because we wanted to avoid pulling `@libp2p/crypto` into front ends, since it had a dependency on `node-forge` which was a large blob of untreeshakable CSJ code.  This dependency has been removed so `@libp2p/crypto` is now comparatively lightweight so we can use the `PublicKey` type instead of `Uint8Array`, which also saves CPU time since we don't need to unmarshal the key before we can use it.

Fixes #2659

BREAKING CHANGE:
  - The `.privateKey` field of the `PeerId` interface has been removed
  - The `.publicKey` field of the `PeerId` interface is now a `PublicKey` instead of a `Uint8Array`
  - `createLibp2p` now accepts a `privateKey` instead of a `peerId`
  - The keychain operates on `PrivateKey` instances instead of `PeerId`s with `.privateKey` fields
  - `@libp2p/peer-id-factory` has been removed, use `generateKeyPair` and `peerIdFromPrivateKey` instead
  • Loading branch information
achingbrain committed Sep 6, 2024
1 parent 636c753 commit dea4045
Show file tree
Hide file tree
Showing 303 changed files with 11,826 additions and 6,453 deletions.
70 changes: 35 additions & 35 deletions .github/workflows/examples.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,38 +19,38 @@ jobs:
node-version: lts/*
- uses: ipfs/aegir/actions/cache-node-modules@master

test-examples:
name: Test example ${{ matrix.example.name }}
runs-on: ubuntu-latest
needs: build
continue-on-error: true
strategy:
matrix:
example:
- name: js-libp2p-example-chat
repo: https://github.com/libp2p/js-libp2p-example-chat.git
deps:
- '@libp2p/peer-id-factory@$PWD/packages/peer-id-factory'
- '@libp2p/tcp@$PWD/packages/transport-tcp'
- '@libp2p/websockets@$PWD/packages/transport-websockets'
- 'libp2p@$PWD/packages/libp2p'
- name: js-libp2p-example-circuit-relay
repo: https://github.com/libp2p/js-libp2p-example-circuit-relay.git
deps:
- '@libp2p/circuit-relay-v2@$PWD/packages/transport-circuit-relay-v2'
- '@libp2p/identify@$PWD/packages/protocol-identify'
- '@libp2p/websockets@$PWD/packages/transport-websockets'
- 'libp2p@$PWD/packages/libp2p'
- name: js-libp2p-example-connection-encryption
repo: https://github.com/libp2p/js-libp2p-example-connection-encryption.git
deps:
- '@libp2p/plaintext@$PWD/packages/connection-encrypter-plaintext'
- '@libp2p/tcp@$PWD/packages/transport-tcp'
- 'libp2p@$PWD/packages/libp2p'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: lts/*
- uses: ipfs/aegir/actions/cache-node-modules@master
- run: npx xvfb-maybe aegir test-dependant ${{ matrix.example.repo }} --deps ${{ join(matrix.example.deps, ',') }}
# re-enable after libp2p@2.x.x release
# test-examples:
# name: Test example ${{ matrix.example.name }}
# runs-on: ubuntu-latest
# needs: build
# continue-on-error: true
# strategy:
# matrix:
# example:
# - name: js-libp2p-example-chat
# repo: https://github.com/libp2p/js-libp2p-example-chat.git
# deps:
# - '@libp2p/tcp@$PWD/packages/transport-tcp'
# - '@libp2p/websockets@$PWD/packages/transport-websockets'
# - 'libp2p@$PWD/packages/libp2p'
# - name: js-libp2p-example-circuit-relay
# repo: https://github.com/libp2p/js-libp2p-example-circuit-relay.git
# deps:
# - '@libp2p/circuit-relay-v2@$PWD/packages/transport-circuit-relay-v2'
# - '@libp2p/identify@$PWD/packages/protocol-identify'
# - '@libp2p/websockets@$PWD/packages/transport-websockets'
# - 'libp2p@$PWD/packages/libp2p'
# - name: js-libp2p-example-connection-encryption
# repo: https://github.com/libp2p/js-libp2p-example-connection-encryption.git
# deps:
# - '@libp2p/plaintext@$PWD/packages/connection-encrypter-plaintext'
# - '@libp2p/tcp@$PWD/packages/transport-tcp'
# - 'libp2p@$PWD/packages/libp2p'
# steps:
# - uses: actions/checkout@v4
# - uses: actions/setup-node@v4
# with:
# node-version: lts/*
# - uses: ipfs/aegir/actions/cache-node-modules@master
# - run: npx xvfb-maybe aegir test-dependant ${{ matrix.example.repo }} --deps ${{ join(matrix.example.deps, ',') }}
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"packages/connection-encrypter-plaintext":"1.1.6","packages/connection-encrypter-tls":"1.1.5","packages/crypto":"4.1.9","packages/interface":"1.7.0","packages/interface-compliance-tests":"5.4.12","packages/interface-internal":"1.3.4","packages/kad-dht":"12.1.5","packages/keychain":"4.1.6","packages/libp2p":"1.9.4","packages/logger":"4.0.20","packages/metrics-devtools":"0.2.5","packages/metrics-prometheus":"3.1.5","packages/metrics-simple":"1.1.5","packages/multistream-select":"5.1.17","packages/peer-collections":"5.2.9","packages/peer-discovery-bootstrap":"10.1.5","packages/peer-discovery-mdns":"10.1.5","packages/peer-id":"4.2.4","packages/peer-id-factory":"4.2.4","packages/peer-record":"7.0.25","packages/peer-store":"10.1.5","packages/protocol-autonat":"1.1.5","packages/protocol-dcutr":"1.1.5","packages/protocol-echo":"1.1.5","packages/protocol-fetch":"1.1.5","packages/protocol-identify":"2.1.5","packages/protocol-perf":"3.1.5","packages/protocol-ping":"1.1.6","packages/pubsub":"9.0.26","packages/pubsub-floodsub":"9.1.5","packages/record":"4.0.4","packages/stream-multiplexer-mplex":"10.1.5","packages/transport-circuit-relay-v2":"1.1.5","packages/transport-tcp":"9.1.6","packages/transport-webrtc":"4.1.10","packages/transport-websockets":"8.2.0","packages/transport-webtransport":"4.1.9","packages/upnp-nat":"1.2.5","packages/utils":"5.4.9"}
{"packages/connection-encrypter-plaintext":"1.1.6","packages/connection-encrypter-tls":"1.1.5","packages/crypto":"4.1.9","packages/interface":"1.7.0","packages/interface-compliance-tests":"5.4.12","packages/interface-internal":"1.3.4","packages/kad-dht":"12.1.5","packages/keychain":"4.1.6","packages/libp2p":"1.9.4","packages/logger":"4.0.20","packages/metrics-devtools":"0.2.5","packages/metrics-prometheus":"3.1.5","packages/metrics-simple":"1.1.5","packages/multistream-select":"5.1.17","packages/peer-collections":"5.2.9","packages/peer-discovery-bootstrap":"10.1.5","packages/peer-discovery-mdns":"10.1.5","packages/peer-id":"4.2.4","packages/peer-record":"7.0.25","packages/peer-store":"10.1.5","packages/protocol-autonat":"1.1.5","packages/protocol-dcutr":"1.1.5","packages/protocol-echo":"1.1.5","packages/protocol-fetch":"1.1.5","packages/protocol-identify":"2.1.5","packages/protocol-perf":"3.1.5","packages/protocol-ping":"1.1.6","packages/pubsub":"9.0.26","packages/pubsub-floodsub":"9.1.5","packages/record":"4.0.4","packages/stream-multiplexer-mplex":"10.1.5","packages/transport-circuit-relay-v2":"1.1.5","packages/transport-tcp":"9.1.6","packages/transport-webrtc":"4.1.10","packages/transport-websockets":"8.2.0","packages/transport-webtransport":"4.1.9","packages/upnp-nat":"1.2.5","packages/utils":"5.4.9"}
1 change: 0 additions & 1 deletion .release-please.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
"packages/peer-discovery-bootstrap": {},
"packages/peer-discovery-mdns": {},
"packages/peer-id": {},
"packages/peer-id-factory": {},
"packages/peer-record": {},
"packages/peer-store": {},
"packages/protocol-autonat": {},
Expand Down
4 changes: 2 additions & 2 deletions doc/LIMITS.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,12 @@ When choosing connections to close the connection manager sorts the list of conn

```TypeScript
import { createLibp2p } from 'libp2p'
import { createEd25519PeerId } from '@libp2p/peer-id-factory'
import { peerIdFromString } from '@libp2p/peer-id'


const libp2p = await createLibp2p({})

const peerId = await createEd25519PeerId()
const peerId = await peerIdFromString('123Koo...')

// tag a peer
await libp2p.peerStore.merge(peerId, {
Expand Down
1 change: 1 addition & 0 deletions interop/BrowserDockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ WORKDIR /app
COPY package.json ./
COPY ./packages ./packages
COPY ./interop ./interop
COPY ./patches ./patches

# disable colored output and CLI animation from test runners
ENV CI=true
Expand Down
1 change: 1 addition & 0 deletions interop/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ WORKDIR /app
COPY package.json ./
COPY ./packages ./packages
COPY ./interop ./interop
COPY ./patches ./patches

# disable colored output and CLI animation from test runners
ENV CI=true
Expand Down
2 changes: 1 addition & 1 deletion interop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"@chainsafe/libp2p-noise": "^15.0.0",
"@chainsafe/libp2p-yamux": "^6.0.2",
"@libp2p/circuit-relay-v2": "^1.0.24",
"@libp2p/interface": "^1.4.0",
"@libp2p/interface": "^1.7.0",
"@libp2p/identify": "^2.0.2",
"@libp2p/mplex": "^10.0.24",
"@libp2p/ping": "^1.0.19",
Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,13 @@
"npm:release": "aegir exec --bail false npm -- publish",
"release:rc": "aegir release-rc",
"docs": "aegir docs",
"docs:no-publish": "aegir docs --publish false -- --exclude interop --exclude doc"
"docs:no-publish": "aegir docs --publish false -- --exclude interop --exclude doc",
"postinstall": "patch-package"
},
"devDependencies": {
"aegir": "^44.0.1",
"npm-run-all": "^4.1.5"
"npm-run-all": "^4.1.5",
"patch-package": "^8.0.0"
},
"eslintConfig": {
"extends": "ipfs",
Expand Down
2 changes: 1 addition & 1 deletion packages/connection-encrypter-plaintext/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@
"dependencies": {
"@libp2p/crypto": "^4.1.9",
"@libp2p/interface": "^1.7.0",
"@libp2p/peer-id-factory": "^4.2.4",
"@libp2p/peer-id": "^4.2.4",
"it-protobuf-stream": "^1.1.3",
"it-stream-types": "^2.0.1",
Expand All @@ -63,6 +62,7 @@
"uint8arrays": "^5.1.0"
},
"devDependencies": {
"@libp2p/crypto": "^4.1.9",
"@libp2p/interface-compliance-tests": "^5.4.12",
"@libp2p/logger": "^4.0.20",
"@multiformats/multiaddr": "^12.2.3",
Expand Down
69 changes: 26 additions & 43 deletions packages/connection-encrypter-plaintext/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,30 +20,30 @@
* ```
*/

import { supportedKeys } from '@libp2p/crypto/keys'
import { UnexpectedPeerError, InvalidCryptoExchangeError, serviceCapabilities } from '@libp2p/interface'
import { peerIdFromBytes } from '@libp2p/peer-id'
import { createFromPubKey } from '@libp2p/peer-id-factory'
import { publicKeyFromRaw } from '@libp2p/crypto/keys'
import { UnexpectedPeerError, InvalidCryptoExchangeError, serviceCapabilities, ProtocolError } from '@libp2p/interface'
import { peerIdFromPublicKey } from '@libp2p/peer-id'
import { pbStream } from 'it-protobuf-stream'
import { Exchange, KeyType, PublicKey } from './pb/proto.js'
import type { ComponentLogger, Logger, MultiaddrConnection, ConnectionEncrypter, SecuredConnection, PeerId, PublicKey as PubKey, SecureConnectionOptions } from '@libp2p/interface'
import { equals as uint8ArrayEquals } from 'uint8arrays/equals'
import { Exchange, KeyType } from './pb/proto.js'
import type { ComponentLogger, Logger, MultiaddrConnection, ConnectionEncrypter, SecuredConnection, PrivateKey, SecureConnectionOptions } from '@libp2p/interface'
import type { Duplex } from 'it-stream-types'
import type { Uint8ArrayList } from 'uint8arraylist'

const PROTOCOL = '/plaintext/2.0.0'

export interface PlaintextComponents {
peerId: PeerId
privateKey: PrivateKey
logger: ComponentLogger
}

class Plaintext implements ConnectionEncrypter {
public protocol: string = PROTOCOL
private readonly peerId: PeerId
private readonly privateKey: PrivateKey
private readonly log: Logger

constructor (components: PlaintextComponents) {
this.peerId = components.peerId
this.privateKey = components.privateKey
this.log = components.logger.forComponent('libp2p:plaintext')
}

Expand All @@ -54,38 +54,32 @@ class Plaintext implements ConnectionEncrypter {
]

async secureInbound<Stream extends Duplex<AsyncGenerator<Uint8Array | Uint8ArrayList>> = MultiaddrConnection>(conn: Stream, options?: SecureConnectionOptions): Promise<SecuredConnection<Stream>> {
return this._encrypt(this.peerId, conn, options)
return this._encrypt(conn, options)
}

async secureOutbound<Stream extends Duplex<AsyncGenerator<Uint8Array | Uint8ArrayList>> = MultiaddrConnection>(conn: Stream, options?: SecureConnectionOptions): Promise<SecuredConnection<Stream>> {
return this._encrypt(this.peerId, conn, options)
return this._encrypt(conn, options)
}

/**
* Encrypt connection
*/
async _encrypt<Stream extends Duplex<AsyncGenerator<Uint8Array | Uint8ArrayList>> = MultiaddrConnection>(localId: PeerId, conn: Stream, options?: SecureConnectionOptions): Promise<SecuredConnection<Stream>> {
async _encrypt<Stream extends Duplex<AsyncGenerator<Uint8Array | Uint8ArrayList>> = MultiaddrConnection>(conn: Stream, options?: SecureConnectionOptions): Promise<SecuredConnection<Stream>> {
const pb = pbStream(conn).pb(Exchange)

let type = KeyType.RSA

if (localId.type === 'Ed25519') {
type = KeyType.Ed25519
} else if (localId.type === 'secp256k1') {
type = KeyType.Secp256k1
}

this.log('write pubkey exchange to peer %p', options?.remotePeer)

const publicKey = this.privateKey.publicKey

const [
, response
] = await Promise.all([
// Encode the public key and write it to the remote peer
pb.write({
id: localId.toBytes(),
id: publicKey.toMultihash().bytes,
pubkey: {
Type: type,
Data: localId.publicKey == null ? new Uint8Array(0) : (PublicKey.decode(localId.publicKey).Data ?? new Uint8Array(0))
Type: KeyType[publicKey.type],
Data: publicKey.raw
}
}, options),
// Get the Exchange message
Expand All @@ -95,37 +89,26 @@ class Plaintext implements ConnectionEncrypter {
let peerId
try {
if (response.pubkey == null) {
throw new Error('Public key missing')
throw new ProtocolError('Public key missing')
}

if (response.pubkey.Data.length === 0) {
throw new Error('Public key data too short')
if (response.pubkey.Data.byteLength === 0) {
throw new ProtocolError('Public key data too short')
}

if (response.id == null) {
throw new Error('Remote id missing')
}

let pubKey: PubKey

if (response.pubkey.Type === KeyType.RSA) {
pubKey = supportedKeys.rsa.unmarshalRsaPublicKey(response.pubkey.Data)
} else if (response.pubkey.Type === KeyType.Ed25519) {
pubKey = supportedKeys.ed25519.unmarshalEd25519PublicKey(response.pubkey.Data)
} else if (response.pubkey.Type === KeyType.Secp256k1) {
pubKey = supportedKeys.secp256k1.unmarshalSecp256k1PublicKey(response.pubkey.Data)
} else {
throw new Error('Unknown public key type')
throw new ProtocolError('Remote id missing')
}

peerId = await createFromPubKey(pubKey)
const pubKey = publicKeyFromRaw(response.pubkey.Data)
peerId = peerIdFromPublicKey(pubKey)

if (!peerId.equals(peerIdFromBytes(response.id))) {
throw new Error('Public key did not match id')
if (!uint8ArrayEquals(peerId.toMultihash().bytes, response.id)) {
throw new InvalidCryptoExchangeError('Public key did not match id')
}
} catch (err: any) {
this.log.error(err)
throw new InvalidCryptoExchangeError('Remote did not provide its public key')
throw new InvalidCryptoExchangeError('Invalid public key - ' + err.message)
}

if (options?.remotePeer != null && !peerId.equals(options?.remotePeer)) {
Expand Down
2 changes: 1 addition & 1 deletion packages/connection-encrypter-plaintext/src/pb/proto.proto
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ message Exchange {
enum KeyType {
RSA = 0;
Ed25519 = 1;
Secp256k1 = 2;
secp256k1 = 2;
ECDSA = 3;
}

Expand Down
4 changes: 2 additions & 2 deletions packages/connection-encrypter-plaintext/src/pb/proto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,14 +81,14 @@ export namespace Exchange {
export enum KeyType {
RSA = 'RSA',
Ed25519 = 'Ed25519',
Secp256k1 = 'Secp256k1',
secp256k1 = 'secp256k1',
ECDSA = 'ECDSA'
}

enum __KeyTypeValues {
RSA = 0,
Ed25519 = 1,
Secp256k1 = 2,
secp256k1 = 2,
ECDSA = 3
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
/* eslint-env mocha */

import { generateKeyPair } from '@libp2p/crypto/keys'
import suite from '@libp2p/interface-compliance-tests/connection-encryption'
import { defaultLogger } from '@libp2p/logger'
import { createEd25519PeerId } from '@libp2p/peer-id-factory'
import { plaintext } from '../src/index.js'

describe('plaintext compliance', () => {
suite({
async setup (opts) {
return plaintext()({
peerId: opts?.peerId ?? await createEd25519PeerId(),
privateKey: opts?.privateKey ?? await generateKeyPair('Ed25519'),
logger: defaultLogger()
})
},
Expand Down
27 changes: 15 additions & 12 deletions packages/connection-encrypter-plaintext/test/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
/* eslint-env mocha */

import { generateKeyPair } from '@libp2p/crypto/keys'
import { mockMultiaddrConnPair } from '@libp2p/interface-compliance-tests/mocks'
import { defaultLogger } from '@libp2p/logger'
import { peerIdFromBytes } from '@libp2p/peer-id'
import { createEd25519PeerId, createRSAPeerId } from '@libp2p/peer-id-factory'
import { peerIdFromMultihash, peerIdFromPrivateKey } from '@libp2p/peer-id'
import { multiaddr } from '@multiformats/multiaddr'
import { expect } from 'aegir/chai'
import sinon from 'sinon'
Expand All @@ -18,18 +18,20 @@ describe('plaintext', () => {
let encrypterRemote: ConnectionEncrypter

beforeEach(async () => {
[localPeer, remotePeer, wrongPeer] = await Promise.all([
createEd25519PeerId(),
createEd25519PeerId(),
createEd25519PeerId()
[remotePeer, wrongPeer] = await Promise.all([
peerIdFromPrivateKey(await generateKeyPair('Ed25519')),
peerIdFromPrivateKey(await generateKeyPair('Ed25519'))
])

const localKeyPair = await generateKeyPair('Ed25519')
localPeer = peerIdFromPrivateKey(localKeyPair)

encrypter = plaintext()({
peerId: localPeer,
privateKey: localKeyPair,
logger: defaultLogger()
})
encrypterRemote = plaintext()({
peerId: remotePeer,
privateKey: await generateKeyPair('Ed25519'),
logger: defaultLogger()
})
})
Expand Down Expand Up @@ -59,11 +61,12 @@ describe('plaintext', () => {
})

it('should fail if the peer does not provide its public key', async () => {
const peer = await createRSAPeerId()
remotePeer = peerIdFromBytes(peer.toBytes())
const keyPair = await generateKeyPair('RSA', 512)
const peer = peerIdFromPrivateKey(keyPair)
remotePeer = peerIdFromMultihash(peer.toMultihash())

encrypter = plaintext()({
peerId: remotePeer,
privateKey: keyPair,
logger: defaultLogger()
})

Expand All @@ -81,6 +84,6 @@ describe('plaintext', () => {
remotePeer: localPeer
})
]))
.to.eventually.be.rejected.with.property('name', 'InvalidCryptoExchangeError')
.to.eventually.be.rejected.with.property('name', 'UnexpectedPeerError')
})
})
Loading

0 comments on commit dea4045

Please sign in to comment.