misc: addressed comments

This commit is contained in:
Sheen Capadngan
2025-05-17 03:42:49 +08:00
parent 60ea4bb579
commit edefa7698c
25 changed files with 744 additions and 303 deletions
@@ -29,7 +29,6 @@ import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns
import { TCertificateAuthorityDALFactory } from "../certificate-authority-dal";
import { CaStatus, CaType } from "../certificate-authority-enums";
import { keyAlgorithmToAlgCfg } from "../certificate-authority-fns";
import { TCertificateAuthority } from "../certificate-authority-types";
import { TExternalCertificateAuthorityDALFactory } from "../external-certificate-authority-dal";
import { AcmeDnsProvider } from "./acme-certificate-authority-enums";
import { AcmeCertificateAuthorityCredentialsSchema } from "./acme-certificate-authority-schemas";
@@ -62,12 +61,13 @@ type DBConfigurationColumn = {
dnsProvider: string;
directoryUrl: string;
accountEmail: string;
hostedZoneId: string;
};
export const castDbEntryToAcmeCertificateAuthority = (
ca: Awaited<ReturnType<TCertificateAuthorityDALFactory["findByIdWithAssociatedCa"]>>
): TAcmeCertificateAuthority & { credentials: unknown } => {
if (!ca.externalCa) {
if (!ca.externalCa?.id) {
throw new BadRequestError({ message: "Malformed ACME certificate authority" });
}
@@ -82,7 +82,10 @@ export const castDbEntryToAcmeCertificateAuthority = (
credentials: ca.externalCa.credentials,
configuration: {
dnsAppConnectionId: ca.externalCa.dnsAppConnectionId as string,
dnsProvider: dbConfigurationCol.dnsProvider as AcmeDnsProvider,
dnsProviderConfig: {
provider: dbConfigurationCol.dnsProvider as AcmeDnsProvider,
hostedZoneId: dbConfigurationCol.hostedZoneId
},
directoryUrl: dbConfigurationCol.directoryUrl,
accountEmail: dbConfigurationCol.accountEmail
},
@@ -90,7 +93,12 @@ export const castDbEntryToAcmeCertificateAuthority = (
};
};
export const route53InsertTxtRecord = async (connection: TAwsConnectionConfig, domain: string, value: string) => {
export const route53InsertTxtRecord = async (
connection: TAwsConnectionConfig,
hostedZoneId: string,
domain: string,
value: string
) => {
const config = await getAwsConnectionConfig(connection, AWSRegion.US_WEST_1); // REGION is irrelevant because Route53 is global
const route53Client = new Route53Client({
credentials: config.credentials!,
@@ -98,7 +106,7 @@ export const route53InsertTxtRecord = async (connection: TAwsConnectionConfig, d
});
const command = new ChangeResourceRecordSetsCommand({
HostedZoneId: "Z040441124N1GOOMCQYX1", // SHEEN TODO: Get this from user input
HostedZoneId: hostedZoneId,
ChangeBatch: {
Comment: "Set ACME challenge TXT record",
Changes: [
@@ -118,7 +126,12 @@ export const route53InsertTxtRecord = async (connection: TAwsConnectionConfig, d
await route53Client.send(command);
};
export const route53DeleteTxtRecord = async (connection: TAwsConnectionConfig, domain: string, value: string) => {
export const route53DeleteTxtRecord = async (
connection: TAwsConnectionConfig,
hostedZoneId: string,
domain: string,
value: string
) => {
const config = await getAwsConnectionConfig(connection, AWSRegion.US_WEST_1); // REGION is irrelevant because Route53 is global
const route53Client = new Route53Client({
credentials: config.credentials!,
@@ -126,7 +139,7 @@ export const route53DeleteTxtRecord = async (connection: TAwsConnectionConfig, d
});
const command = new ChangeResourceRecordSetsCommand({
HostedZoneId: "Z040441124N1GOOMCQYX1", // SHEEN TODO: same here
HostedZoneId: hostedZoneId,
ChangeBatch: {
Comment: "Delete ACME challenge TXT record",
Changes: [
@@ -173,14 +186,14 @@ export const AcmeCertificateAuthorityFns = ({
disableDirectIssuance: boolean;
actor: OrgServiceActor;
}) => {
const { dnsAppConnectionId, directoryUrl, accountEmail, dnsProvider } = configuration;
const { dnsAppConnectionId, directoryUrl, accountEmail, dnsProviderConfig } = configuration;
const appConnection = await appConnectionDAL.findById(dnsAppConnectionId);
if (!appConnection) {
throw new NotFoundError({ message: `App connection with ID '${dnsAppConnectionId}' not found` });
}
if (dnsProvider === AcmeDnsProvider.Route53 && appConnection.app !== AppConnection.AWS) {
if (dnsProviderConfig.provider === AcmeDnsProvider.Route53 && appConnection.app !== AppConnection.AWS) {
throw new BadRequestError({
message: `App connection with ID '${dnsAppConnectionId}' is not an AWS connection`
});
@@ -207,7 +220,8 @@ export const AcmeCertificateAuthorityFns = ({
configuration: {
directoryUrl,
accountEmail,
dnsProvider
dnsProvider: dnsProviderConfig.provider,
hostedZoneId: dnsProviderConfig.hostedZoneId
},
status
},
@@ -217,19 +231,11 @@ export const AcmeCertificateAuthorityFns = ({
return certificateAuthorityDAL.findByIdWithAssociatedCa(ca.id, tx);
});
if (!caEntity.externalCa) {
if (!caEntity.externalCa?.id) {
throw new BadRequestError({ message: "Failed to create external certificate authority" });
}
return {
id: caEntity.id,
type: CaType.ACME,
disableDirectIssuance: caEntity.disableDirectIssuance,
name: caEntity.externalCa.name,
projectId,
status,
configuration: caEntity.externalCa.configuration
} as TCertificateAuthority;
return castDbEntryToAcmeCertificateAuthority(caEntity);
};
const updateCertificateAuthority = async ({
@@ -237,24 +243,26 @@ export const AcmeCertificateAuthorityFns = ({
status,
configuration,
disableDirectIssuance,
actor
actor,
name
}: {
id: string;
status?: CaStatus;
configuration: TUpdateAcmeCertificateAuthorityDTO["configuration"];
disableDirectIssuance?: boolean;
actor: OrgServiceActor;
name?: string;
}) => {
const updatedCa = await certificateAuthorityDAL.transaction(async (tx) => {
if (configuration) {
const { dnsAppConnectionId, directoryUrl, accountEmail, dnsProvider } = configuration;
const { dnsAppConnectionId, directoryUrl, accountEmail, dnsProviderConfig } = configuration;
const appConnection = await appConnectionDAL.findById(dnsAppConnectionId);
if (!appConnection) {
throw new NotFoundError({ message: `App connection with ID '${dnsAppConnectionId}' not found` });
}
if (dnsProvider === AcmeDnsProvider.Route53 && appConnection.app !== AppConnection.AWS) {
if (dnsProviderConfig.provider === AcmeDnsProvider.Route53 && appConnection.app !== AppConnection.AWS) {
throw new BadRequestError({
message: `App connection with ID '${dnsAppConnectionId}' is not an AWS connection`
});
@@ -273,24 +281,29 @@ export const AcmeCertificateAuthorityFns = ({
type: CaType.ACME
},
{
configuration: { directoryUrl, accountEmail, dnsProvider, dnsAppConnectionId }
dnsAppConnectionId,
configuration: {
directoryUrl,
accountEmail,
dnsProvider: dnsProviderConfig.provider,
hostedZoneId: dnsProviderConfig.hostedZoneId
}
},
tx
);
}
if (status) {
await externalCertificateAuthorityDAL.update(
{
certificateAuthorityId: id,
type: CaType.ACME
},
{
status
},
tx
);
}
await externalCertificateAuthorityDAL.update(
{
certificateAuthorityId: id,
type: CaType.ACME
},
{
name,
status
},
tx
);
if (disableDirectIssuance !== undefined) {
await certificateAuthorityDAL.updateById(
@@ -305,19 +318,11 @@ export const AcmeCertificateAuthorityFns = ({
return certificateAuthorityDAL.findByIdWithAssociatedCa(id, tx);
});
if (!updatedCa.externalCa) {
if (!updatedCa.externalCa?.id) {
throw new BadRequestError({ message: "Failed to update external certificate authority" });
}
return {
id: updatedCa.id,
type: CaType.ACME,
disableDirectIssuance: updatedCa.disableDirectIssuance,
name: updatedCa.externalCa.name,
projectId: updatedCa.projectId,
status: updatedCa.externalCa.status,
configuration: updatedCa.externalCa.configuration
};
return castDbEntryToAcmeCertificateAuthority(updatedCa);
};
const listCertificateAuthorities = async ({ projectId }: { projectId: string }) => {
@@ -329,7 +334,7 @@ export const AcmeCertificateAuthorityFns = ({
return cas.map(castDbEntryToAcmeCertificateAuthority);
};
const orderCertificate = async (subscriberId: string) => {
const orderSubscriberCertificate = async (subscriberId: string) => {
const subscriber = await pkiSubscriberDAL.findById(subscriberId);
if (!subscriber.caId) {
throw new BadRequestError({ message: "Subscriber does not have a CA" });
@@ -341,6 +346,9 @@ export const AcmeCertificateAuthorityFns = ({
}
const acmeCa = castDbEntryToAcmeCertificateAuthority(ca);
if (acmeCa.status !== CaStatus.ACTIVE) {
throw new BadRequestError({ message: "CA is disabled" });
}
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId,
@@ -421,16 +429,26 @@ export const AcmeCertificateAuthorityFns = ({
const recordName = `_acme-challenge.${authz.identifier.value}`; // e.g., "_acme-challenge.example.com"
const recordValue = `"${keyAuthorization}"`; // must be double quoted
if (acmeCa.configuration.dnsProvider === AcmeDnsProvider.Route53) {
await route53InsertTxtRecord(connection as TAwsConnection, recordName, recordValue);
if (acmeCa.configuration.dnsProviderConfig.provider === AcmeDnsProvider.Route53) {
await route53InsertTxtRecord(
connection as TAwsConnection,
acmeCa.configuration.dnsProviderConfig.hostedZoneId,
recordName,
recordValue
);
}
},
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
if (acmeCa.configuration.dnsProvider === AcmeDnsProvider.Route53) {
await route53DeleteTxtRecord(connection as TAwsConnection, recordName, recordValue);
if (acmeCa.configuration.dnsProviderConfig.provider === AcmeDnsProvider.Route53) {
await route53DeleteTxtRecord(
connection as TAwsConnection,
acmeCa.configuration.dnsProviderConfig.hostedZoneId,
recordName,
recordValue
);
}
}
});
@@ -466,7 +484,7 @@ export const AcmeCertificateAuthorityFns = ({
notAfter: certObj.notAfter,
keyUsages: subscriber.keyUsages as CertKeyUsage[],
extendedKeyUsages: subscriber.extendedKeyUsages as CertExtendedKeyUsage[],
caCertId: "s" // SHEEN TODO: merge Andrey's PR and then remove this
projectId: ca.projectId
},
tx
);
@@ -494,6 +512,6 @@ export const AcmeCertificateAuthorityFns = ({
createCertificateAuthority,
updateCertificateAuthority,
listCertificateAuthorities,
orderCertificate
orderSubscriberCertificate
};
};
@@ -10,9 +10,13 @@ import { AcmeDnsProvider } from "./acme-certificate-authority-enums";
export const AcmeCertificateAuthorityConfigurationSchema = z.object({
dnsAppConnectionId: z.string().trim(),
dnsProvider: z.nativeEnum(AcmeDnsProvider),
directoryUrl: z.string().trim(),
accountEmail: z.string().trim()
// soon, differentiate via the provider property
dnsProviderConfig: z.object({
provider: z.nativeEnum(AcmeDnsProvider),
hostedZoneId: z.string().trim().min(1)
}),
directoryUrl: z.string().trim().min(1),
accountEmail: z.string().trim().min(1)
});
export const AcmeCertificateAuthorityCredentialsSchema = z.object({
@@ -114,7 +114,7 @@ export const getCaCredentials = async ({
kmsService
}: TGetCaCredentialsDTO) => {
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId);
if (!ca?.internalCa) throw new NotFoundError({ message: `CA with ID '${caId}' not found` });
if (!ca?.internalCa?.id) throw new NotFoundError({ message: `Internal CA with ID '${caId}' not found` });
const caSecret = await certificateAuthoritySecretDAL.findOne({ caId });
if (!caSecret) throw new NotFoundError({ message: `CA secret for CA with ID '${caId}' not found` });
@@ -257,7 +257,7 @@ export const rebuildCaCrl = async ({
kmsService
}: TRebuildCaCrlDTO) => {
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId);
if (!ca?.internalCa) throw new NotFoundError({ message: `CA with ID '${caId}' not found` });
if (!ca?.internalCa?.id) throw new NotFoundError({ message: `Internal CA with ID '${caId}' not found` });
const caSecret = await certificateAuthoritySecretDAL.findOne({ caId: ca.id });
@@ -4,7 +4,7 @@ import crypto from "crypto";
import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env";
import { daysToMillisecond, secondsToMillis } from "@app/lib/dates";
import { NotFoundError } from "@app/lib/errors";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
@@ -114,6 +114,11 @@ export const certificateAuthorityQueueFactory = ({
};
const orderCertificateForSubscriber = async ({ subscriberId, caType }: TOrderCertificateForSubscriberDTO) => {
const entry = await keyStore.getItem(KeyStorePrefixes.CaOrderCertificateForSubscriberLock(subscriberId));
if (entry) {
throw new BadRequestError({ message: `Certificate order already in progress for subscriber ${subscriberId}` });
}
await queueService.queue(
QueueName.CaLifecycle,
QueueJobs.CaOrderCertificateForSubscriber,
@@ -137,8 +142,6 @@ export const certificateAuthorityQueueFactory = ({
try {
lock = await keyStore.acquireLock(
[KeyStorePrefixes.CaOrderCertificateForSubscriberLock(subscriberId)],
// scott: not sure on this duration; syncs can take excessive amounts of time so we need to keep it locked,
// but should always release below...
5 * 60 * 1000
);
} catch (e) {
@@ -148,7 +151,7 @@ export const certificateAuthorityQueueFactory = ({
try {
if (caType === CaType.ACME) {
await acmeFns.orderCertificate(subscriberId);
await acmeFns.orderSubscriberCertificate(subscriberId);
}
} catch (e) {
logger.error(e, `CaOrderCertificate Failed [subscriberId=${subscriberId}] [job=${job.name}]`);
@@ -14,7 +14,10 @@ import { TCertificateSecretDALFactory } from "../certificate/certificate-secret-
import { TKmsServiceFactory } from "../kms/kms-service";
import { TPkiSubscriberDALFactory } from "../pki-subscriber/pki-subscriber-dal";
import { TProjectDALFactory } from "../project/project-dal";
import { AcmeCertificateAuthorityFns } from "./acme/acme-certificate-authority-fns";
import {
AcmeCertificateAuthorityFns,
castDbEntryToAcmeCertificateAuthority
} from "./acme/acme-certificate-authority-fns";
import {
TCreateAcmeCertificateAuthorityDTO,
TUpdateAcmeCertificateAuthorityDTO
@@ -153,6 +156,8 @@ export const certificateAuthorityServiceFactory = ({
actor
});
}
throw new BadRequestError({ message: "Invalid certificate authority type" });
};
const findCertificateAuthorityById = async (
@@ -181,9 +186,9 @@ export const certificateAuthorityServiceFactory = ({
);
if (type === CaType.INTERNAL) {
if (!certificateAuthority.internalCa) {
if (!certificateAuthority.internalCa?.id) {
throw new NotFoundError({
message: `Could not find internal certificate authority with ID "${certificateAuthorityId}"`
message: `Internal certificate authority with ID "${certificateAuthorityId}" not found`
});
}
@@ -204,14 +209,11 @@ export const certificateAuthorityServiceFactory = ({
});
}
return {
id: certificateAuthority.id,
type,
disableDirectIssuance: certificateAuthority.disableDirectIssuance,
name: certificateAuthority.externalCa.name,
projectId: certificateAuthority.projectId,
configuration: certificateAuthority.externalCa.configuration
} as TCertificateAuthority;
if (type === CaType.ACME) {
return castDbEntryToAcmeCertificateAuthority(certificateAuthority);
}
throw new BadRequestError({ message: "Invalid certificate authority type" });
};
const listCertificateAuthoritiesByProjectId = async (
@@ -264,10 +266,12 @@ export const certificateAuthorityServiceFactory = ({
if (type === CaType.ACME) {
return acmeFns.listCertificateAuthorities({ projectId: finalProjectId });
}
throw new BadRequestError({ message: "Invalid certificate authority type" });
};
const updateCertificateAuthority = async (
{ id, type, configuration, disableDirectIssuance, status }: TUpdateCertificateAuthorityDTO,
{ id, type, configuration, disableDirectIssuance, status, name }: TUpdateCertificateAuthorityDTO,
actor: OrgServiceActor
) => {
const certificateAuthority = await certificateAuthorityDAL.findByIdWithAssociatedCa(id);
@@ -292,9 +296,9 @@ export const certificateAuthorityServiceFactory = ({
);
if (type === CaType.INTERNAL) {
if (!certificateAuthority.internalCa) {
if (!certificateAuthority.internalCa?.id) {
throw new NotFoundError({
message: `Could not find internal certificate authority with ID "${id}"`
message: `Internal certificate authority with ID "${id}" not found`
});
}
@@ -328,7 +332,8 @@ export const certificateAuthorityServiceFactory = ({
configuration: configuration as TUpdateAcmeCertificateAuthorityDTO["configuration"],
disableDirectIssuance,
actor,
status
status,
name
});
}
@@ -357,15 +362,15 @@ export const certificateAuthorityServiceFactory = ({
ProjectPermissionSub.CertificateAuthorities
);
if (!certificateAuthority.internalCa && type === CaType.INTERNAL) {
if (!certificateAuthority.internalCa?.id && type === CaType.INTERNAL) {
throw new BadRequestError({
message: "Certificate authority cannot be deleted due to mismatching type"
message: "Internal certificate authority cannot be deleted"
});
}
if (certificateAuthority.externalCa && certificateAuthority.externalCa.type !== type) {
if (certificateAuthority.externalCa?.id && certificateAuthority.externalCa.type !== type) {
throw new BadRequestError({
message: "Certificate authority cannot be deleted due to mismatching type"
message: "External certificate authority cannot be deleted"
});
}
@@ -383,15 +388,11 @@ export const certificateAuthorityServiceFactory = ({
} as TCertificateAuthority;
}
return {
id: certificateAuthority.id,
type,
disableDirectIssuance: certificateAuthority.disableDirectIssuance,
name: certificateAuthority.externalCa?.name,
projectId: certificateAuthority.projectId,
configuration: certificateAuthority.externalCa?.configuration,
status: certificateAuthority.externalCa?.status
} as TCertificateAuthority;
if (type === CaType.ACME) {
return castDbEntryToAcmeCertificateAuthority(certificateAuthority);
}
throw new BadRequestError({ message: "Invalid certificate authority type" });
};
return {
@@ -9,9 +9,7 @@ export type TCertificateAuthority = TInternalCertificateAuthority | TAcmeCertifi
export type TCertificateAuthorityInput = TInternalCertificateAuthorityInput | TAcmeCertificateAuthorityInput;
export type TCreateCertificateAuthorityDTO = Omit<TCertificateAuthority, "type" | "id"> & {
type: CaType;
};
export type TCreateCertificateAuthorityDTO = Omit<TCertificateAuthority, "id">;
export type TUpdateCertificateAuthorityDTO = Partial<Omit<TCreateCertificateAuthorityDTO, "projectId">> & {
type: CaType;
@@ -225,7 +225,8 @@ export const InternalCertificateAuthorityFns = ({
notBefore: notBeforeDate,
notAfter: notAfterDate,
keyUsages: selectedKeyUsages,
extendedKeyUsages: subscriber.extendedKeyUsages as CertExtendedKeyUsage[]
extendedKeyUsages: subscriber.extendedKeyUsages as CertExtendedKeyUsage[],
projectId: ca.projectId
},
tx
);
@@ -1209,8 +1209,8 @@ export const internalCertificateAuthorityServiceFactory = ({
ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(certificateTemplate.caId);
}
if (!ca?.internalCa) {
throw new NotFoundError({ message: `CA with ID '${caId}' not found` });
if (!ca?.internalCa?.id) {
throw new NotFoundError({ message: `Internal CA with ID '${caId}' not found` });
}
const { permission } = await permissionService.getProjectPermission({
@@ -1475,7 +1475,8 @@ export const internalCertificateAuthorityServiceFactory = ({
notBefore: notBeforeDate,
notAfter: notAfterDate,
keyUsages: selectedKeyUsages,
extendedKeyUsages: selectedExtendedKeyUsages
extendedKeyUsages: selectedExtendedKeyUsages,
projectId: ca.projectId
},
tx
);
@@ -1560,8 +1561,8 @@ export const internalCertificateAuthorityServiceFactory = ({
ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(certificateTemplate.caId);
}
if (!ca?.internalCa) {
throw new NotFoundError({ message: `CA with ID '${caId}' not found` });
if (!ca?.internalCa?.id) {
throw new NotFoundError({ message: `Internal CA with ID '${caId}' not found` });
}
if (!dto.isInternal) {
@@ -1854,6 +1855,20 @@ export const internalCertificateAuthorityServiceFactory = ({
plainText: Buffer.from(new Uint8Array(leafCert.rawData))
});
const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({
caCertId: ca.internalCa.activeCaCertId,
certificateAuthorityDAL,
certificateAuthorityCertDAL,
projectDAL,
kmsService
});
const certificateChainPem = `${issuingCaCertificate}\n${caCertChain}`.trim();
const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({
plainText: Buffer.from(certificateChainPem)
});
await certificateDAL.transaction(async (tx) => {
const cert = await certificateDAL.create(
{
@@ -1868,7 +1883,8 @@ export const internalCertificateAuthorityServiceFactory = ({
notBefore: notBeforeDate,
notAfter: notAfterDate,
keyUsages: selectedKeyUsages,
extendedKeyUsages: selectedExtendedKeyUsages
extendedKeyUsages: selectedExtendedKeyUsages,
projectId: ca.projectId
},
tx
);
@@ -1876,7 +1892,8 @@ export const internalCertificateAuthorityServiceFactory = ({
await certificateBodyDAL.create(
{
certId: cert.id,
encryptedCertificate
encryptedCertificate,
encryptedCertificateChain
},
tx
);
@@ -1894,17 +1911,9 @@ export const internalCertificateAuthorityServiceFactory = ({
return cert;
});
const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({
caCertId: ca.internalCa.activeCaCertId,
certificateAuthorityDAL,
certificateAuthorityCertDAL,
projectDAL,
kmsService
});
return {
certificate: leafCert,
certificateChain: `${issuingCaCertificate}\n${caCertChain}`.trim(),
certificateChain: certificateChainPem,
issuingCaCertificate,
serialNumber,
ca: expandInternalCa(ca),
@@ -1923,7 +1932,7 @@ export const internalCertificateAuthorityServiceFactory = ({
actorOrgId
}: TGetCaCertificateTemplatesDTO) => {
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId);
if (!ca?.internalCa) throw new NotFoundError({ message: `CA with ID '${caId}' not found` });
if (!ca?.internalCa?.id) throw new NotFoundError({ message: `Internal CA with ID '${caId}' not found` });
const { permission } = await permissionService.getProjectPermission({
actor,
@@ -1,6 +1,7 @@
import crypto from "node:crypto";
import * as x509 from "@peculiar/x509";
import RE2 from "re2";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
@@ -52,8 +53,11 @@ export const constructPemChainFromCerts = (certificates: x509.X509Certificate[])
.join("\n")
.trim();
export const splitPemChain = (pemText: string) =>
pemText.match(/-----BEGIN CERTIFICATE-----[^-]+-----END CERTIFICATE-----/g) || [];
export const splitPemChain = (pemText: string) => {
const re2Pattern = new RE2("-----BEGIN CERTIFICATE-----[^-]+-----END CERTIFICATE-----", "g");
return re2Pattern.match(pemText) || [];
};
/**
* Return the public and private key of certificate
@@ -22,7 +22,7 @@ import { TProjectDALFactory } from "@app/services/project/project-dal";
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
import { expandInternalCa, getCaCertChain, rebuildCaCrl } from "../certificate-authority/certificate-authority-fns";
import { buildCertificateChain, getCertificateCredentials, revocationReasonToCrlCode } from "./certificate-fns";
import { getCertificateCredentials, revocationReasonToCrlCode, splitPemChain } from "./certificate-fns";
import { TCertificateSecretDALFactory } from "./certificate-secret-dal";
import {
CertExtendedKeyUsage,
@@ -39,9 +39,9 @@ import {
} from "./certificate-types";
type TCertificateServiceFactoryDep = {
certificateDAL: Pick<TCertificateDALFactory, "findOne" | "deleteById" | "update" | "find">;
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne">;
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "findOne">;
certificateDAL: Pick<TCertificateDALFactory, "findOne" | "deleteById" | "update" | "find" | "transaction" | "create">;
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne" | "create">;
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "findOne" | "create">;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById" | "findByIdWithAssociatedCa">;
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findById">;
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "update">;
@@ -77,7 +77,6 @@ export const certificateServiceFactory = ({
*/
const getCert = async ({ serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TGetCertDTO) => {
const cert = await certificateDAL.findOne({ serialNumber });
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(cert.caId);
const { permission } = await permissionService.getProjectPermission({
actor,
@@ -94,8 +93,7 @@ export const certificateServiceFactory = ({
);
return {
cert,
ca: expandInternalCa(ca)
cert
};
};
@@ -110,7 +108,6 @@ export const certificateServiceFactory = ({
actorOrgId
}: TGetCertPrivateKeyDTO) => {
const cert = await certificateDAL.findOne({ serialNumber });
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(cert.caId);
const { permission } = await permissionService.getProjectPermission({
actor,
@@ -135,7 +132,6 @@ export const certificateServiceFactory = ({
});
return {
ca: expandInternalCa(ca),
cert,
certPrivateKey
};
@@ -146,7 +142,6 @@ export const certificateServiceFactory = ({
*/
const deleteCert = async ({ serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TDeleteCertDTO) => {
const cert = await certificateDAL.findOne({ serialNumber });
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(cert.caId);
const { permission } = await permissionService.getProjectPermission({
actor,
@@ -165,8 +160,7 @@ export const certificateServiceFactory = ({
const deletedCert = await certificateDAL.deleteById(cert.id);
return {
deletedCert,
ca: expandInternalCa(ca)
deletedCert
};
};
@@ -184,8 +178,21 @@ export const certificateServiceFactory = ({
actorOrgId
}: TRevokeCertDTO) => {
const cert = await certificateDAL.findOne({ serialNumber });
if (!cert.caId) {
throw new BadRequestError({
message: "Cannot revoke imported certificates"
});
}
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(cert.caId);
if (ca.externalCa?.id) {
throw new BadRequestError({
message: "Cannot revoke external certificates"
});
}
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
@@ -234,7 +241,6 @@ export const certificateServiceFactory = ({
*/
const getCertBody = async ({ serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TGetCertBodyDTO) => {
const cert = await certificateDAL.findOne({ serialNumber });
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(cert.caId);
const { permission } = await permissionService.getProjectPermission({
actor,
@@ -292,8 +298,234 @@ export const certificateServiceFactory = ({
certificate: certObj.toString("pem"),
certificateChain,
serialNumber: certObj.serialNumber,
cert,
ca: expandInternalCa(ca)
cert
};
};
/**
* Import certificate
*/
const importCert = async ({
projectSlug,
pkiCollectionId,
actorId,
actorAuthMethod,
actor,
actorOrgId,
friendlyName,
certificatePem,
chainPem,
privateKeyPem
}: TImportCertDTO) => {
const collectionId = pkiCollectionId;
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
let projectId = project.id;
const certManagerProjectFromSplit = await projectDAL.getProjectFromSplitId(
projectId,
ProjectType.CertificateManager
);
if (certManagerProjectFromSplit) {
projectId = certManagerProjectFromSplit.id;
}
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateActions.Create,
ProjectPermissionSub.Certificates
);
// Check PKI collection
if (collectionId) {
const pkiCollection = await pkiCollectionDAL.findById(collectionId);
if (!pkiCollection) throw new NotFoundError({ message: "PKI collection not found" });
if (pkiCollection.projectId !== projectId) throw new BadRequestError({ message: "Invalid PKI collection" });
}
const leafCert = new x509.X509Certificate(certificatePem);
// Verify the certificate chain
const chainCerts = splitPemChain(chainPem).map((pem) => new x509.X509Certificate(pem));
// Remove leaf cert from the chain if it's present
if (chainCerts[0].equal(leafCert)) {
chainCerts.splice(0, 1);
}
if (chainCerts.length === 0) {
throw new BadRequestError({
message: "Certificate chain must contain at least one issuer certificate"
});
}
// Verify leaf certificate is signed by the first certificate in the chain
const isLeafVerified = await leafCert.verify({ publicKey: chainCerts[0].publicKey }).catch(() => false);
if (!isLeafVerified) {
throw new BadRequestError({ message: "Leaf certificate verification against chain failed" });
}
// Verify the entire chain of trust
const verificationPromises = chainCerts.slice(0, -1).map(async (currentCert, index) => {
const issuerCert = chainCerts[index + 1];
return currentCert.verify({ publicKey: issuerCert.publicKey }).catch(() => false);
});
const verificationResults = await Promise.all(verificationPromises);
if (verificationResults.some((result) => !result)) {
throw new BadRequestError({
message: "Certificate chain verification failed: broken trust chain"
});
}
// Verify private key matches the certificate
let privateKey;
try {
privateKey = createPrivateKey(privateKeyPem);
} catch (err) {
throw new BadRequestError({ message: "Invalid private key format" });
}
try {
const message = Buffer.from(Buffer.alloc(32));
const publicKey = createPublicKey(certificatePem);
const signature = sign(null, message, privateKey);
const isValid = verify(null, message, publicKey, signature);
if (!isValid) {
throw new BadRequestError({ message: "Private key does not match certificate" });
}
} catch (err) {
if (err instanceof BadRequestError) {
throw err;
}
throw new BadRequestError({ message: "Error verifying private key against certificate" });
}
// Get certificate attributes
const commonName = Array.from(leafCert.subjectName.getField("CN")?.values() || [])[0] || "";
let altNames: undefined | string;
const sanExtension = leafCert.extensions.find((ext) => ext.type === "2.5.29.17");
if (sanExtension) {
const sanNames = new x509.GeneralNames(sanExtension.value);
altNames = sanNames.items.map((name) => name.value).join(", ");
}
const { serialNumber, notBefore, notAfter } = leafCert;
// Encrypt certificate for storage
const certificateManagerKeyId = await getProjectKmsCertificateKeyId({
projectId,
projectDAL,
kmsService
});
const kmsEncryptor = await kmsService.encryptWithKmsKey({
kmsId: certificateManagerKeyId
});
const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
plainText: Buffer.from(certificatePem)
});
const { cipherTextBlob: encryptedPrivateKey } = await kmsEncryptor({
plainText: Buffer.from(privateKeyPem)
});
// Extract Key Usage
const keyUsagesExt = leafCert.getExtension("2.5.29.15") as x509.KeyUsagesExtension;
let keyUsages: CertKeyUsage[] = [];
if (keyUsagesExt) {
keyUsages = Object.values(CertKeyUsage).filter(
// eslint-disable-next-line no-bitwise
(keyUsage) => (x509.KeyUsageFlags[keyUsage] & keyUsagesExt.usages) !== 0
);
}
// Extract Extended Key Usage
const extKeyUsageExt = leafCert.getExtension("2.5.29.37") as x509.ExtendedKeyUsageExtension;
let extendedKeyUsages: CertExtendedKeyUsage[] = [];
if (extKeyUsageExt) {
extendedKeyUsages = extKeyUsageExt.usages.map((ekuOid) => CertExtendedKeyUsageOIDToName[ekuOid as string]);
}
const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({
plainText: Buffer.from(chainPem)
});
const cert = await certificateDAL.transaction(async (tx) => {
try {
const txCert = await certificateDAL.create(
{
status: CertStatus.ACTIVE,
friendlyName: friendlyName || commonName,
commonName,
altNames,
serialNumber,
notBefore,
notAfter,
projectId,
keyUsages,
extendedKeyUsages
},
tx
);
await certificateBodyDAL.create(
{
certId: txCert.id,
encryptedCertificate,
encryptedCertificateChain
},
tx
);
await certificateSecretDAL.create(
{
certId: txCert.id,
encryptedPrivateKey
},
tx
);
if (collectionId) {
await pkiCollectionItemDAL.create(
{
pkiCollectionId: collectionId,
certId: txCert.id
},
tx
);
}
return txCert;
} catch (error) {
// @ts-expect-error We're expecting a database error
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (error?.error?.code === "23505") {
throw new BadRequestError({ message: "Certificate serial already exists in your project" });
}
throw error;
}
});
return {
certificate: certificatePem,
certificateChain: chainPem,
privateKey: privateKeyPem,
serialNumber,
cert
};
};
@@ -303,7 +535,6 @@ export const certificateServiceFactory = ({
*/
const getCertBundle = async ({ serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TGetCertBundleDTO) => {
const cert = await certificateDAL.findOne({ serialNumber });
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(cert.caId);
const { permission } = await permissionService.getProjectPermission({
actor,
@@ -375,8 +606,7 @@ export const certificateServiceFactory = ({
certificateChain,
privateKey: certPrivateKey,
serialNumber,
cert,
ca: expandInternalCa(ca)
cert
};
};
@@ -25,19 +25,19 @@ export const pkiAlertDALFactory = (db: TDbClient) => {
recipientEmails: string;
};
// SHEEN TODO: FIX REGRESION HERE
// gets CAs and certificates as part of PKI collection items
const combinedQuery = db
.replicaNode()
.select(
db.raw("? as type", [PkiItemType.CA]),
`${PkiItemType.CA}.id`,
`${PkiItemType.CA}.notAfter as expiryDate`,
`${PkiItemType.CA}.serialNumber`,
`${PkiItemType.CA}.friendlyName`,
"ic.notAfter as expiryDate",
"ic.serialNumber",
"ic.friendlyName",
"pci.pkiCollectionId"
)
.from(`${TableName.CertificateAuthority} as ${PkiItemType.CA}`)
.join(`${TableName.InternalCertificateAuthority} as ic`, `${PkiItemType.CA}.id`, "ic.certificateAuthorityId")
.join(`${TableName.PkiCollectionItem} as pci`, `${PkiItemType.CA}.id`, "pci.caId")
.unionAll((qb) => {
void qb
@@ -320,6 +320,10 @@ export const pkiSubscriberServiceFactory = ({
throw new BadRequestError({ message: "CA does not support ordering of certificates" });
}
if (ca.externalCa?.status !== CaStatus.ACTIVE) {
throw new BadRequestError({ message: "CA is disabled" });
}
if (ca.externalCa?.id && ca.externalCa.type === CaType.ACME) {
await certificateAuthorityQueue.orderCertificateForSubscriber({
subscriberId: subscriber.id,