Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions src/AcmeAccount.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -54,6 +54,7 @@ export type AcmeAccountObjectSnapshot = {
export class AcmeAccount {
readonly client: AcmeClient;
readonly keyPair: CryptoKeyPair;
readonly keyPairAlgorithm?: KeyPairAlgorithm;
readonly url: string;

/**
Expand All @@ -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;
}

Expand Down Expand Up @@ -132,7 +135,7 @@ export class AcmeAccount {
*/
async keyRollover(): Promise<AcmeAccount> {
const [newKeyPair, oldPublicKeyJwk] = await Promise.all([
generateKeyPair(),
generateKeyPair(this.keyPairAlgorithm),
crypto.subtle.exportKey(
"jwk",
this.keyPair.publicKey,
Expand Down
16 changes: 13 additions & 3 deletions src/AcmeClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -184,9 +184,11 @@ export class AcmeClient {
{
emails,
externalAccountBinding,
keyPairAlgorithm,
}: {
emails: readonly string[];
externalAccountBinding?: ExternalAccountBinding;
keyPairAlgorithm?: KeyPairAlgorithm;
},
): Promise<AcmeAccount> {
if (
Expand Down Expand Up @@ -253,6 +255,7 @@ export class AcmeClient {
client: this,
url: accountUrl,
keyPair,
keyPairAlgorithm,
});
}

Expand All @@ -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<AcmeAccount> {
async login({
keyPair,
keyPairAlgorithm,
}: {
keyPair: CryptoKeyPair;
keyPairAlgorithm?: KeyPairAlgorithm;
}): Promise<AcmeAccount> {
const response = await this.jwsFetch(this.directory.newAccount, {
privateKey: keyPair.privateKey,
protected: {
Expand Down Expand Up @@ -298,6 +307,7 @@ export class AcmeClient {
client: this,
url: accountUrl,
keyPair,
keyPairAlgorithm,
});
}
}
3 changes: 2 additions & 1 deletion src/AcmeOrder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,13 +278,14 @@ Expected order status: ${pollUntil}`);
*/
async finalize(): Promise<CryptoKeyPair> {
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,
}),
);

Expand Down
19 changes: 9 additions & 10 deletions src/CryptoKeyUtils/importKeyPairFromPemPrivateKey.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { getAlgorithmProperties, type KeyPairAlgorithm } from "../utils/crypto.ts";
import { extractFirstPemObject } from "../utils/pem.ts";

async function derivePublicKey(privateKey: CryptoKey): Promise<CryptoKey> {
async function derivePublicKey(
privateKey: CryptoKey,
keyPairAlgorithm: KeyPairAlgorithm = "ec",
): Promise<CryptoKey> {
// d contains the private info of the key
const { d: _discardedPrivateInfo, ...jwkPublic } = {
...await crypto.subtle.exportKey("jwk", privateKey),
Expand All @@ -11,10 +15,7 @@ async function derivePublicKey(privateKey: CryptoKey): Promise<CryptoKey> {
return crypto.subtle.importKey(
"jwk",
jwkPublic,
{
name: "ECDSA",
namedCurve: "P-256",
},
getAlgorithmProperties(keyPairAlgorithm),
true,
["verify"],
);
Expand All @@ -25,20 +26,18 @@ async function derivePublicKey(privateKey: CryptoKey): Promise<CryptoKey> {
*/
export async function importKeyPairFromPemPrivateKey(
pemPrivateKey: string,
keyPairAlgorithm: KeyPairAlgorithm = "ec",
): Promise<CryptoKeyPair> {
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),
};
}
30 changes: 28 additions & 2 deletions src/utils/crypto.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,34 @@
import { decodeBase64Url } from "./base64.ts";

export async function generateKeyPair(): Promise<CryptoKeyPair> {
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<CryptoKeyPair> {
return await crypto.subtle.generateKey(
{ name: "ECDSA", namedCurve: "P-256" },
getAlgorithmProperties(keyPairAlgorithm),
true,
["sign", "verify"],
);
Expand Down
52 changes: 29 additions & 23 deletions src/utils/generateCSR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -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<Uint8Array<ArrayBuffer>> {
const certificationRequestInfoSequence = encodeCertificationRequestInfo(
{
domains,
publicKeyDer: new Uint8Array<ArrayBuffer>(
await crypto.subtle.exportKey("raw", keyPair.publicKey),
subjectPKInfo: new Uint8Array<ArrayBuffer>(
await crypto.subtle.exportKey("spki", keyPair.publicKey),
),
},
);
Expand All @@ -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<ArrayBuffer>;
subjectPKInfo: Uint8Array<ArrayBuffer>;
},
): Uint8Array<ArrayBuffer> {
const [mainDomain] = domains;
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -142,11 +144,15 @@ const encodeSubjectAlternativeName = (() => {

const encodeSignatureBitString = (
signature: Uint8Array<ArrayBuffer>,
keyPairAlgorithm?: KeyPairAlgorithm,
): Uint8Array<ArrayBuffer> => {
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);
};
2 changes: 1 addition & 1 deletion src/utils/jws.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down