Skip to content

Commit 95392ef

Browse files
authored
fix: share the key encoding across encrypters and keep RSA peer keys (#3669)
* refactor(plaintext): exchange the public key as protobuf bytes The key type is now read from the key instead of guessed from its length. The wire format is unchanged, pinned by new tests for Ed25519 and RSA. * fix(tls): keep the public key of RSA remote peers Encode the certificate key with publicKeyToProtobuf, dropping the duplicated key schema, and derive the remote peer id from the key directly. * refactor: derive peer ids from public keys directly instead of via a CID * refactor(noise): return the remote peer from the handshake instead of parsing the identity key twice * test: assert connection encrypters return RSA remote peers with their key
1 parent f0e64b9 commit 95392ef

19 files changed

Lines changed: 173 additions & 364 deletions

File tree

‎packages/connection-encrypter-noise/src/noise.ts‎

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
import { publicKeyFromProtobuf } from '@libp2p/crypto/keys'
21
import { InvalidCryptoExchangeError, serviceCapabilities } from '@libp2p/interface'
3-
import { peerIdFromPublicKey } from '@libp2p/peer-id'
42
import { lpStream } from '@libp2p/utils'
53
import { alloc as uint8ArrayAlloc } from 'uint8arrays/alloc'
64
import { NOISE_MSG_MAX_LENGTH_BYTES } from './constants.ts'
@@ -95,12 +93,10 @@ export class Noise implements INoiseConnection {
9593
options?.remotePeer,
9694
options
9795
)
98-
const publicKey = publicKeyFromProtobuf(handshake.payload.identityKey)
99-
10096
return {
10197
connection: toMessageStream(wrappedConnection.unwrap(), handshake, this.metrics),
10298
remoteExtensions: handshake.payload.extensions,
103-
remotePeer: peerIdFromPublicKey(publicKey),
99+
remotePeer: handshake.remotePeer,
104100
streamMuxer: options?.skipStreamMuxerNegotiation === true ? undefined : this.getStreamMuxer(handshake.payload.extensions?.streamMuxers)
105101
}
106102
}
@@ -150,12 +146,10 @@ export class Noise implements INoiseConnection {
150146
options?.remotePeer,
151147
options
152148
)
153-
const publicKey = publicKeyFromProtobuf(handshake.payload.identityKey)
154-
155149
return {
156150
connection: toMessageStream(wrappedConnection.unwrap(), handshake, this.metrics),
157151
remoteExtensions: handshake.payload.extensions,
158-
remotePeer: peerIdFromPublicKey(publicKey),
152+
remotePeer: handshake.remotePeer,
159153
streamMuxer: options?.skipStreamMuxerNegotiation === true ? undefined : this.getStreamMuxer(handshake.payload.extensions?.streamMuxers)
160154
}
161155
}

