Merge pull request #3583 from Infisical/feat/acme-and-external-ca

feat: acme and external CA for PKI
This commit is contained in:
Sheen
2025-05-27 03:47:36 +08:00
committed by GitHub
128 changed files with 10602 additions and 3021 deletions
+1877
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -131,6 +131,7 @@
"@aws-sdk/client-elasticache": "^3.637.0",
"@aws-sdk/client-iam": "^3.525.0",
"@aws-sdk/client-kms": "^3.609.0",
"@aws-sdk/client-route-53": "^3.810.0",
"@aws-sdk/client-secrets-manager": "^3.504.0",
"@aws-sdk/client-sts": "^3.600.0",
"@casl/ability": "^6.5.0",
@@ -174,6 +175,7 @@
"@slack/oauth": "^3.0.2",
"@slack/web-api": "^7.8.0",
"@ucast/mongo2js": "^1.3.4",
"acme-client": "^5.4.0",
"ajv": "^8.12.0",
"argon2": "^0.31.2",
"aws-sdk": "^2.1553.0",
+2
View File
@@ -53,6 +53,7 @@ import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
import { TCertificateServiceFactory } from "@app/services/certificate/certificate-service";
import { TCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service";
import { TInternalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service";
import { TCertificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
import { TCmekServiceFactory } from "@app/services/cmek/cmek-service";
import { TExternalGroupOrgRoleMappingServiceFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-service";
@@ -269,6 +270,7 @@ declare module "fastify" {
microsoftTeams: TMicrosoftTeamsServiceFactory;
assumePrivileges: TAssumePrivilegeServiceFactory;
githubOrgSync: TGithubOrgSyncServiceFactory;
internalCertificateAuthority: TInternalCertificateAuthorityServiceFactory;
};
// this is exclusive use for middlewares in which we need to inject data
// everywhere else access using service layer
+16
View File
@@ -68,6 +68,9 @@ import {
TDynamicSecrets,
TDynamicSecretsInsert,
TDynamicSecretsUpdate,
TExternalCertificateAuthorities,
TExternalCertificateAuthoritiesInsert,
TExternalCertificateAuthoritiesUpdate,
TExternalGroupOrgRoleMappings,
TExternalGroupOrgRoleMappingsInsert,
TExternalGroupOrgRoleMappingsUpdate,
@@ -155,6 +158,9 @@ import {
TIntegrations,
TIntegrationsInsert,
TIntegrationsUpdate,
TInternalCertificateAuthorities,
TInternalCertificateAuthoritiesInsert,
TInternalCertificateAuthoritiesUpdate,
TInternalKms,
TInternalKmsInsert,
TInternalKmsUpdate,
@@ -538,6 +544,16 @@ declare module "knex/types/tables" {
TCertificateAuthorityCrlInsert,
TCertificateAuthorityCrlUpdate
>;
[TableName.InternalCertificateAuthority]: KnexOriginal.CompositeTableType<
TInternalCertificateAuthorities,
TInternalCertificateAuthoritiesInsert,
TInternalCertificateAuthoritiesUpdate
>;
[TableName.ExternalCertificateAuthority]: KnexOriginal.CompositeTableType<
TExternalCertificateAuthorities,
TExternalCertificateAuthoritiesInsert,
TExternalCertificateAuthoritiesUpdate
>;
[TableName.Certificate]: KnexOriginal.CompositeTableType<TCertificates, TCertificatesInsert, TCertificatesUpdate>;
[TableName.CertificateTemplate]: KnexOriginal.CompositeTableType<
TCertificateTemplates,
@@ -0,0 +1,44 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.Certificate)) {
const hasProjectIdColumn = await knex.schema.hasColumn(TableName.Certificate, "projectId");
if (!hasProjectIdColumn) {
await knex.schema.alterTable(TableName.Certificate, (t) => {
t.string("projectId", 36).nullable();
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
});
await knex.raw(`
UPDATE "${TableName.Certificate}" cert
SET "projectId" = ca."projectId"
FROM "${TableName.CertificateAuthority}" ca
WHERE cert."caId" = ca.id
`);
await knex.schema.alterTable(TableName.Certificate, (t) => {
t.string("projectId").notNullable().alter();
});
}
await knex.schema.alterTable(TableName.Certificate, (t) => {
t.uuid("caId").nullable().alter();
t.uuid("caCertId").nullable().alter();
});
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.Certificate)) {
if (await knex.schema.hasColumn(TableName.Certificate, "projectId")) {
await knex.schema.alterTable(TableName.Certificate, (t) => {
t.dropForeign("projectId");
t.dropColumn("projectId");
});
}
}
// Altering back to notNullable for caId and caCertId will fail
}
@@ -0,0 +1,205 @@
import slugify from "@sindresorhus/slugify";
import { Knex } from "knex";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasCATable = await knex.schema.hasTable(TableName.CertificateAuthority);
const hasExternalCATable = await knex.schema.hasTable(TableName.ExternalCertificateAuthority);
const hasInternalCATable = await knex.schema.hasTable(TableName.InternalCertificateAuthority);
if (hasCATable && !hasInternalCATable) {
await knex.schema.createTableLike(TableName.InternalCertificateAuthority, TableName.CertificateAuthority, (t) => {
t.uuid("caId").nullable();
});
// @ts-expect-error intentional: migration
await knex(TableName.InternalCertificateAuthority).insert(knex(TableName.CertificateAuthority).select("*"));
await knex(TableName.InternalCertificateAuthority).update("caId", knex.ref("id"));
await knex.schema.alterTable(TableName.InternalCertificateAuthority, (t) => {
t.dropColumn("projectId");
t.dropColumn("requireTemplateForIssuance");
t.dropColumn("createdAt");
t.dropColumn("updatedAt");
t.dropColumn("status");
t.uuid("parentCaId")
.nullable()
.references("id")
.inTable(TableName.CertificateAuthority)
.onDelete("CASCADE")
.alter();
t.uuid("activeCaCertId").nullable().references("id").inTable(TableName.CertificateAuthorityCert).alter();
t.uuid("caId").notNullable().references("id").inTable(TableName.CertificateAuthority).onDelete("CASCADE").alter();
});
await knex.schema.alterTable(TableName.CertificateAuthority, (t) => {
t.renameColumn("requireTemplateForIssuance", "enableDirectIssuance");
t.string("name").nullable();
});
// prefill name for existing internal CAs and flip enableDirectIssuance
const cas = await knex(TableName.CertificateAuthority).select("id", "friendlyName", "enableDirectIssuance");
await Promise.all(
cas.map((ca) => {
const slugifiedName = ca.friendlyName
? slugify(`${ca.friendlyName.slice(0, 16)}-${alphaNumericNanoId(8)}`)
: slugify(alphaNumericNanoId(12));
return knex(TableName.CertificateAuthority)
.where({ id: ca.id })
.update({ name: slugifiedName, enableDirectIssuance: !ca.enableDirectIssuance });
})
);
await knex.schema.alterTable(TableName.CertificateAuthority, (t) => {
t.dropColumn("parentCaId");
t.dropColumn("type");
t.dropColumn("friendlyName");
t.dropColumn("organization");
t.dropColumn("ou");
t.dropColumn("country");
t.dropColumn("province");
t.dropColumn("locality");
t.dropColumn("commonName");
t.dropColumn("dn");
t.dropColumn("serialNumber");
t.dropColumn("maxPathLength");
t.dropColumn("keyAlgorithm");
t.dropColumn("notBefore");
t.dropColumn("notAfter");
t.dropColumn("activeCaCertId");
t.boolean("enableDirectIssuance").notNullable().defaultTo(true).alter();
t.string("name").notNullable().alter();
t.unique(["name", "projectId"]);
});
}
if (!hasExternalCATable) {
await knex.schema.createTable(TableName.ExternalCertificateAuthority, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("type").notNullable();
t.uuid("appConnectionId").nullable();
t.foreign("appConnectionId").references("id").inTable(TableName.AppConnection);
t.uuid("dnsAppConnectionId").nullable();
t.foreign("dnsAppConnectionId").references("id").inTable(TableName.AppConnection);
t.uuid("caId").notNullable().references("id").inTable(TableName.CertificateAuthority).onDelete("CASCADE");
t.binary("credentials");
t.json("configuration");
});
}
if (await knex.schema.hasTable(TableName.PkiSubscriber)) {
await knex.schema.alterTable(TableName.PkiSubscriber, (t) => {
t.string("ttl").nullable().alter();
t.boolean("enableAutoRenewal").notNullable().defaultTo(false);
t.integer("autoRenewalPeriodInDays");
t.datetime("lastAutoRenewAt");
t.string("lastOperationStatus");
t.text("lastOperationMessage");
t.dateTime("lastOperationAt");
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasCATable = await knex.schema.hasTable(TableName.CertificateAuthority);
const hasExternalCATable = await knex.schema.hasTable(TableName.ExternalCertificateAuthority);
const hasInternalCATable = await knex.schema.hasTable(TableName.InternalCertificateAuthority);
if (hasCATable && hasInternalCATable) {
// First add all columns as nullable
await knex.schema.alterTable(TableName.CertificateAuthority, (t) => {
t.uuid("parentCaId").nullable().references("id").inTable(TableName.CertificateAuthority).onDelete("CASCADE");
t.string("type").nullable();
t.string("friendlyName").nullable();
t.string("organization").nullable();
t.string("ou").nullable();
t.string("country").nullable();
t.string("province").nullable();
t.string("locality").nullable();
t.string("commonName").nullable();
t.string("dn").nullable();
t.string("serialNumber").nullable().unique();
t.integer("maxPathLength").nullable();
t.string("keyAlgorithm").nullable();
t.timestamp("notBefore").nullable();
t.timestamp("notAfter").nullable();
t.uuid("activeCaCertId").nullable().references("id").inTable(TableName.CertificateAuthorityCert);
t.renameColumn("enableDirectIssuance", "requireTemplateForIssuance");
t.dropColumn("name");
});
// flip requireTemplateForIssuance for existing internal CAs
const cas = await knex(TableName.CertificateAuthority).select("id", "requireTemplateForIssuance");
await Promise.all(
cas.map((ca) => {
return (
knex(TableName.CertificateAuthority)
.where({ id: ca.id })
// @ts-expect-error intentional: migration
.update({ requireTemplateForIssuance: !ca.requireTemplateForIssuance })
);
})
);
await knex.raw(`
UPDATE ${TableName.CertificateAuthority} ca
SET
type = ica.type,
"friendlyName" = ica."friendlyName",
organization = ica.organization,
ou = ica.ou,
country = ica.country,
province = ica.province,
locality = ica.locality,
"commonName" = ica."commonName",
dn = ica.dn,
"parentCaId" = ica."parentCaId",
"serialNumber" = ica."serialNumber",
"maxPathLength" = ica."maxPathLength",
"keyAlgorithm" = ica."keyAlgorithm",
"notBefore" = ica."notBefore",
"notAfter" = ica."notAfter",
"activeCaCertId" = ica."activeCaCertId"
FROM ${TableName.InternalCertificateAuthority} ica
WHERE ca.id = ica."caId"
`);
await knex.schema.alterTable(TableName.CertificateAuthority, (t) => {
t.string("type").notNullable().alter();
t.string("friendlyName").notNullable().alter();
t.string("organization").notNullable().alter();
t.string("ou").notNullable().alter();
t.string("country").notNullable().alter();
t.string("province").notNullable().alter();
t.string("locality").notNullable().alter();
t.string("commonName").notNullable().alter();
t.string("dn").notNullable().alter();
t.string("keyAlgorithm").notNullable().alter();
t.boolean("requireTemplateForIssuance").notNullable().defaultTo(false).alter();
});
await knex.schema.dropTable(TableName.InternalCertificateAuthority);
}
if (hasExternalCATable) {
await knex.schema.dropTable(TableName.ExternalCertificateAuthority);
}
if (await knex.schema.hasTable(TableName.PkiSubscriber)) {
await knex.schema.alterTable(TableName.PkiSubscriber, (t) => {
t.dropColumn("enableAutoRenewal");
t.dropColumn("autoRenewalPeriodInDays");
t.dropColumn("lastAutoRenewAt");
t.dropColumn("lastOperationStatus");
t.dropColumn("lastOperationMessage");
t.dropColumn("lastOperationAt");
});
}
}
@@ -11,25 +11,10 @@ export const CertificateAuthoritiesSchema = z.object({
id: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date(),
parentCaId: z.string().uuid().nullable().optional(),
projectId: z.string(),
type: z.string(),
enableDirectIssuance: z.boolean().default(true),
status: z.string(),
friendlyName: z.string(),
organization: z.string(),
ou: z.string(),
country: z.string(),
province: z.string(),
locality: z.string(),
commonName: z.string(),
dn: z.string(),
serialNumber: z.string().nullable().optional(),
maxPathLength: z.number().nullable().optional(),
keyAlgorithm: z.string(),
notBefore: z.date().nullable().optional(),
notAfter: z.date().nullable().optional(),
activeCaCertId: z.string().uuid().nullable().optional(),
requireTemplateForIssuance: z.boolean().default(false)
name: z.string()
});
export type TCertificateAuthorities = z.infer<typeof CertificateAuthoritiesSchema>;
+4 -3
View File
@@ -11,7 +11,7 @@ export const CertificatesSchema = z.object({
id: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date(),
caId: z.string().uuid(),
caId: z.string().uuid().nullable().optional(),
status: z.string(),
serialNumber: z.string(),
friendlyName: z.string(),
@@ -21,11 +21,12 @@ export const CertificatesSchema = z.object({
revokedAt: z.date().nullable().optional(),
revocationReason: z.number().nullable().optional(),
altNames: z.string().nullable().optional(),
caCertId: z.string().uuid(),
caCertId: z.string().uuid().nullable().optional(),
certificateTemplateId: z.string().uuid().nullable().optional(),
keyUsages: z.string().array().nullable().optional(),
extendedKeyUsages: z.string().array().nullable().optional(),
pkiSubscriberId: z.string().uuid().nullable().optional()
pkiSubscriberId: z.string().uuid().nullable().optional(),
projectId: z.string()
});
export type TCertificates = z.infer<typeof CertificatesSchema>;
@@ -0,0 +1,29 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models";
export const ExternalCertificateAuthoritiesSchema = z.object({
id: z.string().uuid(),
type: z.string(),
appConnectionId: z.string().uuid().nullable().optional(),
dnsAppConnectionId: z.string().uuid().nullable().optional(),
caId: z.string().uuid(),
credentials: zodBuffer.nullable().optional(),
configuration: z.unknown().nullable().optional()
});
export type TExternalCertificateAuthorities = z.infer<typeof ExternalCertificateAuthoritiesSchema>;
export type TExternalCertificateAuthoritiesInsert = Omit<
z.input<typeof ExternalCertificateAuthoritiesSchema>,
TImmutableDBKeys
>;
export type TExternalCertificateAuthoritiesUpdate = Partial<
Omit<z.input<typeof ExternalCertificateAuthoritiesSchema>, TImmutableDBKeys>
>;
+2
View File
@@ -20,6 +20,7 @@ export * from "./certificate-templates";
export * from "./certificates";
export * from "./dynamic-secret-leases";
export * from "./dynamic-secrets";
export * from "./external-certificate-authorities";
export * from "./external-group-org-role-mappings";
export * from "./external-kms";
export * from "./gateways";
@@ -49,6 +50,7 @@ export * from "./identity-universal-auths";
export * from "./incident-contacts";
export * from "./integration-auths";
export * from "./integrations";
export * from "./internal-certificate-authorities";
export * from "./internal-kms";
export * from "./kmip-client-certificates";
export * from "./kmip-clients";
@@ -0,0 +1,38 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const InternalCertificateAuthoritiesSchema = z.object({
id: z.string().uuid(),
parentCaId: z.string().uuid().nullable().optional(),
type: z.string(),
friendlyName: z.string(),
organization: z.string(),
ou: z.string(),
country: z.string(),
province: z.string(),
locality: z.string(),
commonName: z.string(),
dn: z.string(),
serialNumber: z.string().nullable().optional(),
maxPathLength: z.number().nullable().optional(),
keyAlgorithm: z.string(),
notBefore: z.date().nullable().optional(),
notAfter: z.date().nullable().optional(),
activeCaCertId: z.string().uuid().nullable().optional(),
caId: z.string().uuid()
});
export type TInternalCertificateAuthorities = z.infer<typeof InternalCertificateAuthoritiesSchema>;
export type TInternalCertificateAuthoritiesInsert = Omit<
z.input<typeof InternalCertificateAuthoritiesSchema>,
TImmutableDBKeys
>;
export type TInternalCertificateAuthoritiesUpdate = Partial<
Omit<z.input<typeof InternalCertificateAuthoritiesSchema>, TImmutableDBKeys>
>;
+2
View File
@@ -13,6 +13,8 @@ export enum TableName {
SshCertificate = "ssh_certificates",
SshCertificateBody = "ssh_certificate_bodies",
CertificateAuthority = "certificate_authorities",
ExternalCertificateAuthority = "external_certificate_authorities",
InternalCertificateAuthority = "internal_certificate_authorities",
CertificateTemplateEstConfig = "certificate_template_est_configs",
CertificateAuthorityCert = "certificate_authority_certs",
CertificateAuthoritySecret = "certificate_authority_secret",
+8 -2
View File
@@ -16,10 +16,16 @@ export const PkiSubscribersSchema = z.object({
name: z.string(),
commonName: z.string(),
subjectAlternativeNames: z.string().array(),
ttl: z.string(),
ttl: z.string().nullable().optional(),
keyUsages: z.string().array(),
extendedKeyUsages: z.string().array(),
status: z.string()
status: z.string(),
enableAutoRenewal: z.boolean().default(false),
autoRenewalPeriodInDays: z.number().nullable().optional(),
lastAutoRenewAt: z.date().nullable().optional(),
lastOperationStatus: z.string().nullable().optional(),
lastOperationMessage: z.string().nullable().optional(),
lastOperationAt: z.date().nullable().optional()
});
export type TPkiSubscribers = z.infer<typeof PkiSubscribersSchema>;
@@ -21,7 +21,7 @@ import { AppConnection } from "@app/services/app-connection/app-connection-enums
import { TCreateAppConnectionDTO, TUpdateAppConnectionDTO } from "@app/services/app-connection/app-connection-types";
import { ActorType } from "@app/services/auth/auth-type";
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types";
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types";
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-enums";
import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
import { TAllowedFields } from "@app/services/identity-ldap-auth/identity-ldap-auth-types";
import { PkiItemType } from "@app/services/pki-collection/pki-collection-types";
@@ -232,6 +232,7 @@ export enum EventType {
REMOVE_HOST_FROM_SSH_HOST_GROUP = "remove-host-from-ssh-host-group",
CREATE_CA = "create-certificate-authority",
GET_CA = "get-certificate-authority",
GET_CAS = "get-certificate-authorities",
UPDATE_CA = "update-certificate-authority",
DELETE_CA = "delete-certificate-authority",
RENEW_CA = "renew-certificate-authority",
@@ -242,6 +243,7 @@ export enum EventType {
IMPORT_CA_CERT = "import-certificate-authority-cert",
GET_CA_CRLS = "get-certificate-authority-crls",
ISSUE_CERT = "issue-cert",
IMPORT_CERT = "import-cert",
SIGN_CERT = "sign-cert",
GET_CA_CERTIFICATE_TEMPLATES = "get-ca-certificate-templates",
GET_CERT = "get-cert",
@@ -267,7 +269,9 @@ export enum EventType {
GET_PKI_SUBSCRIBER = "get-pki-subscriber",
ISSUE_PKI_SUBSCRIBER_CERT = "issue-pki-subscriber-cert",
SIGN_PKI_SUBSCRIBER_CERT = "sign-pki-subscriber-cert",
AUTOMATED_RENEW_SUBSCRIBER_CERT = "automated-renew-subscriber-cert",
LIST_PKI_SUBSCRIBER_CERTS = "list-pki-subscriber-certs",
GET_SUBSCRIBER_ACTIVE_CERT_BUNDLE = "get-subscriber-active-cert-bundle",
CREATE_KMS = "create-kms",
UPDATE_KMS = "update-kms",
DELETE_KMS = "delete-kms",
@@ -1778,7 +1782,8 @@ interface CreateCa {
type: EventType.CREATE_CA;
metadata: {
caId: string;
dn: string;
name: string;
dn?: string;
};
}
@@ -1786,7 +1791,15 @@ interface GetCa {
type: EventType.GET_CA;
metadata: {
caId: string;
dn: string;
name: string;
dn?: string;
};
}
interface GetCAs {
type: EventType.GET_CAS;
metadata: {
caIds: string[];
};
}
@@ -1794,7 +1807,8 @@ interface UpdateCa {
type: EventType.UPDATE_CA;
metadata: {
caId: string;
dn: string;
name: string;
dn?: string;
status: CaStatus;
};
}
@@ -1803,7 +1817,8 @@ interface DeleteCa {
type: EventType.DELETE_CA;
metadata: {
caId: string;
dn: string;
name: string;
dn?: string;
};
}
@@ -1873,6 +1888,15 @@ interface IssueCert {
};
}
interface ImportCert {
type: EventType.IMPORT_CERT;
metadata: {
certId: string;
cn: string;
serialNumber: string;
};
}
interface SignCert {
type: EventType.SIGN_CERT;
metadata: {
@@ -2040,7 +2064,7 @@ interface CreatePkiSubscriber {
caId?: string;
name: string;
commonName: string;
ttl: string;
ttl?: string;
subjectAlternativeNames: string[];
keyUsages: CertKeyUsage[];
extendedKeyUsages: CertExtendedKeyUsage[];
@@ -2082,7 +2106,15 @@ interface IssuePkiSubscriberCert {
metadata: {
subscriberId: string;
name: string;
serialNumber: string;
serialNumber?: string;
};
}
interface AutomatedRenewPkiSubscriberCert {
type: EventType.AUTOMATED_RENEW_SUBSCRIBER_CERT;
metadata: {
subscriberId: string;
name: string;
};
}
@@ -2104,6 +2136,16 @@ interface ListPkiSubscriberCerts {
};
}
interface GetSubscriberActiveCertBundle {
type: EventType.GET_SUBSCRIBER_ACTIVE_CERT_BUNDLE;
metadata: {
subscriberId: string;
name: string;
certId: string;
serialNumber: string;
};
}
interface CreateKmsEvent {
type: EventType.CREATE_KMS;
metadata: {
@@ -3088,6 +3130,7 @@ export type Event =
| IssueSshHostHostCert
| CreateCa
| GetCa
| GetCAs
| UpdateCa
| DeleteCa
| RenewCa
@@ -3098,6 +3141,7 @@ export type Event =
| ImportCaCert
| GetCaCrls
| IssueCert
| ImportCert
| SignCert
| GetCaCertificateTemplates
| GetCert
@@ -3123,7 +3167,9 @@ export type Event =
| GetPkiSubscriber
| IssuePkiSubscriberCert
| SignPkiSubscriberCert
| AutomatedRenewPkiSubscriberCert
| ListPkiSubscriberCerts
| GetSubscriberActiveCertBundle
| CreateKmsEvent
| UpdateKmsEvent
| DeleteKmsEvent
@@ -7,6 +7,7 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { NotFoundError } from "@app/lib/errors";
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
import { expandInternalCa } from "@app/services/certificate-authority/certificate-authority-fns";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
@@ -14,7 +15,7 @@ import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns
import { TGetCaCrlsDTO, TGetCrlById } from "./certificate-authority-crl-types";
type TCertificateAuthorityCrlServiceFactoryDep = {
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa">;
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "find" | "findById">;
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
@@ -37,7 +38,8 @@ export const certificateAuthorityCrlServiceFactory = ({
const caCrl = await certificateAuthorityCrlDAL.findById(crlId);
if (!caCrl) throw new NotFoundError({ message: `CRL with ID '${crlId}' not found` });
const ca = await certificateAuthorityDAL.findById(caCrl.caId);
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caCrl.caId);
if (!ca?.internalCa?.id) throw new NotFoundError({ message: `Internal CA with ID '${caCrl.caId}' not found` });
const keyId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId,
@@ -54,7 +56,7 @@ export const certificateAuthorityCrlServiceFactory = ({
const crl = new x509.X509Crl(decryptedCrl);
return {
ca,
ca: expandInternalCa(ca),
caCrl,
crl: crl.rawData
};
@@ -64,8 +66,8 @@ export const certificateAuthorityCrlServiceFactory = ({
* Returns a list of CRL ids for CA with id [caId]
*/
const getCaCrls = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCaCrlsDTO) => {
const ca = await certificateAuthorityDAL.findById(caId);
if (!ca) throw new NotFoundError({ message: `CA with ID '${caId}' not found` });
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId);
if (!ca?.internalCa?.id) throw new NotFoundError({ message: `Internal CA with ID '${caId}' not found` });
const { permission } = await permissionService.getProjectPermission({
actor,
@@ -108,7 +110,7 @@ export const certificateAuthorityCrlServiceFactory = ({
);
return {
ca,
ca: expandInternalCa(ca),
crls: decryptedCrls
};
};
@@ -6,7 +6,7 @@ import { isCertChainValid } from "@app/services/certificate/certificate-fns";
import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
import { getCaCertChain, getCaCertChains } from "@app/services/certificate-authority/certificate-authority-fns";
import { TCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service";
import { TInternalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service";
import { TCertificateTemplateDALFactory } from "@app/services/certificate-template/certificate-template-dal";
import { TCertificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
@@ -16,10 +16,10 @@ import { TLicenseServiceFactory } from "../license/license-service";
import { convertRawCertsToPkcs7 } from "./certificate-est-fns";
type TCertificateEstServiceFactoryDep = {
certificateAuthorityService: Pick<TCertificateAuthorityServiceFactory, "signCertFromCa">;
internalCertificateAuthorityService: Pick<TInternalCertificateAuthorityServiceFactory, "signCertFromCa">;
certificateTemplateService: Pick<TCertificateTemplateServiceFactory, "getEstConfiguration">;
certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "findById">;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById" | "findByIdWithAssociatedCa">;
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "find" | "findById">;
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
@@ -29,7 +29,7 @@ type TCertificateEstServiceFactoryDep = {
export type TCertificateEstServiceFactory = ReturnType<typeof certificateEstServiceFactory>;
export const certificateEstServiceFactory = ({
certificateAuthorityService,
internalCertificateAuthorityService,
certificateTemplateService,
certificateTemplateDAL,
certificateAuthorityCertDAL,
@@ -127,7 +127,7 @@ export const certificateEstServiceFactory = ({
});
}
const { certificate } = await certificateAuthorityService.signCertFromCa({
const { certificate } = await internalCertificateAuthorityService.signCertFromCa({
isInternal: true,
certificateTemplateId,
csr
@@ -188,7 +188,7 @@ export const certificateEstServiceFactory = ({
}
}
const { certificate } = await certificateAuthorityService.signCertFromCa({
const { certificate } = await internalCertificateAuthorityService.signCertFromCa({
isInternal: true,
certificateTemplateId,
csr
@@ -227,15 +227,15 @@ export const certificateEstServiceFactory = ({
});
}
const ca = await certificateAuthorityDAL.findById(certTemplate.caId);
if (!ca) {
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(certTemplate.caId);
if (!ca?.internalCa?.id) {
throw new NotFoundError({
message: `Certificate Authority with ID '${certTemplate.caId}' not found`
message: `Internal Certificate Authority with ID '${certTemplate.caId}' not found`
});
}
const { caCert, caCertChain } = await getCaCertChain({
caCertId: ca.activeCaCertId as string,
caCertId: ca.internalCa.activeCaCertId as string,
certificateAuthorityDAL,
certificateAuthorityCertDAL,
projectDAL,
+2
View File
@@ -37,6 +37,8 @@ export const KeyStorePrefixes = {
`sync-integration-last-run-${projectId}-${environmentSlug}-${secretPath}` as const,
SecretSyncLock: (syncId: string) => `secret-sync-mutex-${syncId}` as const,
SecretRotationLock: (rotationId: string) => `secret-rotation-v2-mutex-${rotationId}` as const,
CaOrderCertificateForSubscriberLock: (subscriberId: string) =>
`ca-order-certificate-for-subscriber-lock-${subscriberId}` as const,
SecretSyncLastRunTimestamp: (syncId: string) => `secret-sync-last-run-${syncId}` as const,
IdentityAccessTokenStatusUpdate: (identityAccessTokenId: string) =>
`identity-access-token-status:${identityAccessTokenId}`,
+70 -2
View File
@@ -5,6 +5,8 @@ import {
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-maps";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connection-maps";
import { CaType } from "@app/services/certificate-authority/certificate-authority-enums";
import { CERTIFICATE_AUTHORITIES_TYPE_MAP } from "@app/services/certificate-authority/certificate-authority-maps";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { SECRET_SYNC_CONNECTION_MAP, SECRET_SYNC_NAME_MAP } from "@app/services/secret-sync/secret-sync-maps";
@@ -1707,6 +1709,19 @@ export const CERTIFICATES = {
certificateChain: "The certificate chain of the certificate.",
serialNumberRes: "The serial number of the certificate.",
privateKey: "The private key of the certificate."
},
IMPORT: {
projectSlug: "Slug of the project to import the certificate into.",
certificatePem: "The PEM-encoded leaf certificate.",
privateKeyPem: "The PEM-encoded private key corresponding to the certificate.",
chainPem: "The PEM-encoded chain of intermediate certificates.",
friendlyName: "A friendly name for the certificate.",
pkiCollectionId: "The ID of the PKI collection to add the certificate to.",
certificate: "The issued certificate.",
certificateChain: "The certificate chain of the issued certificate.",
privateKey: "The private key of the issued certificate.",
serialNumber: "The serial number of the issued certificate."
}
};
@@ -1778,6 +1793,14 @@ export const PKI_SUBSCRIBERS = {
subscriberName: "The name of the PKI subscriber to get.",
projectId: "The ID of the project to get the PKI subscriber for."
},
GET_LATEST_CERT_BUNDLE: {
subscriberName: "The name of the PKI subscriber to get the active certificate bundle for.",
projectId: "The ID of the project to get the active certificate bundle for.",
certificate: "The active certificate for the subscriber.",
certificateChain: "The certificate chain of the active certificate for the subscriber.",
privateKey: "The private key of the active certificate for the subscriber.",
serialNumber: "The serial number of the active certificate for the subscriber."
},
CREATE: {
projectId: "The ID of the project to create the PKI subscriber in.",
caId: "The ID of the CA that will issue certificates for the PKI subscriber.",
@@ -1788,7 +1811,9 @@ export const PKI_SUBSCRIBERS = {
subjectAlternativeNames:
"A list of Subject Alternative Names (SANs) to be used on certificates issued for this subscriber; these can be host names or email addresses.",
keyUsages: "The key usage extension to be used on certificates issued for this subscriber.",
extendedKeyUsages: "The extended key usage extension to be used on certificates issued for this subscriber."
extendedKeyUsages: "The extended key usage extension to be used on certificates issued for this subscriber.",
enableAutoRenewal: "Whether or not to enable auto renewal for the PKI subscriber.",
autoRenewalPeriodInDays: "The period in days to auto renew the PKI subscriber's certificates."
},
UPDATE: {
projectId: "The ID of the project to update the PKI subscriber in.",
@@ -1802,7 +1827,9 @@ export const PKI_SUBSCRIBERS = {
"A comma-delimited list of Subject Alternative Names (SANs) to be used on certificates issued for this subscriber; these can be host names or email addresses.",
keyUsages: "The key usage extension to be used on certificates issued for this subscriber to update to.",
extendedKeyUsages:
"The extended key usage extension to be used on certificates issued for this subscriber to update to."
"The extended key usage extension to be used on certificates issued for this subscriber to update to.",
enableAutoRenewal: "Whether or not to enable auto renewal for the PKI subscriber.",
autoRenewalPeriodInDays: "The period in days to auto renew the PKI subscriber's certificates."
},
DELETE: {
subscriberName: "The name of the PKI subscriber to delete.",
@@ -1991,6 +2018,47 @@ export const ProjectTemplates = {
}
};
export const CertificateAuthorities = {
CREATE: (type: CaType) => ({
name: `The name of the ${CERTIFICATE_AUTHORITIES_TYPE_MAP[type]} Certificate Authority to create. Must be slug-friendly.`,
projectId: `The ID of the project to create the Certificate Authority in.`,
enableDirectIssuance: `Whether or not to enable direct issuance of certificates for the ${CERTIFICATE_AUTHORITIES_TYPE_MAP[type]} Certificate Authority.`,
status: `The status of the ${CERTIFICATE_AUTHORITIES_TYPE_MAP[type]} Certificate Authority.`
}),
UPDATE: (type: CaType) => ({
caId: `The ID of the ${CERTIFICATE_AUTHORITIES_TYPE_MAP[type]} Certificate Authority to update.`,
projectId: `The ID of the project to update the Certificate Authority in.`,
name: `The updated name of the ${CERTIFICATE_AUTHORITIES_TYPE_MAP[type]} Certificate Authority. Must be slug-friendly.`,
enableDirectIssuance: `Whether or not to enable direct issuance of certificates for the ${CERTIFICATE_AUTHORITIES_TYPE_MAP[type]} Certificate Authority.`,
status: `The updated status of the ${CERTIFICATE_AUTHORITIES_TYPE_MAP[type]} Certificate Authority.`
}),
CONFIGURATIONS: {
ACME: {
dnsAppConnectionId: `The ID of the App Connection to use for creating and managing DNS TXT records required for ACME domain validation. This connection must have permissions to create and delete TXT records in your DNS provider (e.g., Route53) for the ACME challenge process.`,
directoryUrl: `The directory URL for the ACME Certificate Authority.`,
accountEmail: `The email address for the ACME Certificate Authority.`,
provider: `The DNS provider for the ACME Certificate Authority.`,
hostedZoneId: `The hosted zone ID for the ACME Certificate Authority.`
},
INTERNAL: {
type: "The type of CA to create.",
friendlyName: "A friendly name for the CA.",
organization: "The organization (O) for the CA.",
ou: "The organization unit (OU) for the CA.",
country: "The country name (C) for the CA.",
province: "The state of province name for the CA.",
locality: "The locality name for the CA.",
commonName: "The common name (CN) for the CA.",
notBefore: "The date and time when the CA becomes valid in YYYY-MM-DDTHH:mm:ss.sssZ format.",
notAfter: "The date and time when the CA expires in YYYY-MM-DDTHH:mm:ss.sssZ format.",
maxPathLength:
"The maximum number of intermediate CAs that may follow this CA in the certificate / CA chain. A maxPathLength of -1 implies no path limit on the chain.",
keyAlgorithm:
"The type of public key algorithm and size, in bits, of the key pair for the CA; when you create an intermediate CA, you must use a key algorithm supported by the parent CA."
}
}
};
export const AppConnections = {
GET_BY_ID: (app: AppConnection) => ({
connectionId: `The ID of the ${APP_CONNECTION_NAME_MAP[app]} Connection to retrieve.`
+17 -1
View File
@@ -14,6 +14,7 @@ import {
} from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-queue-types";
import { getConfig } from "@app/lib/config/env";
import { logger } from "@app/lib/logger";
import { CaType } from "@app/services/certificate-authority/certificate-authority-enums";
import {
TFailedIntegrationSyncEmailsPayload,
TIntegrationSyncPayload,
@@ -36,6 +37,7 @@ export enum QueueName {
AuditLogPrune = "audit-log-prune",
DailyResourceCleanUp = "daily-resource-cleanup",
DailyExpiringPkiItemAlert = "daily-expiring-pki-item-alert",
PkiSubscriber = "pki-subscriber",
TelemetryInstanceStats = "telemtry-self-hosted-stats",
IntegrationSync = "sync-integrations",
SecretWebhook = "secret-webhook",
@@ -44,6 +46,7 @@ export enum QueueName {
UpgradeProjectToGhost = "upgrade-project-to-ghost",
DynamicSecretRevocation = "dynamic-secret-revocation",
CaCrlRotation = "ca-crl-rotation",
CaLifecycle = "ca-lifecycle", // parent queue to ca-order-certificate-for-subscriber
SecretReplication = "secret-replication",
SecretSync = "secret-sync", // parent queue to push integration sync, webhook, and secret replication
ProjectV3Migration = "project-v3-migration",
@@ -84,7 +87,9 @@ export enum QueueJobs {
SecretRotationV2QueueRotations = "secret-rotation-v2-queue-rotations",
SecretRotationV2RotateSecrets = "secret-rotation-v2-rotate-secrets",
SecretRotationV2SendNotification = "secret-rotation-v2-send-notification",
InvalidateCache = "invalidate-cache"
InvalidateCache = "invalidate-cache",
CaOrderCertificateForSubscriber = "ca-order-certificate-for-subscriber",
PkiSubscriberDailyAutoRenewal = "pki-subscriber-daily-auto-renewal"
}
export type TQueueJobTypes = {
@@ -245,6 +250,17 @@ export type TQueueJobTypes = {
};
};
};
[QueueName.CaLifecycle]: {
name: QueueJobs.CaOrderCertificateForSubscriber;
payload: {
subscriberId: string;
caType: CaType;
};
};
[QueueName.PkiSubscriber]: {
name: QueueJobs.PkiSubscriberDailyAutoRenewal;
payload: undefined;
};
};
export type TQueueServiceFactory = ReturnType<typeof queueServiceFactory>;
+111 -53
View File
@@ -132,6 +132,10 @@ import { certificateAuthorityDALFactory } from "@app/services/certificate-author
import { certificateAuthorityQueueFactory } from "@app/services/certificate-authority/certificate-authority-queue";
import { certificateAuthoritySecretDALFactory } from "@app/services/certificate-authority/certificate-authority-secret-dal";
import { certificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service";
import { externalCertificateAuthorityDALFactory } from "@app/services/certificate-authority/external-certificate-authority-dal";
import { internalCertificateAuthorityDALFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-dal";
import { InternalCertificateAuthorityFns } from "@app/services/certificate-authority/internal/internal-certificate-authority-fns";
import { internalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service";
import { certificateTemplateDALFactory } from "@app/services/certificate-template/certificate-template-dal";
import { certificateTemplateEstConfigDALFactory } from "@app/services/certificate-template/certificate-template-est-config-dal";
import { certificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
@@ -199,6 +203,7 @@ import { pkiCollectionDALFactory } from "@app/services/pki-collection/pki-collec
import { pkiCollectionItemDALFactory } from "@app/services/pki-collection/pki-collection-item-dal";
import { pkiCollectionServiceFactory } from "@app/services/pki-collection/pki-collection-service";
import { pkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal";
import { pkiSubscriberQueueServiceFactory } from "@app/services/pki-subscriber/pki-subscriber-queue";
import { pkiSubscriberServiceFactory } from "@app/services/pki-subscriber/pki-subscriber-service";
import { projectDALFactory } from "@app/services/project/project-dal";
import { projectQueueFactory } from "@app/services/project/project-queue";
@@ -817,6 +822,8 @@ export const registerRoutes = async (
});
const certificateAuthorityDAL = certificateAuthorityDALFactory(db);
const internalCertificateAuthorityDAL = internalCertificateAuthorityDALFactory(db);
const externalCertificateAuthorityDAL = externalCertificateAuthorityDALFactory(db);
const certificateAuthorityCertDAL = certificateAuthorityCertDALFactory(db);
const certificateAuthoritySecretDAL = certificateAuthoritySecretDALFactory(db);
const certificateAuthorityCrlDAL = certificateAuthorityCrlDALFactory(db);
@@ -842,17 +849,9 @@ export const registerRoutes = async (
certificateAuthoritySecretDAL,
projectDAL,
kmsService,
permissionService
});
const certificateAuthorityQueue = certificateAuthorityQueueFactory({
certificateAuthorityCrlDAL,
certificateAuthorityDAL,
certificateAuthoritySecretDAL,
certificateDAL,
projectDAL,
kmsService,
queueService
permissionService,
pkiCollectionDAL,
pkiCollectionItemDAL
});
const sshCertificateAuthorityService = sshCertificateAuthorityServiceFactory({
@@ -901,23 +900,6 @@ export const registerRoutes = async (
groupDAL
});
const certificateAuthorityService = certificateAuthorityServiceFactory({
certificateAuthorityDAL,
certificateAuthorityCertDAL,
certificateAuthoritySecretDAL,
certificateAuthorityCrlDAL,
certificateTemplateDAL,
certificateAuthorityQueue,
certificateDAL,
certificateBodyDAL,
certificateSecretDAL,
pkiCollectionDAL,
pkiCollectionItemDAL,
projectDAL,
kmsService,
permissionService
});
const certificateAuthorityCrlService = certificateAuthorityCrlServiceFactory({
certificateAuthorityDAL,
certificateAuthorityCrlDAL,
@@ -937,17 +919,6 @@ export const registerRoutes = async (
licenseService
});
const certificateEstService = certificateEstServiceFactory({
certificateAuthorityService,
certificateTemplateService,
certificateTemplateDAL,
certificateAuthorityCertDAL,
certificateAuthorityDAL,
projectDAL,
kmsService,
licenseService
});
const pkiAlertService = pkiAlertServiceFactory({
pkiAlertDAL,
pkiCollectionDAL,
@@ -965,20 +936,6 @@ export const registerRoutes = async (
projectDAL
});
const pkiSubscriberService = pkiSubscriberServiceFactory({
pkiSubscriberDAL,
certificateAuthorityDAL,
certificateAuthorityCertDAL,
certificateAuthoritySecretDAL,
certificateAuthorityCrlDAL,
certificateDAL,
certificateBodyDAL,
certificateSecretDAL,
projectDAL,
kmsService,
permissionService
});
const projectTemplateService = projectTemplateServiceFactory({
licenseService,
permissionService,
@@ -1648,6 +1605,52 @@ export const registerRoutes = async (
licenseService
});
const certificateAuthorityQueue = certificateAuthorityQueueFactory({
certificateAuthorityCrlDAL,
certificateAuthorityDAL,
certificateAuthoritySecretDAL,
certificateDAL,
projectDAL,
kmsService,
queueService,
pkiSubscriberDAL,
certificateBodyDAL,
certificateSecretDAL,
externalCertificateAuthorityDAL,
keyStore,
appConnectionDAL,
appConnectionService
});
const internalCertificateAuthorityService = internalCertificateAuthorityServiceFactory({
certificateAuthorityDAL,
certificateAuthorityCertDAL,
certificateAuthoritySecretDAL,
certificateAuthorityCrlDAL,
certificateTemplateDAL,
certificateAuthorityQueue,
certificateDAL,
certificateBodyDAL,
certificateSecretDAL,
pkiCollectionDAL,
pkiCollectionItemDAL,
projectDAL,
internalCertificateAuthorityDAL,
kmsService,
permissionService
});
const certificateEstService = certificateEstServiceFactory({
internalCertificateAuthorityService,
certificateTemplateService,
certificateTemplateDAL,
certificateAuthorityCertDAL,
certificateAuthorityDAL,
projectDAL,
kmsService,
licenseService
});
const kmipService = kmipServiceFactory({
kmipClientDAL,
permissionService,
@@ -1687,6 +1690,59 @@ export const registerRoutes = async (
appConnectionDAL
});
const certificateAuthorityService = certificateAuthorityServiceFactory({
certificateAuthorityDAL,
projectDAL,
permissionService,
appConnectionDAL,
appConnectionService,
externalCertificateAuthorityDAL,
internalCertificateAuthorityService,
certificateDAL,
certificateBodyDAL,
certificateSecretDAL,
kmsService,
pkiSubscriberDAL
});
const internalCaFns = InternalCertificateAuthorityFns({
certificateAuthorityDAL,
certificateAuthorityCertDAL,
certificateAuthoritySecretDAL,
certificateAuthorityCrlDAL,
certificateDAL,
certificateBodyDAL,
certificateSecretDAL,
projectDAL,
kmsService
});
const pkiSubscriberQueue = pkiSubscriberQueueServiceFactory({
queueService,
pkiSubscriberDAL,
certificateAuthorityDAL,
certificateAuthorityQueue,
certificateDAL,
auditLogService,
internalCaFns
});
const pkiSubscriberService = pkiSubscriberServiceFactory({
pkiSubscriberDAL,
certificateAuthorityDAL,
certificateAuthorityCertDAL,
certificateAuthoritySecretDAL,
certificateAuthorityCrlDAL,
certificateDAL,
certificateBodyDAL,
certificateSecretDAL,
projectDAL,
kmsService,
permissionService,
certificateAuthorityQueue,
internalCaFns
});
await secretRotationV2QueueServiceFactory({
secretRotationV2Service,
secretRotationV2DAL,
@@ -1707,6 +1763,7 @@ export const registerRoutes = async (
await telemetryQueue.startTelemetryCheck();
await dailyResourceCleanUp.startCleanUp();
await dailyExpiringPkiItemAlert.startSendingAlerts();
await pkiSubscriberQueue.startDailyAutoRenewalJob();
await kmsService.startService();
await microsoftTeamsService.start();
@@ -1772,6 +1829,7 @@ export const registerRoutes = async (
sshHost: sshHostService,
sshHostGroup: sshHostGroupService,
certificateAuthority: certificateAuthorityService,
internalCertificateAuthority: internalCertificateAuthorityService,
certificateTemplate: certificateTemplateService,
certificateAuthorityCrl: certificateAuthorityCrlService,
certificateEst: certificateEstService,
@@ -1,9 +1,11 @@
import { z } from "zod";
import {
CertificateAuthoritiesSchema,
DynamicSecretsSchema,
IdentityProjectAdditionalPrivilegeSchema,
IntegrationAuthsSchema,
InternalCertificateAuthoritiesSchema,
ProjectRolesSchema,
ProjectsSchema,
SecretApprovalPoliciesSchema,
@@ -272,3 +274,15 @@ export const SanitizedTagSchema = SecretTagsSchema.pick({
}).extend({
name: z.string()
});
export const InternalCertificateAuthorityResponseSchema = CertificateAuthoritiesSchema.merge(
InternalCertificateAuthoritiesSchema.omit({
caId: true,
notAfter: true,
notBefore: true
})
).extend({
requireTemplateForIssuance: z.boolean().optional(),
notAfter: z.string().optional(),
notBefore: z.string().optional()
});
@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-floating-promises */
import { z } from "zod";
import { CertificateAuthoritiesSchema, CertificateTemplatesSchema } from "@app/db/schemas";
import { CertificateTemplatesSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags, CERTIFICATE_AUTHORITIES } from "@app/lib/api-docs";
import { ms } from "@app/lib/ms";
@@ -10,13 +10,19 @@ import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types";
import { CaRenewalType, CaStatus, CaType } from "@app/services/certificate-authority/certificate-authority-types";
import {
CaRenewalType,
CaStatus,
InternalCaType
} from "@app/services/certificate-authority/certificate-authority-enums";
import {
validateAltNamesField,
validateCaDateField
} from "@app/services/certificate-authority/certificate-authority-validators";
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
import { InternalCertificateAuthorityResponseSchema } from "../sanitizedSchemas";
export const registerCaRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
@@ -32,7 +38,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
body: z
.object({
projectSlug: z.string().trim().describe(CERTIFICATE_AUTHORITIES.CREATE.projectSlug),
type: z.nativeEnum(CaType).describe(CERTIFICATE_AUTHORITIES.CREATE.type),
type: z.nativeEnum(InternalCaType).describe(CERTIFICATE_AUTHORITIES.CREATE.type),
friendlyName: z.string().optional().describe(CERTIFICATE_AUTHORITIES.CREATE.friendlyName),
commonName: z.string().trim().describe(CERTIFICATE_AUTHORITIES.CREATE.commonName),
organization: z.string().trim().describe(CERTIFICATE_AUTHORITIES.CREATE.organization),
@@ -68,16 +74,18 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
),
response: {
200: z.object({
ca: CertificateAuthoritiesSchema
ca: InternalCertificateAuthorityResponseSchema
})
}
},
handler: async (req) => {
const ca = await server.services.certificateAuthority.createCa({
const ca = await server.services.internalCertificateAuthority.createCa({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
isInternal: false,
actorOrgId: req.permission.orgId,
enableDirectIssuance: !req.body.requireTemplateForIssuance,
...req.body
});
@@ -87,6 +95,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
event: {
type: EventType.CREATE_CA,
metadata: {
name: ca.name,
caId: ca.id,
dn: ca.dn
}
@@ -115,12 +124,12 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
}),
response: {
200: z.object({
ca: CertificateAuthoritiesSchema
ca: InternalCertificateAuthorityResponseSchema
})
}
},
handler: async (req) => {
const ca = await server.services.certificateAuthority.getCaById({
const ca = await server.services.internalCertificateAuthority.getCaById({
caId: req.params.caId,
actor: req.permission.type,
actorId: req.permission.id,
@@ -135,6 +144,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
type: EventType.GET_CA,
metadata: {
caId: ca.id,
name: ca.name,
dn: ca.dn
}
}
@@ -167,7 +177,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
}
},
handler: async (req, res) => {
const caCert = await server.services.certificateAuthority.getCaCertById(req.params);
const caCert = await server.services.internalCertificateAuthority.getCaCertById(req.params);
res.header("Content-Type", "application/pkix-cert");
@@ -198,17 +208,19 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
}),
response: {
200: z.object({
ca: CertificateAuthoritiesSchema
ca: InternalCertificateAuthorityResponseSchema
})
}
},
handler: async (req) => {
const ca = await server.services.certificateAuthority.updateCaById({
const ca = await server.services.internalCertificateAuthority.updateCaById({
caId: req.params.caId,
actor: req.permission.type,
actorId: req.permission.id,
isInternal: false,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
enableDirectIssuance: !req.body.requireTemplateForIssuance,
...req.body
});
@@ -220,6 +232,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
metadata: {
caId: ca.id,
dn: ca.dn,
name: ca.name,
status: ca.status as CaStatus
}
}
@@ -247,12 +260,12 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
}),
response: {
200: z.object({
ca: CertificateAuthoritiesSchema
ca: InternalCertificateAuthorityResponseSchema
})
}
},
handler: async (req) => {
const ca = await server.services.certificateAuthority.deleteCaById({
const ca = await server.services.internalCertificateAuthority.deleteCaById({
caId: req.params.caId,
actor: req.permission.type,
actorId: req.permission.id,
@@ -266,6 +279,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
event: {
type: EventType.DELETE_CA,
metadata: {
name: ca.name,
caId: ca.id,
dn: ca.dn
}
@@ -299,7 +313,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
}
},
handler: async (req) => {
const { ca, csr } = await server.services.certificateAuthority.getCaCsr({
const { ca, csr } = await server.services.internalCertificateAuthority.getCaCsr({
caId: req.params.caId,
actor: req.permission.type,
actorId: req.permission.id,
@@ -353,7 +367,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
},
handler: async (req) => {
const { certificate, certificateChain, serialNumber, ca } =
await server.services.certificateAuthority.renewCaCert({
await server.services.internalCertificateAuthority.renewCaCert({
caId: req.params.caId,
actor: req.permission.type,
actorId: req.permission.id,
@@ -408,7 +422,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
}
},
handler: async (req) => {
const { caCerts, ca } = await server.services.certificateAuthority.getCaCerts({
const { caCerts, ca } = await server.services.internalCertificateAuthority.getCaCerts({
caId: req.params.caId,
actor: req.permission.type,
actorId: req.permission.id,
@@ -455,13 +469,14 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
}
},
handler: async (req) => {
const { certificate, certificateChain, serialNumber, ca } = await server.services.certificateAuthority.getCaCert({
caId: req.params.caId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
const { certificate, certificateChain, serialNumber, ca } =
await server.services.internalCertificateAuthority.getCaCert({
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,
@@ -517,7 +532,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
},
handler: async (req) => {
const { certificate, certificateChain, issuingCaCertificate, serialNumber, ca } =
await server.services.certificateAuthority.signIntermediate({
await server.services.internalCertificateAuthority.signIntermediate({
caId: req.params.caId,
actor: req.permission.type,
actorId: req.permission.id,
@@ -574,7 +589,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
}
},
handler: async (req) => {
const { ca } = await server.services.certificateAuthority.importCertToCa({
const { ca } = await server.services.internalCertificateAuthority.importCertToCa({
caId: req.params.caId,
actor: req.permission.type,
actorId: req.permission.id,
@@ -653,7 +668,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
},
handler: async (req) => {
const { certificate, certificateChain, issuingCaCertificate, privateKey, serialNumber, ca } =
await server.services.certificateAuthority.issueCertFromCa({
await server.services.internalCertificateAuthority.issueCertFromCa({
caId: req.params.caId,
actor: req.permission.type,
actorId: req.permission.id,
@@ -746,7 +761,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
},
handler: async (req) => {
const { certificate, certificateChain, issuingCaCertificate, serialNumber, ca, commonName } =
await server.services.certificateAuthority.signCertFromCa({
await server.services.internalCertificateAuthority.signCertFromCa({
isInternal: false,
caId: req.params.caId,
actor: req.permission.type,
@@ -809,13 +824,15 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
}
},
handler: async (req) => {
const { certificateTemplates, ca } = await server.services.certificateAuthority.getCaCertificateTemplates({
caId: req.params.caId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
const { certificateTemplates, ca } = await server.services.internalCertificateAuthority.getCaCertificateTemplates(
{
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,
@@ -0,0 +1,18 @@
import {
AcmeCertificateAuthoritySchema,
CreateAcmeCertificateAuthoritySchema,
UpdateAcmeCertificateAuthoritySchema
} from "@app/services/certificate-authority/acme/acme-certificate-authority-schemas";
import { CaType } from "@app/services/certificate-authority/certificate-authority-enums";
import { registerCertificateAuthorityEndpoints } from "./certificate-authority-endpoints";
export const registerAcmeCertificateAuthorityRouter = async (server: FastifyZodProvider) => {
registerCertificateAuthorityEndpoints({
caType: CaType.ACME,
server,
responseSchema: AcmeCertificateAuthoritySchema,
createSchema: CreateAcmeCertificateAuthoritySchema,
updateSchema: UpdateAcmeCertificateAuthoritySchema
});
};
@@ -0,0 +1,258 @@
import { z } from "zod";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags } from "@app/lib/api-docs";
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 { CaStatus, CaType } from "@app/services/certificate-authority/certificate-authority-enums";
import {
TCertificateAuthority,
TCertificateAuthorityInput
} from "@app/services/certificate-authority/certificate-authority-types";
export const registerCertificateAuthorityEndpoints = <
T extends TCertificateAuthority,
I extends TCertificateAuthorityInput
>({
server,
caType,
createSchema,
updateSchema,
responseSchema
}: {
caType: CaType;
server: FastifyZodProvider;
createSchema: z.ZodType<{
name: string;
projectId: string;
status: CaStatus;
configuration: I["configuration"];
enableDirectIssuance: boolean;
}>;
updateSchema: z.ZodType<{
projectId: string;
name?: string;
status?: CaStatus;
configuration?: I["configuration"];
enableDirectIssuance?: boolean;
}>;
responseSchema: z.ZodTypeAny;
}) => {
server.route({
method: "GET",
url: `/`,
config: {
rateLimit: readLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiCertificateAuthorities],
querystring: z.object({
projectId: z.string().trim().min(1, "Project ID required")
}),
response: {
200: responseSchema.array()
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const {
query: { projectId }
} = req;
const certificateAuthorities = (await server.services.certificateAuthority.listCertificateAuthoritiesByProjectId(
{ projectId, type: caType },
req.permission
)) as T[];
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId,
event: {
type: EventType.GET_CAS,
metadata: {
caIds: certificateAuthorities.map((ca) => ca.id)
}
}
});
return certificateAuthorities;
}
});
server.route({
method: "GET",
url: "/:caName",
config: {
rateLimit: readLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiCertificateAuthorities],
params: z.object({
caName: z.string()
}),
querystring: z.object({
projectId: z.string().uuid()
}),
response: {
200: responseSchema
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { caName } = req.params;
const { projectId } = req.query;
const certificateAuthority =
(await server.services.certificateAuthority.findCertificateAuthorityByNameAndProjectId(
{ caName, type: caType, projectId },
req.permission
)) as T;
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: certificateAuthority.projectId,
event: {
type: EventType.GET_CA,
metadata: {
caId: certificateAuthority.id,
name: certificateAuthority.name
}
}
});
return certificateAuthority;
}
});
server.route({
method: "POST",
url: "/",
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiCertificateAuthorities],
body: createSchema,
response: {
200: responseSchema
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const certificateAuthority = (await server.services.certificateAuthority.createCertificateAuthority(
{ ...req.body, type: caType },
req.permission
)) as T;
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: certificateAuthority.projectId,
event: {
type: EventType.CREATE_CA,
metadata: {
name: certificateAuthority.name,
caId: certificateAuthority.id
}
}
});
return certificateAuthority;
}
});
server.route({
method: "PATCH",
url: "/:caName",
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiCertificateAuthorities],
params: z.object({
caName: z.string()
}),
body: updateSchema,
response: {
200: responseSchema
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { caName } = req.params;
const certificateAuthority = (await server.services.certificateAuthority.updateCertificateAuthority(
{
...req.body,
type: caType,
caName
},
req.permission
)) as T;
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: certificateAuthority.projectId,
event: {
type: EventType.UPDATE_CA,
metadata: {
name: certificateAuthority.name,
caId: certificateAuthority.id,
status: certificateAuthority.status
}
}
});
return certificateAuthority;
}
});
server.route({
method: "DELETE",
url: "/:caName",
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiCertificateAuthorities],
params: z.object({
caName: z.string()
}),
body: z.object({
projectId: z.string().uuid()
}),
response: {
200: responseSchema
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { caName } = req.params;
const { projectId } = req.body;
const certificateAuthority = (await server.services.certificateAuthority.deleteCertificateAuthority(
{ caName, type: caType, projectId },
req.permission
)) as T;
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: certificateAuthority.projectId,
event: {
type: EventType.DELETE_CA,
metadata: {
name: certificateAuthority.name,
caId: certificateAuthority.id
}
}
});
return certificateAuthority;
}
});
};
@@ -0,0 +1,12 @@
import { CaType } from "@app/services/certificate-authority/certificate-authority-enums";
import { registerAcmeCertificateAuthorityRouter } from "./acme-certificate-authority-router";
import { registerInternalCertificateAuthorityRouter } from "./internal-certificate-authority-router";
export * from "./internal-certificate-authority-router";
export const CERTIFICATE_AUTHORITY_REGISTER_ROUTER_MAP: Record<CaType, (server: FastifyZodProvider) => Promise<void>> =
{
[CaType.INTERNAL]: registerInternalCertificateAuthorityRouter,
[CaType.ACME]: registerAcmeCertificateAuthorityRouter
};
@@ -0,0 +1,18 @@
import { CaType } from "@app/services/certificate-authority/certificate-authority-enums";
import {
CreateInternalCertificateAuthoritySchema,
InternalCertificateAuthoritySchema,
UpdateInternalCertificateAuthoritySchema
} from "@app/services/certificate-authority/internal/internal-certificate-authority-schemas";
import { registerCertificateAuthorityEndpoints } from "./certificate-authority-endpoints";
export const registerInternalCertificateAuthorityRouter = async (server: FastifyZodProvider) => {
registerCertificateAuthorityEndpoints({
caType: CaType.INTERNAL,
server,
responseSchema: InternalCertificateAuthoritySchema,
createSchema: CreateInternalCertificateAuthoritySchema,
updateSchema: UpdateInternalCertificateAuthoritySchema
});
};
@@ -39,7 +39,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
}
},
handler: async (req) => {
const { cert, ca } = await server.services.certificate.getCert({
const { cert } = await server.services.certificate.getCert({
serialNumber: req.params.serialNumber,
actor: req.permission.type,
actorId: req.permission.id,
@@ -49,7 +49,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: ca.projectId,
projectId: cert.projectId,
event: {
type: EventType.GET_CERT,
metadata: {
@@ -86,7 +86,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
}
},
handler: async (req, reply) => {
const { ca, cert, certPrivateKey } = await server.services.certificate.getCertPrivateKey({
const { cert, certPrivateKey } = await server.services.certificate.getCertPrivateKey({
serialNumber: req.params.serialNumber,
actor: req.permission.type,
actorId: req.permission.id,
@@ -96,7 +96,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: ca.projectId,
projectId: cert.projectId,
event: {
type: EventType.GET_CERT_PRIVATE_KEY,
metadata: {
@@ -138,7 +138,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
}
},
handler: async (req, reply) => {
const { certificate, certificateChain, serialNumber, cert, ca, privateKey } =
const { certificate, certificateChain, serialNumber, cert, privateKey } =
await server.services.certificate.getCertBundle({
serialNumber: req.params.serialNumber,
actor: req.permission.type,
@@ -149,7 +149,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: ca.projectId,
projectId: cert.projectId,
event: {
type: EventType.GET_CERT_BUNDLE,
metadata: {
@@ -242,7 +242,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
},
handler: async (req) => {
const { certificate, certificateChain, issuingCaCertificate, privateKey, serialNumber, ca } =
await server.services.certificateAuthority.issueCertFromCa({
await server.services.internalCertificateAuthority.issueCertFromCa({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
@@ -284,6 +284,68 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
}
});
server.route({
method: "POST",
url: "/import-certificate",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.PkiCertificates],
description: "Import certificate",
body: z.object({
projectSlug: z.string().trim().min(1).describe(CERTIFICATES.IMPORT.projectSlug),
certificatePem: z.string().trim().min(1).describe(CERTIFICATES.IMPORT.certificatePem),
privateKeyPem: z.string().trim().min(1).describe(CERTIFICATES.IMPORT.privateKeyPem),
chainPem: z.string().trim().min(1).describe(CERTIFICATES.IMPORT.chainPem),
friendlyName: z.string().trim().optional().describe(CERTIFICATES.IMPORT.friendlyName),
pkiCollectionId: z.string().trim().optional().describe(CERTIFICATES.IMPORT.pkiCollectionId)
}),
response: {
200: z.object({
certificate: z.string().trim().describe(CERTIFICATES.IMPORT.certificate),
certificateChain: z.string().trim().describe(CERTIFICATES.IMPORT.certificateChain),
privateKey: z.string().trim().describe(CERTIFICATES.IMPORT.privateKey),
serialNumber: z.string().trim().describe(CERTIFICATES.IMPORT.serialNumber)
})
}
},
handler: async (req) => {
const { certificate, certificateChain, privateKey, serialNumber, cert } =
await server.services.certificate.importCert({
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: cert.projectId,
event: {
type: EventType.IMPORT_CERT,
metadata: {
certId: cert.id,
cn: cert.commonName,
serialNumber
}
}
});
return {
certificate,
certificateChain,
privateKey,
serialNumber
};
}
});
server.route({
method: "POST",
url: "/sign-certificate",
@@ -355,7 +417,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
},
handler: async (req) => {
const { certificate, certificateChain, issuingCaCertificate, serialNumber, ca, commonName } =
await server.services.certificateAuthority.signCertFromCa({
await server.services.internalCertificateAuthority.signCertFromCa({
isInternal: false,
actor: req.permission.type,
actorId: req.permission.id,
@@ -474,7 +536,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
}
},
handler: async (req) => {
const { deletedCert, ca } = await server.services.certificate.deleteCert({
const { deletedCert } = await server.services.certificate.deleteCert({
serialNumber: req.params.serialNumber,
actor: req.permission.type,
actorId: req.permission.id,
@@ -484,7 +546,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: ca.projectId,
projectId: deletedCert.projectId,
event: {
type: EventType.DELETE_CERT,
metadata: {
@@ -524,7 +586,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
}
},
handler: async (req) => {
const { certificate, certificateChain, serialNumber, cert, ca } = await server.services.certificate.getCertBody({
const { certificate, certificateChain, serialNumber, cert } = await server.services.certificate.getCertBody({
serialNumber: req.params.serialNumber,
actor: req.permission.type,
actorId: req.permission.id,
@@ -534,7 +596,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: ca.projectId,
projectId: cert.projectId,
event: {
type: EventType.GET_CERT_BODY,
metadata: {
+11
View File
@@ -10,6 +10,7 @@ import { registerAdminRouter } from "./admin-router";
import { registerAuthRoutes } from "./auth-router";
import { registerProjectBotRouter } from "./bot-router";
import { registerCaRouter } from "./certificate-authority-router";
import { CERTIFICATE_AUTHORITY_REGISTER_ROUTER_MAP } from "./certificate-authority-routers";
import { registerCertRouter } from "./certificate-router";
import { registerCertificateTemplateRouter } from "./certificate-template-router";
import { registerExternalGroupOrgRoleMappingRouter } from "./external-group-org-role-mapping-router";
@@ -104,6 +105,16 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
await server.register(
async (pkiRouter) => {
await pkiRouter.register(registerCaRouter, { prefix: "/ca" });
await pkiRouter.register(
async (caRouter) => {
for await (const [caType, router] of Object.entries(CERTIFICATE_AUTHORITY_REGISTER_ROUTER_MAP)) {
await caRouter.register(router, { prefix: `/${caType}` });
}
},
{
prefix: "/ca"
}
);
await pkiRouter.register(registerCertRouter, { prefix: "/certificates" });
await pkiRouter.register(registerCertificateTemplateRouter, { prefix: "/certificate-templates" });
await pkiRouter.register(registerPkiAlertRouter, { prefix: "/alerts" });
@@ -5,6 +5,7 @@ import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags, PKI_SUBSCRIBERS } from "@app/lib/api-docs";
import { ms } from "@app/lib/ms";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { addNoCacheHeaders } from "@app/server/lib/caching";
import { slugSchema } from "@app/server/lib/schemas";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
@@ -90,7 +91,8 @@ export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) =>
ttl: z
.string()
.trim()
.refine((val) => ms(val) > 0, "TTL must be a positive number")
.refine((val) => !val || ms(val) > 0, "TTL must be a positive number")
.optional()
.describe(PKI_SUBSCRIBERS.CREATE.ttl),
subjectAlternativeNames: validateAltNameField
.array()
@@ -108,7 +110,9 @@ export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) =>
.array()
.default([])
.transform((arr) => Array.from(new Set(arr)))
.describe(PKI_SUBSCRIBERS.CREATE.extendedKeyUsages)
.describe(PKI_SUBSCRIBERS.CREATE.extendedKeyUsages),
enableAutoRenewal: z.boolean().optional().describe(PKI_SUBSCRIBERS.CREATE.enableAutoRenewal),
autoRenewalPeriodInDays: z.number().min(1).optional().describe(PKI_SUBSCRIBERS.CREATE.autoRenewalPeriodInDays)
}),
response: {
200: sanitizedPkiSubscriber
@@ -134,7 +138,7 @@ export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) =>
caId: subscriber.caId ?? undefined,
name: subscriber.name,
commonName: subscriber.commonName,
ttl: subscriber.ttl,
ttl: subscriber.ttl ?? undefined,
subjectAlternativeNames: subscriber.subjectAlternativeNames,
keyUsages: subscriber.keyUsages as CertKeyUsage[],
extendedKeyUsages: subscriber.extendedKeyUsages as CertExtendedKeyUsage[]
@@ -179,7 +183,7 @@ export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) =>
ttl: z
.string()
.trim()
.refine((val) => ms(val) > 0, "TTL must be a positive number")
.refine((val) => !val || ms(val) > 0, "TTL must be a positive number")
.optional()
.describe(PKI_SUBSCRIBERS.UPDATE.ttl),
keyUsages: z
@@ -193,7 +197,9 @@ export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) =>
.array()
.transform((arr) => Array.from(new Set(arr)))
.optional()
.describe(PKI_SUBSCRIBERS.UPDATE.extendedKeyUsages)
.describe(PKI_SUBSCRIBERS.UPDATE.extendedKeyUsages),
enableAutoRenewal: z.boolean().optional().describe(PKI_SUBSCRIBERS.UPDATE.enableAutoRenewal),
autoRenewalPeriodInDays: z.number().min(1).optional().describe(PKI_SUBSCRIBERS.UPDATE.autoRenewalPeriodInDays)
}),
response: {
200: sanitizedPkiSubscriber
@@ -219,7 +225,7 @@ export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) =>
caId: subscriber.caId ?? undefined,
name: subscriber.name,
commonName: subscriber.commonName,
ttl: subscriber.ttl,
ttl: subscriber.ttl ?? undefined,
subjectAlternativeNames: subscriber.subjectAlternativeNames,
keyUsages: subscriber.keyUsages as CertKeyUsage[],
extendedKeyUsages: subscriber.extendedKeyUsages as CertExtendedKeyUsage[]
@@ -278,6 +284,67 @@ export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) =>
}
});
server.route({
method: "POST",
url: "/:subscriberName/order-certificate",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.PkiSubscribers],
description: "Order certificate",
params: z.object({
subscriberName: z.string().describe(PKI_SUBSCRIBERS.ISSUE_CERT.subscriberName)
}),
body: z.object({
projectId: z.string().trim().describe(PKI_SUBSCRIBERS.ISSUE_CERT.projectId)
}),
response: {
200: z.object({
message: z.string().trim()
})
}
},
handler: async (req) => {
const subscriber = await server.services.pkiSubscriber.orderSubscriberCert({
subscriberName: req.params.subscriberName,
projectId: req.body.projectId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: subscriber.projectId,
event: {
type: EventType.ISSUE_PKI_SUBSCRIBER_CERT,
metadata: {
subscriberId: subscriber.id,
name: subscriber.name
}
}
});
await server.services.telemetry.sendPostHogEvents({
event: PostHogEventTypes.IssueCert,
distinctId: getTelemetryDistinctId(req),
properties: {
subscriberId: subscriber.id,
commonName: subscriber.commonName,
...req.auditLogInfo
}
});
return {
message: "Successfully placed order for certificate"
};
}
});
server.route({
method: "POST",
url: "/:subscriberName/issue-certificate",
@@ -420,6 +487,72 @@ export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) =>
}
});
server.route({
method: "GET",
url: "/:subscriberName/latest-certificate-bundle",
config: {
rateLimit: readLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiSubscribers],
description: "Get latest certificate bundle of a subscriber",
params: z.object({
subscriberName: z.string().describe(PKI_SUBSCRIBERS.GET_LATEST_CERT_BUNDLE.subscriberName)
}),
querystring: z.object({
projectId: z.string().trim().describe(PKI_SUBSCRIBERS.GET_LATEST_CERT_BUNDLE.projectId)
}),
response: {
200: z.object({
certificate: z.string().trim().describe(PKI_SUBSCRIBERS.GET_LATEST_CERT_BUNDLE.certificate),
certificateChain: z
.string()
.trim()
.nullable()
.describe(PKI_SUBSCRIBERS.GET_LATEST_CERT_BUNDLE.certificateChain),
privateKey: z.string().trim().describe(PKI_SUBSCRIBERS.GET_LATEST_CERT_BUNDLE.privateKey),
serialNumber: z.string().trim().describe(PKI_SUBSCRIBERS.GET_LATEST_CERT_BUNDLE.serialNumber)
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req, reply) => {
const { certificate, certificateChain, serialNumber, cert, privateKey, subscriber } =
await server.services.pkiSubscriber.getSubscriberActiveCertBundle({
subscriberName: req.params.subscriberName,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.query
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: cert.projectId,
event: {
type: EventType.GET_SUBSCRIBER_ACTIVE_CERT_BUNDLE,
metadata: {
subscriberId: subscriber.id,
name: subscriber.name,
certId: cert.id,
serialNumber: cert.serialNumber
}
}
});
addNoCacheHeaders(reply);
return {
certificate,
certificateChain,
serialNumber,
privateKey
};
}
});
server.route({
method: "GET",
url: "/:subscriberName/certificates",
@@ -0,0 +1,71 @@
import { z } from "zod";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags } from "@app/lib/api-docs";
import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { AcmeCertificateAuthoritySchema } from "@app/services/certificate-authority/acme/acme-certificate-authority-schemas";
import { CaType } from "@app/services/certificate-authority/certificate-authority-enums";
import { InternalCertificateAuthoritySchema } from "@app/services/certificate-authority/internal/internal-certificate-authority-schemas";
const CertificateAuthoritySchema = z.discriminatedUnion("type", [
InternalCertificateAuthoritySchema,
AcmeCertificateAuthoritySchema
]);
export const registerCaRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.PkiCertificateAuthorities],
description: "Get Certificate Authorities",
querystring: z.object({
projectId: z.string()
}),
response: {
200: z.object({
certificateAuthorities: CertificateAuthoritySchema.array()
})
}
},
handler: async (req) => {
const internalCas = await server.services.certificateAuthority.listCertificateAuthoritiesByProjectId(
{
projectId: req.query.projectId,
type: CaType.INTERNAL
},
req.permission
);
const acmeCas = await server.services.certificateAuthority.listCertificateAuthoritiesByProjectId(
{
projectId: req.query.projectId,
type: CaType.ACME
},
req.permission
);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: req.query.projectId,
event: {
type: EventType.GET_CAS,
metadata: {
caIds: [...(internalCas ?? []).map((ca) => ca.id), ...(acmeCas ?? []).map((ca) => ca.id)]
}
}
});
return {
certificateAuthorities: [...(internalCas ?? []), ...(acmeCas ?? [])]
};
}
});
};
+2
View File
@@ -1,3 +1,4 @@
import { registerCaRouter } from "./certificate-authority-router";
import { registerGroupProjectRouter } from "./group-project-router";
import { registerIdentityOrgRouter } from "./identity-org-router";
import { registerIdentityProjectRouter } from "./identity-project-router";
@@ -14,6 +15,7 @@ export const registerV2Routes = async (server: FastifyZodProvider) => {
await server.register(registerUserRouter, { prefix: "/users" });
await server.register(registerServiceTokenRouter, { prefix: "/service-token" });
await server.register(registerPasswordRouter, { prefix: "/password" });
await server.register(registerCaRouter, { prefix: "/pki/ca" });
await server.register(
async (orgRouter) => {
await orgRouter.register(registerOrgRouter);
@@ -1,7 +1,6 @@
import { z } from "zod";
import {
CertificateAuthoritiesSchema,
CertificatesSchema,
PkiAlertsSchema,
PkiCollectionsSchema,
@@ -22,13 +21,13 @@ import { slugSchema } from "@app/server/lib/schemas";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types";
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-enums";
import { sanitizedCertificateTemplate } from "@app/services/certificate-template/certificate-template-schema";
import { sanitizedPkiSubscriber } from "@app/services/pki-subscriber/pki-subscriber-schema";
import { ProjectFilterType } from "@app/services/project/project-types";
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
import { SanitizedProjectSchema } from "../sanitizedSchemas";
import { InternalCertificateAuthorityResponseSchema, SanitizedProjectSchema } from "../sanitizedSchemas";
const projectWithEnv = SanitizedProjectSchema.extend({
_id: z.string(),
@@ -385,7 +384,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
}),
response: {
200: z.object({
cas: z.array(CertificateAuthoritiesSchema)
cas: z.array(InternalCertificateAuthorityResponseSchema)
})
}
},
@@ -0,0 +1,3 @@
export enum AcmeDnsProvider {
Route53 = "route53"
}
@@ -0,0 +1,521 @@
import { ChangeResourceRecordSetsCommand, Route53Client } from "@aws-sdk/client-route-53";
import * as x509 from "@peculiar/x509";
import acme from "acme-client";
import { KeyObject } from "crypto";
import { TableName } from "@app/db/schemas";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { OrgServiceActor } from "@app/lib/types";
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
import { AppConnection, AWSRegion } from "@app/services/app-connection/app-connection-enums";
import { decryptAppConnection } from "@app/services/app-connection/app-connection-fns";
import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
import { getAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-fns";
import { TAwsConnection, TAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-types";
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
import {
CertExtendedKeyUsage,
CertKeyAlgorithm,
CertKeyUsage,
CertStatus
} from "@app/services/certificate/certificate-types";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TPkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal";
import { TProjectDALFactory } from "@app/services/project/project-dal";
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 { TExternalCertificateAuthorityDALFactory } from "../external-certificate-authority-dal";
import { AcmeDnsProvider } from "./acme-certificate-authority-enums";
import { AcmeCertificateAuthorityCredentialsSchema } from "./acme-certificate-authority-schemas";
import {
TAcmeCertificateAuthority,
TCreateAcmeCertificateAuthorityDTO,
TUpdateAcmeCertificateAuthorityDTO
} from "./acme-certificate-authority-types";
type TAcmeCertificateAuthorityFnsDeps = {
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById">;
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">;
certificateAuthorityDAL: Pick<
TCertificateAuthorityDALFactory,
"create" | "transaction" | "findByIdWithAssociatedCa" | "updateById" | "findWithAssociatedCa"
>;
externalCertificateAuthorityDAL: Pick<TExternalCertificateAuthorityDALFactory, "create" | "update">;
certificateDAL: Pick<TCertificateDALFactory, "create" | "transaction">;
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "create">;
kmsService: Pick<
TKmsServiceFactory,
"encryptWithKmsKey" | "generateKmsKey" | "createCipherPairWithDataKey" | "decryptWithKmsKey"
>;
pkiSubscriberDAL: Pick<TPkiSubscriberDALFactory, "findById">;
projectDAL: Pick<TProjectDALFactory, "findById" | "findOne" | "updateById" | "transaction">;
};
type DBConfigurationColumn = {
dnsProvider: string;
directoryUrl: string;
accountEmail: string;
hostedZoneId: string;
};
export const castDbEntryToAcmeCertificateAuthority = (
ca: Awaited<ReturnType<TCertificateAuthorityDALFactory["findByIdWithAssociatedCa"]>>
): TAcmeCertificateAuthority & { credentials: unknown } => {
if (!ca.externalCa?.id) {
throw new BadRequestError({ message: "Malformed ACME certificate authority" });
}
const dbConfigurationCol = ca.externalCa.configuration as DBConfigurationColumn;
return {
id: ca.id,
type: CaType.ACME,
enableDirectIssuance: ca.enableDirectIssuance,
name: ca.name,
projectId: ca.projectId,
credentials: ca.externalCa.credentials,
configuration: {
dnsAppConnectionId: ca.externalCa.dnsAppConnectionId as string,
dnsProviderConfig: {
provider: dbConfigurationCol.dnsProvider as AcmeDnsProvider,
hostedZoneId: dbConfigurationCol.hostedZoneId
},
directoryUrl: dbConfigurationCol.directoryUrl,
accountEmail: dbConfigurationCol.accountEmail
},
status: ca.status as CaStatus
};
};
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!,
region: config.region
});
const command = new ChangeResourceRecordSetsCommand({
HostedZoneId: hostedZoneId,
ChangeBatch: {
Comment: "Set ACME challenge TXT record",
Changes: [
{
Action: "UPSERT",
ResourceRecordSet: {
Name: domain,
Type: "TXT",
TTL: 30,
ResourceRecords: [{ Value: value }]
}
}
]
}
});
await route53Client.send(command);
};
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!,
region: config.region
});
const command = new ChangeResourceRecordSetsCommand({
HostedZoneId: hostedZoneId,
ChangeBatch: {
Comment: "Delete ACME challenge TXT record",
Changes: [
{
Action: "DELETE",
ResourceRecordSet: {
Name: domain,
Type: "TXT",
TTL: 30,
ResourceRecords: [{ Value: value }]
}
}
]
}
});
await route53Client.send(command);
};
export const AcmeCertificateAuthorityFns = ({
appConnectionDAL,
appConnectionService,
certificateAuthorityDAL,
externalCertificateAuthorityDAL,
certificateDAL,
certificateBodyDAL,
certificateSecretDAL,
kmsService,
projectDAL,
pkiSubscriberDAL
}: TAcmeCertificateAuthorityFnsDeps) => {
const createCertificateAuthority = async ({
name,
projectId,
configuration,
enableDirectIssuance,
actor,
status
}: {
status: CaStatus;
name: string;
projectId: string;
configuration: TCreateAcmeCertificateAuthorityDTO["configuration"];
enableDirectIssuance: boolean;
actor: OrgServiceActor;
}) => {
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 (dnsProviderConfig.provider === AcmeDnsProvider.Route53 && appConnection.app !== AppConnection.AWS) {
throw new BadRequestError({
message: `App connection with ID '${dnsAppConnectionId}' is not an AWS connection`
});
}
// validates permission to connect
await appConnectionService.connectAppConnectionById(appConnection.app as AppConnection, dnsAppConnectionId, actor);
const caEntity = await certificateAuthorityDAL.transaction(async (tx) => {
try {
const ca = await certificateAuthorityDAL.create(
{
projectId,
enableDirectIssuance,
name,
status
},
tx
);
await externalCertificateAuthorityDAL.create(
{
caId: ca.id,
dnsAppConnectionId,
type: CaType.ACME,
configuration: {
directoryUrl,
accountEmail,
dnsProvider: dnsProviderConfig.provider,
hostedZoneId: dnsProviderConfig.hostedZoneId
}
},
tx
);
return await certificateAuthorityDAL.findByIdWithAssociatedCa(ca.id, tx);
} 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 authority with the same name already exists in your project"
});
}
throw error;
}
});
if (!caEntity.externalCa?.id) {
throw new BadRequestError({ message: "Failed to create external certificate authority" });
}
return castDbEntryToAcmeCertificateAuthority(caEntity);
};
const updateCertificateAuthority = async ({
id,
status,
configuration,
enableDirectIssuance,
actor,
name
}: {
id: string;
status?: CaStatus;
configuration: TUpdateAcmeCertificateAuthorityDTO["configuration"];
enableDirectIssuance?: boolean;
actor: OrgServiceActor;
name?: string;
}) => {
const updatedCa = await certificateAuthorityDAL.transaction(async (tx) => {
if (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 (dnsProviderConfig.provider === AcmeDnsProvider.Route53 && appConnection.app !== AppConnection.AWS) {
throw new BadRequestError({
message: `App connection with ID '${dnsAppConnectionId}' is not an AWS connection`
});
}
// validates permission to connect
await appConnectionService.connectAppConnectionById(
appConnection.app as AppConnection,
dnsAppConnectionId,
actor
);
await externalCertificateAuthorityDAL.update(
{
caId: id,
type: CaType.ACME
},
{
dnsAppConnectionId,
configuration: {
directoryUrl,
accountEmail,
dnsProvider: dnsProviderConfig.provider,
hostedZoneId: dnsProviderConfig.hostedZoneId
}
},
tx
);
}
if (name || status || enableDirectIssuance) {
await certificateAuthorityDAL.updateById(
id,
{
name,
status,
enableDirectIssuance
},
tx
);
}
return certificateAuthorityDAL.findByIdWithAssociatedCa(id, tx);
});
if (!updatedCa.externalCa?.id) {
throw new BadRequestError({ message: "Failed to update external certificate authority" });
}
return castDbEntryToAcmeCertificateAuthority(updatedCa);
};
const listCertificateAuthorities = async ({ projectId }: { projectId: string }) => {
const cas = await certificateAuthorityDAL.findWithAssociatedCa({
[`${TableName.CertificateAuthority}.projectId` as "projectId"]: projectId,
[`${TableName.ExternalCertificateAuthority}.type` as "type"]: CaType.ACME
});
return cas.map(castDbEntryToAcmeCertificateAuthority);
};
const orderSubscriberCertificate = async (subscriberId: string) => {
const subscriber = await pkiSubscriberDAL.findById(subscriberId);
if (!subscriber.caId) {
throw new BadRequestError({ message: "Subscriber does not have a CA" });
}
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(subscriber.caId);
if (!ca.externalCa || ca.externalCa.type !== CaType.ACME) {
throw new BadRequestError({ message: "CA is not an ACME CA" });
}
const acmeCa = castDbEntryToAcmeCertificateAuthority(ca);
if (acmeCa.status !== CaStatus.ACTIVE) {
throw new BadRequestError({ message: "CA is disabled" });
}
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId,
projectDAL,
kmsService
});
const kmsEncryptor = await kmsService.encryptWithKmsKey({
kmsId: certificateManagerKmsId
});
const kmsDecryptor = await kmsService.decryptWithKmsKey({
kmsId: certificateManagerKmsId
});
let accountKey: Buffer | undefined;
if (acmeCa.credentials) {
const decryptedCredentials = await kmsDecryptor({
cipherTextBlob: acmeCa.credentials as Buffer
});
const parsedCredentials = await AcmeCertificateAuthorityCredentialsSchema.parseAsync(
JSON.parse(decryptedCredentials.toString("utf8"))
);
accountKey = Buffer.from(parsedCredentials.accountKey, "base64");
}
if (!accountKey) {
accountKey = await acme.crypto.createPrivateRsaKey();
const newCredentials = {
accountKey: accountKey.toString("base64")
};
const { cipherTextBlob: encryptedNewCredentials } = await kmsEncryptor({
plainText: Buffer.from(JSON.stringify(newCredentials))
});
await externalCertificateAuthorityDAL.update(
{
caId: acmeCa.id
},
{
credentials: encryptedNewCredentials
}
);
}
await blockLocalAndPrivateIpAddresses(acmeCa.configuration.directoryUrl);
const acmeClient = new acme.Client({
directoryUrl: acmeCa.configuration.directoryUrl,
accountKey
});
const alg = keyAlgorithmToAlgCfg(CertKeyAlgorithm.RSA_2048);
const leafKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
const skLeafObj = KeyObject.from(leafKeys.privateKey);
const skLeaf = skLeafObj.export({ format: "pem", type: "pkcs8" }) as string;
const [, certificateCsr] = await acme.crypto.createCsr(
{
altNames: subscriber.subjectAlternativeNames,
commonName: subscriber.commonName
},
skLeaf
);
const appConnection = await appConnectionDAL.findById(acmeCa.configuration.dnsAppConnectionId);
const connection = await decryptAppConnection(appConnection, kmsService);
const pem = await acmeClient.auto({
csr: certificateCsr,
email: acmeCa.configuration.accountEmail,
challengePriority: ["dns-01"],
termsOfServiceAgreed: true,
challengeCreateFn: async (authz, challenge, keyAuthorization) => {
if (challenge.type !== "dns-01") {
throw new Error("Unsupported challenge type");
}
const recordName = `_acme-challenge.${authz.identifier.value}`; // e.g., "_acme-challenge.example.com"
const recordValue = `"${keyAuthorization}"`; // must be double quoted
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.dnsProviderConfig.provider === AcmeDnsProvider.Route53) {
await route53DeleteTxtRecord(
connection as TAwsConnection,
acmeCa.configuration.dnsProviderConfig.hostedZoneId,
recordName,
recordValue
);
}
}
});
const [leafCert, parentCert] = acme.crypto.splitPemChain(pem);
const certObj = new x509.X509Certificate(leafCert);
const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
plainText: Buffer.from(new Uint8Array(certObj.rawData))
});
const certificateChainPem = parentCert.trim();
const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({
plainText: Buffer.from(certificateChainPem)
});
const { cipherTextBlob: encryptedPrivateKey } = await kmsEncryptor({
plainText: Buffer.from(skLeaf)
});
await certificateDAL.transaction(async (tx) => {
const cert = await certificateDAL.create(
{
caId: ca.id,
pkiSubscriberId: subscriber.id,
status: CertStatus.ACTIVE,
friendlyName: subscriber.commonName,
commonName: subscriber.commonName,
altNames: subscriber.subjectAlternativeNames.join(","),
serialNumber: certObj.serialNumber,
notBefore: certObj.notBefore,
notAfter: certObj.notAfter,
keyUsages: subscriber.keyUsages as CertKeyUsage[],
extendedKeyUsages: subscriber.extendedKeyUsages as CertExtendedKeyUsage[],
projectId: ca.projectId
},
tx
);
await certificateBodyDAL.create(
{
certId: cert.id,
encryptedCertificate,
encryptedCertificateChain
},
tx
);
await certificateSecretDAL.create(
{
certId: cert.id,
encryptedPrivateKey
},
tx
);
});
};
return {
createCertificateAuthority,
updateCertificateAuthority,
listCertificateAuthorities,
orderSubscriberCertificate
};
};
@@ -0,0 +1,39 @@
import { z } from "zod";
import { CertificateAuthorities } from "@app/lib/api-docs/constants";
import { CaType } from "../certificate-authority-enums";
import {
BaseCertificateAuthoritySchema,
GenericCreateCertificateAuthorityFieldsSchema,
GenericUpdateCertificateAuthorityFieldsSchema
} from "../certificate-authority-schemas";
import { AcmeDnsProvider } from "./acme-certificate-authority-enums";
export const AcmeCertificateAuthorityConfigurationSchema = z.object({
dnsAppConnectionId: z.string().uuid().trim().describe(CertificateAuthorities.CONFIGURATIONS.ACME.dnsAppConnectionId),
// soon, differentiate via the provider property
dnsProviderConfig: z.object({
provider: z.nativeEnum(AcmeDnsProvider).describe(CertificateAuthorities.CONFIGURATIONS.ACME.provider),
hostedZoneId: z.string().trim().min(1).describe(CertificateAuthorities.CONFIGURATIONS.ACME.hostedZoneId)
}),
directoryUrl: z.string().url().trim().min(1).describe(CertificateAuthorities.CONFIGURATIONS.ACME.directoryUrl),
accountEmail: z.string().trim().min(1).describe(CertificateAuthorities.CONFIGURATIONS.ACME.accountEmail)
});
export const AcmeCertificateAuthorityCredentialsSchema = z.object({
accountKey: z.string()
});
export const AcmeCertificateAuthoritySchema = BaseCertificateAuthoritySchema.extend({
type: z.literal(CaType.ACME),
configuration: AcmeCertificateAuthorityConfigurationSchema
});
export const CreateAcmeCertificateAuthoritySchema = GenericCreateCertificateAuthorityFieldsSchema(CaType.ACME).extend({
configuration: AcmeCertificateAuthorityConfigurationSchema
});
export const UpdateAcmeCertificateAuthoritySchema = GenericUpdateCertificateAuthorityFieldsSchema(CaType.ACME).extend({
configuration: AcmeCertificateAuthorityConfigurationSchema.optional()
});
@@ -0,0 +1,15 @@
import { z } from "zod";
import {
AcmeCertificateAuthoritySchema,
CreateAcmeCertificateAuthoritySchema,
UpdateAcmeCertificateAuthoritySchema
} from "./acme-certificate-authority-schemas";
export type TAcmeCertificateAuthority = z.infer<typeof AcmeCertificateAuthoritySchema>;
export type TAcmeCertificateAuthorityInput = z.infer<typeof CreateAcmeCertificateAuthoritySchema>;
export type TCreateAcmeCertificateAuthorityDTO = z.infer<typeof CreateAcmeCertificateAuthoritySchema>;
export type TUpdateAcmeCertificateAuthorityDTO = z.infer<typeof UpdateAcmeCertificateAuthoritySchema>;
@@ -1,13 +1,188 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { CertificateAuthoritiesSchema, TableName, TCertificateAuthorities } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex";
import { buildFindFilter, ormify, selectAllTableCols, TFindOpt } from "@app/lib/knex";
export type TCertificateAuthorityDALFactory = ReturnType<typeof certificateAuthorityDALFactory>;
export type TCertificateAuthorityWithAssociatedCa = Awaited<
ReturnType<TCertificateAuthorityDALFactory["findByIdWithAssociatedCa"]>
>;
export const certificateAuthorityDALFactory = (db: TDbClient) => {
const caOrm = ormify(db, TableName.CertificateAuthority);
const findByNameAndProjectIdWithAssociatedCa = async (caName: string, projectId: string, tx?: Knex) => {
const result = await (tx || db.replicaNode())(TableName.CertificateAuthority)
.leftJoin(
TableName.InternalCertificateAuthority,
`${TableName.CertificateAuthority}.id`,
`${TableName.InternalCertificateAuthority}.caId`
)
.leftJoin(
TableName.ExternalCertificateAuthority,
`${TableName.CertificateAuthority}.id`,
`${TableName.ExternalCertificateAuthority}.caId`
)
.where(`${TableName.CertificateAuthority}.name`, caName)
.where(`${TableName.CertificateAuthority}.projectId`, projectId)
.select(selectAllTableCols(TableName.CertificateAuthority))
.select(
db.ref("id").withSchema(TableName.InternalCertificateAuthority).as("internalCaId"),
db.ref("parentCaId").withSchema(TableName.InternalCertificateAuthority).as("internalParentCaId"),
db.ref("type").withSchema(TableName.InternalCertificateAuthority).as("internalType"),
db.ref("friendlyName").withSchema(TableName.InternalCertificateAuthority).as("internalFriendlyName"),
db.ref("organization").withSchema(TableName.InternalCertificateAuthority).as("internalOrganization"),
db.ref("ou").withSchema(TableName.InternalCertificateAuthority).as("internalOu"),
db.ref("country").withSchema(TableName.InternalCertificateAuthority).as("internalCountry"),
db.ref("province").withSchema(TableName.InternalCertificateAuthority).as("internalProvince"),
db.ref("locality").withSchema(TableName.InternalCertificateAuthority).as("internalLocality"),
db.ref("commonName").withSchema(TableName.InternalCertificateAuthority).as("internalCommonName"),
db.ref("dn").withSchema(TableName.InternalCertificateAuthority).as("internalDn"),
db.ref("serialNumber").withSchema(TableName.InternalCertificateAuthority).as("internalSerialNumber"),
db.ref("maxPathLength").withSchema(TableName.InternalCertificateAuthority).as("internalMaxPathLength"),
db.ref("keyAlgorithm").withSchema(TableName.InternalCertificateAuthority).as("internalKeyAlgorithm"),
db.ref("notBefore").withSchema(TableName.InternalCertificateAuthority).as("internalNotBefore"),
db.ref("notAfter").withSchema(TableName.InternalCertificateAuthority).as("internalNotAfter"),
db.ref("activeCaCertId").withSchema(TableName.InternalCertificateAuthority).as("internalActiveCaCertId")
)
.select(
db.ref("id").withSchema(TableName.ExternalCertificateAuthority).as("externalCaId"),
db.ref("type").withSchema(TableName.ExternalCertificateAuthority).as("externalType"),
db.ref("configuration").withSchema(TableName.ExternalCertificateAuthority).as("externalConfiguration"),
db.ref("credentials").withSchema(TableName.ExternalCertificateAuthority).as("externalCredentials"),
db
.ref("dnsAppConnectionId")
.withSchema(TableName.ExternalCertificateAuthority)
.as("externalDnsAppConnectionId"),
db.ref("appConnectionId").withSchema(TableName.ExternalCertificateAuthority).as("externalAppConnectionId")
)
.first();
const data = {
...CertificateAuthoritiesSchema.parse(result),
internalCa: result
? {
id: result.internalCaId,
parentCaId: result.internalParentCaId,
type: result.internalType,
friendlyName: result.internalFriendlyName,
organization: result.internalOrganization,
ou: result.internalOu,
country: result.internalCountry,
province: result.internalProvince,
locality: result.internalLocality,
commonName: result.internalCommonName,
dn: result.internalDn,
serialNumber: result.internalSerialNumber,
maxPathLength: result.internalMaxPathLength,
keyAlgorithm: result.internalKeyAlgorithm,
notBefore: result.internalNotBefore?.toISOString(),
notAfter: result.internalNotAfter?.toISOString(),
activeCaCertId: result.internalActiveCaCertId
}
: undefined,
externalCa: result
? {
id: result.externalCaId,
type: result.externalType,
configuration: result.externalConfiguration,
dnsAppConnectionId: result.externalDnsAppConnectionId,
appConnectionId: result.externalAppConnectionId,
credentials: result.externalCredentials
}
: undefined
};
return data;
};
const findByIdWithAssociatedCa = async (caId: string, tx?: Knex) => {
const result = await (tx || db.replicaNode())(TableName.CertificateAuthority)
.leftJoin(
TableName.InternalCertificateAuthority,
`${TableName.CertificateAuthority}.id`,
`${TableName.InternalCertificateAuthority}.caId`
)
.leftJoin(
TableName.ExternalCertificateAuthority,
`${TableName.CertificateAuthority}.id`,
`${TableName.ExternalCertificateAuthority}.caId`
)
.where(`${TableName.CertificateAuthority}.id`, caId)
.select(selectAllTableCols(TableName.CertificateAuthority))
.select(
db.ref("id").withSchema(TableName.InternalCertificateAuthority).as("internalCaId"),
db.ref("parentCaId").withSchema(TableName.InternalCertificateAuthority).as("internalParentCaId"),
db.ref("type").withSchema(TableName.InternalCertificateAuthority).as("internalType"),
db.ref("friendlyName").withSchema(TableName.InternalCertificateAuthority).as("internalFriendlyName"),
db.ref("organization").withSchema(TableName.InternalCertificateAuthority).as("internalOrganization"),
db.ref("ou").withSchema(TableName.InternalCertificateAuthority).as("internalOu"),
db.ref("country").withSchema(TableName.InternalCertificateAuthority).as("internalCountry"),
db.ref("province").withSchema(TableName.InternalCertificateAuthority).as("internalProvince"),
db.ref("locality").withSchema(TableName.InternalCertificateAuthority).as("internalLocality"),
db.ref("commonName").withSchema(TableName.InternalCertificateAuthority).as("internalCommonName"),
db.ref("dn").withSchema(TableName.InternalCertificateAuthority).as("internalDn"),
db.ref("serialNumber").withSchema(TableName.InternalCertificateAuthority).as("internalSerialNumber"),
db.ref("maxPathLength").withSchema(TableName.InternalCertificateAuthority).as("internalMaxPathLength"),
db.ref("keyAlgorithm").withSchema(TableName.InternalCertificateAuthority).as("internalKeyAlgorithm"),
db.ref("notBefore").withSchema(TableName.InternalCertificateAuthority).as("internalNotBefore"),
db.ref("notAfter").withSchema(TableName.InternalCertificateAuthority).as("internalNotAfter"),
db.ref("activeCaCertId").withSchema(TableName.InternalCertificateAuthority).as("internalActiveCaCertId")
)
.select(
db.ref("id").withSchema(TableName.ExternalCertificateAuthority).as("externalCaId"),
db.ref("type").withSchema(TableName.ExternalCertificateAuthority).as("externalType"),
db.ref("configuration").withSchema(TableName.ExternalCertificateAuthority).as("externalConfiguration"),
db.ref("credentials").withSchema(TableName.ExternalCertificateAuthority).as("externalCredentials"),
db
.ref("dnsAppConnectionId")
.withSchema(TableName.ExternalCertificateAuthority)
.as("externalDnsAppConnectionId"),
db.ref("appConnectionId").withSchema(TableName.ExternalCertificateAuthority).as("externalAppConnectionId")
)
.first();
const data = {
...CertificateAuthoritiesSchema.parse(result),
internalCa: result
? {
id: result.internalCaId,
parentCaId: result.internalParentCaId,
type: result.internalType,
friendlyName: result.internalFriendlyName,
organization: result.internalOrganization,
ou: result.internalOu,
country: result.internalCountry,
province: result.internalProvince,
locality: result.internalLocality,
commonName: result.internalCommonName,
dn: result.internalDn,
serialNumber: result.internalSerialNumber,
maxPathLength: result.internalMaxPathLength,
keyAlgorithm: result.internalKeyAlgorithm,
notBefore: result.internalNotBefore?.toISOString(),
notAfter: result.internalNotAfter?.toISOString(),
activeCaCertId: result.internalActiveCaCertId
}
: undefined,
externalCa: result
? {
id: result.externalCaId,
type: result.externalType,
configuration: result.externalConfiguration,
dnsAppConnectionId: result.externalDnsAppConnectionId,
appConnectionId: result.externalAppConnectionId,
credentials: result.externalCredentials
}
: undefined
};
return data;
};
// note: not used
const buildCertificateChain = async (caId: string) => {
try {
@@ -42,8 +217,113 @@ export const certificateAuthorityDALFactory = (db: TDbClient) => {
}
};
const findWithAssociatedCa = async (
filter: Parameters<(typeof caOrm)["find"]>[0] & { dn?: string; type?: string },
{ offset, limit, sort = [["createdAt", "desc"]] }: TFindOpt<TCertificateAuthorities> = {},
tx?: Knex
) => {
try {
const query = (tx || db.replicaNode())(TableName.CertificateAuthority)
.leftJoin(
TableName.InternalCertificateAuthority,
`${TableName.CertificateAuthority}.id`,
`${TableName.InternalCertificateAuthority}.caId`
)
.leftJoin(
TableName.ExternalCertificateAuthority,
`${TableName.CertificateAuthority}.id`,
`${TableName.ExternalCertificateAuthority}.caId`
)
// eslint-disable-next-line @typescript-eslint/no-misused-promises
.where(buildFindFilter(filter))
.select(selectAllTableCols(TableName.CertificateAuthority))
.select(
db.ref("id").withSchema(TableName.InternalCertificateAuthority).as("internalCaId"),
db.ref("parentCaId").withSchema(TableName.InternalCertificateAuthority).as("internalParentCaId"),
db.ref("type").withSchema(TableName.InternalCertificateAuthority).as("internalType"),
db.ref("friendlyName").withSchema(TableName.InternalCertificateAuthority).as("internalFriendlyName"),
db.ref("organization").withSchema(TableName.InternalCertificateAuthority).as("internalOrganization"),
db.ref("ou").withSchema(TableName.InternalCertificateAuthority).as("internalOu"),
db.ref("country").withSchema(TableName.InternalCertificateAuthority).as("internalCountry"),
db.ref("province").withSchema(TableName.InternalCertificateAuthority).as("internalProvince"),
db.ref("locality").withSchema(TableName.InternalCertificateAuthority).as("internalLocality"),
db.ref("commonName").withSchema(TableName.InternalCertificateAuthority).as("internalCommonName"),
db.ref("dn").withSchema(TableName.InternalCertificateAuthority).as("internalDn"),
db.ref("serialNumber").withSchema(TableName.InternalCertificateAuthority).as("internalSerialNumber"),
db.ref("maxPathLength").withSchema(TableName.InternalCertificateAuthority).as("internalMaxPathLength"),
db.ref("keyAlgorithm").withSchema(TableName.InternalCertificateAuthority).as("internalKeyAlgorithm"),
db.ref("notBefore").withSchema(TableName.InternalCertificateAuthority).as("internalNotBefore"),
db.ref("notAfter").withSchema(TableName.InternalCertificateAuthority).as("internalNotAfter"),
db.ref("activeCaCertId").withSchema(TableName.InternalCertificateAuthority).as("internalActiveCaCertId")
)
.select(
db.ref("id").withSchema(TableName.ExternalCertificateAuthority).as("externalCaId"),
db.ref("type").withSchema(TableName.ExternalCertificateAuthority).as("externalType"),
db.ref("configuration").withSchema(TableName.ExternalCertificateAuthority).as("externalConfiguration"),
db
.ref("dnsAppConnectionId")
.withSchema(TableName.ExternalCertificateAuthority)
.as("externalDnsAppConnectionId"),
db.ref("credentials").withSchema(TableName.ExternalCertificateAuthority).as("externalCredentials"),
db.ref("appConnectionId").withSchema(TableName.ExternalCertificateAuthority).as("externalAppConnectionId")
);
if (limit) void query.limit(limit);
if (offset) void query.offset(offset);
if (sort) {
void query.orderBy(
sort.map(([column, order, nulls]) => ({
column,
order,
nulls
}))
);
}
return (await query).map((ca) => ({
...CertificateAuthoritiesSchema.parse(ca),
internalCa: ca
? {
id: ca.internalCaId,
parentCaId: ca.internalParentCaId,
type: ca.internalType,
friendlyName: ca.internalFriendlyName,
organization: ca.internalOrganization,
ou: ca.internalOu,
country: ca.internalCountry,
province: ca.internalProvince,
locality: ca.internalLocality,
commonName: ca.internalCommonName,
dn: ca.internalDn,
serialNumber: ca.internalSerialNumber,
maxPathLength: ca.internalMaxPathLength,
keyAlgorithm: ca.internalKeyAlgorithm,
notBefore: ca.internalNotBefore?.toISOString(),
notAfter: ca.internalNotAfter?.toISOString(),
activeCaCertId: ca.internalActiveCaCertId
}
: undefined,
externalCa: ca
? {
id: ca.externalCaId,
type: ca.externalType,
configuration: ca.externalConfiguration,
dnsAppConnectionId: ca.externalDnsAppConnectionId,
appConnectionId: ca.externalAppConnectionId,
credentials: ca.externalCredentials
}
: undefined
}));
} catch (error) {
throw new DatabaseError({ error, name: "Find - Certificate Authority" });
}
};
return {
...caOrm,
buildCertificateChain
findWithAssociatedCa,
buildCertificateChain,
findByIdWithAssociatedCa,
findByNameAndProjectIdWithAssociatedCa
};
};
@@ -0,0 +1,19 @@
export enum CaType {
INTERNAL = "internal",
ACME = "acme"
}
export enum InternalCaType {
ROOT = "root",
INTERMEDIATE = "intermediate"
}
export enum CaStatus {
ACTIVE = "active",
DISABLED = "disabled",
PENDING_CERTIFICATE = "pending-certificate"
}
export enum CaRenewalType {
EXISTING = "existing"
}
@@ -5,13 +5,14 @@ import { NotFoundError } from "@app/lib/errors";
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
import { CertKeyAlgorithm, CertStatus } from "../certificate/certificate-types";
import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal";
import {
TDNParts,
TGetCaCertChainDTO,
TGetCaCertChainsDTO,
TGetCaCredentialsDTO,
TRebuildCaCrlDTO
} from "./certificate-authority-types";
} from "./internal/internal-certificate-authority-types";
/* eslint-disable no-bitwise */
export const createSerialNumber = () => {
@@ -112,8 +113,8 @@ export const getCaCredentials = async ({
projectDAL,
kmsService
}: TGetCaCredentialsDTO) => {
const ca = await certificateAuthorityDAL.findById(caId);
if (!ca) throw new NotFoundError({ message: `CA with ID '${caId}' not found` });
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId);
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` });
@@ -131,7 +132,7 @@ export const getCaCredentials = async ({
cipherTextBlob: caSecret.encryptedPrivateKey
});
const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm);
const alg = keyAlgorithmToAlgCfg(ca.internalCa.keyAlgorithm as CertKeyAlgorithm);
const skObj = crypto.createPrivateKey({ key: decryptedPrivateKey, format: "der", type: "pkcs8" });
const caPrivateKey = await crypto.subtle.importKey(
"pkcs8",
@@ -255,12 +256,12 @@ export const rebuildCaCrl = async ({
certificateDAL,
kmsService
}: TRebuildCaCrlDTO) => {
const ca = await certificateAuthorityDAL.findById(caId);
if (!ca) throw new NotFoundError({ message: `CA with ID '${caId}' not found` });
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId);
if (!ca?.internalCa?.id) throw new NotFoundError({ message: `Internal CA with ID '${caId}' not found` });
const caSecret = await certificateAuthoritySecretDAL.findOne({ caId: ca.id });
const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm);
const alg = keyAlgorithmToAlgCfg(ca.internalCa.keyAlgorithm as CertKeyAlgorithm);
const keyId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId,
@@ -287,7 +288,7 @@ export const rebuildCaCrl = async ({
});
const crl = await x509.X509CrlGenerator.create({
issuer: ca.dn,
issuer: ca.internalCa.dn,
thisUpdate: new Date(),
nextUpdate: new Date("2025/12/12"),
entries: revokedCerts.map((revokedCert) => {
@@ -318,3 +319,16 @@ export const rebuildCaCrl = async ({
}
);
};
export const expandInternalCa = (
ca: Awaited<ReturnType<TCertificateAuthorityDALFactory["findByIdWithAssociatedCa"]>>
) => {
if (!ca.internalCa) {
throw new Error("Internal CA must be defined");
}
return {
...ca.internalCa,
...ca,
requireTemplateForIssuance: !ca.enableDirectIssuance
} as const;
};
@@ -0,0 +1,6 @@
import { CaType } from "./certificate-authority-enums";
export const CERTIFICATE_AUTHORITIES_TYPE_MAP: Record<CaType, string> = {
[CaType.INTERNAL]: "Internal",
[CaType.ACME]: "ACME"
};
@@ -1,9 +1,10 @@
import * as x509 from "@peculiar/x509";
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";
@@ -13,21 +14,43 @@ import { TProjectDALFactory } from "@app/services/project/project-dal";
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
import { TCertificateAuthorityCrlDALFactory } from "../../ee/services/certificate-authority-crl/certificate-authority-crl-dal";
import { TAppConnectionDALFactory } from "../app-connection/app-connection-dal";
import { TAppConnectionServiceFactory } from "../app-connection/app-connection-service";
import { TCertificateBodyDALFactory } from "../certificate/certificate-body-dal";
import { TCertificateSecretDALFactory } from "../certificate/certificate-secret-dal";
import { TPkiSubscriberDALFactory } from "../pki-subscriber/pki-subscriber-dal";
import { SubscriberOperationStatus } from "../pki-subscriber/pki-subscriber-types";
import { AcmeCertificateAuthorityFns } from "./acme/acme-certificate-authority-fns";
import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal";
import { CaType } from "./certificate-authority-enums";
import { keyAlgorithmToAlgCfg } from "./certificate-authority-fns";
import { TCertificateAuthoritySecretDALFactory } from "./certificate-authority-secret-dal";
import { TRotateCaCrlTriggerDTO } from "./certificate-authority-types";
import { TExternalCertificateAuthorityDALFactory } from "./external-certificate-authority-dal";
import {
TOrderCertificateForSubscriberDTO,
TRotateCaCrlTriggerDTO
} from "./internal/internal-certificate-authority-types";
type TCertificateAuthorityQueueFactoryDep = {
// TODO: Pick
certificateAuthorityDAL: TCertificateAuthorityDALFactory;
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update">;
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">;
externalCertificateAuthorityDAL: Pick<TExternalCertificateAuthorityDALFactory, "create" | "update">;
keyStore: Pick<TKeyStoreFactory, "acquireLock" | "setItemWithExpiry" | "getItem">;
certificateAuthorityCrlDAL: TCertificateAuthorityCrlDALFactory;
certificateAuthoritySecretDAL: TCertificateAuthoritySecretDALFactory;
certificateDAL: TCertificateDALFactory;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction">;
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey">;
kmsService: Pick<
TKmsServiceFactory,
"generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey" | "createCipherPairWithDataKey"
>;
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "create">;
queueService: TQueueServiceFactory;
pkiSubscriberDAL: Pick<TPkiSubscriberDALFactory, "findById" | "updateById">;
};
export type TCertificateAuthorityQueueFactory = ReturnType<typeof certificateAuthorityQueueFactory>;
export const certificateAuthorityQueueFactory = ({
@@ -37,8 +60,28 @@ export const certificateAuthorityQueueFactory = ({
certificateDAL,
projectDAL,
kmsService,
queueService
queueService,
keyStore,
appConnectionDAL,
appConnectionService,
externalCertificateAuthorityDAL,
certificateBodyDAL,
certificateSecretDAL,
pkiSubscriberDAL
}: TCertificateAuthorityQueueFactoryDep) => {
const acmeFns = AcmeCertificateAuthorityFns({
appConnectionDAL,
appConnectionService,
certificateAuthorityDAL,
externalCertificateAuthorityDAL,
certificateDAL,
certificateBodyDAL,
certificateSecretDAL,
kmsService,
pkiSubscriberDAL,
projectDAL
});
// TODO 1: auto-periodic rotation
// TODO 2: manual rotation
@@ -71,16 +114,76 @@ 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,
{
subscriberId,
caType
},
{
attempts: 1,
removeOnComplete: true,
removeOnFail: true
}
);
};
queueService.start(QueueName.CaLifecycle, async (job) => {
if (job.name === QueueJobs.CaOrderCertificateForSubscriber) {
const { subscriberId, caType } = job.data;
let lock: Awaited<ReturnType<typeof keyStore.acquireLock>>;
try {
lock = await keyStore.acquireLock(
[KeyStorePrefixes.CaOrderCertificateForSubscriberLock(subscriberId)],
5 * 60 * 1000
);
} catch (e) {
logger.info(`CaOrderCertificate Failed to acquire lock [subscriberId=${subscriberId}] [job=${job.name}]`);
return;
}
try {
if (caType === CaType.ACME) {
await acmeFns.orderSubscriberCertificate(subscriberId);
await pkiSubscriberDAL.updateById(subscriberId, {
lastOperationStatus: SubscriberOperationStatus.SUCCESS,
lastOperationMessage: "Certificate ordered successfully",
lastOperationAt: new Date()
});
}
} catch (e: unknown) {
if (e instanceof Error) {
await pkiSubscriberDAL.updateById(subscriberId, {
lastOperationStatus: SubscriberOperationStatus.FAILED,
lastOperationMessage: e.message,
lastOperationAt: new Date()
});
}
logger.error(e, `CaOrderCertificate Failed [subscriberId=${subscriberId}] [job=${job.name}]`);
} finally {
await lock.release();
}
}
});
queueService.start(QueueName.CaCrlRotation, async (job) => {
const { caId } = job.data;
logger.info(`secretReminderQueue.process: [secretDocument=${caId}]`);
const ca = await certificateAuthorityDAL.findById(caId);
if (!ca) throw new NotFoundError({ message: `CA with ID '${caId}' not found` });
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId);
if (!ca.internalCa) throw new NotFoundError({ message: `CA with ID '${caId}' not found` });
const caSecret = await certificateAuthoritySecretDAL.findOne({ caId: ca.id });
const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm);
const alg = keyAlgorithmToAlgCfg(ca.internalCa.keyAlgorithm as CertKeyAlgorithm);
const keyId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId,
@@ -106,7 +209,7 @@ export const certificateAuthorityQueueFactory = ({
});
const crl = await x509.X509CrlGenerator.create({
issuer: ca.dn,
issuer: ca.internalCa.dn,
thisUpdate: new Date(),
nextUpdate: new Date("2025/12/12"), // TODO: depends on configured rebuild interval
entries: revokedCerts.map((revokedCert) => {
@@ -115,7 +218,7 @@ export const certificateAuthorityQueueFactory = ({
revocationDate: new Date(revokedCert.revokedAt as Date),
reason: revokedCert.revocationReason as number,
invalidity: new Date("2022/01/01"),
issuer: ca.dn
issuer: ca.internalCa?.dn
};
}),
signingAlgorithm: alg,
@@ -144,6 +247,7 @@ export const certificateAuthorityQueueFactory = ({
});
return {
setCaCrlRotationInterval
setCaCrlRotationInterval,
orderCertificateForSubscriber
};
};
@@ -0,0 +1,32 @@
import z from "zod";
import { CertificateAuthoritiesSchema } from "@app/db/schemas";
import { CertificateAuthorities } from "@app/lib/api-docs/constants";
import { slugSchema } from "@app/server/lib/schemas";
import { CaStatus, CaType } from "./certificate-authority-enums";
export const BaseCertificateAuthoritySchema = CertificateAuthoritiesSchema.pick({
projectId: true,
enableDirectIssuance: true,
name: true,
id: true
}).extend({
status: z.nativeEnum(CaStatus)
});
export const GenericCreateCertificateAuthorityFieldsSchema = (type: CaType) =>
z.object({
name: slugSchema({ field: "name" }).describe(CertificateAuthorities.CREATE(type).name),
projectId: z.string().trim().min(1, "Project ID required").describe(CertificateAuthorities.CREATE(type).projectId),
enableDirectIssuance: z.boolean().describe(CertificateAuthorities.CREATE(type).enableDirectIssuance),
status: z.nativeEnum(CaStatus).describe(CertificateAuthorities.CREATE(type).status)
});
export const GenericUpdateCertificateAuthorityFieldsSchema = (type: CaType) =>
z.object({
name: slugSchema({ field: "name" }).optional().describe(CertificateAuthorities.UPDATE(type).name),
projectId: z.string().trim().min(1, "Project ID required").describe(CertificateAuthorities.UPDATE(type).projectId),
enableDirectIssuance: z.boolean().optional().describe(CertificateAuthorities.UPDATE(type).enableDirectIssuance),
status: z.nativeEnum(CaStatus).optional().describe(CertificateAuthorities.UPDATE(type).status)
});
File diff suppressed because it is too large Load Diff
@@ -1,186 +1,18 @@
import { TProjectPermission } from "@app/lib/types";
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TAcmeCertificateAuthority, TAcmeCertificateAuthorityInput } from "./acme/acme-certificate-authority-types";
import { CaType } from "./certificate-authority-enums";
import {
TInternalCertificateAuthority,
TInternalCertificateAuthorityInput
} from "./internal/internal-certificate-authority-types";
import { TCertificateAuthorityCrlDALFactory } from "../../ee/services/certificate-authority-crl/certificate-authority-crl-dal";
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "../certificate/certificate-types";
import { TCertificateAuthorityCertDALFactory } from "./certificate-authority-cert-dal";
import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal";
import { TCertificateAuthoritySecretDALFactory } from "./certificate-authority-secret-dal";
export type TCertificateAuthority = TInternalCertificateAuthority | TAcmeCertificateAuthority;
export enum CaType {
ROOT = "root",
INTERMEDIATE = "intermediate"
}
export type TCertificateAuthorityInput = TInternalCertificateAuthorityInput | TAcmeCertificateAuthorityInput;
export enum CaStatus {
ACTIVE = "active",
DISABLED = "disabled",
PENDING_CERTIFICATE = "pending-certificate"
}
export type TCreateCertificateAuthorityDTO = Omit<TCertificateAuthority, "id">;
export enum CaRenewalType {
EXISTING = "existing"
}
export type TCreateCaDTO = {
projectSlug: string;
export type TUpdateCertificateAuthorityDTO = Partial<Omit<TCreateCertificateAuthorityDTO, "projectId">> & {
type: CaType;
friendlyName?: string;
commonName: string;
organization: string;
ou: string;
country: string;
province: string;
locality: string;
notBefore?: string;
notAfter?: string;
maxPathLength: number;
keyAlgorithm: CertKeyAlgorithm;
requireTemplateForIssuance: boolean;
} & Omit<TProjectPermission, "projectId">;
export type TGetCaDTO = {
caId: string;
} & Omit<TProjectPermission, "projectId">;
export type TUpdateCaDTO = {
caId: string;
status?: CaStatus;
requireTemplateForIssuance?: boolean;
} & Omit<TProjectPermission, "projectId">;
export type TDeleteCaDTO = {
caId: string;
} & Omit<TProjectPermission, "projectId">;
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">;
export type TSignIntermediateDTO = {
caId: string;
csr: string;
notBefore?: string;
notAfter: string;
maxPathLength: number;
} & Omit<TProjectPermission, "projectId">;
export type TImportCertToCaDTO = {
caId: string;
certificate: string;
certificateChain: string;
} & Omit<TProjectPermission, "projectId">;
export type TIssueCertFromCaDTO = {
caId?: string;
certificateTemplateId?: string;
pkiCollectionId?: string;
friendlyName?: string;
commonName: string;
altNames: string;
ttl: string;
notBefore?: string;
notAfter?: string;
keyUsages?: CertKeyUsage[];
extendedKeyUsages?: CertExtendedKeyUsage[];
} & Omit<TProjectPermission, "projectId">;
export type TSignCertFromCaDTO =
| {
isInternal: true;
caId?: string;
csr: string;
certificateTemplateId?: string;
pkiCollectionId?: string;
friendlyName?: string;
commonName?: string;
altNames?: string;
ttl?: string;
notBefore?: string;
notAfter?: string;
keyUsages?: CertKeyUsage[];
extendedKeyUsages?: CertExtendedKeyUsage[];
}
| ({
isInternal: false;
caId?: string;
csr: string;
certificateTemplateId?: string;
pkiCollectionId?: string;
friendlyName?: string;
commonName?: string;
altNames: string;
ttl: string;
notBefore?: string;
notAfter?: string;
keyUsages?: CertKeyUsage[];
extendedKeyUsages?: CertExtendedKeyUsage[];
} & Omit<TProjectPermission, "projectId">);
export type TGetCaCertificateTemplatesDTO = {
caId: string;
} & Omit<TProjectPermission, "projectId">;
export type TDNParts = {
commonName?: string;
organization?: string;
ou?: string;
country?: string;
province?: string;
locality?: string;
};
export type TGetCaCredentialsDTO = {
caId: string;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "findOne">;
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
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 = {
caCertId: string;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findById">;
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
};
export type TRebuildCaCrlDTO = {
caId: string;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "update">;
certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "findOne">;
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
certificateDAL: Pick<TCertificateDALFactory, "find">;
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "decryptWithKmsKey" | "encryptWithKmsKey">;
};
export type TRotateCaCrlTriggerDTO = {
caId: string;
rotationIntervalDays: number;
caName: string;
projectId: string;
};
@@ -15,7 +15,7 @@ export const validateAltNameField = z
.trim()
.refine(
(name) => {
return isFQDN(name) || z.string().email().safeParse(name).success || isValidIp(name);
return isFQDN(name, { allow_wildcard: true }) || z.string().email().safeParse(name).success || isValidIp(name);
},
{
message: "SAN must be a valid hostname, email address, or IP address"
@@ -0,0 +1,13 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TExternalCertificateAuthorityDALFactory = ReturnType<typeof externalCertificateAuthorityDALFactory>;
export const externalCertificateAuthorityDALFactory = (db: TDbClient) => {
const caOrm = ormify(db, TableName.ExternalCertificateAuthority);
return {
...caOrm
};
};
@@ -0,0 +1,13 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TInternalCertificateAuthorityDALFactory = ReturnType<typeof internalCertificateAuthorityDALFactory>;
export const internalCertificateAuthorityDALFactory = (db: TDbClient) => {
const caOrm = ormify(db, TableName.InternalCertificateAuthority);
return {
...caOrm
};
};
@@ -0,0 +1,263 @@
import * as x509 from "@peculiar/x509";
import { KeyObject } from "crypto";
import { z } from "zod";
import { TPkiSubscribers } from "@app/db/schemas";
import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { ms } from "@app/lib/ms";
import { isFQDN } from "@app/lib/validator/validate-url";
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
import {
CertExtendedKeyUsage,
CertKeyAlgorithm,
CertKeyUsage,
CertStatus
} from "@app/services/certificate/certificate-types";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
import { TCertificateAuthorityCertDALFactory } from "../certificate-authority-cert-dal";
import { TCertificateAuthorityDALFactory } from "../certificate-authority-dal";
import { CaStatus } from "../certificate-authority-enums";
import {
createSerialNumber,
getCaCertChain,
getCaCredentials,
keyAlgorithmToAlgCfg
} from "../certificate-authority-fns";
import { TCertificateAuthoritySecretDALFactory } from "../certificate-authority-secret-dal";
type TInternalCertificateAuthorityFnsDeps = {
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa" | "findById">;
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findById">;
certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "findOne">;
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "findOne">;
projectDAL: Pick<TProjectDALFactory, "findById" | "transaction" | "findOne" | "updateById">;
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "encryptWithKmsKey" | "generateKmsKey">;
certificateDAL: Pick<TCertificateDALFactory, "create" | "transaction">;
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "create">;
};
export const InternalCertificateAuthorityFns = ({
certificateAuthorityDAL,
certificateAuthorityCertDAL,
projectDAL,
kmsService,
certificateAuthoritySecretDAL,
certificateAuthorityCrlDAL,
certificateDAL,
certificateBodyDAL,
certificateSecretDAL
}: TInternalCertificateAuthorityFnsDeps) => {
const issueCertificate = async (
subscriber: TPkiSubscribers,
ca: Awaited<ReturnType<TCertificateAuthorityDALFactory["findByIdWithAssociatedCa"]>>
) => {
if (ca.status !== CaStatus.ACTIVE) throw new BadRequestError({ message: "CA is not active" });
if (!ca.internalCa?.activeCaCertId)
throw new BadRequestError({ message: "CA does not have a certificate installed" });
const caCert = await certificateAuthorityCertDAL.findById(ca.internalCa.activeCaCertId);
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId,
projectDAL,
kmsService
});
const kmsDecryptor = await kmsService.decryptWithKmsKey({
kmsId: certificateManagerKmsId
});
const decryptedCaCert = await kmsDecryptor({
cipherTextBlob: caCert.encryptedCertificate
});
const caCertObj = new x509.X509Certificate(decryptedCaCert);
const notBeforeDate = new Date();
const notAfterDate = new Date(new Date().getTime() + ms(subscriber.ttl ?? "0"));
const caCertNotBeforeDate = new Date(caCertObj.notBefore);
const caCertNotAfterDate = new Date(caCertObj.notAfter);
// check not before constraint
if (notBeforeDate < caCertNotBeforeDate) {
throw new BadRequestError({ message: "notBefore date is before CA certificate's notBefore date" });
}
// check not after constraint
if (notAfterDate > caCertNotAfterDate) {
throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" });
}
const alg = keyAlgorithmToAlgCfg(ca.internalCa.keyAlgorithm as CertKeyAlgorithm);
const leafKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
const csrObj = await x509.Pkcs10CertificateRequestGenerator.create({
name: `CN=${subscriber.commonName}`,
keys: leafKeys,
signingAlgorithm: alg,
extensions: [
// eslint-disable-next-line no-bitwise
new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment)
],
attributes: [new x509.ChallengePasswordAttribute("password")]
});
const { caPrivateKey, caSecret } = await getCaCredentials({
caId: ca.id,
certificateAuthorityDAL,
certificateAuthoritySecretDAL,
projectDAL,
kmsService
});
const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id });
const appCfg = getConfig();
const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/pki/crl/${caCrl.id}/der`;
const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/pki/ca/${ca.id}/certificates/${caCert.id}/der`;
const extensions: x509.Extension[] = [
new x509.BasicConstraintsExtension(false),
new x509.CRLDistributionPointsExtension([distributionPointUrl]),
await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey),
new x509.AuthorityInfoAccessExtension({
caIssuers: new x509.GeneralName("url", caIssuerUrl)
}),
new x509.CertificatePolicyExtension(["2.5.29.32.0"]) // anyPolicy
];
const selectedKeyUsages = subscriber.keyUsages as CertKeyUsage[];
// eslint-disable-next-line no-bitwise
const keyUsagesBitValue = selectedKeyUsages.reduce((accum, keyUsage) => accum | x509.KeyUsageFlags[keyUsage], 0);
if (keyUsagesBitValue) {
extensions.push(new x509.KeyUsagesExtension(keyUsagesBitValue, true));
}
if (subscriber.extendedKeyUsages.length) {
const extendedKeyUsagesExtension = new x509.ExtendedKeyUsageExtension(
subscriber.extendedKeyUsages.map((eku) => x509.ExtendedKeyUsage[eku as CertExtendedKeyUsage]),
true
);
extensions.push(extendedKeyUsagesExtension);
}
let altNamesArray: { type: "email" | "dns"; value: string }[] = [];
if (subscriber.subjectAlternativeNames?.length) {
altNamesArray = subscriber.subjectAlternativeNames.map((altName) => {
if (z.string().email().safeParse(altName).success) {
return { type: "email", value: altName };
}
if (isFQDN(altName, { allow_wildcard: true })) {
return { type: "dns", value: altName };
}
throw new BadRequestError({ message: `Invalid SAN entry: ${altName}` });
});
const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false);
extensions.push(altNamesExtension);
}
const serialNumber = createSerialNumber();
const leafCert = await x509.X509CertificateGenerator.create({
serialNumber,
subject: csrObj.subject,
issuer: caCertObj.subject,
notBefore: notBeforeDate,
notAfter: notAfterDate,
signingKey: caPrivateKey,
publicKey: csrObj.publicKey,
signingAlgorithm: alg,
extensions
});
const skLeafObj = KeyObject.from(leafKeys.privateKey);
const skLeaf = skLeafObj.export({ format: "pem", type: "pkcs8" }) as string;
const kmsEncryptor = await kmsService.encryptWithKmsKey({
kmsId: certificateManagerKmsId
});
const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
plainText: Buffer.from(new Uint8Array(leafCert.rawData))
});
const { cipherTextBlob: encryptedPrivateKey } = await kmsEncryptor({
plainText: Buffer.from(skLeaf)
});
const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({
caCertId: caCert.id,
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(
{
caId: ca.id,
caCertId: caCert.id,
pkiSubscriberId: subscriber.id,
status: CertStatus.ACTIVE,
friendlyName: subscriber.commonName,
commonName: subscriber.commonName,
altNames: subscriber.subjectAlternativeNames.join(","),
serialNumber,
notBefore: notBeforeDate,
notAfter: notAfterDate,
keyUsages: selectedKeyUsages,
extendedKeyUsages: subscriber.extendedKeyUsages as CertExtendedKeyUsage[],
projectId: ca.projectId
},
tx
);
await certificateBodyDAL.create(
{
certId: cert.id,
encryptedCertificate,
encryptedCertificateChain
},
tx
);
await certificateSecretDAL.create(
{
certId: cert.id,
encryptedPrivateKey
},
tx
);
});
return {
certificate: leafCert.toString("pem"),
certificateChain: certificateChainPem,
issuingCaCertificate,
privateKey: skLeaf,
serialNumber,
ca,
subscriber
};
};
return {
issueCertificate
};
};
@@ -0,0 +1,62 @@
import { z } from "zod";
import { CertificateAuthorities } from "@app/lib/api-docs/constants";
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
import { CaType, InternalCaType } from "../certificate-authority-enums";
import {
BaseCertificateAuthoritySchema,
GenericCreateCertificateAuthorityFieldsSchema,
GenericUpdateCertificateAuthorityFieldsSchema
} from "../certificate-authority-schemas";
import { validateCaDateField } from "../certificate-authority-validators";
const InternalCertificateAuthorityConfigurationSchema = z
.object({
type: z.nativeEnum(InternalCaType).describe(CertificateAuthorities.CONFIGURATIONS.INTERNAL.type),
friendlyName: z.string().optional().describe(CertificateAuthorities.CONFIGURATIONS.INTERNAL.friendlyName),
commonName: z.string().trim().describe(CertificateAuthorities.CONFIGURATIONS.INTERNAL.commonName),
organization: z.string().trim().describe(CertificateAuthorities.CONFIGURATIONS.INTERNAL.organization),
ou: z.string().trim().describe(CertificateAuthorities.CONFIGURATIONS.INTERNAL.ou),
country: z.string().trim().describe(CertificateAuthorities.CONFIGURATIONS.INTERNAL.country),
province: z.string().trim().describe(CertificateAuthorities.CONFIGURATIONS.INTERNAL.province),
locality: z.string().trim().describe(CertificateAuthorities.CONFIGURATIONS.INTERNAL.locality),
notBefore: validateCaDateField.optional().describe(CertificateAuthorities.CONFIGURATIONS.INTERNAL.notBefore),
notAfter: validateCaDateField.optional().describe(CertificateAuthorities.CONFIGURATIONS.INTERNAL.notAfter),
maxPathLength: z.number().min(-1).nullish().describe(CertificateAuthorities.CONFIGURATIONS.INTERNAL.maxPathLength),
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm).describe(CertificateAuthorities.CONFIGURATIONS.INTERNAL.keyAlgorithm),
dn: z.string().trim().nullish(),
parentCaId: z.string().uuid().nullish(),
serialNumber: z.string().trim().nullish(),
activeCaCertId: z.string().uuid().nullish()
})
.refine(
(data) => {
// Check that at least one of the specified fields is non-empty
return [data.commonName, data.organization, data.ou, data.country, data.province, data.locality].some(
(field) => field !== ""
);
},
{
message:
"At least one of the fields commonName, organization, ou, country, province, or locality must be non-empty",
path: []
}
);
export const InternalCertificateAuthoritySchema = BaseCertificateAuthoritySchema.extend({
type: z.literal(CaType.INTERNAL),
configuration: InternalCertificateAuthorityConfigurationSchema
});
export const CreateInternalCertificateAuthoritySchema = GenericCreateCertificateAuthorityFieldsSchema(
CaType.INTERNAL
).extend({
configuration: InternalCertificateAuthorityConfigurationSchema
});
export const UpdateInternalCertificateAuthoritySchema = GenericUpdateCertificateAuthorityFieldsSchema(
CaType.INTERNAL
).extend({
configuration: InternalCertificateAuthorityConfigurationSchema.optional()
});
@@ -0,0 +1,223 @@
import { z } from "zod";
import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
import { TProjectPermission } from "@app/lib/types";
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TCertificateAuthorityCertDALFactory } from "../certificate-authority-cert-dal";
import { TCertificateAuthorityDALFactory } from "../certificate-authority-dal";
import { CaRenewalType, CaStatus, CaType, InternalCaType } from "../certificate-authority-enums";
import { TCertificateAuthoritySecretDALFactory } from "../certificate-authority-secret-dal";
import {
CreateInternalCertificateAuthoritySchema,
InternalCertificateAuthoritySchema,
UpdateInternalCertificateAuthoritySchema
} from "./internal-certificate-authority-schemas";
export type TInternalCertificateAuthority = z.infer<typeof InternalCertificateAuthoritySchema>;
export type TInternalCertificateAuthorityInput = z.infer<typeof CreateInternalCertificateAuthoritySchema>;
export type TCreateInternalCertificateAuthorityDTO = z.infer<typeof CreateInternalCertificateAuthoritySchema>;
export type TUpdateInternalCertificateAuthorityDTO = z.infer<typeof UpdateInternalCertificateAuthoritySchema>;
export type TCreateCaDTO =
| {
isInternal: true;
projectId: string;
type: InternalCaType;
friendlyName?: string;
name?: string;
commonName: string;
organization: string;
ou: string;
country: string;
province: string;
locality: string;
notBefore?: string;
notAfter?: string;
maxPathLength?: number | null;
keyAlgorithm: CertKeyAlgorithm;
enableDirectIssuance: boolean;
}
| ({
isInternal: false;
projectSlug: string;
type: InternalCaType;
friendlyName?: string;
name?: string;
commonName: string;
organization: string;
ou: string;
country: string;
province: string;
locality: string;
notBefore?: string;
notAfter?: string;
maxPathLength?: number | null;
keyAlgorithm: CertKeyAlgorithm;
enableDirectIssuance: boolean;
} & Omit<TProjectPermission, "projectId">);
export type TGetCaDTO = {
caId: string;
} & Omit<TProjectPermission, "projectId">;
export type TUpdateCaDTO =
| {
isInternal: true;
caId: string;
name?: string;
status?: CaStatus;
enableDirectIssuance?: boolean;
}
| ({
isInternal: false;
caId: string;
name?: string;
status?: CaStatus;
enableDirectIssuance?: boolean;
} & Omit<TProjectPermission, "projectId">);
export type TDeleteCaDTO = {
caId: string;
} & Omit<TProjectPermission, "projectId">;
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">;
export type TSignIntermediateDTO = {
caId: string;
csr: string;
notBefore?: string;
notAfter: string;
maxPathLength: number;
} & Omit<TProjectPermission, "projectId">;
export type TImportCertToCaDTO = {
caId: string;
certificate: string;
certificateChain: string;
} & Omit<TProjectPermission, "projectId">;
export type TIssueCertFromCaDTO = {
caId?: string;
certificateTemplateId?: string;
pkiCollectionId?: string;
friendlyName?: string;
commonName: string;
altNames: string;
ttl: string;
notBefore?: string;
notAfter?: string;
keyUsages?: CertKeyUsage[];
extendedKeyUsages?: CertExtendedKeyUsage[];
} & Omit<TProjectPermission, "projectId">;
export type TSignCertFromCaDTO =
| {
isInternal: true;
caId?: string;
csr: string;
certificateTemplateId?: string;
pkiCollectionId?: string;
friendlyName?: string;
commonName?: string;
altNames?: string;
ttl?: string;
notBefore?: string;
notAfter?: string;
keyUsages?: CertKeyUsage[];
extendedKeyUsages?: CertExtendedKeyUsage[];
}
| ({
isInternal: false;
caId?: string;
csr: string;
certificateTemplateId?: string;
pkiCollectionId?: string;
friendlyName?: string;
commonName?: string;
altNames: string;
ttl: string;
notBefore?: string;
notAfter?: string;
keyUsages?: CertKeyUsage[];
extendedKeyUsages?: CertExtendedKeyUsage[];
} & Omit<TProjectPermission, "projectId">);
export type TGetCaCertificateTemplatesDTO = {
caId: string;
} & Omit<TProjectPermission, "projectId">;
export type TDNParts = {
commonName?: string;
organization?: string;
ou?: string;
country?: string;
province?: string;
locality?: string;
};
export type TGetCaCredentialsDTO = {
caId: string;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa">;
certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "findOne">;
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
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 = {
caCertId: string;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findById">;
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
};
export type TRebuildCaCrlDTO = {
caId: string;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa">;
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "update">;
certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "findOne">;
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
certificateDAL: Pick<TCertificateDALFactory, "find">;
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "decryptWithKmsKey" | "encryptWithKmsKey">;
};
export type TRotateCaCrlTriggerDTO = {
caId: string;
rotationIntervalDays: number;
};
export type TOrderCertificateForSubscriberDTO = {
subscriberId: string;
caType: CaType;
};
@@ -19,10 +19,15 @@ export const certificateTemplateDALFactory = (db: TDbClient) => {
`${TableName.CertificateAuthority}.id`,
`${TableName.CertificateTemplate}.caId`
)
.join(
TableName.InternalCertificateAuthority,
`${TableName.InternalCertificateAuthority}.caId`,
`${TableName.CertificateAuthority}.id`
)
.where(`${TableName.CertificateAuthority}.projectId`, "=", projectId)
.select(selectAllTableCols(TableName.CertificateTemplate))
.select(
db.ref("friendlyName").as("caName").withSchema(TableName.CertificateAuthority),
db.ref("friendlyName").as("caName").withSchema(TableName.InternalCertificateAuthority),
db.ref("projectId").withSchema(TableName.CertificateAuthority)
);
@@ -41,11 +46,16 @@ export const certificateTemplateDALFactory = (db: TDbClient) => {
`${TableName.CertificateTemplate}.caId`
)
.join(TableName.Project, `${TableName.Project}.id`, `${TableName.CertificateAuthority}.projectId`)
.join(
TableName.InternalCertificateAuthority,
`${TableName.InternalCertificateAuthority}.caId`,
`${TableName.CertificateAuthority}.id`
)
.where(`${TableName.CertificateTemplate}.id`, "=", id)
.select(selectAllTableCols(TableName.CertificateTemplate))
.select(
db.ref("projectId").withSchema(TableName.CertificateAuthority),
db.ref("friendlyName").as("caName").withSchema(TableName.CertificateAuthority),
db.ref("friendlyName").as("caName").withSchema(TableName.InternalCertificateAuthority),
db.ref("orgId").withSchema(TableName.Project)
)
.first();
@@ -3,11 +3,28 @@ import { TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex";
import { CertStatus } from "./certificate-types";
export type TCertificateDALFactory = ReturnType<typeof certificateDALFactory>;
export const certificateDALFactory = (db: TDbClient) => {
const certificateOrm = ormify(db, TableName.Certificate);
const findLatestActiveCertForSubscriber = async ({ subscriberId }: { subscriberId: string }) => {
try {
const cert = await db
.replicaNode()(TableName.Certificate)
.where({ pkiSubscriberId: subscriberId, status: CertStatus.ACTIVE })
.where("notAfter", ">", new Date())
.orderBy("notBefore", "desc")
.first();
return cert;
} catch (error) {
throw new DatabaseError({ error, name: "Find latest active certificate for subscriber" });
}
};
const countCertificatesInProject = async ({
projectId,
friendlyName,
@@ -65,6 +82,7 @@ export const certificateDALFactory = (db: TDbClient) => {
return {
...certificateOrm,
countCertificatesInProject,
countCertificatesForPkiSubscriber
countCertificatesForPkiSubscriber,
findLatestActiveCertForSubscriber
};
};
@@ -1,11 +1,12 @@
import crypto from "node:crypto";
import * as x509 from "@peculiar/x509";
import RE2 from "re2";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { getProjectKmsCertificateKeyId } from "../project/project-fns";
import { CrlReason, TBuildCertificateChainDTO, TGetCertificateCredentialsDTO } from "./certificate-types";
import { CrlReason, TGetCertificateCredentialsDTO } from "./certificate-types";
export const revocationReasonToCrlCode = (crlReason: CrlReason) => {
switch (crlReason) {
@@ -52,6 +53,12 @@ export const constructPemChainFromCerts = (certificates: x509.X509Certificate[])
.join("\n")
.trim();
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
* Note: credentials are returned as PEM strings
@@ -95,29 +102,3 @@ export const getCertificateCredentials = async ({
throw new BadRequestError({ message: `Failed to process private key for certificate with ID '${certId}'` });
}
};
// If the certificate was generated after ~05/01/25 it will have a encryptedCertificateChain attached to it's body
// Otherwise we'll fallback to manually building the chain
export const buildCertificateChain = async ({
caCert,
caCertChain,
encryptedCertificateChain,
kmsService,
kmsId
}: TBuildCertificateChainDTO) => {
if (!encryptedCertificateChain && !caCert) {
return null;
}
let certificateChain = `${caCert}\n${caCertChain}`.trim();
if (encryptedCertificateChain) {
const kmsDecryptor = await kmsService.decryptWithKmsKey({ kmsId });
const decryptedCertChain = await kmsDecryptor({
cipherTextBlob: encryptedCertificateChain
});
certificateChain = decryptedCertChain.toString();
}
return certificateChain;
};
@@ -1,45 +1,57 @@
import { ForbiddenError } from "@casl/ability";
import * as x509 from "@peculiar/x509";
import { createPrivateKey, createPublicKey, sign, verify } from "crypto";
import { ActionProjectType } from "@app/db/schemas";
import { ActionProjectType, ProjectType } from "@app/db/schemas";
import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import {
ProjectPermissionCertificateActions,
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
import { NotFoundError } from "@app/lib/errors";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
import { TCertificateAuthoritySecretDALFactory } from "@app/services/certificate-authority/certificate-authority-secret-dal";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TPkiCollectionDALFactory } from "@app/services/pki-collection/pki-collection-dal";
import { TPkiCollectionItemDALFactory } from "@app/services/pki-collection/pki-collection-item-dal";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
import { getCaCertChain, rebuildCaCrl } from "../certificate-authority/certificate-authority-fns";
import { buildCertificateChain, getCertificateCredentials, revocationReasonToCrlCode } from "./certificate-fns";
import { expandInternalCa, getCaCertChain, rebuildCaCrl } from "../certificate-authority/certificate-authority-fns";
import { getCertificateCredentials, revocationReasonToCrlCode, splitPemChain } from "./certificate-fns";
import { TCertificateSecretDALFactory } from "./certificate-secret-dal";
import {
CertExtendedKeyUsage,
CertExtendedKeyUsageOIDToName,
CertKeyUsage,
CertStatus,
TDeleteCertDTO,
TGetCertBodyDTO,
TGetCertBundleDTO,
TGetCertDTO,
TGetCertPrivateKeyDTO,
TImportCertDTO,
TRevokeCertDTO
} from "./certificate-types";
type TCertificateServiceFactoryDep = {
certificateDAL: Pick<TCertificateDALFactory, "findOne" | "deleteById" | "update" | "find">;
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne">;
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "findOne">;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
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">;
certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "findOne">;
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "findById" | "transaction">;
pkiCollectionDAL: Pick<TPkiCollectionDALFactory, "findById">;
pkiCollectionItemDAL: Pick<TPkiCollectionItemDALFactory, "create">;
projectDAL: Pick<
TProjectDALFactory,
"findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction" | "getProjectFromSplitId"
>;
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
};
@@ -54,6 +66,8 @@ export const certificateServiceFactory = ({
certificateAuthorityCertDAL,
certificateAuthorityCrlDAL,
certificateAuthoritySecretDAL,
pkiCollectionDAL,
pkiCollectionItemDAL,
projectDAL,
kmsService,
permissionService
@@ -63,12 +77,11 @@ export const certificateServiceFactory = ({
*/
const getCert = async ({ serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TGetCertDTO) => {
const cert = await certificateDAL.findOne({ serialNumber });
const ca = await certificateAuthorityDAL.findById(cert.caId);
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: ca.projectId,
projectId: cert.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.CertificateManager
@@ -80,8 +93,7 @@ export const certificateServiceFactory = ({
);
return {
cert,
ca
cert
};
};
@@ -96,12 +108,11 @@ export const certificateServiceFactory = ({
actorOrgId
}: TGetCertPrivateKeyDTO) => {
const cert = await certificateDAL.findOne({ serialNumber });
const ca = await certificateAuthorityDAL.findById(cert.caId);
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: ca.projectId,
projectId: cert.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.CertificateManager
@@ -114,14 +125,13 @@ export const certificateServiceFactory = ({
const { certPrivateKey } = await getCertificateCredentials({
certId: cert.id,
projectId: ca.projectId,
projectId: cert.projectId,
certificateSecretDAL,
projectDAL,
kmsService
});
return {
ca,
cert,
certPrivateKey
};
@@ -132,12 +142,11 @@ export const certificateServiceFactory = ({
*/
const deleteCert = async ({ serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TDeleteCertDTO) => {
const cert = await certificateDAL.findOne({ serialNumber });
const ca = await certificateAuthorityDAL.findById(cert.caId);
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: ca.projectId,
projectId: cert.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.CertificateManager
@@ -151,8 +160,7 @@ export const certificateServiceFactory = ({
const deletedCert = await certificateDAL.deleteById(cert.id);
return {
deletedCert,
ca
deletedCert
};
};
@@ -170,7 +178,20 @@ export const certificateServiceFactory = ({
actorOrgId
}: TRevokeCertDTO) => {
const cert = await certificateDAL.findOne({ serialNumber });
const ca = await certificateAuthorityDAL.findById(cert.caId);
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,
@@ -211,7 +232,7 @@ export const certificateServiceFactory = ({
kmsService
});
return { revokedAt, cert, ca };
return { revokedAt, cert, ca: expandInternalCa(ca) };
};
/**
@@ -220,12 +241,11 @@ export const certificateServiceFactory = ({
*/
const getCertBody = async ({ serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TGetCertBodyDTO) => {
const cert = await certificateDAL.findOne({ serialNumber });
const ca = await certificateAuthorityDAL.findById(cert.caId);
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: ca.projectId,
projectId: cert.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.CertificateManager
@@ -239,7 +259,7 @@ export const certificateServiceFactory = ({
const certBody = await certificateBodyDAL.findOne({ certId: cert.id });
const certificateManagerKeyId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId,
projectId: cert.projectId,
projectDAL,
kmsService
});
@@ -253,28 +273,259 @@ export const certificateServiceFactory = ({
const certObj = new x509.X509Certificate(decryptedCert);
const { caCert, caCertChain } = await getCaCertChain({
caCertId: cert.caCertId,
certificateAuthorityDAL,
certificateAuthorityCertDAL,
projectDAL,
kmsService
});
let certificateChain = null;
const certificateChain = await buildCertificateChain({
caCert,
caCertChain,
kmsId: certificateManagerKeyId,
kmsService,
encryptedCertificateChain: certBody.encryptedCertificateChain || undefined
});
// On newer certs the certBody.encryptedCertificateChain column will always exist.
// Older certs will have a caCertId which will be used as a fallback mechanism for structuring the chain.
if (certBody.encryptedCertificateChain) {
const decryptedCertChain = await kmsDecryptor({
cipherTextBlob: certBody.encryptedCertificateChain
});
certificateChain = decryptedCertChain.toString();
} else if (cert.caCertId) {
const { caCert, caCertChain } = await getCaCertChain({
caCertId: cert.caCertId,
certificateAuthorityDAL,
certificateAuthorityCertDAL,
projectDAL,
kmsService
});
certificateChain = `${caCert}\n${caCertChain}`.trim();
}
return {
certificate: certObj.toString("pem"),
certificateChain,
serialNumber: certObj.serialNumber,
cert,
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
};
};
@@ -284,12 +535,11 @@ export const certificateServiceFactory = ({
*/
const getCertBundle = async ({ serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TGetCertBundleDTO) => {
const cert = await certificateDAL.findOne({ serialNumber });
const ca = await certificateAuthorityDAL.findById(cert.caId);
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: ca.projectId,
projectId: cert.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.CertificateManager
@@ -307,7 +557,7 @@ export const certificateServiceFactory = ({
const certBody = await certificateBodyDAL.findOne({ certId: cert.id });
const certificateManagerKeyId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId,
projectId: cert.projectId,
projectDAL,
kmsService
});
@@ -322,27 +572,32 @@ export const certificateServiceFactory = ({
const certObj = new x509.X509Certificate(decryptedCert);
const certificate = certObj.toString("pem");
const { caCert, caCertChain } = await getCaCertChain({
caCertId: cert.caCertId,
certificateAuthorityDAL,
certificateAuthorityCertDAL,
projectDAL,
kmsService
});
let certificateChain = null;
const certificateChain = await buildCertificateChain({
caCert,
caCertChain,
kmsId: certificateManagerKeyId,
kmsService,
encryptedCertificateChain: certBody.encryptedCertificateChain || undefined
});
// On newer certs the certBody.encryptedCertificateChain column will always exist.
// Older certs will have a caCertId which will be used as a fallback mechanism for structuring the chain.
if (certBody.encryptedCertificateChain) {
const decryptedCertChain = await kmsDecryptor({
cipherTextBlob: certBody.encryptedCertificateChain
});
certificateChain = decryptedCertChain.toString();
} else if (cert.caCertId) {
const { caCert, caCertChain } = await getCaCertChain({
caCertId: cert.caCertId,
certificateAuthorityDAL,
certificateAuthorityCertDAL,
projectDAL,
kmsService
});
certificateChain = `${caCert}\n${caCertChain}`.trim();
}
let privateKey: string | null = null;
try {
const { certPrivateKey } = await getCertificateCredentials({
certId: cert.id,
projectId: ca.projectId,
projectId: cert.projectId,
certificateSecretDAL,
projectDAL,
kmsService
@@ -360,8 +615,7 @@ export const certificateServiceFactory = ({
certificateChain,
privateKey,
serialNumber,
cert,
ca
cert
};
};
@@ -371,6 +625,7 @@ export const certificateServiceFactory = ({
deleteCert,
revokeCert,
getCertBody,
importCert,
getCertBundle
};
};
@@ -78,6 +78,17 @@ export type TGetCertBodyDTO = {
serialNumber: string;
} & Omit<TProjectPermission, "projectId">;
export type TImportCertDTO = {
projectSlug: string;
friendlyName?: string;
pkiCollectionId?: string;
certificatePem: string;
privateKeyPem: string;
chainPem: string;
} & Omit<TProjectPermission, "projectId">;
export type TGetCertPrivateKeyDTO = {
serialNumber: string;
} & Omit<TProjectPermission, "projectId">;
@@ -93,11 +104,3 @@ export type TGetCertificateCredentialsDTO = {
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
};
export type TBuildCertificateChainDTO = {
caCert?: string;
caCertChain?: string;
encryptedCertificateChain?: Buffer;
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey">;
kmsId: string;
};
@@ -31,12 +31,13 @@ export const pkiAlertDALFactory = (db: TDbClient) => {
.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.caId")
.join(`${TableName.PkiCollectionItem} as pci`, `${PkiItemType.CA}.id`, "pci.caId")
.unionAll((qb) => {
void qb
@@ -27,13 +27,13 @@ export const pkiCollectionItemDALFactory = (db: TDbClient) => {
.select(
"pki_collection_items.*",
db.raw(
`COALESCE("${TableName.CertificateAuthority}"."notBefore", "${TableName.Certificate}"."notBefore") as "notBefore"`
`COALESCE("${TableName.InternalCertificateAuthority}"."notBefore", "${TableName.Certificate}"."notBefore") as "notBefore"`
),
db.raw(
`COALESCE("${TableName.CertificateAuthority}"."notAfter", "${TableName.Certificate}"."notAfter") as "notAfter"`
`COALESCE("${TableName.InternalCertificateAuthority}"."notAfter", "${TableName.Certificate}"."notAfter") as "notAfter"`
),
db.raw(
`COALESCE("${TableName.CertificateAuthority}"."friendlyName", "${TableName.Certificate}"."friendlyName") as "friendlyName"`
`COALESCE("${TableName.InternalCertificateAuthority}"."friendlyName", "${TableName.Certificate}"."friendlyName") as "friendlyName"`
)
)
.leftJoin(
@@ -41,6 +41,11 @@ export const pkiCollectionItemDALFactory = (db: TDbClient) => {
`${TableName.PkiCollectionItem}.caId`,
`${TableName.CertificateAuthority}.id`
)
.leftJoin(
TableName.InternalCertificateAuthority,
`${TableName.PkiCollectionItem}.caId`,
`${TableName.InternalCertificateAuthority}.caId`
)
.leftJoin(TableName.Certificate, `${TableName.PkiCollectionItem}.certId`, `${TableName.Certificate}.id`)
.where((builder) => {
void builder.where(`${TableName.PkiCollectionItem}.pkiCollectionId`, collectionId);
@@ -269,14 +269,8 @@ export const pkiCollectionServiceFactory = ({
});
if (isCertAdded) throw new BadRequestError({ message: "Certificate already part of the PKI collection" });
// validate that there exists a certificate in same project as PKI collection
const cas = await certificateAuthorityDAL.find({ projectId: pkiCollection.projectId });
// TODO: consider making this more efficient
const [certificate] = await certificateDAL.find({
$in: {
caId: cas.map((ca) => ca.id)
},
projectId: pkiCollection.projectId,
id: itemId
});
if (!certificate) throw new NotFoundError({ message: `Certificate with ID '${itemId}' not found` });
@@ -0,0 +1,187 @@
import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { logger } from "@app/lib/logger";
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
import { ActorType } from "../auth/auth-type";
import { TCertificateDALFactory } from "../certificate/certificate-dal";
import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal";
import { CaStatus, CaType } from "../certificate-authority/certificate-authority-enums";
import { TCertificateAuthorityQueueFactory } from "../certificate-authority/certificate-authority-queue";
import { InternalCertificateAuthorityFns } from "../certificate-authority/internal/internal-certificate-authority-fns";
import { TPkiSubscriberDALFactory } from "./pki-subscriber-dal";
import { PkiSubscriberStatus, SubscriberOperationStatus } from "./pki-subscriber-types";
type TPkiSubscriberQueueServiceFactoryDep = {
queueService: TQueueServiceFactory;
pkiSubscriberDAL: TPkiSubscriberDALFactory;
certificateAuthorityDAL: TCertificateAuthorityDALFactory;
certificateAuthorityQueue: TCertificateAuthorityQueueFactory;
internalCaFns: ReturnType<typeof InternalCertificateAuthorityFns>;
certificateDAL: TCertificateDALFactory;
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
};
export const pkiSubscriberQueueServiceFactory = ({
queueService,
pkiSubscriberDAL,
certificateAuthorityDAL,
certificateAuthorityQueue,
internalCaFns,
certificateDAL,
auditLogService
}: TPkiSubscriberQueueServiceFactoryDep) => {
queueService.start(QueueName.PkiSubscriber, async (job) => {
if (job.name === QueueJobs.PkiSubscriberDailyAutoRenewal) {
logger.info(`${QueueJobs.PkiSubscriberDailyAutoRenewal}: queue task started`);
const BATCH_SIZE = 100;
let offset = 0;
let hasMore = true;
while (hasMore) {
// fetch PKI subscribers with auto renewal enabled in batches
// eslint-disable-next-line no-await-in-loop
const pkiSubscribers = await pkiSubscriberDAL.find(
{
enableAutoRenewal: true,
$notNull: ["autoRenewalPeriodInDays"],
status: PkiSubscriberStatus.ACTIVE
},
{
limit: BATCH_SIZE,
offset
}
);
if (pkiSubscribers.length === 0) {
hasMore = false;
break;
}
// Process each subscriber in the batch concurrently
// eslint-disable-next-line no-await-in-loop
await Promise.all(
pkiSubscribers.map(async (subscriber) => {
try {
const cert = await certificateDAL.findLatestActiveCertForSubscriber({ subscriberId: subscriber.id });
let shouldRenew = false;
if (!cert || !cert.notAfter) {
shouldRenew = true;
} else {
const now = new Date();
const expiry = new Date(cert.notAfter);
const daysUntilExpiry = (expiry.getTime() - now.getTime()) / (1000 * 60 * 60 * 24);
shouldRenew = daysUntilExpiry <= subscriber.autoRenewalPeriodInDays!;
}
if (shouldRenew) {
// Get the CA for the subscriber
if (!subscriber.caId) {
await pkiSubscriberDAL.updateById(subscriber.id, {
lastOperationStatus: SubscriberOperationStatus.FAILED,
lastOperationMessage: "No CA assigned to subscriber",
lastOperationAt: new Date()
});
return;
}
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(subscriber.caId);
if (!ca) {
await pkiSubscriberDAL.updateById(subscriber.id, {
lastOperationStatus: SubscriberOperationStatus.FAILED,
lastOperationMessage: "CA not found",
lastOperationAt: new Date()
});
return;
}
// Check if CA is active
if (ca.status !== CaStatus.ACTIVE) {
await pkiSubscriberDAL.updateById(subscriber.id, {
lastOperationStatus: SubscriberOperationStatus.FAILED,
lastOperationMessage: "CA is not active",
lastOperationAt: new Date()
});
return;
}
// Order new certificate based on CA type
if (ca.externalCa?.id && ca.externalCa.type === CaType.ACME) {
await certificateAuthorityQueue.orderCertificateForSubscriber({
subscriberId: subscriber.id,
caType: ca.externalCa.type
});
} else if (ca.internalCa?.id) {
// For internal CAs, we can issue certificates directly
await internalCaFns.issueCertificate(subscriber, ca);
}
// Update last auto-renew timestamp
await pkiSubscriberDAL.updateById(subscriber.id, {
lastAutoRenewAt: new Date(),
lastOperationStatus: SubscriberOperationStatus.SUCCESS,
lastOperationMessage: "Triggered certificate auto-renewal",
lastOperationAt: new Date()
});
await auditLogService.createAuditLog({
projectId: subscriber.projectId,
actor: {
type: ActorType.PLATFORM,
metadata: {}
},
event: {
type: EventType.AUTOMATED_RENEW_SUBSCRIBER_CERT,
metadata: {
subscriberId: subscriber.id,
name: subscriber.name
}
}
});
}
} catch (error) {
// Log error and update subscriber status
logger.error(error, `Failed to auto-renew certificate for subscriber ${subscriber.id}`);
await pkiSubscriberDAL.updateById(subscriber.id, {
lastOperationStatus: SubscriberOperationStatus.FAILED,
lastOperationMessage: error instanceof Error ? error.message : "Unknown error",
lastOperationAt: new Date()
});
}
})
);
offset += BATCH_SIZE;
}
logger.info(`${QueueJobs.PkiSubscriberDailyAutoRenewal}: queue task completed`);
}
});
// we do a repeat cron job in utc timezone at 12 Midnight each day
const startDailyAutoRenewalJob = async () => {
// clear previous job
await queueService.stopRepeatableJob(
QueueName.PkiSubscriber,
QueueJobs.PkiSubscriberDailyAutoRenewal,
{ pattern: "0 0 * * *", utc: true },
// { pattern: "*/30 * * * * *", utc: true } // for testing
QueueName.PkiSubscriber // just a job id
);
await queueService.queue(QueueName.PkiSubscriber, QueueJobs.PkiSubscriberDailyAutoRenewal, undefined, {
delay: 5000,
jobId: QueueName.PkiSubscriber,
// { pattern: "*/30 * * * * *", utc: true } // for testing
repeat: { pattern: "0 0 * * *", utc: true }
});
};
queueService.listen(QueueName.PkiSubscriber, "failed", (_, err) => {
logger.error(err, `${QueueName.PkiSubscriber}: failed`);
});
return {
startDailyAutoRenewalJob
};
};
@@ -1,3 +1,5 @@
import { z } from "zod";
import { PkiSubscribersSchema } from "@app/db/schemas";
export const sanitizedPkiSubscriber = PkiSubscribersSchema.pick({
@@ -10,5 +12,13 @@ export const sanitizedPkiSubscriber = PkiSubscribersSchema.pick({
subjectAlternativeNames: true,
ttl: true,
keyUsages: true,
extendedKeyUsages: true
extendedKeyUsages: true,
lastOperationStatus: true,
lastOperationMessage: true,
lastOperationAt: true,
enableAutoRenewal: true,
autoRenewalPeriodInDays: true,
lastAutoRenewAt: true
}).extend({
supportsImmediateCertIssuance: z.boolean().optional()
});
@@ -1,23 +1,20 @@
/* eslint-disable no-bitwise */
import { ForbiddenError, subject } from "@casl/ability";
import * as x509 from "@peculiar/x509";
import crypto, { KeyObject } from "crypto";
import { z } from "zod";
import { ActionProjectType } from "@app/db/schemas";
import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import {
ProjectPermissionCertificateActions,
ProjectPermissionPkiSubscriberActions,
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { ms } from "@app/lib/ms";
import { isFQDN } from "@app/lib/validator/validate-url";
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
import {
CertExtendedKeyUsage,
CertExtendedKeyUsageOIDToName,
@@ -27,27 +24,34 @@ import {
} from "@app/services/certificate/certificate-types";
import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
import { CaStatus, CaType } from "@app/services/certificate-authority/certificate-authority-enums";
import {
createSerialNumber,
expandInternalCa,
getCaCertChain,
getCaCredentials,
keyAlgorithmToAlgCfg,
parseDistinguishedName
} from "@app/services/certificate-authority/certificate-authority-fns";
import { TCertificateAuthoritySecretDALFactory } from "@app/services/certificate-authority/certificate-authority-secret-dal";
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TPkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
import { getCertificateCredentials } from "../certificate/certificate-fns";
import { TCertificateSecretDALFactory } from "../certificate/certificate-secret-dal";
import { TCertificateAuthorityQueueFactory } from "../certificate-authority/certificate-authority-queue";
import { InternalCertificateAuthorityFns } from "../certificate-authority/internal/internal-certificate-authority-fns";
import {
PkiSubscriberStatus,
TCreatePkiSubscriberDTO,
TDeletePkiSubscriberDTO,
TGetPkiSubscriberDTO,
TGetSubscriberActiveCertBundleDTO,
TIssuePkiSubscriberCertDTO,
TListPkiSubscriberCertsDTO,
TOrderPkiSubscriberCertDTO,
TSignPkiSubscriberCertDTO,
TUpdatePkiSubscriberDTO
} from "./pki-subscriber-types";
@@ -57,16 +61,24 @@ type TPkiSubscriberServiceFactoryDep = {
TPkiSubscriberDALFactory,
"create" | "findById" | "updateById" | "deleteById" | "transaction" | "find" | "findOne"
>;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
certificateAuthorityDAL: Pick<
TCertificateAuthorityDALFactory,
"findByIdWithAssociatedCa" | "findById" | "transaction" | "create" | "updateById" | "findWithAssociatedCa"
>;
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findById">;
certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "findOne">;
certificateAuthorityQueue: Pick<TCertificateAuthorityQueueFactory, "orderCertificateForSubscriber">;
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "findOne">;
certificateDAL: Pick<TCertificateDALFactory, "create" | "transaction" | "countCertificatesForPkiSubscriber" | "find">;
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "create">;
certificateDAL: Pick<
TCertificateDALFactory,
"create" | "transaction" | "countCertificatesForPkiSubscriber" | "findLatestActiveCertForSubscriber" | "find"
>;
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "create" | "findOne">;
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create" | "findOne">;
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction" | "findById" | "find">;
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "decryptWithKmsKey" | "encryptWithKmsKey">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
internalCaFns: ReturnType<typeof InternalCertificateAuthorityFns>;
};
export type TPkiSubscriberServiceFactory = ReturnType<typeof pkiSubscriberServiceFactory>;
@@ -78,11 +90,13 @@ export const pkiSubscriberServiceFactory = ({
certificateAuthoritySecretDAL,
certificateAuthorityCrlDAL,
certificateDAL,
certificateBodyDAL,
certificateSecretDAL,
certificateBodyDAL,
projectDAL,
kmsService,
permissionService
permissionService,
certificateAuthorityQueue,
internalCaFns
}: TPkiSubscriberServiceFactoryDep) => {
const createSubscriber = async ({
name,
@@ -93,6 +107,8 @@ export const pkiSubscriberServiceFactory = ({
subjectAlternativeNames,
keyUsages,
extendedKeyUsages,
enableAutoRenewal,
autoRenewalPeriodInDays,
projectId,
actorId,
actorAuthMethod,
@@ -115,6 +131,12 @@ export const pkiSubscriberServiceFactory = ({
})
);
if (enableAutoRenewal) {
if (!autoRenewalPeriodInDays) {
throw new BadRequestError({ message: "autoRenewalPeriodInDays is required when enableAutoRenewal is true" });
}
}
const newSubscriber = await pkiSubscriberDAL.create({
caId,
projectId,
@@ -124,7 +146,9 @@ export const pkiSubscriberServiceFactory = ({
ttl,
subjectAlternativeNames,
keyUsages,
extendedKeyUsages
extendedKeyUsages,
enableAutoRenewal,
autoRenewalPeriodInDays
});
return newSubscriber;
@@ -161,7 +185,18 @@ export const pkiSubscriberServiceFactory = ({
})
);
return subscriber;
let supportsImmediateCertIssuance = false;
if (subscriber.caId) {
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(subscriber.caId);
if (ca.internalCa?.id) {
supportsImmediateCertIssuance = true;
}
}
return {
...subscriber,
supportsImmediateCertIssuance
};
};
const updateSubscriber = async ({
@@ -175,6 +210,8 @@ export const pkiSubscriberServiceFactory = ({
subjectAlternativeNames,
keyUsages,
extendedKeyUsages,
enableAutoRenewal,
autoRenewalPeriodInDays,
actorId,
actorAuthMethod,
actor,
@@ -202,6 +239,12 @@ export const pkiSubscriberServiceFactory = ({
})
);
if (enableAutoRenewal) {
if (!autoRenewalPeriodInDays && !subscriber.autoRenewalPeriodInDays) {
throw new BadRequestError({ message: "autoRenewalPeriodInDays is required when enableAutoRenewal is true" });
}
}
const updatedSubscriber = await pkiSubscriberDAL.updateById(subscriber.id, {
caId,
name,
@@ -210,7 +253,9 @@ export const pkiSubscriberServiceFactory = ({
ttl,
subjectAlternativeNames,
keyUsages,
extendedKeyUsages
extendedKeyUsages,
enableAutoRenewal,
autoRenewalPeriodInDays
});
return updatedSubscriber;
@@ -251,28 +296,26 @@ export const pkiSubscriberServiceFactory = ({
return subscriber;
};
const issueSubscriberCert = async ({
const orderSubscriberCert = async ({
subscriberName,
projectId,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TIssuePkiSubscriberCertDTO) => {
}: TOrderPkiSubscriberCertDTO) => {
const subscriber = await pkiSubscriberDAL.findOne({
name: subscriberName,
projectId
});
if (!subscriber) throw new NotFoundError({ message: `PKI subscriber named '${subscriberName}' not found` });
if (!subscriber.caId) throw new BadRequestError({ message: "Subscriber does not have an assigned issuing CA" });
const ca = await certificateAuthorityDAL.findById(subscriber.caId);
if (!ca) throw new NotFoundError({ message: `CA with ID '${subscriber.caId}' not found` });
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: ca.projectId,
projectId: subscriber.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.CertificateManager
@@ -287,200 +330,69 @@ export const pkiSubscriberServiceFactory = ({
if (subscriber.status !== PkiSubscriberStatus.ACTIVE)
throw new BadRequestError({ message: "Subscriber is not active" });
if (ca.status !== CaStatus.ACTIVE) throw new BadRequestError({ message: "CA is not active" });
if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" });
if (ca.requireTemplateForIssuance) {
throw new BadRequestError({ message: "Certificate template is required for issuance" });
}
const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId);
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId,
projectDAL,
kmsService
});
const kmsDecryptor = await kmsService.decryptWithKmsKey({
kmsId: certificateManagerKmsId
});
const decryptedCaCert = await kmsDecryptor({
cipherTextBlob: caCert.encryptedCertificate
});
const caCertObj = new x509.X509Certificate(decryptedCaCert);
const notBeforeDate = new Date();
const notAfterDate = new Date(new Date().getTime() + ms(subscriber.ttl));
const caCertNotBeforeDate = new Date(caCertObj.notBefore);
const caCertNotAfterDate = new Date(caCertObj.notAfter);
// check not before constraint
if (notBeforeDate < caCertNotBeforeDate) {
throw new BadRequestError({ message: "notBefore date is before CA certificate's notBefore date" });
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(subscriber.caId);
if (ca.internalCa?.id) {
throw new BadRequestError({ message: "CA does not support ordering of certificates" });
}
// check not after constraint
if (notAfterDate > caCertNotAfterDate) {
throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" });
if (ca.status !== CaStatus.ACTIVE) {
throw new BadRequestError({ message: "CA is disabled" });
}
const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm);
const leafKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
const csrObj = await x509.Pkcs10CertificateRequestGenerator.create({
name: `CN=${subscriber.commonName}`,
keys: leafKeys,
signingAlgorithm: alg,
extensions: [
// eslint-disable-next-line no-bitwise
new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment)
],
attributes: [new x509.ChallengePasswordAttribute("password")]
});
const { caPrivateKey, caSecret } = await getCaCredentials({
caId: ca.id,
certificateAuthorityDAL,
certificateAuthoritySecretDAL,
projectDAL,
kmsService
});
const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id });
const appCfg = getConfig();
const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/pki/crl/${caCrl.id}/der`;
const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/pki/ca/${ca.id}/certificates/${caCert.id}/der`;
const extensions: x509.Extension[] = [
new x509.BasicConstraintsExtension(false),
new x509.CRLDistributionPointsExtension([distributionPointUrl]),
await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey),
new x509.AuthorityInfoAccessExtension({
caIssuers: new x509.GeneralName("url", caIssuerUrl)
}),
new x509.CertificatePolicyExtension(["2.5.29.32.0"]) // anyPolicy
];
const selectedKeyUsages = subscriber.keyUsages as CertKeyUsage[];
const keyUsagesBitValue = selectedKeyUsages.reduce((accum, keyUsage) => accum | x509.KeyUsageFlags[keyUsage], 0);
if (keyUsagesBitValue) {
extensions.push(new x509.KeyUsagesExtension(keyUsagesBitValue, true));
}
if (subscriber.extendedKeyUsages.length) {
const extendedKeyUsagesExtension = new x509.ExtendedKeyUsageExtension(
subscriber.extendedKeyUsages.map((eku) => x509.ExtendedKeyUsage[eku as CertExtendedKeyUsage]),
true
);
extensions.push(extendedKeyUsagesExtension);
}
let altNamesArray: { type: "email" | "dns"; value: string }[] = [];
if (subscriber.subjectAlternativeNames?.length) {
altNamesArray = subscriber.subjectAlternativeNames.map((altName) => {
if (z.string().email().safeParse(altName).success) {
return { type: "email", value: altName };
}
if (isFQDN(altName, { allow_wildcard: true })) {
return { type: "dns", value: altName };
}
throw new BadRequestError({ message: `Invalid SAN entry: ${altName}` });
if (ca.externalCa?.id && ca.externalCa.type === CaType.ACME) {
await certificateAuthorityQueue.orderCertificateForSubscriber({
subscriberId: subscriber.id,
caType: ca.externalCa.type
});
const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false);
extensions.push(altNamesExtension);
return subscriber;
}
const serialNumber = createSerialNumber();
const leafCert = await x509.X509CertificateGenerator.create({
serialNumber,
subject: csrObj.subject,
issuer: caCertObj.subject,
notBefore: notBeforeDate,
notAfter: notAfterDate,
signingKey: caPrivateKey,
publicKey: csrObj.publicKey,
signingAlgorithm: alg,
extensions
throw new BadRequestError({ message: "Unsupported CA type" });
};
const issueSubscriberCert = async ({
subscriberName,
projectId,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TIssuePkiSubscriberCertDTO) => {
const subscriber = await pkiSubscriberDAL.findOne({
name: subscriberName,
projectId
});
const skLeafObj = KeyObject.from(leafKeys.privateKey);
const skLeaf = skLeafObj.export({ format: "pem", type: "pkcs8" }) as string;
if (!subscriber) throw new NotFoundError({ message: `PKI subscriber named '${subscriberName}' not found` });
if (!subscriber.caId) throw new BadRequestError({ message: "Subscriber does not have an assigned issuing CA" });
const kmsEncryptor = await kmsService.encryptWithKmsKey({
kmsId: certificateManagerKmsId
});
const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
plainText: Buffer.from(new Uint8Array(leafCert.rawData))
});
const { cipherTextBlob: encryptedPrivateKey } = await kmsEncryptor({
plainText: Buffer.from(skLeaf)
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: subscriber.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.CertificateManager
});
const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({
caCertId: caCert.id,
certificateAuthorityDAL,
certificateAuthorityCertDAL,
projectDAL,
kmsService
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionPkiSubscriberActions.IssueCert,
subject(ProjectPermissionSub.PkiSubscribers, {
name: subscriber.name
})
);
const certificateChainPem = `${issuingCaCertificate}\n${caCertChain}`.trim();
if (subscriber.status !== PkiSubscriberStatus.ACTIVE)
throw new BadRequestError({ message: "Subscriber is not active" });
const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({
plainText: Buffer.from(certificateChainPem)
});
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(subscriber.caId);
if (ca.internalCa?.id) {
return internalCaFns.issueCertificate(subscriber, ca);
}
await certificateDAL.transaction(async (tx) => {
const cert = await certificateDAL.create(
{
caId: ca.id,
caCertId: caCert.id,
pkiSubscriberId: subscriber.id,
status: CertStatus.ACTIVE,
friendlyName: subscriber.commonName,
commonName: subscriber.commonName,
altNames: subscriber.subjectAlternativeNames.join(","),
serialNumber,
notBefore: notBeforeDate,
notAfter: notAfterDate,
keyUsages: selectedKeyUsages,
extendedKeyUsages: subscriber.extendedKeyUsages as CertExtendedKeyUsage[]
},
tx
);
await certificateBodyDAL.create(
{
certId: cert.id,
encryptedCertificate,
encryptedCertificateChain
},
tx
);
await certificateSecretDAL.create(
{
certId: cert.id,
encryptedPrivateKey
},
tx
);
});
return {
certificate: leafCert.toString("pem"),
certificateChain: certificateChainPem,
issuingCaCertificate,
privateKey: skLeaf,
serialNumber,
ca,
subscriber
};
throw new BadRequestError({ message: "CA does not support immediate issuance of certificates" });
};
const signSubscriberCert = async ({
@@ -500,8 +412,8 @@ export const pkiSubscriberServiceFactory = ({
if (!subscriber) throw new NotFoundError({ message: `PKI subscriber named '${subscriberName}' not found` });
if (!subscriber.caId) throw new BadRequestError({ message: "Subscriber does not have an assigned issuing CA" });
const ca = await certificateAuthorityDAL.findById(subscriber.caId);
if (!ca) throw new NotFoundError({ message: `CA with ID '${subscriber.caId}' not found` });
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(subscriber.caId);
if (!ca?.internalCa) throw new NotFoundError({ message: `CA with ID '${subscriber.caId}' not found` });
const { permission } = await permissionService.getProjectPermission({
actor,
@@ -522,11 +434,10 @@ export const pkiSubscriberServiceFactory = ({
if (subscriber.status !== PkiSubscriberStatus.ACTIVE)
throw new BadRequestError({ message: "Subscriber is not active" });
if (ca.status !== CaStatus.ACTIVE) throw new BadRequestError({ message: "CA is not active" });
if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" });
if (ca.requireTemplateForIssuance) {
throw new BadRequestError({ message: "Certificate template is required for issuance" });
}
const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId);
if (!ca.internalCa?.activeCaCertId)
throw new BadRequestError({ message: "CA does not have a certificate installed" });
const caCert = await certificateAuthorityCertDAL.findById(ca.internalCa.activeCaCertId);
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId,
@@ -543,7 +454,7 @@ export const pkiSubscriberServiceFactory = ({
const caCertObj = new x509.X509Certificate(decryptedCaCert);
const notBeforeDate = new Date();
const notAfterDate = new Date(new Date().getTime() + ms(subscriber.ttl));
const notAfterDate = new Date(new Date().getTime() + ms(subscriber.ttl ?? "0"));
const caCertNotBeforeDate = new Date(caCertObj.notBefore);
const caCertNotAfterDate = new Date(caCertObj.notAfter);
@@ -557,7 +468,7 @@ export const pkiSubscriberServiceFactory = ({
throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" });
}
const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm);
const alg = keyAlgorithmToAlgCfg(ca.internalCa.keyAlgorithm as CertKeyAlgorithm);
const csrObj = new x509.Pkcs10CertificateRequest(csr);
@@ -691,7 +602,7 @@ export const pkiSubscriberServiceFactory = ({
});
const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({
caCertId: ca.activeCaCertId,
caCertId: ca.internalCa.activeCaCertId,
certificateAuthorityDAL,
certificateAuthorityCertDAL,
projectDAL,
@@ -718,7 +629,8 @@ export const pkiSubscriberServiceFactory = ({
notBefore: notBeforeDate,
notAfter: notAfterDate,
keyUsages: selectedKeyUsages,
extendedKeyUsages: selectedExtendedKeyUsages
extendedKeyUsages: selectedExtendedKeyUsages,
projectId
},
tx
);
@@ -740,7 +652,7 @@ export const pkiSubscriberServiceFactory = ({
certificateChain: `${issuingCaCertificate}\n${caCertChain}`.trim(),
issuingCaCertificate,
serialNumber,
ca,
ca: expandInternalCa(ca),
commonName: subscriber.commonName,
subscriber
};
@@ -793,6 +705,114 @@ export const pkiSubscriberServiceFactory = ({
};
};
const getSubscriberActiveCertBundle = async ({
subscriberName,
projectId,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TGetSubscriberActiveCertBundleDTO) => {
const subscriber = await pkiSubscriberDAL.findOne({
name: subscriberName,
projectId
});
if (!subscriber) {
throw new NotFoundError({ message: `PKI subscriber named '${subscriberName}' not found` });
}
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: subscriber.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionPkiSubscriberActions.ListCerts,
subject(ProjectPermissionSub.PkiSubscribers, {
name: subscriber.name
})
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateActions.Read,
ProjectPermissionSub.Certificates
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateActions.ReadPrivateKey,
ProjectPermissionSub.Certificates
);
const cert = await certificateDAL.findLatestActiveCertForSubscriber({
subscriberId: subscriber.id
});
if (!cert) {
throw new NotFoundError({ message: "No active certificate found for subscriber" });
}
const certBody = await certificateBodyDAL.findOne({ certId: cert.id });
const certificateManagerKeyId = await getProjectKmsCertificateKeyId({
projectId: cert.projectId,
projectDAL,
kmsService
});
const kmsDecryptor = await kmsService.decryptWithKmsKey({
kmsId: certificateManagerKeyId
});
const decryptedCert = await kmsDecryptor({
cipherTextBlob: certBody.encryptedCertificate
});
const certObj = new x509.X509Certificate(decryptedCert);
const certificate = certObj.toString("pem");
let certificateChain = null;
// On newer certs the certBody.encryptedCertificateChain column will always exist.
// Older certs will have a caCertId which will be used as a fallback mechanism for structuring the chain.
if (certBody.encryptedCertificateChain) {
const decryptedCertChain = await kmsDecryptor({
cipherTextBlob: certBody.encryptedCertificateChain
});
certificateChain = decryptedCertChain.toString();
} else if (cert.caCertId) {
const { caCert, caCertChain } = await getCaCertChain({
caCertId: cert.caCertId,
certificateAuthorityDAL,
certificateAuthorityCertDAL,
projectDAL,
kmsService
});
certificateChain = `${caCert}\n${caCertChain}`.trim();
}
const { certPrivateKey } = await getCertificateCredentials({
certId: cert.id,
projectId: cert.projectId,
certificateSecretDAL,
projectDAL,
kmsService
});
return {
certificate,
certificateChain,
privateKey: certPrivateKey,
serialNumber: cert.serialNumber,
cert,
subscriber
};
};
return {
createSubscriber,
getSubscriber,
@@ -800,6 +820,8 @@ export const pkiSubscriberServiceFactory = ({
deleteSubscriber,
issueSubscriberCert,
signSubscriberCert,
listSubscriberCerts
listSubscriberCerts,
orderSubscriberCert,
getSubscriberActiveCertBundle
};
};
@@ -12,10 +12,12 @@ export type TCreatePkiSubscriberDTO = {
name: string;
commonName: string;
status: PkiSubscriberStatus;
ttl: string;
ttl?: string;
subjectAlternativeNames: string[];
keyUsages: CertKeyUsage[];
extendedKeyUsages: CertExtendedKeyUsage[];
enableAutoRenewal?: boolean;
autoRenewalPeriodInDays?: number;
} & TProjectPermission;
export type TGetPkiSubscriberDTO = {
@@ -32,6 +34,8 @@ export type TUpdatePkiSubscriberDTO = {
subjectAlternativeNames?: string[];
keyUsages?: CertKeyUsage[];
extendedKeyUsages?: CertExtendedKeyUsage[];
enableAutoRenewal?: boolean;
autoRenewalPeriodInDays?: number;
} & TProjectPermission;
export type TDeletePkiSubscriberDTO = {
@@ -42,6 +46,10 @@ export type TIssuePkiSubscriberCertDTO = {
subscriberName: string;
} & TProjectPermission;
export type TOrderPkiSubscriberCertDTO = {
subscriberName: string;
} & TProjectPermission;
export type TSignPkiSubscriberCertDTO = {
subscriberName: string;
csr: string;
@@ -52,3 +60,12 @@ export type TListPkiSubscriberCertsDTO = {
offset: number;
limit: number;
} & TProjectPermission;
export type TGetSubscriberActiveCertBundleDTO = {
subscriberName: string;
} & TProjectPermission;
export enum SubscriberOperationStatus {
SUCCESS = "success",
FAILED = "failed"
}
+13 -12
View File
@@ -6,6 +6,7 @@ import {
ProjectMembershipRole,
ProjectType,
ProjectVersion,
TableName,
TProjectEnvironments
} from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
@@ -41,6 +42,7 @@ import { TPkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subsc
import { ActorType } from "../auth/auth-type";
import { TCertificateDALFactory } from "../certificate/certificate-dal";
import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal";
import { expandInternalCa } from "../certificate-authority/certificate-authority-fns";
import { TCertificateTemplateDALFactory } from "../certificate-template/certificate-template-dal";
import { TGroupProjectDALFactory } from "../group-project/group-project-dal";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
@@ -149,7 +151,7 @@ type TProjectServiceFactoryDep = {
>;
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">;
pkiSubscriberDAL: Pick<TPkiSubscriberDALFactory, "find">;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "find">;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "find" | "findWithAssociatedCa">;
certificateDAL: Pick<TCertificateDALFactory, "find" | "countCertificatesInProject">;
certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "getCertTemplatesByProjectId">;
pkiAlertDAL: Pick<TPkiAlertDALFactory, "find">;
@@ -914,17 +916,20 @@ export const projectServiceFactory = ({
ProjectPermissionSub.CertificateAuthorities
);
const cas = await certificateAuthorityDAL.find(
const cas = await certificateAuthorityDAL.findWithAssociatedCa(
{
projectId,
...(status && { status }),
...(friendlyName && { friendlyName }),
...(commonName && { commonName })
[`${TableName.CertificateAuthority}.projectId` as "projectId"]: projectId,
$notNull: [`${TableName.InternalCertificateAuthority}.id` as "id"],
...(status && { [`${TableName.CertificateAuthority}.status` as "status"]: status }),
...(friendlyName && {
[`${TableName.InternalCertificateAuthority}.friendlyName` as "friendlyName"]: friendlyName
}),
...(commonName && { [`${TableName.InternalCertificateAuthority}.commonName` as "commonName"]: commonName })
},
{ offset, limit, sort: [["updatedAt", "desc"]] }
);
return cas;
return cas.map((ca) => expandInternalCa(ca));
};
/**
@@ -965,13 +970,9 @@ export const projectServiceFactory = ({
ProjectPermissionSub.Certificates
);
const cas = await certificateAuthorityDAL.find({ projectId });
const certificates = await certificateDAL.find(
{
$in: {
caId: cas.map((ca) => ca.id)
},
projectId,
...(friendlyName && { friendlyName }),
...(commonName && { commonName })
},