Update CA certificate tracking impl to use foreign ref instead of number

This commit is contained in:
Tuan Dang
2024-08-13 11:22:45 -07:00
parent 5af53d3398
commit bb6416acb7
10 changed files with 77 additions and 58 deletions

View File

@@ -4,16 +4,19 @@ import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> { export async function up(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.CertificateAuthority)) { if (await knex.schema.hasTable(TableName.CertificateAuthority)) {
const hasActiveCaCertVersionColumn = await knex.schema.hasColumn( const hasActiveCaCertIdColumn = await knex.schema.hasColumn(TableName.CertificateAuthority, "activeCaCertId");
TableName.CertificateAuthority, if (!hasActiveCaCertIdColumn) {
"activeCaCertVersion"
);
if (!hasActiveCaCertVersionColumn) {
await knex.schema.alterTable(TableName.CertificateAuthority, (t) => { await knex.schema.alterTable(TableName.CertificateAuthority, (t) => {
t.integer("activeCaCertVersion").nullable(); t.uuid("activeCaCertId").nullable();
t.foreign("activeCaCertId").references("id").inTable(TableName.CertificateAuthorityCert);
}); });
await knex(TableName.CertificateAuthority).where("status", "active").update({ activeCaCertVersion: 1 }); await knex.raw(`
UPDATE "${TableName.CertificateAuthority}" ca
SET "activeCaCertId" = cac.id
FROM "${TableName.CertificateAuthorityCert}" cac
WHERE ca.id = cac."caId"
`);
} }
} }
@@ -63,9 +66,9 @@ export async function up(knex: Knex): Promise<void> {
export async function down(knex: Knex): Promise<void> { export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.CertificateAuthority)) { if (await knex.schema.hasTable(TableName.CertificateAuthority)) {
if (await knex.schema.hasColumn(TableName.CertificateAuthority, "activeCaCertVersion")) { if (await knex.schema.hasColumn(TableName.CertificateAuthority, "activeCaCertId")) {
await knex.schema.alterTable(TableName.CertificateAuthority, (t) => { await knex.schema.alterTable(TableName.CertificateAuthority, (t) => {
t.dropColumn("activeCaCertVersion"); t.dropColumn("activeCaCertId");
}); });
} }
} }

View File

@@ -28,7 +28,7 @@ export const CertificateAuthoritiesSchema = z.object({
keyAlgorithm: z.string(), keyAlgorithm: z.string(),
notBefore: z.date().nullable().optional(), notBefore: z.date().nullable().optional(),
notAfter: z.date().nullable().optional(), notAfter: z.date().nullable().optional(),
activeCaCertVersion: z.number().nullable().optional() activeCaCertId: z.string().uuid().nullable().optional()
}); });
export type TCertificateAuthorities = z.infer<typeof CertificateAuthoritiesSchema>; export type TCertificateAuthorities = z.infer<typeof CertificateAuthoritiesSchema>;

View File

