Skip to content

Commit

Permalink
fix!: remove private key field from peer id
Browse files Browse the repository at this point in the history
The only time you'll ever see a private key on a peer id is for the
id of the currently running node.

At runtime a service can obtain the unmarhsalled 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`.

BREAKING CHANGE: the `.privateKey` field of the `PeerId` interface has been removed
  • Loading branch information
achingbrain committed Aug 15, 2024
1 parent d6226fe commit a3226cd
Show file tree
Hide file tree
Showing 105 changed files with 846 additions and 1,449 deletions.
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
1 change: 1 addition & 0 deletions packages/connection-encrypter-plaintext/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"uint8arrays": "^5.1.0"
},
"devDependencies": {
"@libp2p/crypto": "^4.1.9",
"@libp2p/interface-compliance-tests": "^5.4.12",
"@libp2p/logger": "^4.0.20",
"@libp2p/peer-id-factory": "^4.2.4",
Expand Down
30 changes: 11 additions & 19 deletions packages/connection-encrypter-plaintext/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,14 @@ import { UnexpectedPeerError, InvalidCryptoExchangeError, serviceCapabilities }
import { peerIdFromBytes, peerIdFromKeys } from '@libp2p/peer-id'
import { pbStream } from 'it-protobuf-stream'
import { Exchange, KeyType } from './pb/proto.js'
import type { ComponentLogger, Logger, MultiaddrConnection, ConnectionEncrypter, SecuredConnection, PeerId } from '@libp2p/interface'
import type { ComponentLogger, Logger, MultiaddrConnection, ConnectionEncrypter, SecuredConnection, PeerId, PrivateKey } 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
}

Expand All @@ -45,12 +45,12 @@ export interface PlaintextInit {

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

constructor (components: PlaintextComponents, init: PlaintextInit = {}) {
this.peerId = components.peerId
this.privateKey = components.privateKey
this.log = components.logger.forComponent('libp2p:plaintext')
this.timeout = init.timeout ?? 1000
}
Expand All @@ -62,39 +62,31 @@ class Plaintext implements ConnectionEncrypter {
]

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

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

/**
* Encrypt connection
*/
async _encrypt <Stream extends Duplex<AsyncGenerator<Uint8Array | Uint8ArrayList>> = MultiaddrConnection> (localId: PeerId, conn: Stream, remoteId?: PeerId): Promise<SecuredConnection<Stream>> {
async _encrypt <Stream extends Duplex<AsyncGenerator<Uint8Array | Uint8ArrayList>> = MultiaddrConnection> (privateKey: PrivateKey, conn: Stream, remoteId?: PeerId): Promise<SecuredConnection<Stream>> {
const signal = AbortSignal.timeout(this.timeout)
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', remoteId)

const [
, response
] = await Promise.all([
// Encode the public key and write it to the remote peer
pb.write({
id: localId.toBytes(),
id: await privateKey.public.hash(),
pubkey: {
Type: type,
Data: localId.publicKey ?? new Uint8Array(0)
Type: KeyType[privateKey.type],
Data: privateKey.public.marshal()
}
}, {
signal
Expand Down Expand Up @@ -126,7 +118,7 @@ class Plaintext implements ConnectionEncrypter {
}
} 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 (remoteId != null && !peerId.equals(remoteId)) {
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
36 changes: 5 additions & 31 deletions packages/connection-encrypter-plaintext/test/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,33 @@
/* 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 { createEd25519PeerId } from '@libp2p/peer-id-factory'
import { multiaddr } from '@multiformats/multiaddr'
import { expect } from 'aegir/chai'
import sinon from 'sinon'
import { plaintext } from '../src/index.js'
import type { ConnectionEncrypter, PeerId } from '@libp2p/interface'

describe('plaintext', () => {
let localPeer: PeerId
let remotePeer: PeerId
let wrongPeer: PeerId
let encrypter: ConnectionEncrypter
let encrypterRemote: ConnectionEncrypter

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

encrypter = plaintext()({
peerId: localPeer,
privateKey: await generateKeyPair('Ed25519'),
logger: defaultLogger()
})
encrypterRemote = plaintext()({
peerId: remotePeer,
privateKey: await generateKeyPair('Ed25519'),
logger: defaultLogger()
})
})
Expand All @@ -55,28 +53,4 @@ describe('plaintext', () => {
expect(err).to.have.property('name', 'UnexpectedPeerError')
})
})

it('should fail if the peer does not provide its public key', async () => {
const peer = await createRSAPeerId()
remotePeer = peerIdFromBytes(peer.toBytes())

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

const { inbound, outbound } = mockMultiaddrConnPair({
remotePeer,
addrs: [
multiaddr('/ip4/127.0.0.1/tcp/1234'),
multiaddr('/ip4/127.0.0.1/tcp/1235')
]
})

await expect(Promise.all([
encrypter.secureInbound(inbound),
encrypterRemote.secureOutbound(outbound, localPeer)
]))
.to.eventually.be.rejected.with.property('name', 'InvalidCryptoExchangeError')
})
})
4 changes: 2 additions & 2 deletions packages/connection-encrypter-tls/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@
*/

import { TLS } from './tls.js'
import type { ComponentLogger, ConnectionEncrypter, PeerId } from '@libp2p/interface'
import type { ComponentLogger, ConnectionEncrypter, PrivateKey } from '@libp2p/interface'

export const PROTOCOL = '/tls/1.0.0'

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

Expand Down
2 changes: 1 addition & 1 deletion packages/connection-encrypter-tls/src/pb/index.proto
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ syntax = "proto3";
enum KeyType {
RSA = 0;
Ed25519 = 1;
Secp256k1 = 2;
secp256k1 = 2;
ECDSA = 3;
}

Expand Down
4 changes: 2 additions & 2 deletions packages/connection-encrypter-tls/src/pb/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ import type { Uint8ArrayList } from 'uint8arraylist'
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
8 changes: 4 additions & 4 deletions packages/connection-encrypter-tls/src/tls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,19 @@ import { HandshakeTimeoutError } from './errors.js'
import { generateCertificate, verifyPeerCertificate, itToStream, streamToIt } from './utils.js'
import { PROTOCOL } from './index.js'
import type { TLSComponents, TLSInit } from './index.js'
import type { MultiaddrConnection, ConnectionEncrypter, SecuredConnection, PeerId, Logger } from '@libp2p/interface'
import type { MultiaddrConnection, ConnectionEncrypter, SecuredConnection, PeerId, Logger, PrivateKey } from '@libp2p/interface'
import type { Duplex } from 'it-stream-types'
import type { Uint8ArrayList } from 'uint8arraylist'

export class TLS implements ConnectionEncrypter {
public protocol: string = PROTOCOL
private readonly log: Logger
private readonly peerId: PeerId
private readonly privateKey: PrivateKey
private readonly timeout: number

constructor (components: TLSComponents, init: TLSInit = {}) {
this.log = components.logger.forComponent('libp2p:tls')
this.peerId = components.peerId
this.privateKey = components.privateKey
this.timeout = init.timeout ?? 1000
}

Expand All @@ -59,7 +59,7 @@ export class TLS implements ConnectionEncrypter {
*/
async _encrypt <Stream extends Duplex<AsyncGenerator<Uint8Array | Uint8ArrayList>> = MultiaddrConnection> (conn: Stream, isServer: boolean, remoteId?: PeerId): Promise<SecuredConnection<Stream>> {
const opts: TLSSocketOptions = {
...await generateCertificate(this.peerId),
...await generateCertificate(this.privateKey),
isServer,
// require TLS 1.3 or later
minVersion: 'TLSv1.3',
Expand Down
43 changes: 7 additions & 36 deletions packages/connection-encrypter-tls/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Duplex as DuplexStream } from 'node:stream'
import { Ed25519PublicKey, Secp256k1PublicKey, marshalPublicKey, supportedKeys, unmarshalPrivateKey, unmarshalPublicKey } from '@libp2p/crypto/keys'
import { InvalidCryptoExchangeError, InvalidParametersError, UnexpectedPeerError } from '@libp2p/interface'
import { Ed25519PublicKey, Secp256k1PublicKey, marshalPublicKey, supportedKeys } from '@libp2p/crypto/keys'
import { InvalidCryptoExchangeError, UnexpectedPeerError } from '@libp2p/interface'
import { peerIdFromKeys } from '@libp2p/peer-id'
import { AsnConvert } from '@peculiar/asn1-schema'
import * as asn1X509 from '@peculiar/asn1-x509'
Expand All @@ -13,7 +13,7 @@ import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
import { InvalidCertificateError } from './errors.js'
import { KeyType, PublicKey } from './pb/index.js'
import type { PeerId, PublicKey as Libp2pPublicKey, Logger } from '@libp2p/interface'
import type { PeerId, PublicKey as Libp2pPublicKey, Logger, PrivateKey } from '@libp2p/interface'
import type { Duplex } from 'it-stream-types'
import type { Uint8ArrayList } from 'uint8arraylist'

Expand Down Expand Up @@ -74,7 +74,7 @@ export async function verifyPeerCertificate (rawCertificate: Uint8Array, expecte

if (remotePublicKey.type === KeyType.Ed25519) {
remoteLibp2pPublicKey = new Ed25519PublicKey(remotePublicKeyData)
} else if (remotePublicKey.type === KeyType.Secp256k1) {
} else if (remotePublicKey.type === KeyType.secp256k1) {
remoteLibp2pPublicKey = new Secp256k1PublicKey(remotePublicKeyData)
} else if (remotePublicKey.type === KeyType.RSA) {
remoteLibp2pPublicKey = supportedKeys.rsa.unmarshalRsaPublicKey(remotePublicKeyData)
Expand Down Expand Up @@ -104,35 +104,7 @@ export async function verifyPeerCertificate (rawCertificate: Uint8Array, expecte
return remotePeerId
}

export async function generateCertificate (peerId: PeerId): Promise<{ cert: string, key: string }> {
if (peerId.privateKey == null) {
throw new InvalidParametersError('Private key was missing from PeerId')
}

if (peerId.publicKey == null) {
throw new InvalidParametersError('Public key missing from PeerId')
}

const publicKey = unmarshalPublicKey(peerId.publicKey)
let keyType: KeyType
let keyData: Uint8Array

if (peerId.type === 'Ed25519') {
// Ed25519: Only the 32 bytes of the public key
keyType = KeyType.Ed25519
keyData = publicKey.marshal()
} else if (peerId.type === 'secp256k1') {
// Secp256k1: Only the compressed form of the public key. 33 bytes.
keyType = KeyType.Secp256k1
keyData = publicKey.marshal()
} else if (peerId.type === 'RSA') {
// The rest of the keys are encoded as a SubjectPublicKeyInfo structure in PKIX, ASN.1 DER form.
keyType = KeyType.RSA
keyData = publicKey.marshal()
} else {
throw new InvalidParametersError('PeerId had unknown or unsupported type')
}

export async function generateCertificate (privateKey: PrivateKey): Promise<{ cert: string, key: string }> {
const now = Date.now()

const alg = {
Expand All @@ -144,7 +116,6 @@ export async function generateCertificate (peerId: PeerId): Promise<{ cert: stri
const keys = await crypto.subtle.generateKey(alg, true, ['sign'])
const certPublicKeySpki = await crypto.subtle.exportKey('spki', keys.publicKey)
const dataToSign = encodeSignatureData(certPublicKeySpki)
const privateKey = await unmarshalPrivateKey(peerId.privateKey)
const sig = await privateKey.sign(dataToSign)
const notAfter = new Date(now + CERT_VALIDITY_PERIOD_TO)
// workaround for https://github.com/PeculiarVentures/x509/issues/73
Expand All @@ -163,8 +134,8 @@ export async function generateCertificate (peerId: PeerId): Promise<{ cert: stri
// publicKey
new asn1js.OctetString({
valueHex: PublicKey.encode({
type: keyType,
data: keyData
type: KeyType[privateKey.type],
data: privateKey.public.marshal()
})
}),
// signature
Expand Down
4 changes: 2 additions & 2 deletions packages/connection-encrypter-tls/test/compliance.spec.ts
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 { tls } from '../src/index.js'

describe('tls compliance', () => {
suite({
async setup (opts) {
return tls()({
peerId: opts?.peerId ?? await createEd25519PeerId(),
privateKey: opts?.privateKey ?? await generateKeyPair('Ed25519'),
logger: defaultLogger()
})
},
Expand Down
Loading

0 comments on commit a3226cd

Please sign in to comment.