Skip to content

Commit

Permalink
chore: use same crypto provider as x509
Browse files Browse the repository at this point in the history
  • Loading branch information
achingbrain committed Nov 15, 2024
1 parent a7666c5 commit 3f6b47c
Show file tree
Hide file tree
Showing 2 changed files with 63 additions and 18 deletions.
30 changes: 13 additions & 17 deletions packages/auto-tls/src/auto-tls.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ClientAuth } from '@libp2p/http-fetch/auth'
import { serviceDependencies, start, stop } from '@libp2p/interface'
import { serviceCapabilities, serviceDependencies, start, stop } from '@libp2p/interface'
import { debounce } from '@libp2p/utils/debounce'
import { X509Certificate } from '@peculiar/x509'
import * as acme from 'acme-client'
Expand All @@ -10,15 +10,14 @@ import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
import { DEFAULT_ACCOUNT_PRIVATE_KEY_BITS, DEFAULT_ACCOUNT_PRIVATE_KEY_NAME, DEFAULT_ACME_DIRECTORY, DEFAULT_CERTIFICATE_DATASTORE_KEY, DEFAULT_CERTIFICATE_PRIVATE_KEY_BITS, DEFAULT_CERTIFICATE_PRIVATE_KEY_NAME, DEFAULT_FORGE_DOMAIN, DEFAULT_FORGE_ENDPOINT, DEFAULT_PROVISION_DELAY, DEFAULT_PROVISION_TIMEOUT, DEFAULT_RENEWAL_THRESHOLD } from './constants.js'
import { DomainMapper } from './domain-mapper.js'
import { importFromPem, loadOrCreateKey, supportedAddressesFilter } from './utils.js'
import { createCsr, importFromPem, loadOrCreateKey, supportedAddressesFilter } from './utils.js'
import type { AutoTLSComponents, AutoTLSInit, AutoTLS as AutoTLSInterface } from './index.js'
import type { PeerId, PrivateKey, Logger, TypedEventTarget, Libp2pEvents, AbortOptions } from '@libp2p/interface'
import type { AddressManager } from '@libp2p/interface-internal'
import type { Keychain } from '@libp2p/keychain'
import type { DebouncedFunction } from '@libp2p/utils/debounce'
import type { Multiaddr } from '@multiformats/multiaddr'
import type { Datastore } from 'interface-datastore'
import type { Buffer } from 'node:buffer'

type CertificateEvent = 'certificate:provision' | 'certificate:renew'

Expand Down Expand Up @@ -90,6 +89,10 @@ export class AutoTLS implements AutoTLSInterface {
})
}

readonly [serviceCapabilities]: string[] = [
'@libp2p/auto-tls'
]

