diff --git a/src/AcmeAccount.ts b/src/AcmeAccount.ts index 4ce80cc..2c2856c 100644 --- a/src/AcmeAccount.ts +++ b/src/AcmeAccount.ts @@ -1,6 +1,6 @@ import type { AcmeClient } from "./AcmeClient.ts"; import { AcmeOrder, type AcmeOrderObjectSnapshot } from "./AcmeOrder.ts"; -import { generateKeyPair } from "./utils/crypto.ts"; +import { generateKeyPair, type KeyPairAlgorithm } from "./utils/crypto.ts"; import { emailsToAccountContacts } from "./utils/emailsToAccountContacts.ts"; import { jws } from "./utils/jws.ts"; import { AcmeError } from "./errors.ts"; @@ -54,6 +54,7 @@ export type AcmeAccountObjectSnapshot = { export class AcmeAccount { readonly client: AcmeClient; readonly keyPair: CryptoKeyPair; + readonly keyPairAlgorithm?: KeyPairAlgorithm; readonly url: string; /** @@ -67,10 +68,12 @@ export class AcmeAccount { constructor(init: { client: AcmeClient; keyPair: CryptoKeyPair; + keyPairAlgorithm?: KeyPairAlgorithm; url: string; }) { this.client = init.client; this.keyPair = init.keyPair; + this.keyPairAlgorithm = init.keyPairAlgorithm; this.url = init.url; } @@ -132,7 +135,7 @@ export class AcmeAccount { */ async keyRollover(): Promise { const [newKeyPair, oldPublicKeyJwk] = await Promise.all([ - generateKeyPair(), + generateKeyPair(this.keyPairAlgorithm), crypto.subtle.exportKey( "jwk", this.keyPair.publicKey, diff --git a/src/AcmeClient.ts b/src/AcmeClient.ts index c4eabd3..bb48aaa 100644 --- a/src/AcmeClient.ts +++ b/src/AcmeClient.ts @@ -7,7 +7,7 @@ import { AcmeError, BadNonceError, } from "./errors.ts"; -import { generateKeyPair, importHmacKey } from "./utils/crypto.ts"; +import { generateKeyPair, importHmacKey, type KeyPairAlgorithm } from "./utils/crypto.ts"; import { emailsToAccountContacts } from "./utils/emailsToAccountContacts.ts"; import { jws, jwsFetch } from "./utils/jws.ts"; @@ -139,7 +139,7 @@ export class AcmeClient { this.#nonceQueue.push( response.headers.get(REPLAY_NONCE_HEADER_KEY) ?? - await this.#fetchNonce(), + await this.#fetchNonce(), ); if (!response.ok) { @@ -184,9 +184,11 @@ export class AcmeClient { { emails, externalAccountBinding, + keyPairAlgorithm, }: { emails: readonly string[]; externalAccountBinding?: ExternalAccountBinding; + keyPairAlgorithm?: KeyPairAlgorithm; }, ): Promise { if ( @@ -253,6 +255,7 @@ export class AcmeClient { client: this, url: accountUrl, keyPair, + keyPairAlgorithm, }); } @@ -261,7 +264,13 @@ export class AcmeClient { * * @see https://datatracker.ietf.org/doc/html/rfc8555#section-7.3.1 */ - async login({ keyPair }: { keyPair: CryptoKeyPair }): Promise { + async login({ + keyPair, + keyPairAlgorithm, + }: { + keyPair: CryptoKeyPair; + keyPairAlgorithm?: KeyPairAlgorithm; + }): Promise { const response = await this.jwsFetch(this.directory.newAccount, { privateKey: keyPair.privateKey, protected: { @@ -298,6 +307,7 @@ export class AcmeClient { client: this, url: accountUrl, keyPair, + keyPairAlgorithm, }); } } diff --git a/src/AcmeOrder.ts b/src/AcmeOrder.ts index 5866ee9..d78c043 100644 --- a/src/AcmeOrder.ts +++ b/src/AcmeOrder.ts @@ -278,13 +278,14 @@ Expected order status: ${pollUntil}`); */ async finalize(): Promise { const [certKeyPair, orderResponse] = await Promise.all([ - generateKeyPair(), + generateKeyPair(this.account.keyPairAlgorithm), this.fetch(), ]); const csr = encodeBase64Url( await generateCSR({ domains: this.domains, keyPair: certKeyPair, + keyPairAlgorithm: this.account.keyPairAlgorithm, }), ); diff --git a/src/CryptoKeyUtils/importKeyPairFromPemPrivateKey.ts b/src/CryptoKeyUtils/importKeyPairFromPemPrivateKey.ts index 55f1082..9a84a83 100644 --- a/src/CryptoKeyUtils/importKeyPairFromPemPrivateKey.ts +++ b/src/CryptoKeyUtils/importKeyPairFromPemPrivateKey.ts @@ -1,6 +1,10 @@ +import { getAlgorithmProperties, type KeyPairAlgorithm } from "../utils/crypto.ts"; import { extractFirstPemObject } from "../utils/pem.ts"; -async function derivePublicKey(privateKey: CryptoKey): Promise { +async function derivePublicKey( + privateKey: CryptoKey, + keyPairAlgorithm: KeyPairAlgorithm = "ec", +): Promise { // d contains the private info of the key const { d: _discardedPrivateInfo, ...jwkPublic } = { ...await crypto.subtle.exportKey("jwk", privateKey), @@ -11,10 +15,7 @@ async function derivePublicKey(privateKey: CryptoKey): Promise { return crypto.subtle.importKey( "jwk", jwkPublic, - { - name: "ECDSA", - namedCurve: "P-256", - }, + getAlgorithmProperties(keyPairAlgorithm), true, ["verify"], ); @@ -25,20 +26,18 @@ async function derivePublicKey(privateKey: CryptoKey): Promise { */ export async function importKeyPairFromPemPrivateKey( pemPrivateKey: string, + keyPairAlgorithm: KeyPairAlgorithm = "ec", ): Promise { const privateKey = await crypto.subtle.importKey( "pkcs8", extractFirstPemObject(pemPrivateKey), - { - name: "ECDSA", - namedCurve: "P-256", - }, + getAlgorithmProperties(keyPairAlgorithm), true, ["sign"], ); return { privateKey, - publicKey: await derivePublicKey(privateKey), + publicKey: await derivePublicKey(privateKey, keyPairAlgorithm), }; } diff --git a/src/utils/crypto.ts b/src/utils/crypto.ts index b658355..03fa610 100644 --- a/src/utils/crypto.ts +++ b/src/utils/crypto.ts @@ -1,8 +1,34 @@ import { decodeBase64Url } from "./base64.ts"; -export async function generateKeyPair(): Promise { +export type KeyPairAlgorithm = "ec" | "rsa" | "rsa-4096"; + +export function getAlgorithmProperties(keyPairAlgorithm: KeyPairAlgorithm) { + switch (keyPairAlgorithm){ + case "ec": + return { + name: "ECDSA", + namedCurve: "P-256", + }; + case "rsa": + return { + name: "RSASSA-PKCS1-v1_5", + modulusLength: 2048, + publicExponent: new Uint8Array([0x01, 0x00, 0x01]), + hash: { name: "SHA-256" }, + }; + case "rsa-4096": + return { + name: "RSASSA-PKCS1-v1_5", + modulusLength: 4096, + publicExponent: new Uint8Array([0x01, 0x00, 0x01]), + hash: { name: "SHA-256" }, + }; + } +} + +export async function generateKeyPair(keyPairAlgorithm: KeyPairAlgorithm = "ec"): Promise { return await crypto.subtle.generateKey( - { name: "ECDSA", namedCurve: "P-256" }, + getAlgorithmProperties(keyPairAlgorithm), true, ["sign", "verify"], ); diff --git a/src/utils/generateCSR.ts b/src/utils/generateCSR.ts index f37b5bf..d1b0fc4 100644 --- a/src/utils/generateCSR.ts +++ b/src/utils/generateCSR.ts @@ -6,15 +6,14 @@ import { Asn1Encoder } from "../Asn1/Asn1Encoder.ts"; import { splitAtIndex } from "./array.ts"; -import { sign } from "./crypto.ts"; +import { type KeyPairAlgorithm, sign } from "./crypto.ts"; const OIDS = { COMMON_NAME: "2.5.4.3", SUBJECT_ALT_NAME: "2.5.29.17", - ID_EC_PUBLIC_KEY: "1.2.840.10045.2.1", - PRIME256V1: "1.2.840.10045.3.1.7", ECDSA_WITH_SHA256: "1.2.840.10045.4.3.2", - EXTENSION_REQUET: "1.2.840.113549.1.9.14", + RSA_WITH_SHA256: "1.2.840.113549.1.1.11", + EXTENSION_REQUEST: "1.2.840.113549.1.9.14", } as const; /** @@ -23,13 +22,21 @@ const OIDS = { * @see https://datatracker.ietf.org/doc/html/rfc2986 */ export async function generateCSR( - { domains, keyPair }: { domains: readonly string[]; keyPair: CryptoKeyPair }, + { + domains, + keyPair, + keyPairAlgorithm, + }: { + domains: readonly string[]; + keyPair: CryptoKeyPair; + keyPairAlgorithm?: KeyPairAlgorithm; + }, ): Promise> { const certificationRequestInfoSequence = encodeCertificationRequestInfo( { domains, - publicKeyDer: new Uint8Array( - await crypto.subtle.exportKey("raw", keyPair.publicKey), + subjectPKInfo: new Uint8Array( + await crypto.subtle.exportKey("spki", keyPair.publicKey), ), }, ); @@ -47,19 +54,20 @@ export async function generateCSR( certificationRequestInfoSequence, // signatureAlgorithm Asn1Encoder.sequence( - Asn1Encoder.oid(OIDS.ECDSA_WITH_SHA256), + Asn1Encoder.oid(keyPairAlgorithm === "ec" ? OIDS.ECDSA_WITH_SHA256 : OIDS.RSA_WITH_SHA256), ), // signature encodeSignatureBitString( await sign(keyPair.privateKey, certificationRequestInfoSequence), + keyPairAlgorithm, ), ); } function encodeCertificationRequestInfo( - { domains, publicKeyDer }: { + { domains, subjectPKInfo }: { domains: readonly string[]; - publicKeyDer: Uint8Array; + subjectPKInfo: Uint8Array; }, ): Uint8Array { const [mainDomain] = domains; @@ -89,18 +97,12 @@ function encodeCertificationRequestInfo( ), ), // subjectPKInfo - Asn1Encoder.sequence( - Asn1Encoder.sequence( - Asn1Encoder.oid(OIDS.ID_EC_PUBLIC_KEY), - Asn1Encoder.oid(OIDS.PRIME256V1), - ), - Asn1Encoder.bitString(publicKeyDer), - ), + subjectPKInfo, // attributes Asn1Encoder.custom( 0xA0, // Tag: [0]. Asn1Encoder.sequence( - Asn1Encoder.oid(OIDS.EXTENSION_REQUET), + Asn1Encoder.oid(OIDS.EXTENSION_REQUEST), Asn1Encoder.set( Asn1Encoder.sequence( encodeSubjectAlternativeName(domains), @@ -142,11 +144,15 @@ const encodeSubjectAlternativeName = (() => { const encodeSignatureBitString = ( signature: Uint8Array, + keyPairAlgorithm?: KeyPairAlgorithm, ): Uint8Array => { - const [r, s] = splitAtIndex(signature, signature.byteLength / 2); + if (keyPairAlgorithm === "ec") { + const [r, s] = splitAtIndex(signature, signature.byteLength / 2); - return Asn1Encoder.bitString(Asn1Encoder.sequence( - Asn1Encoder.uintBytes(r), - Asn1Encoder.uintBytes(s), - )); + return Asn1Encoder.bitString(Asn1Encoder.sequence( + Asn1Encoder.uintBytes(r), + Asn1Encoder.uintBytes(s), + )); + } + return Asn1Encoder.bitString(signature); }; diff --git a/src/utils/jws.ts b/src/utils/jws.ts index db66105..13b98d6 100644 --- a/src/utils/jws.ts +++ b/src/utils/jws.ts @@ -42,7 +42,7 @@ export const jws = async ( }> => { const jwsWithoutSignature = { protected: encodeBase64Url(JSON.stringify({ - alg: "ES256", + alg: privateKey.algorithm.name.startsWith("RSA") ? "RS256" : "ES256", ...data.protected, })), payload: data.payload === undefined