diff --git a/packages/connection-encrypter-plaintext/src/index.ts b/packages/connection-encrypter-plaintext/src/index.ts index 1baa1a8b11..5c4d4f94c9 100644 --- a/packages/connection-encrypter-plaintext/src/index.ts +++ b/packages/connection-encrypter-plaintext/src/index.ts @@ -26,7 +26,7 @@ import { peerIdFromBytes } from '@libp2p/peer-id' import { createFromPubKey } from '@libp2p/peer-id-factory' 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 } from '@libp2p/interface' +import type { ComponentLogger, Logger, MultiaddrConnection, ConnectionEncrypter, SecuredConnection, PeerId, PublicKey as PubKey, SecureConnectionOptions } from '@libp2p/interface' import type { Duplex } from 'it-stream-types' import type { Uint8ArrayList } from 'uint8arraylist' @@ -37,24 +37,14 @@ export interface PlaintextComponents { logger: ComponentLogger } -export interface PlaintextInit { - /** - * The peer id exchange must complete within this many milliseconds - * (default: 1000) - */ - timeout?: number -} - class Plaintext implements ConnectionEncrypter { public protocol: string = PROTOCOL private readonly peerId: PeerId private readonly log: Logger - private readonly timeout: number - constructor (components: PlaintextComponents, init: PlaintextInit = {}) { + constructor (components: PlaintextComponents) { this.peerId = components.peerId this.log = components.logger.forComponent('libp2p:plaintext') - this.timeout = init.timeout ?? 1000 } readonly [Symbol.toStringTag] = '@libp2p/plaintext' @@ -63,19 +53,18 @@ class Plaintext implements ConnectionEncrypter { '@libp2p/connection-encryption' ] - async secureInbound > = MultiaddrConnection> (conn: Stream, remoteId?: PeerId): Promise> { - return this._encrypt(this.peerId, conn, remoteId) + async secureInbound> = MultiaddrConnection>(conn: Stream, options?: SecureConnectionOptions): Promise> { + return this._encrypt(this.peerId, conn, options) } - async secureOutbound > = MultiaddrConnection> (conn: Stream, remoteId?: PeerId): Promise> { - return this._encrypt(this.peerId, conn, remoteId) + async secureOutbound> = MultiaddrConnection>(conn: Stream, options?: SecureConnectionOptions): Promise> { + return this._encrypt(this.peerId, conn, options) } /** * Encrypt connection */ - async _encrypt > = MultiaddrConnection> (localId: PeerId, conn: Stream, remoteId?: PeerId): Promise> { - const signal = AbortSignal.timeout(this.timeout) + async _encrypt> = MultiaddrConnection>(localId: PeerId, conn: Stream, options?: SecureConnectionOptions): Promise> { const pb = pbStream(conn).pb(Exchange) let type = KeyType.RSA @@ -86,7 +75,7 @@ class Plaintext implements ConnectionEncrypter { type = KeyType.Secp256k1 } - this.log('write pubkey exchange to peer %p', remoteId) + this.log('write pubkey exchange to peer %p', options?.remotePeer) const [ , response @@ -98,13 +87,9 @@ class Plaintext implements ConnectionEncrypter { Type: type, Data: localId.publicKey == null ? new Uint8Array(0) : (PublicKey.decode(localId.publicKey).Data ?? new Uint8Array(0)) } - }, { - signal - }), + }, options), // Get the Exchange message - pb.read({ - signal - }) + pb.read(options) ]) let peerId @@ -143,7 +128,7 @@ class Plaintext implements ConnectionEncrypter { throw new InvalidCryptoExchangeError('Remote did not provide its public key') } - if (remoteId != null && !peerId.equals(remoteId)) { + if (options?.remotePeer != null && !peerId.equals(options?.remotePeer)) { throw new UnexpectedPeerError() } @@ -156,6 +141,6 @@ class Plaintext implements ConnectionEncrypter { } } -export function plaintext (init?: PlaintextInit): (components: PlaintextComponents) => ConnectionEncrypter { - return (components) => new Plaintext(components, init) +export function plaintext (): (components: PlaintextComponents) => ConnectionEncrypter { + return (components) => new Plaintext(components) } diff --git a/packages/connection-encrypter-plaintext/test/index.spec.ts b/packages/connection-encrypter-plaintext/test/index.spec.ts index c2a72f2dbb..50df1ced89 100644 --- a/packages/connection-encrypter-plaintext/test/index.spec.ts +++ b/packages/connection-encrypter-plaintext/test/index.spec.ts @@ -48,8 +48,10 @@ describe('plaintext', () => { }) await Promise.all([ - encrypterRemote.secureInbound(inbound), - encrypter.secureOutbound(outbound, wrongPeer) + encrypter.secureInbound(inbound), + encrypterRemote.secureOutbound(outbound, { + remotePeer: wrongPeer + }) ]).then(() => expect.fail('should have failed'), (err) => { expect(err).to.exist() expect(err).to.have.property('name', 'UnexpectedPeerError') @@ -75,7 +77,9 @@ describe('plaintext', () => { await expect(Promise.all([ encrypter.secureInbound(inbound), - encrypterRemote.secureOutbound(outbound, localPeer) + encrypterRemote.secureOutbound(outbound, { + remotePeer: localPeer + }) ])) .to.eventually.be.rejected.with.property('name', 'InvalidCryptoExchangeError') }) diff --git a/packages/connection-encrypter-tls/src/index.ts b/packages/connection-encrypter-tls/src/index.ts index 36d843d8c0..18c43a2290 100644 --- a/packages/connection-encrypter-tls/src/index.ts +++ b/packages/connection-encrypter-tls/src/index.ts @@ -28,14 +28,6 @@ export interface TLSComponents { logger: ComponentLogger } -export interface TLSInit { - /** - * The peer id exchange must complete within this many milliseconds - * (default: 1000) - */ - timeout?: number -} - -export function tls (init?: TLSInit): (components: TLSComponents) => ConnectionEncrypter { - return (components) => new TLS(components, init) +export function tls (): (components: TLSComponents) => ConnectionEncrypter { + return (components) => new TLS(components) } diff --git a/packages/connection-encrypter-tls/src/tls.ts b/packages/connection-encrypter-tls/src/tls.ts index d64b19033f..8ecf0f5883 100644 --- a/packages/connection-encrypter-tls/src/tls.ts +++ b/packages/connection-encrypter-tls/src/tls.ts @@ -23,8 +23,8 @@ import { serviceCapabilities } from '@libp2p/interface' 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 { TLSComponents } from './index.js' +import type { MultiaddrConnection, ConnectionEncrypter, SecuredConnection, PeerId, Logger, SecureConnectionOptions } from '@libp2p/interface' import type { Duplex } from 'it-stream-types' import type { Uint8ArrayList } from 'uint8arraylist' @@ -32,12 +32,10 @@ export class TLS implements ConnectionEncrypter { public protocol: string = PROTOCOL private readonly log: Logger private readonly peerId: PeerId - private readonly timeout: number - constructor (components: TLSComponents, init: TLSInit = {}) { + constructor (components: TLSComponents) { this.log = components.logger.forComponent('libp2p:tls') this.peerId = components.peerId - this.timeout = init.timeout ?? 1000 } readonly [Symbol.toStringTag] = '@libp2p/tls' @@ -46,18 +44,18 @@ export class TLS implements ConnectionEncrypter { '@libp2p/connection-encryption' ] - async secureInbound > = MultiaddrConnection> (conn: Stream, remoteId?: PeerId): Promise> { - return this._encrypt(conn, true, remoteId) + async secureInbound > = MultiaddrConnection> (conn: Stream, options?: SecureConnectionOptions): Promise> { + return this._encrypt(conn, true, options) } - async secureOutbound > = MultiaddrConnection> (conn: Stream, remoteId?: PeerId): Promise> { - return this._encrypt(conn, false, remoteId) + async secureOutbound > = MultiaddrConnection> (conn: Stream, options?: SecureConnectionOptions): Promise> { + return this._encrypt(conn, false, options) } /** * Encrypt connection */ - async _encrypt > = MultiaddrConnection> (conn: Stream, isServer: boolean, remoteId?: PeerId): Promise> { + async _encrypt > = MultiaddrConnection> (conn: Stream, isServer: boolean, options?: SecureConnectionOptions): Promise> { const opts: TLSSocketOptions = { ...await generateCertificate(this.peerId), isServer, @@ -84,14 +82,14 @@ export class TLS implements ConnectionEncrypter { } return new Promise((resolve, reject) => { - const abortTimeout = setTimeout(() => { + options?.signal?.addEventListener('abort', () => { socket.destroy(new HandshakeTimeoutError()) - }, this.timeout) + }) const verifyRemote = (): void => { const remote = socket.getPeerCertificate() - verifyPeerCertificate(remote.raw, remoteId, this.log) + verifyPeerCertificate(remote.raw, options?.remotePeer, this.log) .then(remotePeer => { this.log('remote certificate ok, remote peer %p', remotePeer) @@ -106,14 +104,10 @@ export class TLS implements ConnectionEncrypter { .catch((err: Error) => { reject(err) }) - .finally(() => { - clearTimeout(abortTimeout) - }) } socket.on('error', (err: Error) => { reject(err) - clearTimeout(abortTimeout) }) socket.once('secure', (evt) => { this.log('verifying remote certificate') diff --git a/packages/connection-encrypter-tls/test/index.spec.ts b/packages/connection-encrypter-tls/test/index.spec.ts index 135d3a3f03..f97e4bd647 100644 --- a/packages/connection-encrypter-tls/test/index.spec.ts +++ b/packages/connection-encrypter-tls/test/index.spec.ts @@ -43,8 +43,12 @@ describe('tls', () => { }) await Promise.all([ - encrypter.secureInbound(inbound, remotePeer), - encrypter.secureOutbound(outbound, wrongPeer) + encrypter.secureInbound(inbound, { + remotePeer + }), + encrypter.secureOutbound(outbound, { + remotePeer: wrongPeer + }) ]).then(() => expect.fail('should have failed'), (err) => { expect(err).to.exist() expect(err).to.have.property('name', 'UnexpectedPeerError') @@ -69,8 +73,12 @@ describe('tls', () => { }) await expect(Promise.all([ - encrypter.secureInbound(inbound), - encrypter.secureOutbound(outbound, localPeer) + encrypter.secureInbound(inbound, { + remotePeer + }), + encrypter.secureOutbound(outbound, { + remotePeer: localPeer + }) ])) .to.eventually.be.rejected.with.property('name', 'InvalidParametersError') }) diff --git a/packages/interface-compliance-tests/src/connection-encryption/index.ts b/packages/interface-compliance-tests/src/connection-encryption/index.ts index 7918e69b7a..d0154f9dee 100644 --- a/packages/interface-compliance-tests/src/connection-encryption/index.ts +++ b/packages/interface-compliance-tests/src/connection-encryption/index.ts @@ -54,7 +54,9 @@ export default (common: TestSetup expect.fail(), (err) => { expect(err).to.exist() expect(err).to.have.property('name', 'UnexpectedPeerError') diff --git a/packages/interface/src/connection-encrypter/index.ts b/packages/interface/src/connection-encrypter/index.ts index 40e85919e4..580c5c47b5 100644 --- a/packages/interface/src/connection-encrypter/index.ts +++ b/packages/interface/src/connection-encrypter/index.ts @@ -1,8 +1,18 @@ import type { MultiaddrConnection } from '../connection/index.js' +import type { AbortOptions } from '../index.js' import type { PeerId } from '../peer-id/index.js' import type { Duplex } from 'it-stream-types' import type { Uint8ArrayList } from 'uint8arraylist' +/** + * If the remote PeerId is known and passed as an option, the securing operation + * will throw if the remote peer cannot prove it has the private key that + * corresponds to the public key the remote PeerId is derived from. + */ +export interface SecureConnectionOptions extends AbortOptions { + remotePeer?: PeerId +} + /** * A libp2p connection encrypter module must be compliant to this interface * to ensure all exchanged data between two peers is encrypted. @@ -15,14 +25,14 @@ export interface ConnectionEncrypter { * pass it for extra verification, otherwise it will be determined during * the handshake. */ - secureOutbound > = MultiaddrConnection> (connection: Stream, remotePeer?: PeerId): Promise> + secureOutbound > = MultiaddrConnection> (connection: Stream, options?: SecureConnectionOptions): Promise> /** * Decrypt incoming data. If the remote PeerId is known, * pass it for extra verification, otherwise it will be determined during * the handshake */ - secureInbound > = MultiaddrConnection> (connection: Stream, remotePeer?: PeerId): Promise> + secureInbound > = MultiaddrConnection> (connection: Stream, options?: SecureConnectionOptions): Promise> } export interface SecuredConnection { diff --git a/packages/interface/src/transport/index.ts b/packages/interface/src/transport/index.ts index 1cb8ab6ce9..b0db5d0a42 100644 --- a/packages/interface/src/transport/index.ts +++ b/packages/interface/src/transport/index.ts @@ -100,7 +100,7 @@ export enum FaultTolerance { NO_FATAL } -export interface UpgraderOptions extends ProgressOptions { +export interface UpgraderOptions extends ProgressOptions, AbortOptions { skipEncryption?: boolean skipProtection?: boolean muxerFactory?: StreamMuxerFactory diff --git a/packages/libp2p/src/upgrader.ts b/packages/libp2p/src/upgrader.ts index d3d8d49186..a6a2f7ff8c 100644 --- a/packages/libp2p/src/upgrader.ts +++ b/packages/libp2p/src/upgrader.ts @@ -6,7 +6,7 @@ import { createConnection } from './connection/index.js' import { INBOUND_UPGRADE_TIMEOUT } from './connection-manager/constants.js' import { ConnectionDeniedError, ConnectionInterceptedError, EncryptionFailedError, MuxerUnavailableError } from './errors.js' import { DEFAULT_MAX_INBOUND_STREAMS, DEFAULT_MAX_OUTBOUND_STREAMS } from './registrar.js' -import type { Libp2pEvents, AbortOptions, ComponentLogger, MultiaddrConnection, Connection, Stream, ConnectionProtector, NewStreamOptions, ConnectionEncrypter, SecuredConnection, ConnectionGater, TypedEventTarget, Metrics, PeerId, PeerStore, StreamMuxer, StreamMuxerFactory, Upgrader, UpgraderOptions, ConnectionLimits } from '@libp2p/interface' +import type { Libp2pEvents, AbortOptions, ComponentLogger, MultiaddrConnection, Connection, Stream, ConnectionProtector, NewStreamOptions, ConnectionEncrypter, SecuredConnection, ConnectionGater, TypedEventTarget, Metrics, PeerId, PeerStore, StreamMuxer, StreamMuxerFactory, Upgrader, UpgraderOptions, ConnectionLimits, SecureConnectionOptions } from '@libp2p/interface' import type { ConnectionManager, Registrar } from '@libp2p/interface-internal' const DEFAULT_PROTOCOL_SELECT_TIMEOUT = 30000 @@ -296,7 +296,10 @@ export class DefaultUpgrader implements Upgrader { conn: encryptedConn, remotePeer, protocol: cryptoProtocol - } = await this._encryptOutbound(protectedConn, remotePeerId)) + } = await this._encryptOutbound(protectedConn, { + ...opts, + remotePeer: remotePeerId + })) const maConn: MultiaddrConnection = { ...protectedConn, @@ -623,7 +626,7 @@ export class DefaultUpgrader implements Upgrader { /** * Attempts to encrypt the incoming `connection` with the provided `cryptos` */ - async _encryptInbound (connection: MultiaddrConnection): Promise { + async _encryptInbound (connection: MultiaddrConnection, options?: AbortOptions): Promise { const protocols = Array.from(this.connectionEncryption.keys()) connection.log('handling inbound crypto protocol selection', protocols) @@ -640,7 +643,7 @@ export class DefaultUpgrader implements Upgrader { connection.log('encrypting inbound connection using', protocol) return { - ...await encrypter.secureInbound(stream), + ...await encrypter.secureInbound(stream, options), protocol } } catch (err: any) { @@ -653,7 +656,7 @@ export class DefaultUpgrader implements Upgrader { * Attempts to encrypt the given `connection` with the provided connection encrypters. * The first `ConnectionEncrypter` module to succeed will be used */ - async _encryptOutbound (connection: MultiaddrConnection, remotePeerId?: PeerId): Promise { + async _encryptOutbound (connection: MultiaddrConnection, options?: SecureConnectionOptions): Promise { const protocols = Array.from(this.connectionEncryption.keys()) connection.log('selecting outbound crypto protocol', protocols) @@ -674,14 +677,14 @@ export class DefaultUpgrader implements Upgrader { throw new Error(`no crypto module found for ${protocol}`) } - connection.log('encrypting outbound connection to %p using %s', remotePeerId, encrypter) + connection.log('encrypting outbound connection to %p using %s', options?.remotePeer, encrypter) return { - ...await encrypter.secureOutbound(stream, remotePeerId), + ...await encrypter.secureOutbound(stream, options), protocol } } catch (err: any) { - connection.log.error('encrypting outbound connection to %p failed', remotePeerId, err) + connection.log.error('encrypting outbound connection to %p failed', options?.remotePeer, err) throw new EncryptionFailedError(err.message) } } diff --git a/packages/transport-webrtc/src/private-to-public/transport.ts b/packages/transport-webrtc/src/private-to-public/transport.ts index d10b926cbc..6c70fd4520 100644 --- a/packages/transport-webrtc/src/private-to-public/transport.ts +++ b/packages/transport-webrtc/src/private-to-public/transport.ts @@ -258,7 +258,10 @@ export class WebRTCDirectTransport implements Transport { // For outbound connections, the remote is expected to start the noise handshake. // Therefore, we need to secure an inbound noise connection from the remote. - await connectionEncrypter.secureInbound(wrappedDuplex, theirPeerId) + await connectionEncrypter.secureInbound(wrappedDuplex, { + signal, + remotePeer: theirPeerId + }) return await options.upgrader.upgradeOutbound(maConn, { skipProtection: true, skipEncryption: true, muxerFactory }) } catch (err) { diff --git a/packages/transport-webtransport/src/index.ts b/packages/transport-webtransport/src/index.ts index d87852eca5..df2a6ffd79 100644 --- a/packages/transport-webtransport/src/index.ts +++ b/packages/transport-webtransport/src/index.ts @@ -288,7 +288,10 @@ class WebTransportTransport implements Transport { const n = noise()(this.components) onProgress?.(new CustomProgressEvent('webtransport:secure-outbound-connection')) - const { remoteExtensions } = await n.secureOutbound(duplex, remotePeer) + const { remoteExtensions } = await n.secureOutbound(duplex, { + signal, + remotePeer + }) onProgress?.(new CustomProgressEvent('webtransport:close-authentication-stream')) // We're done with this authentication stream