get [serviceDependencies] (): string[] {
return [
'@libp2p/identify',
Expand Down Expand Up @@ -185,6 +188,7 @@ export class AutoTLS implements AutoTLSInterface {
}, Math.min(renewAt.getTime() - Date.now(), Math.pow(2, 31) - 1))

// emit a certificate event
this.log('dispatching %s', event)
this.events.safeDispatchEvent(event, {
detail: this.certificate
})
Expand All @@ -200,7 +204,7 @@ export class AutoTLS implements AutoTLSInterface {
this.log('creating new csr')

// create CSR
const csr = await this.loadOrCreateCSR(certificatePrivateKey)
const csr = await createCsr(`*.${this.domain}`, certificatePrivateKey)

this.log('fetching new certificate')

Expand Down Expand Up @@ -256,16 +260,7 @@ export class AutoTLS implements AutoTLSInterface {
}
}

private async loadOrCreateCSR (certificatePrivateKey: string): Promise<Buffer> {
const [, csr] = await acme.crypto.createCsr({
commonName: `*.${this.domain}`,
altNames: []
}, certificatePrivateKey)

return csr
}

async fetchAcmeCertificate (csr: Buffer, multiaddrs: Multiaddr[], options?: AbortOptions): Promise<string> {
async fetchAcmeCertificate (csr: string, multiaddrs: Multiaddr[], options?: AbortOptions): Promise<string> {
const client = new acme.Client({
directoryUrl: this.acmeDirectory.toString(),
accountKey: await loadOrCreateKey(this.keychain, this.accountPrivateKeyName, this.accountPrivateKeyBits)
Expand All @@ -289,9 +284,10 @@ export class AutoTLS implements AutoTLSInterface {
async configureAcmeChallengeResponse (multiaddrs: Multiaddr[], keyAuthorization: string, options?: AbortOptions): Promise<void> {
const addresses = multiaddrs.map(ma => ma.toString())

this.log('asking https://%s/v1/_acme-challenge to respond to the acme DNS challenge on our behalf', this.forgeEndpoint)
const endpoint = `${this.forgeEndpoint}v1/_acme-challenge`
this.log('asking %sv1/_acme-challenge to respond to the acme DNS challenge on our behalf', endpoint)
this.log('dialback public addresses: %s', addresses.join(', '))
const response = await this.clientAuth.authenticatedFetch(`https://${this.forgeEndpoint}/v1/_acme-challenge`, {
const response = await this.clientAuth.authenticatedFetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
Expand All @@ -308,7 +304,7 @@ export class AutoTLS implements AutoTLSInterface {
throw new Error('Invalid response status')
}

this.log('https://%s/v1/_acme-challenge will respond to the acme DNS challenge on our behalf', this.forgeEndpoint)
this.log('%s will respond to the acme DNS challenge on our behalf', endpoint)
}

Check warning on line 308 in packages/auto-tls/src/auto-tls.ts

View check run for this annotation

Codecov / codecov/patch

packages/auto-tls/src/auto-tls.ts#L285-L308

Added lines #L285 - L308 were not covered by tests

private needsRenewal (notAfter?: Date): boolean {
Expand Down
51 changes: 50 additions & 1 deletion packages/auto-tls/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Buffer } from 'node:buffer'
import { createPrivateKey } from 'node:crypto'
import { createPrivateKey, createPublicKey } from 'node:crypto'
import { isIPv4, isIPv6 } from '@chainsafe/is-ip'
import { generateKeyPair, privateKeyFromRaw } from '@libp2p/crypto/keys'
import { isLoopback } from '@libp2p/utils/multiaddr/is-loopback'
import { isPrivate } from '@libp2p/utils/multiaddr/is-private'
import { IP, QUICV1, TCP, WebSockets, WebSocketsSecure, WebTransport } from '@multiformats/multiaddr-matcher'
import { KeyUsageFlags, KeyUsagesExtension, PemConverter, Pkcs10CertificateRequestGenerator, SubjectAlternativeNameExtension, cryptoProvider } from '@peculiar/x509'
import { IncorrectKeyType } from './errors.js'
import type { RSAPrivateKey } from '@libp2p/interface'
import type { Keychain } from '@libp2p/keychain'
Expand Down Expand Up @@ -99,3 +100,51 @@ export function getPublicIps (addrs: Multiaddr[]): Set<string> {

return output
}

export async function createCsr (domain: string, keyPem: string): Promise<string> {
const signingAlgorithm = {
name: 'RSASSA-PKCS1-v1_5',
hash: { name: 'SHA-256' }
}

// have to use the same crypto provider as Pkcs10CertificateRequestGenerator
const crypto = cryptoProvider.get()

const jwk = createPublicKey({
format: 'pem',
key: keyPem
}).export({
format: 'jwk'
})

/* Decode PEM and import into CryptoKeyPair */
const privateKeyDec = PemConverter.decodeFirst(keyPem.toString())
const privateKey = await crypto.subtle.importKey('pkcs8', privateKeyDec, signingAlgorithm, true, ['sign'])
const publicKey = await crypto.subtle.importKey('jwk', jwk, signingAlgorithm, true, ['verify'])

const extensions = [
/* https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.3 */
new KeyUsagesExtension(KeyUsageFlags.digitalSignature | KeyUsageFlags.keyEncipherment), // eslint-disable-line no-bitwise

/* https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.6 */
new SubjectAlternativeNameExtension([{ type: 'dns', value: domain }])
]

/* Create CSR */
const csr = await Pkcs10CertificateRequestGenerator.create({
keys: {
privateKey,
publicKey
},
extensions,
signingAlgorithm,
name: [{
// @ts-expect-error herp
CN: [{
utf8String: domain
}]
}]
}, crypto)

return csr.toString('pem')
}

0 comments on commit 3f6b47c

Please sign in to comment.