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> {
if (await knex.schema.hasTable(TableName.CertificateAuthority)) {
const hasActiveCaCertVersionColumn = await knex.schema.hasColumn(
TableName.CertificateAuthority,
"activeCaCertVersion"
);
if (!hasActiveCaCertVersionColumn) {
const hasActiveCaCertIdColumn = await knex.schema.hasColumn(TableName.CertificateAuthority, "activeCaCertId");
if (!hasActiveCaCertIdColumn) {
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> {
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) => {
t.dropColumn("activeCaCertVersion");
t.dropColumn("activeCaCertId");
});
}
}

View File

@@ -28,7 +28,7 @@ export const CertificateAuthoritiesSchema = z.object({
keyAlgorithm: z.string(),
notBefore: 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>;

View File

@@ -5,8 +5,6 @@
import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models";
export const DynamicSecretsSchema = z.object({
@@ -16,12 +14,16 @@ export const DynamicSecretsSchema = z.object({
type: z.string(),
defaultTTL: z.string(),
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(),
status: z.string().nullable().optional(),
statusDetails: z.string().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date(),
encryptedConfig: zodBuffer
updatedAt: z.date()
});
export type TDynamicSecrets = z.infer<typeof DynamicSecretsSchema>;

View File

@@ -5,22 +5,27 @@
import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models";
export const WebhooksSchema = z.object({
id: z.string().uuid(),
secretPath: z.string().default("/"),
url: z.string(),
lastStatus: z.string().nullable().optional(),
lastRunErrorMessage: z.string().nullable().optional(),
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(),
updatedAt: z.date(),
envId: z.string().uuid(),
type: z.string().default("general").nullable().optional(),
encryptedSecretKeyWithKms: zodBuffer.nullable().optional(),
encryptedUrl: zodBuffer
urlCipherText: z.string().nullable().optional(),
urlIV: z.string().nullable().optional(),
urlTag: z.string().nullable().optional(),
type: z.string().default("general").nullable().optional()
});
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]),
schema: {
description: "Renew CA certificate for CA",
description: "Perform CA certificate renewal",
params: z.object({
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.RENEW_CA_CERT.caId)
}),

View File

@@ -206,8 +206,9 @@ export const getCaCertChain = async ({
}: TGetCaCertChainDTO) => {
const ca = await certificateAuthorityDAL.findById(caId);
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({
projectId: ca.projectId,

View File

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

View File

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

View File

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

View File

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