Make progress on ca cert versioning

This commit is contained in:
Tuan Dang
2024-08-05 11:00:22 -07:00
parent a0490d0fde
commit 587a4a1120
28 changed files with 1490 additions and 74 deletions
@@ -0,0 +1,114 @@
import { Knex } from "knex";
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) {
await knex.schema.alterTable(TableName.CertificateAuthority, (t) => {
t.integer("activeCaCertVersion").nullable();
});
await knex(TableName.CertificateAuthority).where("status", "active").update({ activeCaCertVersion: 1 });
}
}
if (await knex.schema.hasTable(TableName.CertificateAuthorityCert)) {
const hasVersionColumn = await knex.schema.hasColumn(TableName.CertificateAuthorityCert, "version");
if (!hasVersionColumn) {
await knex.schema.alterTable(TableName.CertificateAuthorityCert, (t) => {
t.integer("version").nullable();
// t.dropUnique(["caId"]);
});
await knex(TableName.CertificateAuthorityCert).update({ version: 1 }).whereNull("version");
await knex.schema.alterTable(TableName.CertificateAuthorityCert, (t) => {
t.integer("version").notNullable().alter();
});
}
const hasCaSecretIdColumn = await knex.schema.hasColumn(TableName.CertificateAuthorityCert, "caSecretId");
if (!hasCaSecretIdColumn) {
await knex.schema.alterTable(TableName.CertificateAuthorityCert, (t) => {
t.uuid("caSecretId").nullable();
t.foreign("caSecretId").references("id").inTable(TableName.CertificateAuthoritySecret).onDelete("CASCADE");
});
await knex.raw(`
UPDATE "${TableName.CertificateAuthorityCert}" cert
SET "caSecretId" = (
SELECT sec.id
FROM "${TableName.CertificateAuthoritySecret}" sec
WHERE sec."caId" = cert."caId"
)
`);
// await knex.raw(`
// UPDATE ${TableName.CertificateAuthorityCert} cert
// SET caSecretId = (
// SELECT sec.id
// FROM ${TableName.CertificateAuthoritySecret} sec
// WHERE sec."caId" = cert."caId"
// )
// `);
// await knex(TableName.CertificateAuthorityCert).update({
// caSecretId: knex(TableName.CertificateAuthoritySecret)
// .select("id")
// .whereRaw("?? = ??", ["CertificateAuthoritySecret.caId", "CertificateAuthorityCert.caId"])
// });
// await knex(TableName.CertificateAuthorityCert).update({
// caSecretId: function () {
// this.select("id")
// .from(TableName.CertificateAuthoritySecret)
// .whereRaw("??.?? = ??.??", [
// TableName.CertificateAuthoritySecret,
// "caId",
// TableName.CertificateAuthorityCert,
// "caId"
// ]);
// }
// });
await knex.schema.alterTable(TableName.CertificateAuthorityCert, (t) => {
t.uuid("caSecretId").notNullable().alter();
});
}
}
// if (await knex.schema.hasTable(TableName.CertificateAuthoritySecret)) {
// await knex.schema.alterTable(TableName.CertificateAuthoritySecret, (t) => {
// t.dropUnique(["caId"]);
// });
// }
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.CertificateAuthority)) {
if (await knex.schema.hasColumn(TableName.CertificateAuthority, "activeCaCertVersion")) {
await knex.schema.alterTable(TableName.CertificateAuthority, (t) => {
t.dropColumn("activeCaCertVersion");
});
}
}
if (await knex.schema.hasTable(TableName.CertificateAuthorityCert)) {
if (await knex.schema.hasColumn(TableName.CertificateAuthorityCert, "version")) {
await knex.schema.alterTable(TableName.CertificateAuthorityCert, (t) => {
t.dropColumn("version");
});
}
if (await knex.schema.hasColumn(TableName.CertificateAuthorityCert, "caSecretId")) {
await knex.schema.alterTable(TableName.CertificateAuthorityCert, (t) => {
t.dropColumn("caSecretId");
});
}
}
}
@@ -27,7 +27,8 @@ export const CertificateAuthoritiesSchema = z.object({
maxPathLength: z.number().nullable().optional(),
keyAlgorithm: z.string(),
notBefore: z.date().nullable().optional(),
notAfter: z.date().nullable().optional()
notAfter: z.date().nullable().optional(),
activeCaCertVersion: z.number().nullable().optional()
});
export type TCertificateAuthorities = z.infer<typeof CertificateAuthoritiesSchema>;
@@ -15,7 +15,9 @@ export const CertificateAuthorityCertsSchema = z.object({
updatedAt: z.date(),
caId: z.string().uuid(),
encryptedCertificate: zodBuffer,
encryptedCertificateChain: zodBuffer
encryptedCertificateChain: zodBuffer,
version: z.number(),
caSecretId: z.string().uuid()
});
export type TCertificateAuthorityCerts = z.infer<typeof CertificateAuthorityCertsSchema>;
+4 -6
View File
@@ -5,6 +5,8 @@
import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models";
export const DynamicSecretsSchema = z.object({
@@ -14,16 +16,12 @@ 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()
updatedAt: z.date(),
encryptedConfig: zodBuffer
});
export type TDynamicSecrets = z.infer<typeof DynamicSecretsSchema>;
+5 -10
View File
@@ -5,27 +5,22 @@
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(),
urlCipherText: z.string().nullable().optional(),
urlIV: z.string().nullable().optional(),
urlTag: z.string().nullable().optional(),
type: z.string().default("general").nullable().optional()
type: z.string().default("general").nullable().optional(),
encryptedSecretKeyWithKms: zodBuffer.nullable().optional(),
encryptedUrl: zodBuffer
});
export type TWebhooks = z.infer<typeof WebhooksSchema>;
@@ -131,6 +131,7 @@ export enum EventType {
UPDATE_CA = "update-certificate-authority",
DELETE_CA = "delete-certificate-authority",
GET_CA_CSR = "get-certificate-authority-csr",
GET_CA_CERTS = "get-certificate-authority-certs",
GET_CA_CERT = "get-certificate-authority-cert",
SIGN_INTERMEDIATE = "sign-intermediate",
IMPORT_CA_CERT = "import-certificate-authority-cert",
@@ -1101,6 +1102,14 @@ interface GetCaCsr {
};
}
interface GetCaCerts {
type: EventType.GET_CA_CERTS;
metadata: {
caId: string;
dn: string;
};
}
interface GetCaCert {
type: EventType.GET_CA_CERT;
metadata: {
@@ -1328,6 +1337,7 @@ export type Event =
| UpdateCa
| DeleteCa
| GetCaCsr
| GetCaCerts
| GetCaCert
| SignIntermediate
| ImportCaCert
+8
View File
@@ -1048,6 +1048,14 @@ export const CERTIFICATE_AUTHORITIES = {
caId: "The ID of the CA to generate CSR from",
csr: "The generated CSR from the CA"
},
RENEW_CA_CERT: {
caId: "The ID of the CA to renew the CA certificate for",
type: "The type of behavior to use for the renewal operation. Currently Infisical is only able to renew a CA certificate with the same key pair.",
notAfter: "The expiry date and time for the renewed CA certificatre in YYYY-MM-DDTHH:mm:ss.sssZ format",
certificate: "The renewed CA certificate body",
certificateChain: "The certificate chain of the CA",
serialNumber: "The serial number of the renewed CA certificate"
},
GET_CERT: {
caId: "The ID of the CA to get the certificate body and certificate chain from",
certificate: "The certificate body of the CA",
@@ -8,7 +8,7 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
import { CaStatus, CaType } from "@app/services/certificate-authority/certificate-authority-types";
import { CaRenewalType, CaStatus, CaType } from "@app/services/certificate-authority/certificate-authority-types";
import {
validateAltNamesField,
validateCaDateField
@@ -275,15 +275,119 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
}
});
server.route({
method: "POST",
url: "/:caId/renew", // TODO
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Renew CA certificate for CA",
params: z.object({
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.RENEW_CA_CERT.caId)
}),
body: z.object({
type: z.nativeEnum(CaRenewalType).describe(CERTIFICATE_AUTHORITIES.RENEW_CA_CERT.type),
notAfter: validateCaDateField.describe(CERTIFICATE_AUTHORITIES.RENEW_CA_CERT.notAfter)
}),
response: {
200: z.object({
certificate: z.string().trim().describe(CERTIFICATE_AUTHORITIES.RENEW_CA_CERT.certificate),
certificateChain: z.string().trim().describe(CERTIFICATE_AUTHORITIES.RENEW_CA_CERT.certificateChain),
serialNumber: z.string().trim().describe(CERTIFICATE_AUTHORITIES.RENEW_CA_CERT.serialNumber)
})
}
},
handler: async (req) => {
const { certificate, certificateChain, serialNumber } = await server.services.certificateAuthority.renewCaCert({
caId: req.params.caId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body
});
// await server.services.auditLog.createAuditLog({
// ...req.auditLogInfo,
// projectId: ca.projectId,
// event: {
// type: EventType.SIGN_INTERMEDIATE,
// metadata: {
// caId: ca.id,
// dn: ca.dn,
// serialNumber
// }
// }
// });
return {
certificate,
certificateChain,
serialNumber
};
}
});
server.route({
method: "GET",
url: "/:caId/certificate",
url: "/:caId/ca-certificates",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Get cert and cert chain of a CA",
description: "Get list of past and current CA certificates for a CA",
params: z.object({
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET_CERT.caId)
}),
response: {
200: z.array(
z.object({
// TODO: consider not before and not after dates
certificate: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CERT.certificate),
certificateChain: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CERT.certificateChain),
serialNumber: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CERT.serialNumber),
version: z.number()
})
)
}
},
handler: async (req) => {
const { caCerts, ca } = await server.services.certificateAuthority.getCaCerts({
caId: req.params.caId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: ca.projectId,
event: {
type: EventType.GET_CA_CERTS,
metadata: {
caId: ca.id,
dn: ca.dn
}
}
});
return caCerts;
}
});
server.route({
method: "GET",
url: "/:caId/certificate", // TODO: consider updating endpoint structure considering CA certificates
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Get current CA cert and cert chain of a CA",
params: z.object({
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET_CERT.caId)
}),
@@ -5,7 +5,13 @@ import { BadRequestError } from "@app/lib/errors";
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
import { CertKeyAlgorithm, CertStatus } from "../certificate/certificate-types";
import { TDNParts, TGetCaCertChainDTO, TGetCaCredentialsDTO, TRebuildCaCrlDTO } from "./certificate-authority-types";
import {
TDNParts,
TGetCaCertChainDTO,
TGetCaCertChainsDTO,
TGetCaCredentialsDTO,
TRebuildCaCrlDTO
} from "./certificate-authority-types";
export const createDistinguishedName = (parts: TDNParts) => {
const dnParts = [];
@@ -98,11 +104,59 @@ export const getCaCredentials = async ({
]);
return {
caSecret,
caPrivateKey,
caPublicKey
};
};
/**
* Return the list of decrypted pem-encoded certificates and certificate chains
* for CA with id [caId].
*/
export const getCaCertChains = async ({
caId,
certificateAuthorityDAL,
certificateAuthorityCertDAL,
projectDAL,
kmsService
}: TGetCaCertChainsDTO) => {
const ca = await certificateAuthorityDAL.findById(caId);
if (!ca) throw new BadRequestError({ message: "CA not found" });
const keyId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId,
projectDAL,
kmsService
});
const kmsDecryptor = await kmsService.decryptWithKmsKey({
kmsId: keyId
});
const caCerts = await certificateAuthorityCertDAL.find({ caId: ca.id }, { sort: [["version", "asc"]] });
const decryptedChains = await Promise.all(
caCerts.map(async (caCert) => {
const decryptedCaCert = await kmsDecryptor({
cipherTextBlob: caCert.encryptedCertificate
});
const caCertObj = new x509.X509Certificate(decryptedCaCert);
const decryptedChain = await kmsDecryptor({
cipherTextBlob: caCert.encryptedCertificateChain
});
return {
certificate: caCertObj.toString("pem"),
certificateChain: decryptedChain.toString("utf-8"),
serialNumber: caCertObj.serialNumber,
version: caCert.version
};
})
);
return decryptedChains;
};
/**
* Return the decrypted pem-encoded certificate and certificate chain
* for CA with id [caId].
@@ -20,7 +20,8 @@ import { TCertificateAuthorityCertDALFactory } from "./certificate-authority-cer
import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal";
import {
createDistinguishedName,
getCaCertChain,
getCaCertChain, // TODO: consider rename
getCaCertChains,
getCaCredentials,
keyAlgorithmToAlgCfg
} from "./certificate-authority-fns";
@@ -32,10 +33,12 @@ import {
TCreateCaDTO,
TDeleteCaDTO,
TGetCaCertDTO,
TGetCaCertsDTO,
TGetCaCsrDTO,
TGetCaDTO,
TImportCertToCaDTO,
TIssueCertFromCaDTO,
TRenewCaCertDTO,
TSignIntermediateDTO,
TUpdateCaDTO
} from "./certificate-authority-types";
@@ -46,7 +49,7 @@ type TCertificateAuthorityServiceFactoryDep = {
TCertificateAuthorityDALFactory,
"transaction" | "create" | "findById" | "updateById" | "deleteById" | "findOne"
>;
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "create" | "findOne" | "transaction">;
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "create" | "findOne" | "transaction" | "find">;
certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "create" | "findOne">;
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "create" | "findOne" | "update">;
certificateAuthorityQueue: TCertificateAuthorityQueueFactory; // TODO: Pick
@@ -148,7 +151,8 @@ export const certificateAuthorityServiceFactory = ({
maxPathLength,
notBefore: notBeforeDate,
notAfter: notAfterDate,
serialNumber
serialNumber,
activeCaCertVersion: 1
})
},
tx
@@ -163,6 +167,24 @@ export const certificateAuthorityServiceFactory = ({
kmsId: certificateManagerKmsId
});
// https://nodejs.org/api/crypto.html#static-method-keyobjectfromkey
const skObj = KeyObject.from(keys.privateKey);
const { cipherTextBlob: encryptedPrivateKey } = await kmsEncryptor({
plainText: skObj.export({
type: "pkcs8",
format: "der"
})
});
const caSecret = await certificateAuthoritySecretDAL.create(
{
caId: ca.id,
encryptedPrivateKey
},
tx
);
if (type === CaType.ROOT) {
// note: create self-signed cert only applicable for root CA
const cert = await x509.X509CertificateGenerator.createSelfSigned({
@@ -193,7 +215,9 @@ export const certificateAuthorityServiceFactory = ({
{
caId: ca.id,
encryptedCertificate,
encryptedCertificateChain
encryptedCertificateChain,
version: 1,
caSecretId: caSecret.id
},
tx
);
@@ -221,24 +245,6 @@ export const certificateAuthorityServiceFactory = ({
tx
);
// https://nodejs.org/api/crypto.html#static-method-keyobjectfromkey
const skObj = KeyObject.from(keys.privateKey);
const { cipherTextBlob: encryptedPrivateKey } = await kmsEncryptor({
plainText: skObj.export({
type: "pkcs8",
format: "der"
})
});
await certificateAuthoritySecretDAL.create(
{
caId: ca.id,
encryptedPrivateKey
},
tx
);
return ca;
});
@@ -379,7 +385,316 @@ export const certificateAuthorityServiceFactory = ({
};
/**
* Return certificate and certificate chain for CA
* Renew certificate for CA with id [caId]
* Note: Currently implements CA renewal with same key-pair only
*/
const renewCaCert = async ({ caId, notAfter, actorId, actorAuthMethod, actor, actorOrgId }: TRenewCaCertDTO) => {
const ca = await certificateAuthorityDAL.findById(caId);
if (!ca) throw new BadRequestError({ message: "CA not found" });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
ca.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.CertificateAuthorities
);
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 serialNumber = crypto.randomBytes(32).toString("hex");
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId,
projectDAL,
kmsService
});
const kmsEncryptor = await kmsService.encryptWithKmsKey({
kmsId: certificateManagerKmsId
});
const { caPrivateKey, caPublicKey, caSecret } = await getCaCredentials({
caId: ca.id,
certificateAuthorityDAL,
certificateAuthoritySecretDAL,
projectDAL,
kmsService
});
const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm);
const kmsDecryptor = await kmsService.decryptWithKmsKey({
kmsId: certificateManagerKmsId
});
const decryptedCaCert = await kmsDecryptor({
cipherTextBlob: caCert.encryptedCertificate
});
const caCertObj = new x509.X509Certificate(decryptedCaCert);
let certificate = "";
let certificateChain = "";
switch (ca.type) {
case CaType.ROOT: {
if (new Date(notAfter) <= new Date(caCertObj.notAfter)) {
throw new BadRequestError({
message:
"New Root CA certificate must have notAfter date that is greater than the current certificate notAfter date"
});
}
const notBeforeDate = new Date();
const cert = await x509.X509CertificateGenerator.createSelfSigned({
name: ca.dn,
serialNumber,
notBefore: notBeforeDate,
notAfter: new Date(notAfter),
signingAlgorithm: alg,
keys: {
privateKey: caPrivateKey,
publicKey: caPublicKey
},
extensions: [
new x509.BasicConstraintsExtension(
true,
ca.maxPathLength === -1 || !ca.maxPathLength ? undefined : ca.maxPathLength,
true
),
new x509.ExtendedKeyUsageExtension(["1.2.3.4.5.6.7", "2.3.4.5.6.7.8"], true),
// eslint-disable-next-line no-bitwise
new x509.KeyUsagesExtension(x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.cRLSign, true),
await x509.SubjectKeyIdentifierExtension.create(caPublicKey)
]
});
const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
plainText: Buffer.from(new Uint8Array(cert.rawData))
});
const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({
plainText: Buffer.alloc(0)
});
await certificateAuthorityDAL.transaction(async (tx) => {
const newActiveCaCertVersion = caCert.version + 1;
await certificateAuthorityCertDAL.create(
{
caId: ca.id,
encryptedCertificate,
encryptedCertificateChain,
version: newActiveCaCertVersion,
caSecretId: caSecret.id
},
tx
);
await certificateAuthorityDAL.updateById(
ca.id,
{
activeCaCertVersion: newActiveCaCertVersion,
notBefore: notBeforeDate,
notAfter: new Date(notAfter)
},
tx
);
});
certificate = cert.toString("pem");
break;
}
case CaType.INTERMEDIATE: {
if (!ca.parentCaId) {
// TODO: look into optimal way to support renewal of intermediate CA with external parent CA
throw new BadRequestError({
message: "Failed to renew intermediate CA certificate with external parent CA"
});
}
const parentCa = await certificateAuthorityDAL.findById(ca.parentCaId);
const { caPrivateKey: parentCaPrivateKey } = await getCaCredentials({
caId: parentCa.id,
certificateAuthorityDAL,
certificateAuthoritySecretDAL,
projectDAL,
kmsService
});
// get latest parent CA certificate
const [parentCaCert] = await certificateAuthorityCertDAL.find(
{ caId: parentCa.id },
{ sort: [["version", "desc"]] }
);
const decryptedParentCaCert = await kmsDecryptor({
cipherTextBlob: parentCaCert.encryptedCertificate
});
const parentCaCertObj = new x509.X509Certificate(decryptedParentCaCert);
if (new Date(notAfter) <= new Date(caCertObj.notAfter)) {
throw new BadRequestError({
message:
"New Intermediate CA certificate must have notAfter date that is greater than the current certificate notAfter date"
});
}
if (new Date(notAfter) > new Date(parentCaCertObj.notAfter)) {
throw new BadRequestError({
message:
"New Intermediate CA certificate must have notAfter date that is equal to or smaller than the notAfter date of the parent CA certificate current certificate notAfter date"
});
}
const csrObj = await x509.Pkcs10CertificateRequestGenerator.create({
name: ca.dn,
keys: {
privateKey: caPrivateKey,
publicKey: caPublicKey
},
signingAlgorithm: alg,
extensions: [
// eslint-disable-next-line no-bitwise
new x509.KeyUsagesExtension(
x509.KeyUsageFlags.keyCertSign |
x509.KeyUsageFlags.cRLSign |
x509.KeyUsageFlags.digitalSignature |
x509.KeyUsageFlags.keyEncipherment
)
],
attributes: [new x509.ChallengePasswordAttribute("password")]
});
const notBeforeDate = new Date();
const intermediateCert = await x509.X509CertificateGenerator.create({
serialNumber,
subject: csrObj.subject,
issuer: caCertObj.subject,
notBefore: notBeforeDate,
notAfter: new Date(notAfter),
signingKey: parentCaPrivateKey,
publicKey: csrObj.publicKey,
signingAlgorithm: alg,
extensions: [
new x509.KeyUsagesExtension(
x509.KeyUsageFlags.keyCertSign |
x509.KeyUsageFlags.cRLSign |
x509.KeyUsageFlags.digitalSignature |
x509.KeyUsageFlags.keyEncipherment,
true
),
new x509.BasicConstraintsExtension(
true,
ca.maxPathLength === -1 || !ca.maxPathLength ? undefined : ca.maxPathLength,
true
),
await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
]
});
const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
plainText: Buffer.from(new Uint8Array(intermediateCert.rawData))
});
const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({
plainText: Buffer.alloc(0)
});
await certificateAuthorityDAL.transaction(async (tx) => {
const newActiveCaCertVersion = caCert.version + 1;
await certificateAuthorityCertDAL.create(
{
caId: ca.id,
encryptedCertificate,
encryptedCertificateChain,
version: newActiveCaCertVersion,
caSecretId: caSecret.id
},
tx
);
await certificateAuthorityDAL.updateById(
ca.id,
{
activeCaCertVersion: newActiveCaCertVersion,
notBefore: notBeforeDate,
notAfter: new Date(notAfter)
},
tx
);
});
const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({
caId,
certificateAuthorityDAL,
certificateAuthorityCertDAL,
projectDAL,
kmsService
});
certificate = intermediateCert.toString("pem");
certificateChain = `${issuingCaCertificate}\n${caCertChain}`.trim();
break;
}
default: {
throw new BadRequestError({
message: "Unrecognized CA type"
});
}
}
return {
certificate,
certificateChain,
serialNumber
};
};
const getCaCerts = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCaCertsDTO) => {
const ca = await certificateAuthorityDAL.findById(caId);
if (!ca) throw new BadRequestError({ message: "CA not found" });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
ca.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.CertificateAuthorities
);
const caCertChains = await getCaCertChains({
caId,
certificateAuthorityDAL,
certificateAuthorityCertDAL,
projectDAL,
kmsService
});
return {
ca,
caCerts: caCertChains
};
};
/**
* Return current certificate and certificate chain for CA
* get latest?? ca cert
*/
const getCaCert = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCaCertDTO) => {
const ca = await certificateAuthorityDAL.findById(caId);
@@ -628,7 +943,8 @@ export const certificateAuthorityServiceFactory = ({
{
caId: ca.id,
encryptedCertificate,
encryptedCertificateChain
encryptedCertificateChain,
version: 1
},
tx
);
@@ -857,6 +1173,8 @@ export const certificateAuthorityServiceFactory = ({
updateCaById,
deleteCaById,
getCaCsr,
renewCaCert,
getCaCerts,
getCaCert,
signIntermediate,
importCertToCa,
@@ -20,6 +20,10 @@ export enum CaStatus {
PENDING_CERTIFICATE = "pending-certificate"
}
export enum CaRenewalType {
EXISTING = "existing"
}
export type TCreateCaDTO = {
projectSlug: string;
type: CaType;
@@ -53,6 +57,16 @@ export type TGetCaCsrDTO = {
caId: string;
} & Omit<TProjectPermission, "projectId">;
export type TRenewCaCertDTO = {
caId: string;
notAfter: string;
type: CaRenewalType;
} & Omit<TProjectPermission, "projectId">;
export type TGetCaCertsDTO = {
caId: string;
} & Omit<TProjectPermission, "projectId">;
export type TGetCaCertDTO = {
caId: string;
} & Omit<TProjectPermission, "projectId">;
@@ -98,6 +112,14 @@ export type TGetCaCredentialsDTO = {
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
};
export type TGetCaCertChainsDTO = {
caId: string;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "find">;
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
};
export type TGetCaCertChainDTO = {
caId: string;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;