Skip to content

Commit 51f7b57

Browse files
achingbrainmaschad
andcommitted
fix!: make connection securing abortable (#2662)
To allow doing things like having a single `AbortSignal` that can be used as a timeout for incoming connection establishment, allow passing it as an option to the `ConnectionEncrypter` `secureOutbound` and `secureInbound` methods. Previously we'd wrap the stream to be secured in an `AbortableSource`, however this has some [serious performance implications](ChainSafe/js-libp2p-gossipsub#361) and it's generally better to just use a signal to cancel an ongoing operation instead of racing every chunk that comes out of the source. BREAKING CHANGE: the final argument to `secureOutbound` and `secureInbound` in the `ConnectionEncrypter` interface is now an options object --------- Co-authored-by: chad <[email protected]>
1 parent ab90179 commit 51f7b57

File tree

11 files changed

+89
-79
lines changed

11 files changed

+89
-79
lines changed

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

Lines changed: 13 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import { peerIdFromBytes } from '@libp2p/peer-id'
2626
import { createFromPubKey } from '@libp2p/peer-id-factory'
2727
import { pbStream } from 'it-protobuf-stream'
2828
import { Exchange, KeyType, PublicKey } from './pb/proto.js'
29-
import type { ComponentLogger, Logger, MultiaddrConnection, ConnectionEncrypter, SecuredConnection, PeerId, PublicKey as PubKey } from '@libp2p/interface'
29+
import type { ComponentLogger, Logger, MultiaddrConnection, ConnectionEncrypter, SecuredConnection, PeerId, PublicKey as PubKey, SecureConnectionOptions } from '@libp2p/interface'
3030
import type { Duplex } from 'it-stream-types'
3131
import type { Uint8ArrayList } from 'uint8arraylist'
3232

@@ -37,24 +37,14 @@ export interface PlaintextComponents {
3737
logger: ComponentLogger
3838
}
3939

40-
export interface PlaintextInit {
41-
/**
42-
* The peer id exchange must complete within this many milliseconds
43-
* (default: 1000)
44-
*/
45-
timeout?: number
46-
}
47-
4840
class Plaintext implements ConnectionEncrypter {
4941
public protocol: string = PROTOCOL
5042
private readonly peerId: PeerId
5143
private readonly log: Logger
52-
private readonly timeout: number
5344

54-
constructor (components: PlaintextComponents, init: PlaintextInit = {}) {
45+
constructor (components: PlaintextComponents) {
5546
this.peerId = components.peerId
5647
this.log = components.logger.forComponent('libp2p:plaintext')
57-
this.timeout = init.timeout ?? 1000
5848
}
5949

6050
readonly [Symbol.toStringTag] = '@libp2p/plaintext'
@@ -63,19 +53,18 @@ class Plaintext implements ConnectionEncrypter {
6353
'@libp2p/connection-encryption'
6454
]
6555

66-
async secureInbound <Stream extends Duplex<AsyncGenerator<Uint8Array | Uint8ArrayList>> = MultiaddrConnection> (conn: Stream, remoteId?: PeerId): Promise<SecuredConnection<Stream>> {
67-
return this._encrypt(this.peerId, conn, remoteId)
56+
async secureInbound<Stream extends Duplex<AsyncGenerator<Uint8Array | Uint8ArrayList>> = MultiaddrConnection>(conn: Stream, options?: SecureConnectionOptions): Promise<SecuredConnection<Stream>> {
57+
return this._encrypt(this.peerId, conn, options)
6858
}
6959

70-
async secureOutbound <Stream extends Duplex<AsyncGenerator<Uint8Array | Uint8ArrayList>> = MultiaddrConnection> (conn: Stream, remoteId?: PeerId): Promise<SecuredConnection<Stream>> {
71-
return this._encrypt(this.peerId, conn, remoteId)
60+
async secureOutbound<Stream extends Duplex<AsyncGenerator<Uint8Array | Uint8ArrayList>> = MultiaddrConnection>(conn: Stream, options?: SecureConnectionOptions): Promise<SecuredConnection<Stream>> {
61+
return this._encrypt(this.peerId, conn, options)
7262
}
7363

7464
/**
7565
* Encrypt connection
7666
*/
77-
async _encrypt <Stream extends Duplex<AsyncGenerator<Uint8Array | Uint8ArrayList>> = MultiaddrConnection> (localId: PeerId, conn: Stream, remoteId?: PeerId): Promise<SecuredConnection<Stream>> {
78-
const signal = AbortSignal.timeout(this.timeout)
67+
async _encrypt<Stream extends Duplex<AsyncGenerator<Uint8Array | Uint8ArrayList>> = MultiaddrConnection>(localId: PeerId, conn: Stream, options?: SecureConnectionOptions): Promise<SecuredConnection<Stream>> {
7968
const pb = pbStream(conn).pb(Exchange)
8069

8170
let type = KeyType.RSA
@@ -86,7 +75,7 @@ class Plaintext implements ConnectionEncrypter {
8675
type = KeyType.Secp256k1
8776
}
8877

89-
this.log('write pubkey exchange to peer %p', remoteId)
78+
this.log('write pubkey exchange to peer %p', options?.remotePeer)
9079

9180
const [
9281
, response
@@ -98,13 +87,9 @@ class Plaintext implements ConnectionEncrypter {
9887
Type: type,
9988
Data: localId.publicKey == null ? new Uint8Array(0) : (PublicKey.decode(localId.publicKey).Data ?? new Uint8Array(0))
10089
}
101-
}, {
102-
signal
103-
}),
90+
}, options),
10491
// Get the Exchange message
105-
pb.read({
106-
signal
107-
})
92+
pb.read(options)
10893
])
10994

11095
let peerId
@@ -143,7 +128,7 @@ class Plaintext implements ConnectionEncrypter {
143128
throw new InvalidCryptoExchangeError('Remote did not provide its public key')
144129
}
145130

146-
if (remoteId != null && !peerId.equals(remoteId)) {
131+
if (options?.remotePeer != null && !peerId.equals(options?.remotePeer)) {
147132
throw new UnexpectedPeerError()
148133
}
149134

@@ -156,6 +141,6 @@ class Plaintext implements ConnectionEncrypter {
156141
}
157142
}
158143

159-
export function plaintext (init?: PlaintextInit): (components: PlaintextComponents) => ConnectionEncrypter {
160-
return (components) => new Plaintext(components, init)
144+
export function plaintext (): (components: PlaintextComponents) => ConnectionEncrypter {
145+
return (components) => new Plaintext(components)
161146
}

packages/connection-encrypter-plaintext/test/index.spec.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,10 @@ describe('plaintext', () => {
4848
})
4949

5050
await Promise.all([
51-
encrypterRemote.secureInbound(inbound),
52-
encrypter.secureOutbound(outbound, wrongPeer)
51+
encrypter.secureInbound(inbound),
52+
encrypterRemote.secureOutbound(outbound, {
53+
remotePeer: wrongPeer
54+
})
5355
]).then(() => expect.fail('should have failed'), (err) => {
5456
expect(err).to.exist()
5557
expect(err).to.have.property('name', 'UnexpectedPeerError')
@@ -75,7 +77,9 @@ describe('plaintext', () => {
7577

7678
await expect(Promise.all([
7779
encrypter.secureInbound(inbound),
78-
encrypterRemote.secureOutbound(outbound, localPeer)
80+
encrypterRemote.secureOutbound(outbound, {
81+
remotePeer: localPeer
82+
})
7983
]))
8084
.to.eventually.be.rejected.with.property('name', 'InvalidCryptoExchangeError')
8185
})

packages/connection-encrypter-tls/src/index.ts

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,6 @@ export interface TLSComponents {
2828
logger: ComponentLogger
2929
}
3030

31-
export interface TLSInit {
32-
/**
33-
* The peer id exchange must complete within this many milliseconds
34-
* (default: 1000)
35-
*/
36-
timeout?: number
37-
}
38-
39-
export function tls (init?: TLSInit): (components: TLSComponents) => ConnectionEncrypter {
40-
return (components) => new TLS(components, init)
31+
export function tls (): (components: TLSComponents) => ConnectionEncrypter {
32+
return (components) => new TLS(components)
4133
}

packages/connection-encrypter-tls/src/tls.ts

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,21 +23,19 @@ import { serviceCapabilities } from '@libp2p/interface'
2323
import { HandshakeTimeoutError } from './errors.js'
2424
import { generateCertificate, verifyPeerCertificate, itToStream, streamToIt } from './utils.js'
2525
import { PROTOCOL } from './index.js'
26-
import type { TLSComponents, TLSInit } from './index.js'
27-
import type { MultiaddrConnection, ConnectionEncrypter, SecuredConnection, PeerId, Logger } from '@libp2p/interface'
26+
import type { TLSComponents } from './index.js'
27+
import type { MultiaddrConnection, ConnectionEncrypter, SecuredConnection, PeerId, Logger, SecureConnectionOptions } from '@libp2p/interface'
2828
import type { Duplex } from 'it-stream-types'
2929
import type { Uint8ArrayList } from 'uint8arraylist'
3030

3131
export class TLS implements ConnectionEncrypter {
3232
public protocol: string = PROTOCOL
3333
private readonly log: Logger
3434
private readonly peerId: PeerId
35-
private readonly timeout: number
3635

37-
constructor (components: TLSComponents, init: TLSInit = {}) {
36+
constructor (components: TLSComponents) {
3837
this.log = components.logger.forComponent('libp2p:tls')
3938
this.peerId = components.peerId
40-
this.timeout = init.timeout ?? 1000
4139
}
4240

4341
readonly [Symbol.toStringTag] = '@libp2p/tls'
@@ -46,18 +44,18 @@ export class TLS implements ConnectionEncrypter {
4644
'@libp2p/connection-encryption'
4745
]
4846

49-
async secureInbound <Stream extends Duplex<AsyncGenerator<Uint8Array | Uint8ArrayList>> = MultiaddrConnection> (conn: Stream, remoteId?: PeerId): Promise<SecuredConnection<Stream>> {
50-
return this._encrypt(conn, true, remoteId)
47+
async secureInbound <Stream extends Duplex<AsyncGenerator<Uint8Array | Uint8ArrayList>> = MultiaddrConnection> (conn: Stream, options?: SecureConnectionOptions): Promise<SecuredConnection<Stream>> {
48+
return this._encrypt(conn, true, options)
5149
}
5250

53-
async secureOutbound <Stream extends Duplex<AsyncGenerator<Uint8Array | Uint8ArrayList>> = MultiaddrConnection> (conn: Stream, remoteId?: PeerId): Promise<SecuredConnection<Stream>> {
54-
return this._encrypt(conn, false, remoteId)
51+
async secureOutbound <Stream extends Duplex<AsyncGenerator<Uint8Array | Uint8ArrayList>> = MultiaddrConnection> (conn: Stream, options?: SecureConnectionOptions): Promise<SecuredConnection<Stream>> {
52+
return this._encrypt(conn, false, options)
5553
}
5654

5755
/**
5856
* Encrypt connection
5957
*/
60-
async _encrypt <Stream extends Duplex<AsyncGenerator<Uint8Array | Uint8ArrayList>> = MultiaddrConnection> (conn: Stream, isServer: boolean, remoteId?: PeerId): Promise<SecuredConnection<Stream>> {
58+
async _encrypt <Stream extends Duplex<AsyncGenerator<Uint8Array | Uint8ArrayList>> = MultiaddrConnection> (conn: Stream, isServer: boolean, options?: SecureConnectionOptions): Promise<SecuredConnection<Stream>> {
6159
const opts: TLSSocketOptions = {
6260
...await generateCertificate(this.peerId),
6361
isServer,
@@ -84,14 +82,14 @@ export class TLS implements ConnectionEncrypter {
8482
}
8583

8684
return new Promise((resolve, reject) => {
87-
const abortTimeout = setTimeout(() => {
85+
options?.signal?.addEventListener('abort', () => {
8886
socket.destroy(new HandshakeTimeoutError())
89-
}, this.timeout)
87+
})
9088

9189
const verifyRemote = (): void => {
9290
const remote = socket.getPeerCertificate()
9391

94-
verifyPeerCertificate(remote.raw, remoteId, this.log)
92+
verifyPeerCertificate(remote.raw, options?.remotePeer, this.log)
9593
.then(remotePeer => {
9694
this.log('remote certificate ok, remote peer %p', remotePeer)
9795

@@ -106,14 +104,10 @@ export class TLS implements ConnectionEncrypter {
106104
.catch((err: Error) => {
107105
reject(err)
108106
})
109-
.finally(() => {
110-
clearTimeout(abortTimeout)
111-
})
112107
}
113108

114109
socket.on('error', (err: Error) => {
115110
reject(err)
116-
clearTimeout(abortTimeout)
117111
})
118112
socket.once('secure', (evt) => {
119113
this.log('verifying remote certificate')

packages/connection-encrypter-tls/test/index.spec.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,12 @@ describe('tls', () => {
4343
})
4444

4545
await Promise.all([
46-
encrypter.secureInbound(inbound, remotePeer),
47-
encrypter.secureOutbound(outbound, wrongPeer)
46+
encrypter.secureInbound(inbound, {
47+
remotePeer
48+
}),
49+
encrypter.secureOutbound(outbound, {
50+
remotePeer: wrongPeer
51+
})
4852
]).then(() => expect.fail('should have failed'), (err) => {
4953
expect(err).to.exist()
5054
expect(err).to.have.property('name', 'UnexpectedPeerError')
@@ -69,8 +73,12 @@ describe('tls', () => {
6973
})
7074

7175
await expect(Promise.all([
72-
encrypter.secureInbound(inbound),
73-
encrypter.secureOutbound(outbound, localPeer)
76+
encrypter.secureInbound(inbound, {
77+
remotePeer
78+
}),
79+
encrypter.secureOutbound(outbound, {
80+
remotePeer: localPeer
81+
})
7482
]))
7583
.to.eventually.be.rejected.with.property('name', 'InvalidParametersError')
7684
})

packages/interface-compliance-tests/src/connection-encryption/index.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,9 @@ export default (common: TestSetup<ConnectionEncrypter, ConnectionEncrypterSetupA
5454
outboundResult
5555
] = await Promise.all([
5656
cryptoRemote.secureInbound(localConn),
57-
crypto.secureOutbound(remoteConn, remotePeer)
57+
crypto.secureOutbound(remoteConn, {
58+
remotePeer
59+
})
5860
])
5961

6062
// Echo server
@@ -84,7 +86,9 @@ export default (common: TestSetup<ConnectionEncrypter, ConnectionEncrypterSetupA
8486
outboundResult
8587
] = await Promise.all([
8688
cryptoRemote.secureInbound(localConn),
87-
crypto.secureOutbound(remoteConn, remotePeer)
89+
crypto.secureOutbound(remoteConn, {
90+
remotePeer
91+
})
8892
])
8993

9094
// Inbound should return the initiator (local) peer
@@ -97,8 +101,12 @@ export default (common: TestSetup<ConnectionEncrypter, ConnectionEncrypterSetupA
97101
const [localConn, remoteConn] = createMaConnPair()
98102

99103
await Promise.all([
100-
cryptoRemote.secureInbound(localConn, mitmPeer),
101-
crypto.secureOutbound(remoteConn, remotePeer)
104+
cryptoRemote.secureInbound(localConn, {
105+
remotePeer: mitmPeer
106+
}),
107+
crypto.secureOutbound(remoteConn, {
108+
remotePeer
109+
})
102110
]).then(() => expect.fail(), (err) => {
103111
expect(err).to.exist()
104112
expect(err).to.have.property('name', 'UnexpectedPeerError')

packages/interface/src/connection-encrypter/index.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,18 @@
11
import type { MultiaddrConnection } from '../connection/index.js'
2+
import type { AbortOptions } from '../index.js'
23
import type { PeerId } from '../peer-id/index.js'
34
import type { Duplex } from 'it-stream-types'
45
import type { Uint8ArrayList } from 'uint8arraylist'
56

7+
/**
8+
* If the remote PeerId is known and passed as an option, the securing operation
9+
* will throw if the remote peer cannot prove it has the private key that
10+
* corresponds to the public key the remote PeerId is derived from.
11+
*/
12+
export interface SecureConnectionOptions extends AbortOptions {
13+
remotePeer?: PeerId
14+
}
15+
616
/**
717
* A libp2p connection encrypter module must be compliant to this interface
818
* to ensure all exchanged data between two peers is encrypted.
@@ -15,14 +25,14 @@ export interface ConnectionEncrypter<Extension = unknown> {
1525
* pass it for extra verification, otherwise it will be determined during
1626
* the handshake.
1727
*/
18-
secureOutbound <Stream extends Duplex<AsyncGenerator<Uint8Array | Uint8ArrayList>> = MultiaddrConnection> (connection: Stream, remotePeer?: PeerId): Promise<SecuredConnection<Stream, Extension>>
28+
secureOutbound <Stream extends Duplex<AsyncGenerator<Uint8Array | Uint8ArrayList>> = MultiaddrConnection> (connection: Stream, options?: SecureConnectionOptions): Promise<SecuredConnection<Stream, Extension>>
1929

2030
/**
2131
* Decrypt incoming data. If the remote PeerId is known,
2232
* pass it for extra verification, otherwise it will be determined during
2333
* the handshake
2434
*/
25-
secureInbound <Stream extends Duplex<AsyncGenerator<Uint8Array | Uint8ArrayList>> = MultiaddrConnection> (connection: Stream, remotePeer?: PeerId): Promise<SecuredConnection<Stream, Extension>>
35+
secureInbound <Stream extends Duplex<AsyncGenerator<Uint8Array | Uint8ArrayList>> = MultiaddrConnection> (connection: Stream, options?: SecureConnectionOptions): Promise<SecuredConnection<Stream, Extension>>
2636
}
2737

2838
export interface SecuredConnection<Stream = any, Extension = unknown> {

packages/interface/src/transport/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ export enum FaultTolerance {
100100
NO_FATAL
101101
}
102102

103-
export interface UpgraderOptions<ConnectionUpgradeEvents extends ProgressEvent = ProgressEvent> extends ProgressOptions<ConnectionUpgradeEvents> {
103+
export interface UpgraderOptions<ConnectionUpgradeEvents extends ProgressEvent = ProgressEvent> extends ProgressOptions<ConnectionUpgradeEvents>, AbortOptions {
104104
skipEncryption?: boolean
105105
skipProtection?: boolean
106106
muxerFactory?: StreamMuxerFactory

0 commit comments

Comments
 (0)