Call order cert

This commit is contained in:
Fang-Pen Lin
2025-11-13 09:21:34 -08:00
parent b2297a3ecd
commit 01b6c52788
2 changed files with 302 additions and 243 deletions
@@ -29,6 +29,7 @@ import { TProjectDALFactory } from "@app/services/project/project-dal";
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns"; import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { orderCertificate } from "@app/services/certificate-authority/acme/acme-certificate-authority-fns";
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal"; import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
import { CaType } from "@app/services/certificate-authority/certificate-authority-enums"; import { CaType } from "@app/services/certificate-authority/certificate-authority-enums";
import { extractCertificateRequestFromCSR } from "@app/services/certificate-common/certificate-csr-utils"; import { extractCertificateRequestFromCSR } from "@app/services/certificate-common/certificate-csr-utils";
@@ -712,13 +713,30 @@ export const pkiAcmeServiceFactory = ({
return { certificateId: result.certificateId }; return { certificateId: result.certificateId };
} else { } else {
const { certificateAuthority } = (await certificateProfileDAL.findByIdWithConfigs(profileId, tx))!; const { certificateAuthority } = (await certificateProfileDAL.findByIdWithConfigs(profileId, tx))!;
const cert = await acmeCertificateAuthorityFns.orderCertificate({ const cert = await orderCertificate(
caId: certificateAuthority.id, {
commonName: certificateRequest.commonName, caId: certificateAuthority.id,
altNames: certificateRequest.subjectAlternativeNames?.map((san) => san.value), commonName: certificateRequest.commonName!,
keyUsages: [CertKeyUsage.DIGITAL_SIGNATURE, CertKeyUsage.KEY_ENCIPHERMENT, CertKeyUsage.KEY_AGREEMENT], altNames: certificateRequest.subjectAlternativeNames?.map((san) => san.value),
extendedKeyUsages: [CertExtendedKeyUsage.SERVER_AUTH] // TODO: not 100% sure what are these columns for, but let's put the values for common website SSL certs for now
}); keyUsages: [
CertKeyUsage.DIGITAL_SIGNATURE,
CertKeyUsage.KEY_ENCIPHERMENT,
CertKeyUsage.KEY_AGREEMENT
],
extendedKeyUsages: [CertExtendedKeyUsage.SERVER_AUTH]
},
{
appConnectionDAL,
certificateAuthorityDAL,
externalCertificateAuthorityDAL,
certificateDAL,
certificateBodyDAL,
certificateSecretDAL,
kmsService,
projectDAL
}
);
return { certificateId: cert.id }; return { certificateId: cert.id };
} }
})(); })();
@@ -29,6 +29,7 @@ import { triggerAutoSyncForSubscriber } from "@app/services/pki-sync/pki-sync-ut
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns"; import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
import { Knex } from "knex";
import { TCertificateAuthorityDALFactory } from "../certificate-authority-dal"; import { TCertificateAuthorityDALFactory } from "../certificate-authority-dal";
import { CaStatus, CaType } from "../certificate-authority-enums"; import { CaStatus, CaType } from "../certificate-authority-enums";
import { keyAlgorithmToAlgCfg } from "../certificate-authority-fns"; import { keyAlgorithmToAlgCfg } from "../certificate-authority-fns";
@@ -64,6 +65,20 @@ type TAcmeCertificateAuthorityFnsDeps = {
projectDAL: Pick<TProjectDALFactory, "findById" | "findOne" | "updateById" | "transaction">; projectDAL: Pick<TProjectDALFactory, "findById" | "findOne" | "updateById" | "transaction">;
}; };
type TOrderCertificateDeps = {
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById">;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa">;
externalCertificateAuthorityDAL: Pick<TExternalCertificateAuthorityDALFactory, "update">;
certificateDAL: Pick<TCertificateDALFactory, "create" | "transaction">;
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "create">;
kmsService: Pick<
TKmsServiceFactory,
"encryptWithKmsKey" | "generateKmsKey" | "createCipherPairWithDataKey" | "decryptWithKmsKey"
>;
projectDAL: Pick<TProjectDALFactory, "findById" | "findOne" | "updateById" | "transaction">;
};
type DBConfigurationColumn = { type DBConfigurationColumn = {
dnsProvider: string; dnsProvider: string;
directoryUrl: string; directoryUrl: string;
@@ -104,6 +119,248 @@ export const castDbEntryToAcmeCertificateAuthority = (
}; };
}; };
export const orderCertificate = async (
{
caId,
subscriberId,
commonName,
altNames,
keyUsages,
extendedKeyUsages
}: {
caId: string;
subscriberId?: string;
commonName: string;
altNames?: string[];
keyUsages?: CertKeyUsage[];
extendedKeyUsages?: CertExtendedKeyUsage[];
},
deps: TOrderCertificateDeps,
tx?: Knex
) => {
const {
appConnectionDAL,
certificateAuthorityDAL,
externalCertificateAuthorityDAL,
certificateDAL,
certificateBodyDAL,
certificateSecretDAL,
kmsService,
projectDAL
} = deps;
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId, tx);
if (!ca.externalCa || ca.externalCa.type !== CaType.ACME) {
throw new BadRequestError({ message: "CA is not an ACME CA" });
}
const acmeCa = castDbEntryToAcmeCertificateAuthority(ca);
if (acmeCa.status !== CaStatus.ACTIVE) {
throw new BadRequestError({ message: "CA is disabled" });
}
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId,
projectDAL,
kmsService
});
const kmsEncryptor = await kmsService.encryptWithKmsKey({
kmsId: certificateManagerKmsId
});
const kmsDecryptor = await kmsService.decryptWithKmsKey({
kmsId: certificateManagerKmsId
});
let accountKey: Buffer | undefined;
if (acmeCa.credentials) {
const decryptedCredentials = await kmsDecryptor({
cipherTextBlob: acmeCa.credentials as Buffer
});
const parsedCredentials = await AcmeCertificateAuthorityCredentialsSchema.parseAsync(
JSON.parse(decryptedCredentials.toString("utf8"))
);
accountKey = Buffer.from(parsedCredentials.accountKey, "base64");
}
if (!accountKey) {
accountKey = await acme.crypto.createPrivateRsaKey();
const newCredentials = {
accountKey: accountKey.toString("base64")
};
const { cipherTextBlob: encryptedNewCredentials } = await kmsEncryptor({
plainText: Buffer.from(JSON.stringify(newCredentials))
});
await externalCertificateAuthorityDAL.update(
{
caId: acmeCa.id
},
{
credentials: encryptedNewCredentials
}
);
}
await blockLocalAndPrivateIpAddresses(acmeCa.configuration.directoryUrl);
const acmeClientOptions: acme.ClientOptions = {
directoryUrl: acmeCa.configuration.directoryUrl,
accountKey
};
if (acmeCa.configuration.eabKid && acmeCa.configuration.eabHmacKey) {
acmeClientOptions.externalAccountBinding = {
kid: acmeCa.configuration.eabKid,
hmacKey: acmeCa.configuration.eabHmacKey
};
}
const acmeClient = new acme.Client(acmeClientOptions);
const alg = keyAlgorithmToAlgCfg(CertKeyAlgorithm.RSA_2048);
const leafKeys = await crypto.nativeCrypto.subtle.generateKey(alg, true, ["sign", "verify"]);
const skLeafObj = crypto.nativeCrypto.KeyObject.from(leafKeys.privateKey);
const skLeaf = skLeafObj.export({ format: "pem", type: "pkcs8" }) as string;
const [, certificateCsr] = await acme.crypto.createCsr(
{
altNames,
commonName
},
skLeaf
);
const appConnection = await appConnectionDAL.findById(acmeCa.configuration.dnsAppConnectionId);
const connection = await decryptAppConnection(appConnection, kmsService);
const pem = await acmeClient.auto({
csr: certificateCsr,
email: acmeCa.configuration.accountEmail,
challengePriority: ["dns-01"],
termsOfServiceAgreed: true,
challengeCreateFn: async (authz, challenge, keyAuthorization) => {
if (challenge.type !== "dns-01") {
throw new Error("Unsupported challenge type");
}
const recordName = `_acme-challenge.${authz.identifier.value}`; // e.g., "_acme-challenge.example.com"
const recordValue = `"${keyAuthorization}"`; // must be double quoted
switch (acmeCa.configuration.dnsProviderConfig.provider) {
case AcmeDnsProvider.Route53: {
await route53InsertTxtRecord(
connection as TAwsConnection,
acmeCa.configuration.dnsProviderConfig.hostedZoneId,
recordName,
recordValue
);
break;
}
case AcmeDnsProvider.Cloudflare: {
await cloudflareInsertTxtRecord(
connection as TCloudflareConnection,
acmeCa.configuration.dnsProviderConfig.hostedZoneId,
recordName,
recordValue
);
break;
}
default: {
throw new Error(`Unsupported DNS provider: ${acmeCa.configuration.dnsProviderConfig.provider as string}`);
}
}
},
challengeRemoveFn: async (authz, challenge, keyAuthorization) => {
const recordName = `_acme-challenge.${authz.identifier.value}`; // e.g., "_acme-challenge.example.com"
const recordValue = `"${keyAuthorization}"`; // must be double quoted
switch (acmeCa.configuration.dnsProviderConfig.provider) {
case AcmeDnsProvider.Route53: {
await route53DeleteTxtRecord(
connection as TAwsConnection,
acmeCa.configuration.dnsProviderConfig.hostedZoneId,
recordName,
recordValue
);
break;
}
case AcmeDnsProvider.Cloudflare: {
await cloudflareDeleteTxtRecord(
connection as TCloudflareConnection,
acmeCa.configuration.dnsProviderConfig.hostedZoneId,
recordName,
recordValue
);
break;
}
default: {
throw new Error(`Unsupported DNS provider: ${acmeCa.configuration.dnsProviderConfig.provider as string}`);
}
}
}
});
const [leafCert, parentCert] = acme.crypto.splitPemChain(pem);
const certObj = new x509.X509Certificate(leafCert);
const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
plainText: Buffer.from(new Uint8Array(certObj.rawData))
});
const certificateChainPem = parentCert.trim();
const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({
plainText: Buffer.from(certificateChainPem)
});
const { cipherTextBlob: encryptedPrivateKey } = await kmsEncryptor({
plainText: Buffer.from(skLeaf)
});
return (tx || certificateDAL).transaction(async (innerTx: Knex) => {
const cert = await certificateDAL.create(
{
caId: ca.id,
pkiSubscriberId: subscriberId,
status: CertStatus.ACTIVE,
friendlyName: commonName,
commonName,
altNames: altNames?.join(","),
serialNumber: certObj.serialNumber,
notBefore: certObj.notBefore,
notAfter: certObj.notAfter,
keyUsages: keyUsages,
extendedKeyUsages: extendedKeyUsages,
projectId: ca.projectId
},
innerTx
);
await certificateBodyDAL.create(
{
certId: cert.id,
encryptedCertificate,
encryptedCertificateChain
},
innerTx
);
await certificateSecretDAL.create(
{
certId: cert.id,
encryptedPrivateKey
},
innerTx
);
return cert;
});
};
export const AcmeCertificateAuthorityFns = ({ export const AcmeCertificateAuthorityFns = ({
appConnectionDAL, appConnectionDAL,
appConnectionService, appConnectionService,
@@ -317,246 +574,31 @@ export const AcmeCertificateAuthorityFns = ({
return cas.map(castDbEntryToAcmeCertificateAuthority); return cas.map(castDbEntryToAcmeCertificateAuthority);
}; };
const orderCertificate = async ({
caId,
subscriberId,
commonName,
altNames,
keyUsages,
extendedKeyUsages
}: {
caId: string;
subscriberId?: string;
commonName: string;
altNames?: string[];
keyUsages?: CertKeyUsage[];
extendedKeyUsages?: CertExtendedKeyUsage[];
}) => {
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId);
if (!ca.externalCa || ca.externalCa.type !== CaType.ACME) {
throw new BadRequestError({ message: "CA is not an ACME CA" });
}
const acmeCa = castDbEntryToAcmeCertificateAuthority(ca);
if (acmeCa.status !== CaStatus.ACTIVE) {
throw new BadRequestError({ message: "CA is disabled" });
}
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId,
projectDAL,
kmsService
});
const kmsEncryptor = await kmsService.encryptWithKmsKey({
kmsId: certificateManagerKmsId
});
const kmsDecryptor = await kmsService.decryptWithKmsKey({
kmsId: certificateManagerKmsId
});
let accountKey: Buffer | undefined;
if (acmeCa.credentials) {
const decryptedCredentials = await kmsDecryptor({
cipherTextBlob: acmeCa.credentials as Buffer
});
const parsedCredentials = await AcmeCertificateAuthorityCredentialsSchema.parseAsync(
JSON.parse(decryptedCredentials.toString("utf8"))
);
accountKey = Buffer.from(parsedCredentials.accountKey, "base64");
}
if (!accountKey) {
accountKey = await acme.crypto.createPrivateRsaKey();
const newCredentials = {
accountKey: accountKey.toString("base64")
};
const { cipherTextBlob: encryptedNewCredentials } = await kmsEncryptor({
plainText: Buffer.from(JSON.stringify(newCredentials))
});
await externalCertificateAuthorityDAL.update(
{
caId: acmeCa.id
},
{
credentials: encryptedNewCredentials
}
);
}
await blockLocalAndPrivateIpAddresses(acmeCa.configuration.directoryUrl);
const acmeClientOptions: acme.ClientOptions = {
directoryUrl: acmeCa.configuration.directoryUrl,
accountKey
};
if (acmeCa.configuration.eabKid && acmeCa.configuration.eabHmacKey) {
acmeClientOptions.externalAccountBinding = {
kid: acmeCa.configuration.eabKid,
hmacKey: acmeCa.configuration.eabHmacKey
};
}
const acmeClient = new acme.Client(acmeClientOptions);
const alg = keyAlgorithmToAlgCfg(CertKeyAlgorithm.RSA_2048);
const leafKeys = await crypto.nativeCrypto.subtle.generateKey(alg, true, ["sign", "verify"]);
const skLeafObj = crypto.nativeCrypto.KeyObject.from(leafKeys.privateKey);
const skLeaf = skLeafObj.export({ format: "pem", type: "pkcs8" }) as string;
const [, certificateCsr] = await acme.crypto.createCsr(
{
altNames,
commonName
},
skLeaf
);
const appConnection = await appConnectionDAL.findById(acmeCa.configuration.dnsAppConnectionId);
const connection = await decryptAppConnection(appConnection, kmsService);
const pem = await acmeClient.auto({
csr: certificateCsr,
email: acmeCa.configuration.accountEmail,
challengePriority: ["dns-01"],
termsOfServiceAgreed: true,
challengeCreateFn: async (authz, challenge, keyAuthorization) => {
if (challenge.type !== "dns-01") {
throw new Error("Unsupported challenge type");
}
const recordName = `_acme-challenge.${authz.identifier.value}`; // e.g., "_acme-challenge.example.com"
const recordValue = `"${keyAuthorization}"`; // must be double quoted
switch (acmeCa.configuration.dnsProviderConfig.provider) {
case AcmeDnsProvider.Route53: {
await route53InsertTxtRecord(
connection as TAwsConnection,
acmeCa.configuration.dnsProviderConfig.hostedZoneId,
recordName,
recordValue
);
break;
}
case AcmeDnsProvider.Cloudflare: {
await cloudflareInsertTxtRecord(
connection as TCloudflareConnection,
acmeCa.configuration.dnsProviderConfig.hostedZoneId,
recordName,
recordValue
);
break;
}
default: {
throw new Error(`Unsupported DNS provider: ${acmeCa.configuration.dnsProviderConfig.provider as string}`);
}
}
},
challengeRemoveFn: async (authz, challenge, keyAuthorization) => {
const recordName = `_acme-challenge.${authz.identifier.value}`; // e.g., "_acme-challenge.example.com"
const recordValue = `"${keyAuthorization}"`; // must be double quoted
switch (acmeCa.configuration.dnsProviderConfig.provider) {
case AcmeDnsProvider.Route53: {
await route53DeleteTxtRecord(
connection as TAwsConnection,
acmeCa.configuration.dnsProviderConfig.hostedZoneId,
recordName,
recordValue
);
break;
}
case AcmeDnsProvider.Cloudflare: {
await cloudflareDeleteTxtRecord(
connection as TCloudflareConnection,
acmeCa.configuration.dnsProviderConfig.hostedZoneId,
recordName,
recordValue
);
break;
}
default: {
throw new Error(`Unsupported DNS provider: ${acmeCa.configuration.dnsProviderConfig.provider as string}`);
}
}
}
});
const [leafCert, parentCert] = acme.crypto.splitPemChain(pem);
const certObj = new x509.X509Certificate(leafCert);
const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
plainText: Buffer.from(new Uint8Array(certObj.rawData))
});
const certificateChainPem = parentCert.trim();
const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({
plainText: Buffer.from(certificateChainPem)
});
const { cipherTextBlob: encryptedPrivateKey } = await kmsEncryptor({
plainText: Buffer.from(skLeaf)
});
return certificateDAL.transaction(async (tx) => {
const cert = await certificateDAL.create(
{
caId: ca.id,
pkiSubscriberId: subscriberId,
status: CertStatus.ACTIVE,
friendlyName: commonName,
commonName,
altNames: altNames?.join(","),
serialNumber: certObj.serialNumber,
notBefore: certObj.notBefore,
notAfter: certObj.notAfter,
keyUsages: keyUsages,
extendedKeyUsages: extendedKeyUsages,
projectId: ca.projectId
},
tx
);
await certificateBodyDAL.create(
{
certId: cert.id,
encryptedCertificate,
encryptedCertificateChain
},
tx
);
await certificateSecretDAL.create(
{
certId: cert.id,
encryptedPrivateKey
},
tx
);
return cert;
});
};
const orderSubscriberCertificate = async (subscriberId: string) => { const orderSubscriberCertificate = async (subscriberId: string) => {
const subscriber = await pkiSubscriberDAL.findById(subscriberId); const subscriber = await pkiSubscriberDAL.findById(subscriberId);
if (!subscriber.caId) { if (!subscriber.caId) {
throw new BadRequestError({ message: "Subscriber does not have a CA" }); throw new BadRequestError({ message: "Subscriber does not have a CA" });
} }
await orderCertificate({ await orderCertificate(
caId: subscriber.caId, {
subscriberId: subscriber.id, caId: subscriber.caId,
commonName: subscriber.commonName, subscriberId: subscriber.id,
altNames: subscriber.subjectAlternativeNames, commonName: subscriber.commonName,
keyUsages: subscriber.keyUsages as CertKeyUsage[], altNames: subscriber.subjectAlternativeNames,
extendedKeyUsages: subscriber.extendedKeyUsages as CertExtendedKeyUsage[] keyUsages: subscriber.keyUsages as CertKeyUsage[],
}); extendedKeyUsages: subscriber.extendedKeyUsages as CertExtendedKeyUsage[]
},
{
appConnectionDAL,
certificateAuthorityDAL,
externalCertificateAuthorityDAL,
certificateDAL,
certificateBodyDAL,
certificateSecretDAL,
kmsService,
projectDAL
}
);
await triggerAutoSyncForSubscriber(subscriber.id, { pkiSyncDAL, pkiSyncQueue }); await triggerAutoSyncForSubscriber(subscriber.id, { pkiSyncDAL, pkiSyncQueue });
}; };
@@ -564,7 +606,6 @@ export const AcmeCertificateAuthorityFns = ({
createCertificateAuthority, createCertificateAuthority,
updateCertificateAuthority, updateCertificateAuthority,
listCertificateAuthorities, listCertificateAuthorities,
orderCertificate,
orderSubscriberCertificate orderSubscriberCertificate
}; };
}; };