Skip to content

Commit

Permalink
feat!: use .name property instead of .code for errors (#2655)
Browse files Browse the repository at this point in the history
JavaScript errors have a `.name` property that can be used to
disambiguate the type of error.

libp2p has used the `.code` property for this until now, but we will
soon use that field to indicate remote errors, so switch to using
the `.name` property.

BREAKING CHANGE: The `.code` property has been removed from most errors, use `.name` instead
  • Loading branch information
achingbrain committed Sep 6, 2024
1 parent b637e8b commit 8d18c3d
Show file tree
Hide file tree
Showing 194 changed files with 1,862 additions and 1,394 deletions.
5 changes: 3 additions & 2 deletions packages/connection-encrypter-plaintext/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
"build": "aegir build",
"test": "aegir test",
"clean": "aegir clean",
"generate": "protons ./src/pb/index.proto",
"generate": "protons ./src/pb/proto.proto",
"lint": "aegir lint",
"test:chrome": "aegir test -t browser --cov",
"test:chrome-webworker": "aegir test -t webworker",
Expand All @@ -59,7 +59,8 @@
"it-protobuf-stream": "^1.1.3",
"it-stream-types": "^2.0.1",
"protons-runtime": "^5.4.0",
"uint8arraylist": "^2.4.8"
"uint8arraylist": "^2.4.8",
"uint8arrays": "^5.1.0"
},
"devDependencies": {
"@libp2p/interface-compliance-tests": "^5.4.12",
Expand Down
40 changes: 24 additions & 16 deletions packages/connection-encrypter-plaintext/src/pb/proto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
/* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */
/* eslint-disable @typescript-eslint/no-empty-interface */

import { encodeMessage, decodeMessage, message, enumeration } from 'protons-runtime'
import type { Codec } from 'protons-runtime'
import { type Codec, decodeMessage, type DecodeOptions, encodeMessage, enumeration, message } from 'protons-runtime'
import { alloc as uint8ArrayAlloc } from 'uint8arrays/alloc'
import type { Uint8ArrayList } from 'uint8arraylist'

export interface Exchange {
Expand Down Expand Up @@ -36,7 +36,7 @@ export namespace Exchange {
if (opts.lengthDelimited !== false) {
w.ldelim()
}
}, (reader, length) => {
}, (reader, length, opts = {}) => {
const obj: any = {}

const end = length == null ? reader.len : reader.pos + length
Expand All @@ -45,15 +45,20 @@ export namespace Exchange {
const tag = reader.uint32()

switch (tag >>> 3) {
case 1:
case 1: {
obj.id = reader.bytes()
break
case 2:
obj.pubkey = PublicKey.codec().decode(reader, reader.uint32())
}
case 2: {
obj.pubkey = PublicKey.codec().decode(reader, reader.uint32(), {
limits: opts.limits?.pubkey
})
break
default:
}
default: {
reader.skipType(tag & 7)
break
}
}
}

Expand All @@ -68,8 +73,8 @@ export namespace Exchange {
return encodeMessage(obj, Exchange.codec())
}

export const decode = (buf: Uint8Array | Uint8ArrayList): Exchange => {
return decodeMessage(buf, Exchange.codec())
export const decode = (buf: Uint8Array | Uint8ArrayList, opts?: DecodeOptions<Exchange>): Exchange => {
return decodeMessage(buf, Exchange.codec(), opts)
}
}

Expand Down Expand Up @@ -120,10 +125,10 @@ export namespace PublicKey {
if (opts.lengthDelimited !== false) {
w.ldelim()
}
}, (reader, length) => {
}, (reader, length, opts = {}) => {
const obj: any = {
Type: KeyType.RSA,
Data: new Uint8Array(0)
Data: uint8ArrayAlloc(0)
}

const end = length == null ? reader.len : reader.pos + length
Expand All @@ -132,15 +137,18 @@ export namespace PublicKey {
const tag = reader.uint32()

switch (tag >>> 3) {
case 1:
case 1: {
obj.Type = KeyType.codec().decode(reader)
break
case 2:
}
case 2: {
obj.Data = reader.bytes()
break
default:
}
default: {
reader.skipType(tag & 7)
break
}
}
}

Expand All @@ -155,7 +163,7 @@ export namespace PublicKey {
return encodeMessage(obj, PublicKey.codec())
}

export const decode = (buf: Uint8Array | Uint8ArrayList): PublicKey => {
return decodeMessage(buf, PublicKey.codec())
export const decode = (buf: Uint8Array | Uint8ArrayList, opts?: DecodeOptions<PublicKey>): PublicKey => {
return decodeMessage(buf, PublicKey.codec(), opts)
}
}
8 changes: 2 additions & 6 deletions packages/connection-encrypter-plaintext/test/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
/* eslint-env mocha */

import {
InvalidCryptoExchangeError,
UnexpectedPeerError
} from '@libp2p/interface'
import { mockMultiaddrConnPair } from '@libp2p/interface-compliance-tests/mocks'
import { defaultLogger } from '@libp2p/logger'
import { peerIdFromBytes } from '@libp2p/peer-id'
Expand Down Expand Up @@ -56,7 +52,7 @@ describe('plaintext', () => {
encrypter.secureOutbound(outbound, wrongPeer)
]).then(() => expect.fail('should have failed'), (err) => {
expect(err).to.exist()
expect(err).to.have.property('code', UnexpectedPeerError.code)
expect(err).to.have.property('name', 'UnexpectedPeerError')
})
})

Expand All @@ -81,6 +77,6 @@ describe('plaintext', () => {
encrypter.secureInbound(inbound),
encrypterRemote.secureOutbound(outbound, localPeer)
]))
.to.eventually.be.rejected.with.property('code', InvalidCryptoExchangeError.code)
.to.eventually.be.rejected.with.property('name', 'InvalidCryptoExchangeError')
})
})
19 changes: 19 additions & 0 deletions packages/connection-encrypter-tls/src/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* The handshake timed out
*/
export class HandshakeTimeoutError extends Error {
constructor (message = 'Handshake timeout') {
super(message)
this.name = 'HandshakeTimeoutError'
}
}