@@ -5,8 +5,6 @@
import { z } from "zod"; import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models"; import { TImmutableDBKeys } from "./models";
export const DynamicSecretsSchema = z.object({ export const DynamicSecretsSchema = z.object({
@@ -16,12 +14,16 @@ export const DynamicSecretsSchema = z.object({
type: z.string(), type: z.string(),
defaultTTL: z.string(), defaultTTL: z.string(),
maxTTL: z.string().nullable().optional(), maxTTL: z.string().nullable().optional(),
inputIV: z.string(),
inputCiphertext: z.string(),
inputTag: z.string(),
algorithm: z.string().default("aes-256-gcm"),
keyEncoding: z.string().default("utf8"),
folderId: z.string().uuid(), folderId: z.string().uuid(),
status: z.string().nullable().optional(), status: z.string().nullable().optional(),
statusDetails: z.string().nullable().optional(), statusDetails: z.string().nullable().optional(),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date(), updatedAt: z.date()
encryptedConfig: zodBuffer
}); });
export type TDynamicSecrets = z.infer<typeof DynamicSecretsSchema>; export type TDynamicSecrets = z.infer<typeof DynamicSecretsSchema>;

View File

@@ -5,22 +5,27 @@
import { z } from "zod"; import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models"; import { TImmutableDBKeys } from "./models";
export const WebhooksSchema = z.object({ export const WebhooksSchema = z.object({
id: z.string().uuid(), id: z.string().uuid(),
secretPath: z.string().default("/"), secretPath: z.string().default("/"),
url: z.string(),
lastStatus: z.string().nullable().optional(), lastStatus: z.string().nullable().optional(),
lastRunErrorMessage: z.string().nullable().optional(), lastRunErrorMessage: z.string().nullable().optional(),
isDisabled: z.boolean().default(false), isDisabled: z.boolean().default(false),
encryptedSecretKey: z.string().nullable().optional(),
iv: z.string().nullable().optional(),
tag: z.string().nullable().optional(),
algorithm: z.string().nullable().optional(),
keyEncoding: z.string().nullable().optional(),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date(), updatedAt: z.date(),
envId: z.string().uuid(), envId: z.string().uuid(),
type: z.string().default("general").nullable().optional(), urlCipherText: z.string().nullable().optional(),
encryptedSecretKeyWithKms: zodBuffer.nullable().optional(), urlIV: z.string().nullable().optional(),
encryptedUrl: zodBuffer urlTag: z.string().nullable().optional(),
type: z.string().default("general").nullable().optional()
}); });
export type TWebhooks = z.infer<typeof WebhooksSchema>; export type TWebhooks = z.infer<typeof WebhooksSchema>;

View File

@@ -283,7 +283,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
description: "Renew CA certificate for CA", description: "Perform CA certificate renewal",
params: z.object({ params: z.object({
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.RENEW_CA_CERT.caId) caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.RENEW_CA_CERT.caId)
}), }),

View File