‎packages/connection-encrypter-noise/src/performHandshake.ts‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export async function performHandshakeInitiator (init: HandshakeParams, options?
3535
logRemoteStaticKey(xx.rs, log)
3636

3737
log.trace("Initiator going to check remote's signature...")
38-
const receivedPayload = await decodeHandshakePayload(plaintext, xx.rs, remotePeer)
38+
const received = await decodeHandshakePayload(plaintext, xx.rs, remotePeer)
3939
log.trace('All good with the signature!')
4040

4141
log.trace('Stage 2 - Initiator sending third handshake message.')
@@ -46,7 +46,8 @@ export async function performHandshakeInitiator (init: HandshakeParams, options?
4646
logCipherState(cs1, cs2, log)
4747

4848
return {
49-
payload: receivedPayload,
49+
payload: received.payload,
50+
remotePeer: received.remotePeer,
5051
encrypt: (plaintext) => cs1.encryptWithAd(ZEROLEN, plaintext),
5152
decrypt: (ciphertext, dst) => cs2.decryptWithAd(ZEROLEN, ciphertext, dst)
5253
}
@@ -78,13 +79,14 @@ export async function performHandshakeResponder (init: HandshakeParams, options?
7879
log.trace('Stage 2 - Responder waiting for third handshake message...')
7980
const plaintext = xx.readMessageC(await connection.read(options))
8081
log.trace('Stage 2 - Responder received the message, finished handshake.')
81-
const receivedPayload = await decodeHandshakePayload(plaintext, xx.rs, remotePeer)
82+
const received = await decodeHandshakePayload(plaintext, xx.rs, remotePeer)
8283

8384
const [cs1, cs2] = xx.ss.split()
8485
logCipherState(cs1, cs2, log)
8586

8687
return {
87-
payload: receivedPayload,
88+
payload: received.payload,
89+
remotePeer: received.remotePeer,
8890
encrypt: (plaintext) => cs2.encryptWithAd(ZEROLEN, plaintext),
8991
decrypt: (ciphertext, dst) => cs1.decryptWithAd(ZEROLEN, ciphertext, dst)
9092
}

‎packages/connection-encrypter-noise/src/types.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export interface HandshakeParams {
3131

3232
export interface HandshakeResult {
3333
payload: NoiseHandshakePayload
34+
remotePeer: PeerId
3435
encrypt (plaintext: Uint8Array | Uint8ArrayList): Uint8Array | Uint8ArrayList
3536
decrypt (ciphertext: Uint8Array | Uint8ArrayList, dst?: Uint8Array): Uint8Array | Uint8ArrayList
3637
}

‎packages/connection-encrypter-noise/src/utils.ts‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,14 @@ export async function decodeHandshakePayload (
3232
payloadBytes: Uint8Array | Uint8ArrayList,
3333
remoteStaticKey?: Uint8Array | Uint8ArrayList,
3434
remotePeer?: PeerId
35-
): Promise<NoiseHandshakePayload> {
35+
): Promise<{ payload: NoiseHandshakePayload, remotePeer: PeerId }> {
3636
try {
3737
const payload = NoiseHandshakePayload.decode(payloadBytes)
3838
const publicKey = publicKeyFromProtobuf(payload.identityKey)
39+
const payloadPeer = peerIdFromPublicKey(publicKey)
3940

4041
// compare peer ids, an expected peer id may not carry its public key (e.g. RSA)
4142
if (remotePeer != null) {
42-
const payloadPeer = peerIdFromPublicKey(publicKey)
43-
4443
if (!remotePeer.equals(payloadPeer)) {
4544
throw new Error(`Payload peer id ${payloadPeer} does not match expected remote peer id ${remotePeer}`)
4645
}
@@ -56,7 +55,7 @@ export async function decodeHandshakePayload (
5655
throw new Error('Invalid payload signature')
5756
}
5857

59-
return payload
58+
return { payload, remotePeer: payloadPeer }
6059
} catch (e) {
6160
throw new UnexpectedPeerError((e as Error).message)
6261
}

‎packages/connection-encrypter-plaintext/package.json‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@
5959
"@libp2p/logger": "^6.2.15",
6060
"aegir": "^48.1.1",
6161
"protons": "^9.0.1",
62-
"sinon": "^22.0.0"
62+
"sinon": "^22.0.0",
63+
"uint8-varint": "^3.0.0"
6364
},
6465
"sideEffects": false
6566
}

‎packages/connection-encrypter-plaintext/src/index.ts‎

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,13 @@
2121
* ```
2222
*/
2323

24-
import { publicKeyFromRaw } from '@libp2p/crypto/keys'
24+
import { publicKeyFromProtobuf, publicKeyToProtobuf } from '@libp2p/crypto/keys'
2525
import { UnexpectedPeerError, InvalidCryptoExchangeError, serviceCapabilities, ProtocolError } from '@libp2p/interface'
2626
import { peerIdFromPublicKey } from '@libp2p/peer-id'
2727
import { pbStream } from '@libp2p/utils'
2828
import { equals as uint8ArrayEquals } from 'uint8arrays/equals'
29-
import { Exchange, KeyType } from './pb/proto.ts'
29+
import { withArrayBuffer } from 'uint8arrays/with-array-buffer'
30+
import { Exchange } from './pb/proto.ts'
3031
import type { ComponentLogger, Logger, ConnectionEncrypter, SecuredConnection, PrivateKey, SecureConnectionOptions, MessageStream } from '@libp2p/interface'
3132

3233
const PROTOCOL = '/plaintext/2.0.0'
@@ -74,10 +75,7 @@ class Plaintext implements ConnectionEncrypter {
7475
// Encode the public key and write it to the remote peer
7576
await pb.write({
7677
id: publicKey.toMultihash().bytes,
77-
pubkey: {
78-
Type: KeyType[publicKey.type],
79-
Data: publicKey.raw
80-
}
78+
pubkey: withArrayBuffer(publicKeyToProtobuf(publicKey))
8179
}, options)
8280

8381
// Get the Exchange message
@@ -89,20 +87,21 @@ class Plaintext implements ConnectionEncrypter {
8987
throw new ProtocolError('Public key missing')
9088
}
9189

92-
if (response.pubkey.Data.byteLength === 0) {
90+
if (response.pubkey.byteLength === 0) {
9391
throw new ProtocolError('Public key data too short')
9492
}
9593

9694
if (response.id == null) {
9795
throw new ProtocolError('Remote id missing')
9896
}
9997

100-
const pubKey = publicKeyFromRaw(response.pubkey.Data)
101-
peerId = peerIdFromPublicKey(pubKey)
98+
const pubKey = publicKeyFromProtobuf(response.pubkey)
10299

103-
if (!uint8ArrayEquals(peerId.toMultihash().bytes, response.id)) {
100+
if (!uint8ArrayEquals(pubKey.toMultihash().bytes, response.id)) {
104101
throw new InvalidCryptoExchangeError('Public key did not match id')
105102
}
103+
104+
peerId = peerIdFromPublicKey(pubKey)
106105
} catch (err: any) {
107106
log.error('invalid public key - %e', err)
108107
throw new InvalidCryptoExchangeError(`Invalid public key - ${err.message}`)

‎packages/connection-encrypter-plaintext/src/pb/proto.proto‎

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,6 @@ syntax = "proto3";
22

33
message Exchange {
44
optional bytes id = 1;
5-
optional PublicKey pubkey = 2;
6-
}
7-
8-
enum KeyType {
9-
RSA = 0;
10-
Ed25519 = 1;
11-
secp256k1 = 2;
12-
ECDSA = 3;
13-
}
14-
15-
message PublicKey {
16-
optional KeyType Type = 1;
17-
bytes Data = 2;
5+
// a protobuf encoded libp2p PublicKey
6+
optional bytes pubkey = 2;
187
}

0 commit comments

Comments
 (0)