/**
* The certificate was invalid
*/
export class InvalidCertificateError extends Error {
constructor (message = 'Invalid certificate') {
super(message)
this.name = 'InvalidCertificateError'
}
}
8 changes: 4 additions & 4 deletions packages/connection-encrypter-tls/src/pb/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
/* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */
/* eslint-disable @typescript-eslint/no-empty-interface */

import { type Codec, decodeMessage, encodeMessage, enumeration, message } from 'protons-runtime'
import { type Codec, decodeMessage, type DecodeOptions, encodeMessage, enumeration, message } from 'protons-runtime'
import type { Uint8ArrayList } from 'uint8arraylist'

export enum KeyType {
Expand Down Expand Up @@ -54,7 +54,7 @@ export namespace PublicKey {
if (opts.lengthDelimited !== false) {
w.ldelim()
}
}, (reader, length) => {
}, (reader, length, opts = {}) => {
const obj: any = {}

const end = length == null ? reader.len : reader.pos + length
Expand Down Expand Up @@ -89,7 +89,7 @@ export namespace PublicKey {
return encodeMessage(obj, PublicKey.codec())
}

export const decode = (buf: Uint8Array | Uint8ArrayList): PublicKey => {
return decodeMessage(buf, PublicKey.codec())
export const decode = (buf: Uint8Array | Uint8ArrayList, opts?: DecodeOptions<PublicKey>): PublicKey => {
return decodeMessage(buf, PublicKey.codec(), opts)
}
}
5 changes: 3 additions & 2 deletions packages/connection-encrypter-tls/src/tls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
*/

import { TLSSocket, type TLSSocketOptions, connect } from 'node:tls'
import { CodeError, serviceCapabilities } from '@libp2p/interface'
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'
Expand Down Expand Up @@ -84,7 +85,7 @@ export class TLS implements ConnectionEncrypter {

return new Promise((resolve, reject) => {
const abortTimeout = setTimeout(() => {
socket.destroy(new CodeError('Handshake timeout', 'ERR_HANDSHAKE_TIMEOUT'))
socket.destroy(new HandshakeTimeoutError())
}, this.timeout)

const verifyRemote = (): void => {
Expand Down
51 changes: 24 additions & 27 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 { CodeError, InvalidCryptoExchangeError, UnexpectedPeerError } from '@libp2p/interface'
import { InvalidCryptoExchangeError, InvalidParametersError, 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 @@ -11,7 +11,8 @@ import { pushable } from 'it-pushable'
import { concat as uint8ArrayConcat } from 'uint8arrays/concat'
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
import { KeyType, PublicKey } from '../src/pb/index.js'
import { InvalidCertificateError } from './errors.js'
import { KeyType, PublicKey } from './pb/index.js'
import type { PeerId, PublicKey as Libp2pPublicKey, Logger } from '@libp2p/interface'
import type { Duplex } from 'it-stream-types'
import type { Uint8ArrayList } from 'uint8arraylist'
Expand All @@ -33,12 +34,12 @@ export async function verifyPeerCertificate (rawCertificate: Uint8Array, expecte

if (x509Cert.notBefore.getTime() > now) {
log?.error('the certificate was not valid yet')
throw new CodeError('The certificate is not valid yet', 'ERR_INVALID_CERTIFICATE')
throw new InvalidCertificateError('The certificate is not valid yet')
}

if (x509Cert.notAfter.getTime() < now) {
log?.error('the certificate has expired')
throw new CodeError('The certificate has expired', 'ERR_INVALID_CERTIFICATE')
throw new InvalidCertificateError('The certificate has expired')
}

const certSignatureValid = await x509Cert.verify()
Expand All @@ -59,7 +60,7 @@ export async function verifyPeerCertificate (rawCertificate: Uint8Array, expecte

if (libp2pPublicKeyExtension == null || libp2pPublicKeyExtension.type !== LIBP2P_PUBLIC_KEY_EXTENSION) {
log?.error('the certificate did not include the libp2p public key extension')
throw new CodeError('The certificate did not include the libp2p public key extension', 'ERR_INVALID_CERTIFICATE')
throw new InvalidCertificateError('The certificate did not include the libp2p public key extension')
}

const { result: libp2pKeySequence } = asn1js.fromBER(libp2pPublicKeyExtension.value)
Expand Down Expand Up @@ -104,34 +105,17 @@ export async function verifyPeerCertificate (rawCertificate: Uint8Array, expecte
}

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

const alg = {
name: 'ECDSA',
namedCurve: 'P-256',
hash: 'SHA-256'
}

const keys = await crypto.subtle.generateKey(alg, true, ['sign'])

const certPublicKeySpki = await crypto.subtle.exportKey('spki', keys.publicKey)
const dataToSign = encodeSignatureData(certPublicKeySpki)

if (peerId.privateKey == null) {
throw new InvalidCryptoExchangeError('Private key was missing from PeerId')
throw new InvalidParametersError('Private key was missing from PeerId')
}

const privateKey = await unmarshalPrivateKey(peerId.privateKey)
const sig = await privateKey.sign(dataToSign)

let keyType: KeyType
let keyData: Uint8Array

if (peerId.publicKey == null) {
throw new CodeError('Public key missing from PeerId', 'ERR_INVALID_PEER_ID')
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
Expand All @@ -146,9 +130,22 @@ export async function generateCertificate (peerId: PeerId): Promise<{ cert: stri
keyType = KeyType.RSA
keyData = publicKey.marshal()
} else {
throw new CodeError('Unknown PeerId type', 'ERR_UNKNOWN_PEER_ID_TYPE')
throw new InvalidParametersError('PeerId had unknown or unsupported type')
}

const now = Date.now()

const alg = {
name: 'ECDSA',
namedCurve: 'P-256',
hash: 'SHA-256'
}

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
notAfter.setMilliseconds(0)
Expand Down
8 changes: 2 additions & 6 deletions packages/connection-encrypter-tls/test/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
/* eslint-env mocha */

import {
InvalidCryptoExchangeError,
UnexpectedPeerError
} from '@libp2p/interface'
import { mockMultiaddrConnPair } from '@libp2p/interface-compliance-tests/mocks'
import { defaultLogger } from '@libp2p/logger'
import { peerIdFromBytes } from '@libp2p/peer-id'
Expand Down Expand Up @@ -51,7 +47,7 @@ describe('tls', () => {
encrypter.secureOutbound(outbound, wrongPeer)
]).then(() => expect.fail('should have failed'), (err) => {
expect(err).to.exist()
expect(err).to.have.property('code', UnexpectedPeerError.code)
expect(err).to.have.property('name', 'UnexpectedPeerError')
})
})

Expand All @@ -76,6 +72,6 @@ describe('tls', () => {
encrypter.secureInbound(inbound),
encrypter.secureOutbound(outbound, localPeer)
]))
.to.eventually.be.rejected.with.property('code', InvalidCryptoExchangeError.code)
.to.eventually.be.rejected.with.property('name', 'InvalidParametersError')
})
})
6 changes: 3 additions & 3 deletions packages/connection-encrypter-tls/test/utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,12 @@ describe('utils', () => {

it('should reject certificate with a the wrong peer id in the extension', async () => {
await expect(verifyPeerCertificate(testVectors.wrongPeerIdInExtension.cert, undefined, logger('libp2p'))).to.eventually.be.rejected
.with.property('code', 'ERR_INVALID_CRYPTO_EXCHANGE')
.with.property('name', 'InvalidCryptoExchangeError')
})

it('should reject certificate with invalid self signature', async () => {
await expect(verifyPeerCertificate(testVectors.invalidCertificateSignature.cert, undefined, logger('libp2p'))).to.eventually.be.rejected
.with.property('code', 'ERR_INVALID_CRYPTO_EXCHANGE')
.with.property('name', 'InvalidCryptoExchangeError')
})

it('should reject certificate with a chain', async () => {
Expand Down Expand Up @@ -72,6 +72,6 @@ describe('utils', () => {
})

await expect(verifyPeerCertificate(new Uint8Array(cert.rawData), undefined, logger('libp2p'))).to.eventually.be.rejected
.with.property('code', 'ERR_INVALID_CRYPTO_EXCHANGE')
.with.property('name', 'InvalidCryptoExchangeError')
})
})
29 changes: 29 additions & 0 deletions packages/crypto/src/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* Signing a message failed
*/
export class SigningError extends Error {
constructor (message = 'An error occurred while signing a message') {
super(message)
this.name = 'SigningError'
}
}

/**
* Verifying a message signature failed
*/
export class VerificationError extends Error {
constructor (message = 'An error occurred while verifying a message') {
super(message)
this.name = 'VerificationError'
}
}

/**
* WebCrypto was not available in the current context
*/
export class WebCryptoMissingError extends Error {
constructor (message = 'Missing Web Crypto API') {
super(message)
this.name = 'WebCryptoMissingError'
}
}
Loading

0 comments on commit 8d18c3d

Please sign in to comment.