@@ -206,8 +206,9 @@ export const getCaCertChain = async ({
}: TGetCaCertChainDTO) => { }: TGetCaCertChainDTO) => {
const ca = await certificateAuthorityDAL.findById(caId); const ca = await certificateAuthorityDAL.findById(caId);
if (!ca) throw new BadRequestError({ message: "CA not found" }); if (!ca) throw new BadRequestError({ message: "CA not found" });
if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" });
const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id }); const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId);
const keyId = await getProjectKmsCertificateKeyId({ const keyId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId, projectId: ca.projectId,

View File

@@ -51,7 +51,10 @@ type TCertificateAuthorityServiceFactoryDep = {
TCertificateAuthorityDALFactory, TCertificateAuthorityDALFactory,
"transaction" | "create" | "findById" | "updateById" | "deleteById" | "findOne" "transaction" | "create" | "findById" | "updateById" | "deleteById" | "findOne"
>; >;
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "create" | "findOne" | "transaction" | "find">; certificateAuthorityCertDAL: Pick<
TCertificateAuthorityCertDALFactory,
"create" | "findOne" | "transaction" | "find" | "findById"
>;
certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "create" | "findOne">; certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "create" | "findOne">;
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "create" | "findOne" | "update">; certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "create" | "findOne" | "update">;
certificateAuthorityQueue: TCertificateAuthorityQueueFactory; // TODO: Pick certificateAuthorityQueue: TCertificateAuthorityQueueFactory; // TODO: Pick
@@ -153,8 +156,7 @@ export const certificateAuthorityServiceFactory = ({
maxPathLength, maxPathLength,
notBefore: notBeforeDate, notBefore: notBeforeDate,
notAfter: notAfterDate, notAfter: notAfterDate,
serialNumber, serialNumber
activeCaCertVersion: 1
}) })
}, },
tx tx
@@ -213,7 +215,7 @@ export const certificateAuthorityServiceFactory = ({
plainText: Buffer.alloc(0) plainText: Buffer.alloc(0)
}); });
await certificateAuthorityCertDAL.create( const caCert = await certificateAuthorityCertDAL.create(
{ {
caId: ca.id, caId: ca.id,
encryptedCertificate, encryptedCertificate,
@@ -223,6 +225,14 @@ export const certificateAuthorityServiceFactory = ({
}, },
tx tx
); );
await certificateAuthorityDAL.updateById(
ca.id,
{
activeCaCertId: caCert.id
},
tx
);
} }
// create empty CRL // create empty CRL
@@ -347,9 +357,7 @@ export const certificateAuthorityServiceFactory = ({
); );
if (ca.type === CaType.ROOT) throw new BadRequestError({ message: "Root CA cannot generate CSR" }); if (ca.type === CaType.ROOT) throw new BadRequestError({ message: "Root CA cannot generate CSR" });
if (ca.activeCaCertId) throw new BadRequestError({ message: "CA already has a certificate installed" });
const [caCert] = await certificateAuthorityCertDAL.find({ caId: ca.id }, { sort: [["version", "desc"]] });
if (caCert) throw new BadRequestError({ message: "CA already has a certificate installed" });
const { caPrivateKey, caPublicKey } = await getCaCredentials({ const { caPrivateKey, caPublicKey } = await getCaCredentials({
caId, caId,
@@ -394,6 +402,8 @@ export const certificateAuthorityServiceFactory = ({
const ca = await certificateAuthorityDAL.findById(caId); const ca = await certificateAuthorityDAL.findById(caId);
if (!ca) throw new BadRequestError({ message: "CA not found" }); if (!ca) throw new BadRequestError({ message: "CA not found" });
if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" });
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
actor, actor,
actorId, actorId,
@@ -410,8 +420,7 @@ export const certificateAuthorityServiceFactory = ({
if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" }); if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" });
// get latest CA certificate // get latest CA certificate
const [caCert] = await certificateAuthorityCertDAL.find({ caId: ca.id }, { sort: [["version", "desc"]] }); const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId);
if (!caCert) throw new BadRequestError({ message: "CA does not have a certificate installed" });
const serialNumber = crypto.randomBytes(32).toString("hex"); const serialNumber = crypto.randomBytes(32).toString("hex");
@@ -489,13 +498,12 @@ export const certificateAuthorityServiceFactory = ({
}); });
await certificateAuthorityDAL.transaction(async (tx) => { await certificateAuthorityDAL.transaction(async (tx) => {
const newActiveCaCertVersion = caCert.version + 1; const newCaCert = await certificateAuthorityCertDAL.create(
await certificateAuthorityCertDAL.create(
{ {
caId: ca.id, caId: ca.id,
encryptedCertificate, encryptedCertificate,
encryptedCertificateChain, encryptedCertificateChain,
version: newActiveCaCertVersion, version: caCert.version + 1,
caSecretId: caSecret.id caSecretId: caSecret.id
}, },
tx tx
@@ -504,7 +512,7 @@ export const certificateAuthorityServiceFactory = ({
await certificateAuthorityDAL.updateById( await certificateAuthorityDAL.updateById(
ca.id, ca.id,
{ {
activeCaCertVersion: newActiveCaCertVersion, activeCaCertId: newCaCert.id,
notBefore: notBeforeDate, notBefore: notBeforeDate,
notAfter: new Date(notAfter) notAfter: new Date(notAfter)
}, },
@@ -533,10 +541,9 @@ export const certificateAuthorityServiceFactory = ({
}); });
// get latest parent CA certificate // get latest parent CA certificate
const [parentCaCert] = await certificateAuthorityCertDAL.find( if (!parentCa.activeCaCertId)
{ caId: parentCa.id }, throw new BadRequestError({ message: "Parent CA does not have a certificate installed" });
{ sort: [["version", "desc"]] } const parentCaCert = await certificateAuthorityCertDAL.findById(parentCa.activeCaCertId);
);
const decryptedParentCaCert = await kmsDecryptor({ const decryptedParentCaCert = await kmsDecryptor({
cipherTextBlob: parentCaCert.encryptedCertificate cipherTextBlob: parentCaCert.encryptedCertificate
@@ -581,7 +588,7 @@ export const certificateAuthorityServiceFactory = ({
const intermediateCert = await x509.X509CertificateGenerator.create({ const intermediateCert = await x509.X509CertificateGenerator.create({
serialNumber, serialNumber,
subject: csrObj.subject, subject: csrObj.subject,
issuer: caCertObj.subject, issuer: parentCaCertObj.subject,
notBefore: notBeforeDate, notBefore: notBeforeDate,
notAfter: new Date(notAfter), notAfter: new Date(notAfter),
signingKey: parentCaPrivateKey, signingKey: parentCaPrivateKey,
@@ -600,7 +607,7 @@ export const certificateAuthorityServiceFactory = ({
ca.maxPathLength === -1 || !ca.maxPathLength ? undefined : ca.maxPathLength, ca.maxPathLength === -1 || !ca.maxPathLength ? undefined : ca.maxPathLength,
true true
), ),
await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false), await x509.AuthorityKeyIdentifierExtension.create(parentCaCertObj, false),
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey) await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
] ]
}); });
@@ -624,13 +631,12 @@ export const certificateAuthorityServiceFactory = ({
}); });
await certificateAuthorityDAL.transaction(async (tx) => { await certificateAuthorityDAL.transaction(async (tx) => {
const newActiveCaCertVersion = caCert.version + 1; const newCaCert = await certificateAuthorityCertDAL.create(
await certificateAuthorityCertDAL.create(
{ {
caId: ca.id, caId: ca.id,
encryptedCertificate, encryptedCertificate,
encryptedCertificateChain, encryptedCertificateChain,
version: newActiveCaCertVersion, version: caCert.version + 1,
caSecretId: caSecret.id caSecretId: caSecret.id
}, },
tx tx
@@ -639,7 +645,7 @@ export const certificateAuthorityServiceFactory = ({
await certificateAuthorityDAL.updateById( await certificateAuthorityDAL.updateById(
ca.id, ca.id,
{ {
activeCaCertVersion: newActiveCaCertVersion, activeCaCertId: newCaCert.id,
notBefore: notBeforeDate, notBefore: notBeforeDate,
notAfter: new Date(notAfter) notAfter: new Date(notAfter)
}, },
@@ -764,9 +770,9 @@ export const certificateAuthorityServiceFactory = ({
); );
if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" }); if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" });
if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" });
const [caCert] = await certificateAuthorityCertDAL.find({ caId: ca.id }, { sort: [["version", "desc"]] }); const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId);
if (!caCert) throw new BadRequestError({ message: "CA does not have a certificate installed" });
if (ca.notAfter && new Date() > new Date(ca.notAfter)) { if (ca.notAfter && new Date() > new Date(ca.notAfter)) {
throw new BadRequestError({ message: "CA is expired" }); throw new BadRequestError({ message: "CA is expired" });
@@ -900,8 +906,7 @@ export const certificateAuthorityServiceFactory = ({
ProjectPermissionSub.CertificateAuthorities ProjectPermissionSub.CertificateAuthorities
); );
const [caCert] = await certificateAuthorityCertDAL.find({ caId: ca.id }, { sort: [["version", "desc"]] }); if (ca.activeCaCertId) throw new BadRequestError({ message: "CA has already imported a certificate" });
if (caCert) throw new BadRequestError({ message: "CA has already imported a certificate" });
const certObj = new x509.X509Certificate(certificate); const certObj = new x509.X509Certificate(certificate);
const maxPathLength = certObj.getExtension(x509.BasicConstraintsExtension)?.pathLength; const maxPathLength = certObj.getExtension(x509.BasicConstraintsExtension)?.pathLength;
@@ -967,7 +972,7 @@ export const certificateAuthorityServiceFactory = ({
} }
await certificateAuthorityCertDAL.transaction(async (tx) => { await certificateAuthorityCertDAL.transaction(async (tx) => {
await certificateAuthorityCertDAL.create( const newCaCert = await certificateAuthorityCertDAL.create(
{ {
caId: ca.id, caId: ca.id,
encryptedCertificate, encryptedCertificate,
@@ -986,7 +991,8 @@ export const certificateAuthorityServiceFactory = ({
notBefore: new Date(certObj.notBefore), notBefore: new Date(certObj.notBefore),
notAfter: new Date(certObj.notAfter), notAfter: new Date(certObj.notAfter),
serialNumber: certObj.serialNumber, serialNumber: certObj.serialNumber,
parentCaId: parentCa?.id parentCaId: parentCa?.id,
activeCaCertId: newCaCert.id
}, },
tx tx
); );
@@ -1026,9 +1032,8 @@ export const certificateAuthorityServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates);
if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" }); if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" });
if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" });
const [caCert] = await certificateAuthorityCertDAL.find({ caId: ca.id }, { sort: [["version", "desc"]] }); const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId);
if (!caCert) throw new BadRequestError({ message: "CA does not have a certificate installed" });
if (ca.notAfter && new Date() > new Date(ca.notAfter)) { if (ca.notAfter && new Date() > new Date(ca.notAfter)) {
throw new BadRequestError({ message: "CA is expired" }); throw new BadRequestError({ message: "CA is expired" });
@@ -1233,9 +1238,9 @@ export const certificateAuthorityServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates);
if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" }); if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" });
if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" });
const [caCert] = await certificateAuthorityCertDAL.find({ caId: ca.id }, { sort: [["version", "desc"]] }); const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId);
if (!caCert) throw new BadRequestError({ message: "CA does not have a certificate installed" });
if (ca.notAfter && new Date() > new Date(ca.notAfter)) { if (ca.notAfter && new Date() > new Date(ca.notAfter)) {
throw new BadRequestError({ message: "CA is expired" }); throw new BadRequestError({ message: "CA is expired" });

View File

@@ -134,7 +134,7 @@ export type TGetCaCertChainsDTO = {
export type TGetCaCertChainDTO = { export type TGetCaCertChainDTO = {
caId: string; caId: string;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">; certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findOne">; certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findById">;
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">; projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">; kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
}; };

View File

@@ -16,7 +16,8 @@ import {
TRenewCaResponse, TRenewCaResponse,
TSignIntermediateDTO, TSignIntermediateDTO,
TSignIntermediateResponse, TSignIntermediateResponse,
TUpdateCaDTO} from "./types"; TUpdateCaDTO
} from "./types";
export const useCreateCa = () => { export const useCreateCa = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@@ -123,6 +124,7 @@ export const useRenewCa = () => {
}, },
onSuccess: (_, { caId, projectSlug }) => { onSuccess: (_, { caId, projectSlug }) => {
queryClient.invalidateQueries(workspaceKeys.getWorkspaceCas({ projectSlug })); queryClient.invalidateQueries(workspaceKeys.getWorkspaceCas({ projectSlug }));
queryClient.invalidateQueries(caKeys.getCaById(caId));
queryClient.invalidateQueries(caKeys.getCaCert(caId)); queryClient.invalidateQueries(caKeys.getCaCert(caId));
queryClient.invalidateQueries(caKeys.getCaCerts(caId)); queryClient.invalidateQueries(caKeys.getCaCerts(caId));
queryClient.invalidateQueries(caKeys.getCaCsr(caId)); queryClient.invalidateQueries(caKeys.getCaCsr(caId));

View File

@@ -1,5 +1,5 @@
import { CertKeyAlgorithm } from "../certificates/enums"; import { CertKeyAlgorithm } from "../certificates/enums";
import { CaRenewalType,CaStatus, CaType } from "./enums"; import { CaRenewalType, CaStatus, CaType } from "./enums";
export type TCertificateAuthority = { export type TCertificateAuthority = {
id: string; id: string;
@@ -19,6 +19,7 @@ export type TCertificateAuthority = {
notAfter?: string; notAfter?: string;
notBefore?: string; notBefore?: string;
keyAlgorithm: CertKeyAlgorithm; keyAlgorithm: CertKeyAlgorithm;
activeCaCertId?: string;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
}; };