mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Merge pull request #4925 from Infisical/feat/PKI-28
Add self-sign certs support
This commit is contained in:
@@ -0,0 +1,27 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const hasIssuerTypeColumn = await knex.schema.hasColumn(TableName.PkiCertificateProfile, "issuerType");
|
||||||
|
|
||||||
|
if (!hasIssuerTypeColumn) {
|
||||||
|
await knex.schema.alterTable(TableName.PkiCertificateProfile, (t) => {
|
||||||
|
t.string("issuerType").notNullable().defaultTo("ca");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await knex.schema.alterTable(TableName.PkiCertificateProfile, (t) => {
|
||||||
|
t.uuid("caId").nullable().alter();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
const hasIssuerTypeColumn = await knex.schema.hasColumn(TableName.PkiCertificateProfile, "issuerType");
|
||||||
|
|
||||||
|
if (hasIssuerTypeColumn) {
|
||||||
|
await knex.schema.alterTable(TableName.PkiCertificateProfile, (t) => {
|
||||||
|
t.dropColumn("issuerType");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ import { TImmutableDBKeys } from "./models";
|
|||||||
export const PkiCertificateProfilesSchema = z.object({
|
export const PkiCertificateProfilesSchema = z.object({
|
||||||
id: z.string().uuid(),
|
id: z.string().uuid(),
|
||||||
projectId: z.string(),
|
projectId: z.string(),
|
||||||
caId: z.string().uuid(),
|
caId: z.string().uuid().nullable().optional(),
|
||||||
certificateTemplateId: z.string().uuid(),
|
certificateTemplateId: z.string().uuid(),
|
||||||
slug: z.string(),
|
slug: z.string(),
|
||||||
description: z.string().nullable().optional(),
|
description: z.string().nullable().optional(),
|
||||||
@@ -19,7 +19,8 @@ export const PkiCertificateProfilesSchema = z.object({
|
|||||||
apiConfigId: z.string().uuid().nullable().optional(),
|
apiConfigId: z.string().uuid().nullable().optional(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
acmeConfigId: z.string().uuid().nullable().optional()
|
acmeConfigId: z.string().uuid().nullable().optional(),
|
||||||
|
issuerType: z.string().default("ca")
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TPkiCertificateProfiles = z.infer<typeof PkiCertificateProfilesSchema>;
|
export type TPkiCertificateProfiles = z.infer<typeof PkiCertificateProfilesSchema>;
|
||||||
|
|||||||
@@ -2787,6 +2787,7 @@ interface CreateCertificateProfile {
|
|||||||
name: string;
|
name: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
enrollmentType: string;
|
enrollmentType: string;
|
||||||
|
issuerType: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -683,6 +683,13 @@ export const pkiAcmeServiceFactory = ({
|
|||||||
payload: TFinalizeAcmeOrderPayload;
|
payload: TFinalizeAcmeOrderPayload;
|
||||||
}): Promise<TAcmeResponse<TAcmeOrderResource>> => {
|
}): Promise<TAcmeResponse<TAcmeOrderResource>> => {
|
||||||
const profile = (await certificateProfileDAL.findByIdWithConfigs(profileId))!;
|
const profile = (await certificateProfileDAL.findByIdWithConfigs(profileId))!;
|
||||||
|
|
||||||
|
if (!profile.caId) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Self-signed certificates are not supported for ACME enrollment"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let order = await acmeOrderDAL.findByAccountAndOrderIdWithAuthorizations(accountId, orderId);
|
let order = await acmeOrderDAL.findByAccountAndOrderIdWithAuthorizations(accountId, orderId);
|
||||||
if (!order) {
|
if (!order) {
|
||||||
throw new NotFoundError({ message: "ACME order not found" });
|
throw new NotFoundError({ message: "ACME order not found" });
|
||||||
@@ -729,7 +736,7 @@ export const pkiAcmeServiceFactory = ({
|
|||||||
throw new AcmeBadCSRError({ message: "Invalid CSR: Common name + SANs mismatch with order identifiers" });
|
throw new AcmeBadCSRError({ message: "Invalid CSR: Common name + SANs mismatch with order identifiers" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(profile.caId);
|
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(profile.caId!);
|
||||||
if (!ca) {
|
if (!ca) {
|
||||||
throw new NotFoundError({ message: "Certificate Authority not found" });
|
throw new NotFoundError({ message: "Certificate Authority not found" });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2219,7 +2219,10 @@ export const registerRoutes = async (
|
|||||||
permissionService,
|
permissionService,
|
||||||
certificateSyncDAL,
|
certificateSyncDAL,
|
||||||
pkiSyncDAL,
|
pkiSyncDAL,
|
||||||
pkiSyncQueue
|
pkiSyncQueue,
|
||||||
|
kmsService,
|
||||||
|
projectDAL,
|
||||||
|
certificateBodyDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
const certificateV3Queue = certificateV3QueueServiceFactory({
|
const certificateV3Queue = certificateV3QueueServiceFactory({
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
|||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
import { CertStatus } from "@app/services/certificate/certificate-types";
|
import { CertStatus } from "@app/services/certificate/certificate-types";
|
||||||
import { EnrollmentType } from "@app/services/certificate-profile/certificate-profile-types";
|
import { EnrollmentType, IssuerType } from "@app/services/certificate-profile/certificate-profile-types";
|
||||||
|
|
||||||
export const registerCertificateProfilesRouter = async (server: FastifyZodProvider) => {
|
export const registerCertificateProfilesRouter = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
server.route({
|
||||||
@@ -23,7 +23,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
|||||||
body: z
|
body: z
|
||||||
.object({
|
.object({
|
||||||
projectId: z.string().min(1),
|
projectId: z.string().min(1),
|
||||||
caId: z.string().uuid(),
|
caId: z.string().uuid().optional(),
|
||||||
certificateTemplateId: z.string().uuid(),
|
certificateTemplateId: z.string().uuid(),
|
||||||
slug: z
|
slug: z
|
||||||
.string()
|
.string()
|
||||||
@@ -32,6 +32,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
|||||||
.regex(new RE2("^[a-z0-9-]+$"), "Slug must contain only lowercase letters, numbers, and hyphens"),
|
.regex(new RE2("^[a-z0-9-]+$"), "Slug must contain only lowercase letters, numbers, and hyphens"),
|
||||||
description: z.string().max(1000).optional(),
|
description: z.string().max(1000).optional(),
|
||||||
enrollmentType: z.nativeEnum(EnrollmentType),
|
enrollmentType: z.nativeEnum(EnrollmentType),
|
||||||
|
issuerType: z.nativeEnum(IssuerType).default(IssuerType.CA),
|
||||||
estConfig: z
|
estConfig: z
|
||||||
.object({
|
.object({
|
||||||
disableBootstrapCaValidation: z.boolean().default(false),
|
disableBootstrapCaValidation: z.boolean().default(false),
|
||||||
@@ -50,43 +51,100 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
|||||||
.refine(
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
if (data.enrollmentType === EnrollmentType.EST) {
|
if (data.enrollmentType === EnrollmentType.EST) {
|
||||||
if (!data.estConfig) {
|
return !!data.estConfig;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (data.apiConfig) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (data.acmeConfig) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (data.enrollmentType === EnrollmentType.API) {
|
|
||||||
if (!data.apiConfig) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (data.estConfig) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (data.acmeConfig) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (data.enrollmentType === EnrollmentType.ACME) {
|
|
||||||
if (!data.acmeConfig) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (data.estConfig) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (data.apiConfig) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
message:
|
message: "EST enrollment type requires EST configuration"
|
||||||
"EST enrollment type requires EST configuration and cannot have API or ACME configuration. API enrollment type requires API configuration and cannot have EST or ACME configuration. ACME enrollment type requires ACME configuration and cannot have EST or API configuration."
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.enrollmentType === EnrollmentType.API) {
|
||||||
|
return !!data.apiConfig;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "API enrollment type requires API configuration"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.enrollmentType === EnrollmentType.ACME) {
|
||||||
|
return !!data.acmeConfig;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "ACME enrollment type requires ACME configuration"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.enrollmentType === EnrollmentType.EST) {
|
||||||
|
return !data.apiConfig && !data.acmeConfig;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "EST enrollment type cannot have API or ACME configuration"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.enrollmentType === EnrollmentType.API) {
|
||||||
|
return !data.estConfig && !data.acmeConfig;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "API enrollment type cannot have EST or ACME configuration"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.enrollmentType === EnrollmentType.ACME) {
|
||||||
|
return !data.estConfig && !data.apiConfig;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "ACME enrollment type cannot have EST or API configuration"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.issuerType === IssuerType.CA) {
|
||||||
|
return !!data.caId;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "CA issuer type requires a CA ID"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.issuerType === IssuerType.SELF_SIGNED) {
|
||||||
|
return !data.caId;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "Self-signed issuer type cannot have a CA ID"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.issuerType === IssuerType.SELF_SIGNED) {
|
||||||
|
return data.enrollmentType === EnrollmentType.API;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "Self-signed issuer type only supports API enrollment"
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
response: {
|
response: {
|
||||||
@@ -115,7 +173,8 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
|||||||
certificateProfileId: certificateProfile.id,
|
certificateProfileId: certificateProfile.id,
|
||||||
name: certificateProfile.slug,
|
name: certificateProfile.slug,
|
||||||
projectId: certificateProfile.projectId,
|
projectId: certificateProfile.projectId,
|
||||||
enrollmentType: certificateProfile.enrollmentType
|
enrollmentType: certificateProfile.enrollmentType,
|
||||||
|
issuerType: certificateProfile.issuerType
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -139,6 +198,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
|||||||
limit: z.coerce.number().min(1).max(100).default(20),
|
limit: z.coerce.number().min(1).max(100).default(20),
|
||||||
search: z.string().optional(),
|
search: z.string().optional(),
|
||||||
enrollmentType: z.nativeEnum(EnrollmentType).optional(),
|
enrollmentType: z.nativeEnum(EnrollmentType).optional(),
|
||||||
|
issuerType: z.nativeEnum(IssuerType).optional(),
|
||||||
caId: z.string().uuid().optional()
|
caId: z.string().uuid().optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
@@ -339,6 +399,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
|||||||
.optional(),
|
.optional(),
|
||||||
description: z.string().max(1000).optional(),
|
description: z.string().max(1000).optional(),
|
||||||
enrollmentType: z.nativeEnum(EnrollmentType).optional(),
|
enrollmentType: z.nativeEnum(EnrollmentType).optional(),
|
||||||
|
issuerType: z.nativeEnum(IssuerType).optional(),
|
||||||
estConfig: z
|
estConfig: z
|
||||||
.object({
|
.object({
|
||||||
disableBootstrapCaValidation: z.boolean().default(false),
|
disableBootstrapCaValidation: z.boolean().default(false),
|
||||||
|
|||||||
@@ -67,6 +67,12 @@ export const certificateEstV3ServiceFactory = ({
|
|||||||
throw new BadRequestError({ message: "EST enrollment not configured for this profile" });
|
throw new BadRequestError({ message: "EST enrollment not configured for this profile" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!profile.caId) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Self-signed certificates are not supported for EST enrollment"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const estConfig = await estEnrollmentConfigDAL.findById(profile.estConfigId);
|
const estConfig = await estEnrollmentConfigDAL.findById(profile.estConfigId);
|
||||||
if (!estConfig) {
|
if (!estConfig) {
|
||||||
throw new NotFoundError({ message: "EST configuration not found" });
|
throw new NotFoundError({ message: "EST configuration not found" });
|
||||||
@@ -169,6 +175,12 @@ export const certificateEstV3ServiceFactory = ({
|
|||||||
throw new BadRequestError({ message: "EST enrollment not configured for this profile" });
|
throw new BadRequestError({ message: "EST enrollment not configured for this profile" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!profile.caId) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Self-signed certificates are not supported for EST enrollment"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const estConfig = await estEnrollmentConfigDAL.findById(profile.estConfigId);
|
const estConfig = await estEnrollmentConfigDAL.findById(profile.estConfigId);
|
||||||
if (!estConfig) {
|
if (!estConfig) {
|
||||||
throw new NotFoundError({ message: "EST configuration not found" });
|
throw new NotFoundError({ message: "EST configuration not found" });
|
||||||
@@ -281,6 +293,12 @@ export const certificateEstV3ServiceFactory = ({
|
|||||||
throw new BadRequestError({ message: "EST enrollment not configured for this profile" });
|
throw new BadRequestError({ message: "EST enrollment not configured for this profile" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!profile.caId) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Self-signed certificates are not supported for EST enrollment"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const estConfig = await estEnrollmentConfigDAL.findById(profile.estConfigId);
|
const estConfig = await estEnrollmentConfigDAL.findById(profile.estConfigId);
|
||||||
if (!estConfig) {
|
if (!estConfig) {
|
||||||
throw new NotFoundError({ message: "EST configuration not found" });
|
throw new NotFoundError({ message: "EST configuration not found" });
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { ormify, selectAllTableCols } from "@app/lib/knex";
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
EnrollmentType,
|
EnrollmentType,
|
||||||
|
IssuerType,
|
||||||
TCertificateProfile,
|
TCertificateProfile,
|
||||||
TCertificateProfileCertificate,
|
TCertificateProfileCertificate,
|
||||||
TCertificateProfileInsert,
|
TCertificateProfileInsert,
|
||||||
@@ -198,6 +199,7 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
|||||||
slug: result.slug,
|
slug: result.slug,
|
||||||
description: result.description,
|
description: result.description,
|
||||||
enrollmentType: result.enrollmentType as EnrollmentType,
|
enrollmentType: result.enrollmentType as EnrollmentType,
|
||||||
|
issuerType: result.issuerType as IssuerType,
|
||||||
estConfigId: result.estConfigId,
|
estConfigId: result.estConfigId,
|
||||||
apiConfigId: result.apiConfigId,
|
apiConfigId: result.apiConfigId,
|
||||||
acmeConfigId: result.acmeConfigId,
|
acmeConfigId: result.acmeConfigId,
|
||||||
@@ -239,12 +241,13 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
|||||||
limit?: number;
|
limit?: number;
|
||||||
search?: string;
|
search?: string;
|
||||||
enrollmentType?: EnrollmentType;
|
enrollmentType?: EnrollmentType;
|
||||||
|
issuerType?: IssuerType;
|
||||||
caId?: string;
|
caId?: string;
|
||||||
} = {},
|
} = {},
|
||||||
tx?: Knex
|
tx?: Knex
|
||||||
): Promise<TCertificateProfile[] | TCertificateProfileWithConfigs[]> => {
|
): Promise<TCertificateProfile[] | TCertificateProfileWithConfigs[]> => {
|
||||||
try {
|
try {
|
||||||
const { offset = 0, limit = 20, search, enrollmentType, caId } = options;
|
const { offset = 0, limit = 20, search, enrollmentType, issuerType, caId } = options;
|
||||||
|
|
||||||
let baseQuery = (tx || db)(TableName.PkiCertificateProfile).where(
|
let baseQuery = (tx || db)(TableName.PkiCertificateProfile).where(
|
||||||
`${TableName.PkiCertificateProfile}.projectId`,
|
`${TableName.PkiCertificateProfile}.projectId`,
|
||||||
@@ -269,6 +272,10 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
|||||||
baseQuery = baseQuery.where(`${TableName.PkiCertificateProfile}.caId`, caId);
|
baseQuery = baseQuery.where(`${TableName.PkiCertificateProfile}.caId`, caId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (issuerType) {
|
||||||
|
baseQuery = baseQuery.where(`${TableName.PkiCertificateProfile}.issuerType`, issuerType);
|
||||||
|
}
|
||||||
|
|
||||||
const query = baseQuery
|
const query = baseQuery
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
TableName.PkiEstEnrollmentConfig,
|
TableName.PkiEstEnrollmentConfig,
|
||||||
@@ -338,8 +345,10 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
|||||||
slug: result.slug,
|
slug: result.slug,
|
||||||
description: result.description,
|
description: result.description,
|
||||||
enrollmentType: result.enrollmentType as EnrollmentType,
|
enrollmentType: result.enrollmentType as EnrollmentType,
|
||||||
|
issuerType: result.issuerType as IssuerType,
|
||||||
estConfigId: result.estConfigId,
|
estConfigId: result.estConfigId,
|
||||||
apiConfigId: result.apiConfigId,
|
apiConfigId: result.apiConfigId,
|
||||||
|
acmeConfigId: result.acmeConfigId,
|
||||||
createdAt: result.createdAt,
|
createdAt: result.createdAt,
|
||||||
updatedAt: result.updatedAt,
|
updatedAt: result.updatedAt,
|
||||||
estConfig,
|
estConfig,
|
||||||
@@ -359,12 +368,13 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
|||||||
options: {
|
options: {
|
||||||
search?: string;
|
search?: string;
|
||||||
enrollmentType?: EnrollmentType;
|
enrollmentType?: EnrollmentType;
|
||||||
|
issuerType?: IssuerType;
|
||||||
caId?: string;
|
caId?: string;
|
||||||
} = {},
|
} = {},
|
||||||
tx?: Knex
|
tx?: Knex
|
||||||
): Promise<number> => {
|
): Promise<number> => {
|
||||||
try {
|
try {
|
||||||
const { search, enrollmentType, caId } = options;
|
const { search, enrollmentType, issuerType, caId } = options;
|
||||||
|
|
||||||
let query = (tx || db)(TableName.PkiCertificateProfile).where({ projectId });
|
let query = (tx || db)(TableName.PkiCertificateProfile).where({ projectId });
|
||||||
|
|
||||||
@@ -384,6 +394,10 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
|||||||
query = query.where({ caId });
|
query = query.where({ caId });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (issuerType) {
|
||||||
|
query = query.where({ issuerType });
|
||||||
|
}
|
||||||
|
|
||||||
const result = await query.count("*").first();
|
const result = await query.count("*").first();
|
||||||
return parseInt((result as unknown as { count: string }).count || "0", 10);
|
return parseInt((result as unknown as { count: string }).count || "0", 10);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import RE2 from "re2";
|
import RE2 from "re2";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { EnrollmentType } from "./certificate-profile-types";
|
import { CertStatus } from "../certificate/certificate-types";
|
||||||
|
import { EnrollmentType, IssuerType } from "./certificate-profile-types";
|
||||||
|
|
||||||
export const createCertificateProfileSchema = z
|
export const createCertificateProfileSchema = z
|
||||||
.object({
|
.object({
|
||||||
projectId: z.string().uuid("Project ID must be valid"),
|
projectId: z.string().uuid("Project ID must be valid"),
|
||||||
caId: z.string().uuid(),
|
caId: z.string().uuid().nullable().optional(),
|
||||||
certificateTemplateId: z.string().uuid(),
|
certificateTemplateId: z.string().uuid(),
|
||||||
slug: z
|
slug: z
|
||||||
.string()
|
.string()
|
||||||
@@ -15,6 +16,7 @@ export const createCertificateProfileSchema = z
|
|||||||
.regex(new RE2("^[a-z0-9-]+$"), "Slug must contain only lowercase letters, numbers, and hyphens"),
|
.regex(new RE2("^[a-z0-9-]+$"), "Slug must contain only lowercase letters, numbers, and hyphens"),
|
||||||
description: z.string().max(1000).optional(),
|
description: z.string().max(1000).optional(),
|
||||||
enrollmentType: z.nativeEnum(EnrollmentType),
|
enrollmentType: z.nativeEnum(EnrollmentType),
|
||||||
|
issuerType: z.nativeEnum(IssuerType).default(IssuerType.CA),
|
||||||
estConfig: z
|
estConfig: z
|
||||||
.object({
|
.object({
|
||||||
disableBootstrapCaValidation: z.boolean().default(false),
|
disableBootstrapCaValidation: z.boolean().default(false),
|
||||||
@@ -33,43 +35,100 @@ export const createCertificateProfileSchema = z
|
|||||||
.refine(
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
if (data.enrollmentType === EnrollmentType.EST) {
|
if (data.enrollmentType === EnrollmentType.EST) {
|
||||||
if (!data.estConfig) {
|
return !!data.estConfig;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (data.apiConfig) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (data.acmeConfig) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (data.enrollmentType === EnrollmentType.API) {
|
|
||||||
if (!data.apiConfig) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (data.estConfig) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (data.acmeConfig) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (data.enrollmentType === EnrollmentType.ACME) {
|
|
||||||
if (!data.acmeConfig) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (data.estConfig) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (data.apiConfig) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
message:
|
message: "EST enrollment type requires EST configuration"
|
||||||
"EST enrollment type requires EST configuration and cannot have API configuration. API enrollment type requires API configuration and cannot have EST configuration."
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.enrollmentType === EnrollmentType.API) {
|
||||||
|
return !!data.apiConfig;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "API enrollment type requires API configuration"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.enrollmentType === EnrollmentType.ACME) {
|
||||||
|
return !!data.acmeConfig;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "ACME enrollment type requires ACME configuration"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.enrollmentType === EnrollmentType.EST) {
|
||||||
|
return !data.apiConfig && !data.acmeConfig;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "EST enrollment type cannot have API or ACME configuration"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.enrollmentType === EnrollmentType.API) {
|
||||||
|
return !data.estConfig && !data.acmeConfig;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "API enrollment type cannot have EST or ACME configuration"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.enrollmentType === EnrollmentType.ACME) {
|
||||||
|
return !data.estConfig && !data.apiConfig;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "ACME enrollment type cannot have EST or API configuration"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.issuerType === IssuerType.CA) {
|
||||||
|
return !!data.caId;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "CA issuer type requires a CA ID"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.issuerType === IssuerType.SELF_SIGNED) {
|
||||||
|
return !data.caId;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "Self-signed issuer type cannot have a CA ID"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.issuerType === IssuerType.SELF_SIGNED) {
|
||||||
|
return data.enrollmentType === EnrollmentType.API;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "Self-signed issuer type only supports API enrollment"
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -83,6 +142,7 @@ export const updateCertificateProfileSchema = z
|
|||||||
.optional(),
|
.optional(),
|
||||||
description: z.string().max(1000).optional(),
|
description: z.string().max(1000).optional(),
|
||||||
enrollmentType: z.nativeEnum(EnrollmentType).optional(),
|
enrollmentType: z.nativeEnum(EnrollmentType).optional(),
|
||||||
|
issuerType: z.nativeEnum(IssuerType).optional(),
|
||||||
estConfig: z
|
estConfig: z
|
||||||
.object({
|
.object({
|
||||||
disableBootstrapCaValidation: z.boolean().default(false),
|
disableBootstrapCaValidation: z.boolean().default(false),
|
||||||
@@ -100,19 +160,34 @@ export const updateCertificateProfileSchema = z
|
|||||||
.refine(
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
if (data.enrollmentType === EnrollmentType.EST) {
|
if (data.enrollmentType === EnrollmentType.EST) {
|
||||||
if (data.apiConfig) {
|
return !data.apiConfig;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (data.enrollmentType === EnrollmentType.API) {
|
|
||||||
if (data.estConfig) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
message: "Cannot have EST config with API enrollment type or API config with EST enrollment type."
|
message: "EST enrollment type cannot have API configuration"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.enrollmentType === EnrollmentType.API) {
|
||||||
|
return !data.estConfig;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "API enrollment type cannot have EST configuration"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.issuerType === IssuerType.SELF_SIGNED) {
|
||||||
|
return !data.enrollmentType || data.enrollmentType === EnrollmentType.API;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "Self-signed issuer type only supports API enrollment"
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -131,6 +206,7 @@ export const listCertificateProfilesSchema = z.object({
|
|||||||
limit: z.coerce.number().min(1).max(100).default(20),
|
limit: z.coerce.number().min(1).max(100).default(20),
|
||||||
search: z.string().optional(),
|
search: z.string().optional(),
|
||||||
enrollmentType: z.nativeEnum(EnrollmentType).optional(),
|
enrollmentType: z.nativeEnum(EnrollmentType).optional(),
|
||||||
|
issuerType: z.nativeEnum(IssuerType).optional(),
|
||||||
caId: z.string().uuid().optional()
|
caId: z.string().uuid().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -142,6 +218,6 @@ export const listCertificatesByProfileSchema = z.object({
|
|||||||
profileId: z.string().uuid(),
|
profileId: z.string().uuid(),
|
||||||
offset: z.coerce.number().min(0).default(0),
|
offset: z.coerce.number().min(0).default(0),
|
||||||
limit: z.coerce.number().min(1).max(100).default(20),
|
limit: z.coerce.number().min(1).max(100).default(20),
|
||||||
status: z.enum(["active", "expired", "revoked"]).optional(),
|
status: z.nativeEnum(CertStatus).optional(),
|
||||||
search: z.string().optional()
|
search: z.string().optional()
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -22,7 +22,12 @@ import type { TKmsServiceFactory } from "../kms/kms-service";
|
|||||||
import type { TProjectDALFactory } from "../project/project-dal";
|
import type { TProjectDALFactory } from "../project/project-dal";
|
||||||
import type { TCertificateProfileDALFactory } from "./certificate-profile-dal";
|
import type { TCertificateProfileDALFactory } from "./certificate-profile-dal";
|
||||||
import { certificateProfileServiceFactory, TCertificateProfileServiceFactory } from "./certificate-profile-service";
|
import { certificateProfileServiceFactory, TCertificateProfileServiceFactory } from "./certificate-profile-service";
|
||||||
import { EnrollmentType, TCertificateProfile, TCertificateProfileWithConfigs } from "./certificate-profile-types";
|
import {
|
||||||
|
EnrollmentType,
|
||||||
|
IssuerType,
|
||||||
|
TCertificateProfile,
|
||||||
|
TCertificateProfileWithConfigs
|
||||||
|
} from "./certificate-profile-types";
|
||||||
|
|
||||||
vi.mock("@app/lib/crypto/cryptography", () => ({
|
vi.mock("@app/lib/crypto/cryptography", () => ({
|
||||||
crypto: {
|
crypto: {
|
||||||
@@ -90,6 +95,7 @@ describe("CertificateProfileService", () => {
|
|||||||
description: "Test certificate profile",
|
description: "Test certificate profile",
|
||||||
slug: "test-profile",
|
slug: "test-profile",
|
||||||
enrollmentType: EnrollmentType.API,
|
enrollmentType: EnrollmentType.API,
|
||||||
|
issuerType: IssuerType.CA,
|
||||||
caId: "ca-123",
|
caId: "ca-123",
|
||||||
certificateTemplateId: "template-123",
|
certificateTemplateId: "template-123",
|
||||||
apiConfigId: "api-config-123",
|
apiConfigId: "api-config-123",
|
||||||
@@ -272,6 +278,7 @@ describe("CertificateProfileService", () => {
|
|||||||
slug: "new-profile",
|
slug: "new-profile",
|
||||||
description: "New test profile",
|
description: "New test profile",
|
||||||
enrollmentType: EnrollmentType.API,
|
enrollmentType: EnrollmentType.API,
|
||||||
|
issuerType: IssuerType.CA,
|
||||||
caId: "ca-123",
|
caId: "ca-123",
|
||||||
certificateTemplateId: "template-123",
|
certificateTemplateId: "template-123",
|
||||||
apiConfig: {
|
apiConfig: {
|
||||||
@@ -312,6 +319,7 @@ describe("CertificateProfileService", () => {
|
|||||||
slug: "new-profile",
|
slug: "new-profile",
|
||||||
description: "New test profile",
|
description: "New test profile",
|
||||||
enrollmentType: EnrollmentType.API,
|
enrollmentType: EnrollmentType.API,
|
||||||
|
issuerType: IssuerType.CA,
|
||||||
caId: "ca-123",
|
caId: "ca-123",
|
||||||
certificateTemplateId: "template-123",
|
certificateTemplateId: "template-123",
|
||||||
apiConfigId: "api-config-123",
|
apiConfigId: "api-config-123",
|
||||||
@@ -383,6 +391,7 @@ describe("CertificateProfileService", () => {
|
|||||||
slug: "invalid-profile",
|
slug: "invalid-profile",
|
||||||
description: "Invalid test profile",
|
description: "Invalid test profile",
|
||||||
enrollmentType: EnrollmentType.API,
|
enrollmentType: EnrollmentType.API,
|
||||||
|
issuerType: IssuerType.CA,
|
||||||
caId: "ca-123",
|
caId: "ca-123",
|
||||||
certificateTemplateId: "template-123"
|
certificateTemplateId: "template-123"
|
||||||
};
|
};
|
||||||
@@ -401,6 +410,7 @@ describe("CertificateProfileService", () => {
|
|||||||
slug: "api-profile",
|
slug: "api-profile",
|
||||||
description: "Profile with API enrollment",
|
description: "Profile with API enrollment",
|
||||||
enrollmentType: EnrollmentType.API,
|
enrollmentType: EnrollmentType.API,
|
||||||
|
issuerType: IssuerType.CA,
|
||||||
caId: "ca-123",
|
caId: "ca-123",
|
||||||
certificateTemplateId: "template-123",
|
certificateTemplateId: "template-123",
|
||||||
apiConfig: {
|
apiConfig: {
|
||||||
@@ -726,6 +736,7 @@ describe("CertificateProfileService", () => {
|
|||||||
slug: "est-profile",
|
slug: "est-profile",
|
||||||
description: "Profile with EST enrollment",
|
description: "Profile with EST enrollment",
|
||||||
enrollmentType: EnrollmentType.EST,
|
enrollmentType: EnrollmentType.EST,
|
||||||
|
issuerType: IssuerType.CA,
|
||||||
caId: "ca-123",
|
caId: "ca-123",
|
||||||
certificateTemplateId: "template-123",
|
certificateTemplateId: "template-123",
|
||||||
estConfig: {
|
estConfig: {
|
||||||
@@ -776,6 +787,7 @@ describe("CertificateProfileService", () => {
|
|||||||
slug: "different-profile-name",
|
slug: "different-profile-name",
|
||||||
description: "Profile with duplicate slug",
|
description: "Profile with duplicate slug",
|
||||||
enrollmentType: EnrollmentType.API,
|
enrollmentType: EnrollmentType.API,
|
||||||
|
issuerType: IssuerType.CA,
|
||||||
caId: "ca-123",
|
caId: "ca-123",
|
||||||
certificateTemplateId: "template-123",
|
certificateTemplateId: "template-123",
|
||||||
apiConfig: {
|
apiConfig: {
|
||||||
@@ -801,6 +813,7 @@ describe("CertificateProfileService", () => {
|
|||||||
slug: "auto-renew-profile",
|
slug: "auto-renew-profile",
|
||||||
description: "Profile with auto-renewal",
|
description: "Profile with auto-renewal",
|
||||||
enrollmentType: EnrollmentType.API,
|
enrollmentType: EnrollmentType.API,
|
||||||
|
issuerType: IssuerType.CA,
|
||||||
caId: "ca-123",
|
caId: "ca-123",
|
||||||
certificateTemplateId: "template-123",
|
certificateTemplateId: "template-123",
|
||||||
apiConfig: {
|
apiConfig: {
|
||||||
@@ -965,6 +978,7 @@ describe("CertificateProfileService", () => {
|
|||||||
slug: "invalid-template-profile",
|
slug: "invalid-template-profile",
|
||||||
description: "Profile with invalid template",
|
description: "Profile with invalid template",
|
||||||
enrollmentType: EnrollmentType.API,
|
enrollmentType: EnrollmentType.API,
|
||||||
|
issuerType: IssuerType.CA,
|
||||||
caId: "ca-123",
|
caId: "ca-123",
|
||||||
certificateTemplateId: "nonexistent-template",
|
certificateTemplateId: "nonexistent-template",
|
||||||
apiConfig: {
|
apiConfig: {
|
||||||
@@ -990,6 +1004,7 @@ describe("CertificateProfileService", () => {
|
|||||||
slug: "concurrent-profile",
|
slug: "concurrent-profile",
|
||||||
description: "Profile created concurrently",
|
description: "Profile created concurrently",
|
||||||
enrollmentType: EnrollmentType.API,
|
enrollmentType: EnrollmentType.API,
|
||||||
|
issuerType: IssuerType.CA,
|
||||||
caId: "ca-123",
|
caId: "ca-123",
|
||||||
certificateTemplateId: "template-123",
|
certificateTemplateId: "template-123",
|
||||||
apiConfig: {
|
apiConfig: {
|
||||||
@@ -1018,6 +1033,7 @@ describe("CertificateProfileService", () => {
|
|||||||
slug: "cross-project-profile",
|
slug: "cross-project-profile",
|
||||||
description: "Profile using template from different project",
|
description: "Profile using template from different project",
|
||||||
enrollmentType: EnrollmentType.API,
|
enrollmentType: EnrollmentType.API,
|
||||||
|
issuerType: IssuerType.CA,
|
||||||
caId: "ca-123",
|
caId: "ca-123",
|
||||||
certificateTemplateId: "template-456",
|
certificateTemplateId: "template-456",
|
||||||
apiConfig: {
|
apiConfig: {
|
||||||
@@ -1047,6 +1063,7 @@ describe("CertificateProfileService", () => {
|
|||||||
slug: "invalid-slug-profile",
|
slug: "invalid-slug-profile",
|
||||||
description: "Profile with invalid slug format",
|
description: "Profile with invalid slug format",
|
||||||
enrollmentType: EnrollmentType.API,
|
enrollmentType: EnrollmentType.API,
|
||||||
|
issuerType: IssuerType.CA,
|
||||||
caId: "ca-123",
|
caId: "ca-123",
|
||||||
certificateTemplateId: "template-123",
|
certificateTemplateId: "template-123",
|
||||||
apiConfig: {
|
apiConfig: {
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import { getProjectKmsCertificateKeyId } from "../project/project-fns";
|
|||||||
import { TCertificateProfileDALFactory } from "./certificate-profile-dal";
|
import { TCertificateProfileDALFactory } from "./certificate-profile-dal";
|
||||||
import {
|
import {
|
||||||
EnrollmentType,
|
EnrollmentType,
|
||||||
|
IssuerType,
|
||||||
TCertificateProfile,
|
TCertificateProfile,
|
||||||
TCertificateProfileCertificate,
|
TCertificateProfileCertificate,
|
||||||
TCertificateProfileInsert,
|
TCertificateProfileInsert,
|
||||||
@@ -39,6 +40,34 @@ import {
|
|||||||
TCertificateProfileWithConfigs
|
TCertificateProfileWithConfigs
|
||||||
} from "./certificate-profile-types";
|
} from "./certificate-profile-types";
|
||||||
|
|
||||||
|
const validateIssuerTypeConstraints = (
|
||||||
|
issuerType: IssuerType,
|
||||||
|
enrollmentType: EnrollmentType,
|
||||||
|
caId: string | null,
|
||||||
|
existingCaId?: string | null
|
||||||
|
) => {
|
||||||
|
if (issuerType === IssuerType.CA) {
|
||||||
|
if (!caId && !existingCaId) {
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message: "CA issuer type requires a Certificate Authority to be selected"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (issuerType === IssuerType.SELF_SIGNED) {
|
||||||
|
if (caId) {
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message: "Self-signed issuer type cannot have a Certificate Authority"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (enrollmentType !== EnrollmentType.API) {
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message: "Self-signed issuer type only supports API enrollment"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const generateAndEncryptAcmeEabSecret = async (
|
const generateAndEncryptAcmeEabSecret = async (
|
||||||
projectId: string,
|
projectId: string,
|
||||||
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey">,
|
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey">,
|
||||||
@@ -163,7 +192,8 @@ export type TCertificateProfileServiceFactory = ReturnType<typeof certificatePro
|
|||||||
const convertDalToService = (dalResult: Record<string, unknown>): TCertificateProfile => {
|
const convertDalToService = (dalResult: Record<string, unknown>): TCertificateProfile => {
|
||||||
return {
|
return {
|
||||||
...dalResult,
|
...dalResult,
|
||||||
enrollmentType: dalResult.enrollmentType as EnrollmentType
|
enrollmentType: dalResult.enrollmentType as EnrollmentType,
|
||||||
|
issuerType: dalResult.issuerType as IssuerType
|
||||||
} as TCertificateProfile;
|
} as TCertificateProfile;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -240,6 +270,8 @@ export const certificateProfileServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validateIssuerTypeConstraints(data.issuerType, data.enrollmentType, data.caId ?? null);
|
||||||
|
|
||||||
// Validate enrollment configuration requirements
|
// Validate enrollment configuration requirements
|
||||||
if (data.enrollmentType === EnrollmentType.EST && !data.estConfig) {
|
if (data.enrollmentType === EnrollmentType.EST && !data.estConfig) {
|
||||||
throw new ForbiddenRequestError({
|
throw new ForbiddenRequestError({
|
||||||
@@ -376,7 +408,16 @@ export const certificateProfileServiceFactory = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { estConfig, apiConfig, ...profileUpdateData } = data;
|
const finalIssuerType = data.issuerType || existingProfile.issuerType;
|
||||||
|
const finalEnrollmentType = data.enrollmentType || existingProfile.enrollmentType;
|
||||||
|
const finalCaId = data.caId !== undefined ? data.caId : existingProfile.caId;
|
||||||
|
|
||||||
|
validateIssuerTypeConstraints(finalIssuerType, finalEnrollmentType, finalCaId ?? null, existingProfile.caId);
|
||||||
|
|
||||||
|
const updatedData =
|
||||||
|
finalIssuerType === IssuerType.SELF_SIGNED && existingProfile.caId ? { ...data, caId: null } : data;
|
||||||
|
|
||||||
|
const { estConfig, apiConfig, ...profileUpdateData } = updatedData;
|
||||||
|
|
||||||
const updatedProfile = await certificateProfileDAL.transaction(async (tx) => {
|
const updatedProfile = await certificateProfileDAL.transaction(async (tx) => {
|
||||||
if (estConfig && existingProfile.estConfigId) {
|
if (estConfig && existingProfile.estConfigId) {
|
||||||
@@ -569,6 +610,7 @@ export const certificateProfileServiceFactory = ({
|
|||||||
limit = 20,
|
limit = 20,
|
||||||
search,
|
search,
|
||||||
enrollmentType,
|
enrollmentType,
|
||||||
|
issuerType,
|
||||||
caId
|
caId
|
||||||
}: {
|
}: {
|
||||||
actor: ActorType;
|
actor: ActorType;
|
||||||
@@ -580,6 +622,7 @@ export const certificateProfileServiceFactory = ({
|
|||||||
limit?: number;
|
limit?: number;
|
||||||
search?: string;
|
search?: string;
|
||||||
enrollmentType?: EnrollmentType;
|
enrollmentType?: EnrollmentType;
|
||||||
|
issuerType?: IssuerType;
|
||||||
caId?: string;
|
caId?: string;
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
profiles: TCertificateProfileWithConfigs[];
|
profiles: TCertificateProfileWithConfigs[];
|
||||||
@@ -603,12 +646,14 @@ export const certificateProfileServiceFactory = ({
|
|||||||
limit,
|
limit,
|
||||||
search,
|
search,
|
||||||
enrollmentType,
|
enrollmentType,
|
||||||
|
issuerType,
|
||||||
caId
|
caId
|
||||||
});
|
});
|
||||||
|
|
||||||
const totalCount = await certificateProfileDAL.countByProjectId(projectId, {
|
const totalCount = await certificateProfileDAL.countByProjectId(projectId, {
|
||||||
search,
|
search,
|
||||||
enrollmentType,
|
enrollmentType,
|
||||||
|
issuerType,
|
||||||
caId
|
caId
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -10,16 +10,24 @@ export enum EnrollmentType {
|
|||||||
ACME = "acme"
|
ACME = "acme"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TCertificateProfile = Omit<TPkiCertificateProfiles, "enrollmentType"> & {
|
export enum IssuerType {
|
||||||
|
CA = "ca",
|
||||||
|
SELF_SIGNED = "self-signed"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TCertificateProfile = Omit<TPkiCertificateProfiles, "enrollmentType" | "issuerType"> & {
|
||||||
enrollmentType: EnrollmentType;
|
enrollmentType: EnrollmentType;
|
||||||
|
issuerType: IssuerType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TCertificateProfileInsert = Omit<TPkiCertificateProfilesInsert, "enrollmentType"> & {
|
export type TCertificateProfileInsert = Omit<TPkiCertificateProfilesInsert, "enrollmentType" | "issuerType"> & {
|
||||||
enrollmentType: EnrollmentType;
|
enrollmentType: EnrollmentType;
|
||||||
|
issuerType: IssuerType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TCertificateProfileUpdate = Omit<TPkiCertificateProfilesUpdate, "enrollmentType"> & {
|
export type TCertificateProfileUpdate = Omit<TPkiCertificateProfilesUpdate, "enrollmentType" | "issuerType"> & {
|
||||||
enrollmentType?: EnrollmentType;
|
enrollmentType?: EnrollmentType;
|
||||||
|
issuerType?: IssuerType;
|
||||||
estConfig?: {
|
estConfig?: {
|
||||||
disableBootstrapCaValidation?: boolean;
|
disableBootstrapCaValidation?: boolean;
|
||||||
passphrase?: string;
|
passphrase?: string;
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
CertSubjectAttributeType
|
CertSubjectAttributeType
|
||||||
} from "@app/services/certificate-common/certificate-constants";
|
} from "@app/services/certificate-common/certificate-constants";
|
||||||
import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal";
|
import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal";
|
||||||
import { EnrollmentType } from "@app/services/certificate-profile/certificate-profile-types";
|
import { EnrollmentType, IssuerType } from "@app/services/certificate-profile/certificate-profile-types";
|
||||||
import { TCertificateTemplateV2ServiceFactory } from "@app/services/certificate-template-v2/certificate-template-v2-service";
|
import { TCertificateTemplateV2ServiceFactory } from "@app/services/certificate-template-v2/certificate-template-v2-service";
|
||||||
|
|
||||||
import { ActorType, AuthMethod } from "../auth/auth-type";
|
import { ActorType, AuthMethod } from "../auth/auth-type";
|
||||||
@@ -40,18 +40,29 @@ vi.mock("../certificate-common/certificate-csr-utils", () => ({
|
|||||||
describe("CertificateV3Service", () => {
|
describe("CertificateV3Service", () => {
|
||||||
let service: TCertificateV3ServiceFactory;
|
let service: TCertificateV3ServiceFactory;
|
||||||
|
|
||||||
const mockCertificateDAL: Pick<TCertificateDALFactory, "findOne" | "findById" | "updateById" | "transaction"> = {
|
const mockCertificateDAL: Pick<
|
||||||
|
TCertificateDALFactory,
|
||||||
|
"findOne" | "findById" | "updateById" | "transaction" | "create"
|
||||||
|
> = {
|
||||||
findOne: vi.fn(),
|
findOne: vi.fn(),
|
||||||
findById: vi.fn(),
|
findById: vi.fn(),
|
||||||
updateById: vi.fn(),
|
updateById: vi.fn(),
|
||||||
|
create: vi.fn().mockResolvedValue({
|
||||||
|
id: "new-cert-id",
|
||||||
|
serialNumber: "123456789",
|
||||||
|
friendlyName: "Test Certificate",
|
||||||
|
commonName: "test.example.com",
|
||||||
|
status: "ACTIVE"
|
||||||
|
}),
|
||||||
transaction: vi.fn().mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
|
transaction: vi.fn().mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
|
||||||
const mockTx = {};
|
const mockTx = {};
|
||||||
return callback(mockTx);
|
return callback(mockTx);
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockCertificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne"> = {
|
const mockCertificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne" | "create"> = {
|
||||||
findOne: vi.fn()
|
findOne: vi.fn(),
|
||||||
|
create: vi.fn()
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockCertificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa"> = {
|
const mockCertificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa"> = {
|
||||||
@@ -150,7 +161,24 @@ describe("CertificateV3Service", () => {
|
|||||||
},
|
},
|
||||||
pkiSyncQueue: {
|
pkiSyncQueue: {
|
||||||
queuePkiSyncSyncCertificatesById: vi.fn().mockResolvedValue(undefined)
|
queuePkiSyncSyncCertificatesById: vi.fn().mockResolvedValue(undefined)
|
||||||
}
|
},
|
||||||
|
certificateBodyDAL: {
|
||||||
|
create: vi.fn().mockResolvedValue({ id: "body-123" })
|
||||||
|
},
|
||||||
|
kmsService: {
|
||||||
|
generateKmsKey: vi.fn().mockResolvedValue("kms-key-123"),
|
||||||
|
encryptWithKmsKey: vi.fn().mockResolvedValue(vi.fn().mockResolvedValue(Buffer.from("encrypted"))),
|
||||||
|
decryptWithKmsKey: vi.fn().mockResolvedValue(vi.fn().mockResolvedValue(Buffer.from("decrypted")))
|
||||||
|
},
|
||||||
|
projectDAL: {
|
||||||
|
findOne: vi.fn().mockResolvedValue({ id: "project-123" }),
|
||||||
|
findById: vi.fn().mockResolvedValue({ id: "project-123" }),
|
||||||
|
updateById: vi.fn().mockResolvedValue({ id: "project-123" }),
|
||||||
|
transaction: vi.fn().mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
|
||||||
|
const mockTx = {};
|
||||||
|
return callback(mockTx);
|
||||||
|
})
|
||||||
|
} as any
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -175,6 +203,7 @@ describe("CertificateV3Service", () => {
|
|||||||
id: profileId,
|
id: profileId,
|
||||||
projectId: "project-123",
|
projectId: "project-123",
|
||||||
enrollmentType: EnrollmentType.API,
|
enrollmentType: EnrollmentType.API,
|
||||||
|
issuerType: IssuerType.CA,
|
||||||
caId: "ca-123",
|
caId: "ca-123",
|
||||||
certificateTemplateId: "template-123",
|
certificateTemplateId: "template-123",
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
@@ -319,6 +348,7 @@ describe("CertificateV3Service", () => {
|
|||||||
id: profileId,
|
id: profileId,
|
||||||
projectId: "project-123",
|
projectId: "project-123",
|
||||||
enrollmentType: EnrollmentType.API,
|
enrollmentType: EnrollmentType.API,
|
||||||
|
issuerType: IssuerType.CA,
|
||||||
caId: "ca-123",
|
caId: "ca-123",
|
||||||
certificateTemplateId: "template-123",
|
certificateTemplateId: "template-123",
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
@@ -508,6 +538,7 @@ describe("CertificateV3Service", () => {
|
|||||||
id: profileId,
|
id: profileId,
|
||||||
projectId: "project-123",
|
projectId: "project-123",
|
||||||
enrollmentType: EnrollmentType.EST, // Wrong enrollment type
|
enrollmentType: EnrollmentType.EST, // Wrong enrollment type
|
||||||
|
issuerType: IssuerType.CA,
|
||||||
caId: "ca-123",
|
caId: "ca-123",
|
||||||
certificateTemplateId: "template-123",
|
certificateTemplateId: "template-123",
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
@@ -561,6 +592,7 @@ describe("CertificateV3Service", () => {
|
|||||||
id: profileId,
|
id: profileId,
|
||||||
projectId: "project-123",
|
projectId: "project-123",
|
||||||
enrollmentType: EnrollmentType.API,
|
enrollmentType: EnrollmentType.API,
|
||||||
|
issuerType: IssuerType.CA,
|
||||||
caId: "ca-123",
|
caId: "ca-123",
|
||||||
certificateTemplateId: "template-123",
|
certificateTemplateId: "template-123",
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
@@ -721,6 +753,7 @@ describe("CertificateV3Service", () => {
|
|||||||
id: profileId,
|
id: profileId,
|
||||||
projectId: "project-123",
|
projectId: "project-123",
|
||||||
enrollmentType: EnrollmentType.EST, // Wrong enrollment type
|
enrollmentType: EnrollmentType.EST, // Wrong enrollment type
|
||||||
|
issuerType: IssuerType.CA,
|
||||||
caId: "ca-123",
|
caId: "ca-123",
|
||||||
certificateTemplateId: "template-123",
|
certificateTemplateId: "template-123",
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
@@ -772,6 +805,7 @@ describe("CertificateV3Service", () => {
|
|||||||
id: profileId,
|
id: profileId,
|
||||||
projectId: "project-123",
|
projectId: "project-123",
|
||||||
enrollmentType: EnrollmentType.API,
|
enrollmentType: EnrollmentType.API,
|
||||||
|
issuerType: IssuerType.CA,
|
||||||
caId: "ca-123",
|
caId: "ca-123",
|
||||||
certificateTemplateId: "template-123",
|
certificateTemplateId: "template-123",
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
@@ -933,6 +967,7 @@ describe("CertificateV3Service", () => {
|
|||||||
id: profileId,
|
id: profileId,
|
||||||
projectId: "project-123",
|
projectId: "project-123",
|
||||||
enrollmentType: EnrollmentType.EST, // Wrong enrollment type
|
enrollmentType: EnrollmentType.EST, // Wrong enrollment type
|
||||||
|
issuerType: IssuerType.CA,
|
||||||
caId: "ca-123",
|
caId: "ca-123",
|
||||||
certificateTemplateId: "template-123",
|
certificateTemplateId: "template-123",
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
@@ -971,6 +1006,7 @@ describe("CertificateV3Service", () => {
|
|||||||
caId: "ca-1",
|
caId: "ca-1",
|
||||||
certificateTemplateId: "template-1",
|
certificateTemplateId: "template-1",
|
||||||
enrollmentType: EnrollmentType.API,
|
enrollmentType: EnrollmentType.API,
|
||||||
|
issuerType: IssuerType.CA,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
description: "Test profile for algorithm compatibility",
|
description: "Test profile for algorithm compatibility",
|
||||||
@@ -1552,6 +1588,7 @@ describe("CertificateV3Service", () => {
|
|||||||
id: "profile-123",
|
id: "profile-123",
|
||||||
projectId: "project-123",
|
projectId: "project-123",
|
||||||
enrollmentType: EnrollmentType.API,
|
enrollmentType: EnrollmentType.API,
|
||||||
|
issuerType: IssuerType.CA,
|
||||||
caId: "ca-123",
|
caId: "ca-123",
|
||||||
certificateTemplateId: "template-123",
|
certificateTemplateId: "template-123",
|
||||||
apiConfig: {
|
apiConfig: {
|
||||||
@@ -1733,9 +1770,9 @@ describe("CertificateV3Service", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should reject renewal if certificate is not from a profile", async () => {
|
it("should reject renewal if certificate has no profile and no CA", async () => {
|
||||||
const certWithoutProfile = { ...mockOriginalCert, profileId: null };
|
const certWithoutProfileAndCA = { ...mockOriginalCert, profileId: null, caId: null };
|
||||||
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(certWithoutProfile);
|
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(certWithoutProfileAndCA);
|
||||||
|
|
||||||
// Set up transaction mock to properly handle errors
|
// Set up transaction mock to properly handle errors
|
||||||
vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
|
vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
|
||||||
@@ -2008,6 +2045,7 @@ describe("CertificateV3Service", () => {
|
|||||||
const mockProfile = {
|
const mockProfile = {
|
||||||
id: "profile-123",
|
id: "profile-123",
|
||||||
enrollmentType: EnrollmentType.API,
|
enrollmentType: EnrollmentType.API,
|
||||||
|
issuerType: IssuerType.CA,
|
||||||
projectId: "project-123"
|
projectId: "project-123"
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2084,6 +2122,7 @@ describe("CertificateV3Service", () => {
|
|||||||
const mockProfile = {
|
const mockProfile = {
|
||||||
id: "profile-123",
|
id: "profile-123",
|
||||||
enrollmentType: EnrollmentType.API,
|
enrollmentType: EnrollmentType.API,
|
||||||
|
issuerType: IssuerType.CA,
|
||||||
projectId: "project-123"
|
projectId: "project-123"
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2129,6 +2168,7 @@ describe("CertificateV3Service", () => {
|
|||||||
const mockProfile = {
|
const mockProfile = {
|
||||||
id: "profile-123",
|
id: "profile-123",
|
||||||
enrollmentType: EnrollmentType.API,
|
enrollmentType: EnrollmentType.API,
|
||||||
|
issuerType: IssuerType.CA,
|
||||||
projectId: "project-123"
|
projectId: "project-123"
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2172,6 +2212,7 @@ describe("CertificateV3Service", () => {
|
|||||||
const mockProfile = {
|
const mockProfile = {
|
||||||
id: "profile-123",
|
id: "profile-123",
|
||||||
enrollmentType: EnrollmentType.API,
|
enrollmentType: EnrollmentType.API,
|
||||||
|
issuerType: IssuerType.CA,
|
||||||
projectId: "project-123"
|
projectId: "project-123"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import * as x509 from "@peculiar/x509";
|
||||||
import { randomUUID } from "crypto";
|
import { randomUUID } from "crypto";
|
||||||
import RE2 from "re2";
|
import RE2 from "re2";
|
||||||
|
|
||||||
import { ActionProjectType } from "@app/db/schemas";
|
import { ActionProjectType, TCertificates } from "@app/db/schemas";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
||||||
import {
|
import {
|
||||||
ProjectPermissionCertificateActions,
|
ProjectPermissionCertificateActions,
|
||||||
@@ -10,8 +11,11 @@ import {
|
|||||||
ProjectPermissionSub
|
ProjectPermissionSub
|
||||||
} from "@app/ee/services/permission/project-permission";
|
} from "@app/ee/services/permission/project-permission";
|
||||||
import { TPkiAcmeAccountDALFactory } from "@app/ee/services/pki-acme/pki-acme-account-dal";
|
import { TPkiAcmeAccountDALFactory } from "@app/ee/services/pki-acme/pki-acme-account-dal";
|
||||||
|
import { crypto } from "@app/lib/crypto/cryptography";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
import { ms } from "@app/lib/ms";
|
||||||
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
|
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
|
||||||
|
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
|
||||||
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||||
import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
|
import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
|
||||||
import {
|
import {
|
||||||
@@ -28,12 +32,25 @@ import {
|
|||||||
TCertificateAuthorityWithAssociatedCa
|
TCertificateAuthorityWithAssociatedCa
|
||||||
} from "@app/services/certificate-authority/certificate-authority-dal";
|
} from "@app/services/certificate-authority/certificate-authority-dal";
|
||||||
import { CaStatus, CaType } from "@app/services/certificate-authority/certificate-authority-enums";
|
import { CaStatus, CaType } from "@app/services/certificate-authority/certificate-authority-enums";
|
||||||
|
import {
|
||||||
|
createDistinguishedName,
|
||||||
|
createSerialNumber,
|
||||||
|
keyAlgorithmToAlgCfg,
|
||||||
|
signatureAlgorithmToAlgCfg
|
||||||
|
} from "@app/services/certificate-authority/certificate-authority-fns";
|
||||||
import { TInternalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service";
|
import { TInternalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service";
|
||||||
import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal";
|
import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal";
|
||||||
import { EnrollmentType } from "@app/services/certificate-profile/certificate-profile-types";
|
import { EnrollmentType, IssuerType } from "@app/services/certificate-profile/certificate-profile-types";
|
||||||
import { TCertificateTemplateV2ServiceFactory } from "@app/services/certificate-template-v2/certificate-template-v2-service";
|
import { TCertificateTemplateV2ServiceFactory } from "@app/services/certificate-template-v2/certificate-template-v2-service";
|
||||||
|
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 { CertSubjectAlternativeNameType } from "../certificate-common/certificate-constants";
|
import {
|
||||||
|
CertExtendedKeyUsageType,
|
||||||
|
CertKeyUsageType,
|
||||||
|
CertSubjectAlternativeNameType
|
||||||
|
} from "../certificate-common/certificate-constants";
|
||||||
import {
|
import {
|
||||||
extractAlgorithmsFromCSR,
|
extractAlgorithmsFromCSR,
|
||||||
extractCertificateRequestFromCSR
|
extractCertificateRequestFromCSR
|
||||||
@@ -68,8 +85,9 @@ import {
|
|||||||
} from "./certificate-v3-types";
|
} from "./certificate-v3-types";
|
||||||
|
|
||||||
type TCertificateV3ServiceFactoryDep = {
|
type TCertificateV3ServiceFactoryDep = {
|
||||||
certificateDAL: Pick<TCertificateDALFactory, "findOne" | "findById" | "updateById" | "transaction">;
|
certificateDAL: Pick<TCertificateDALFactory, "findOne" | "findById" | "updateById" | "transaction" | "create">;
|
||||||
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne">;
|
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
|
||||||
|
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne" | "create">;
|
||||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa">;
|
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa">;
|
||||||
certificateProfileDAL: Pick<TCertificateProfileDALFactory, "findByIdWithConfigs">;
|
certificateProfileDAL: Pick<TCertificateProfileDALFactory, "findByIdWithConfigs">;
|
||||||
acmeAccountDAL: Pick<TPkiAcmeAccountDALFactory, "findById">;
|
acmeAccountDAL: Pick<TPkiAcmeAccountDALFactory, "findById">;
|
||||||
@@ -85,6 +103,8 @@ type TCertificateV3ServiceFactoryDep = {
|
|||||||
>;
|
>;
|
||||||
pkiSyncDAL: Pick<TPkiSyncDALFactory, "find">;
|
pkiSyncDAL: Pick<TPkiSyncDALFactory, "find">;
|
||||||
pkiSyncQueue: Pick<TPkiSyncQueueFactory, "queuePkiSyncSyncCertificatesById">;
|
pkiSyncQueue: Pick<TPkiSyncQueueFactory, "queuePkiSyncSyncCertificatesById">;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey">;
|
||||||
|
projectDAL: TProjectDALFactory;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TCertificateV3ServiceFactory = ReturnType<typeof certificateV3ServiceFactory>;
|
export type TCertificateV3ServiceFactory = ReturnType<typeof certificateV3ServiceFactory>;
|
||||||
@@ -329,6 +349,158 @@ const parseTtlToDays = (ttl: string): number => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const generateSelfSignedCertificate = async ({
|
||||||
|
certificateRequest,
|
||||||
|
template,
|
||||||
|
effectiveSignatureAlgorithm,
|
||||||
|
effectiveKeyAlgorithm
|
||||||
|
}: {
|
||||||
|
certificateRequest: {
|
||||||
|
commonName?: string;
|
||||||
|
keyUsages?: CertKeyUsageType[];
|
||||||
|
extendedKeyUsages?: CertExtendedKeyUsageType[];
|
||||||
|
altNames?: Array<{
|
||||||
|
type: CertSubjectAlternativeNameType;
|
||||||
|
value: string;
|
||||||
|
}>;
|
||||||
|
validity: { ttl: string };
|
||||||
|
notBefore?: Date;
|
||||||
|
notAfter?: Date;
|
||||||
|
};
|
||||||
|
template?: {
|
||||||
|
subject?: Array<{
|
||||||
|
type: string;
|
||||||
|
allowed?: string[];
|
||||||
|
required?: string[];
|
||||||
|
denied?: string[];
|
||||||
|
}>;
|
||||||
|
sans?: Array<{
|
||||||
|
type: string;
|
||||||
|
allowed?: string[];
|
||||||
|
required?: string[];
|
||||||
|
denied?: string[];
|
||||||
|
}>;
|
||||||
|
} | null;
|
||||||
|
effectiveSignatureAlgorithm: CertSignatureAlgorithm;
|
||||||
|
effectiveKeyAlgorithm: CertKeyAlgorithm;
|
||||||
|
}): Promise<{
|
||||||
|
certificate: Buffer;
|
||||||
|
privateKey: Buffer;
|
||||||
|
serialNumber: string;
|
||||||
|
notBefore: Date;
|
||||||
|
notAfter: Date;
|
||||||
|
certificateSubject: Record<string, unknown>;
|
||||||
|
subjectAlternativeNames: Array<{
|
||||||
|
type: CertSubjectAlternativeNameType;
|
||||||
|
value: string;
|
||||||
|
}>;
|
||||||
|
}> => {
|
||||||
|
const certificateSubject = buildCertificateSubjectFromTemplate(certificateRequest, template?.subject);
|
||||||
|
const subjectAlternativeNames = buildSubjectAlternativeNamesFromTemplate(
|
||||||
|
{ subjectAlternativeNames: certificateRequest.altNames },
|
||||||
|
template?.sans
|
||||||
|
);
|
||||||
|
|
||||||
|
const keyGenAlg = keyAlgorithmToAlgCfg(effectiveKeyAlgorithm);
|
||||||
|
const keyPair = await crypto.nativeCrypto.subtle.generateKey(keyGenAlg, true, ["sign", "verify"]);
|
||||||
|
|
||||||
|
const signatureAlgorithmConfig = signatureAlgorithmToAlgCfg(effectiveSignatureAlgorithm, effectiveKeyAlgorithm);
|
||||||
|
|
||||||
|
const notBeforeDate = certificateRequest.notBefore ? new Date(certificateRequest.notBefore) : new Date();
|
||||||
|
|
||||||
|
let notAfterDate: Date;
|
||||||
|
if (certificateRequest.notAfter) {
|
||||||
|
notAfterDate = new Date(certificateRequest.notAfter);
|
||||||
|
} else if (certificateRequest.validity.ttl) {
|
||||||
|
notAfterDate = new Date(new Date().getTime() + ms(certificateRequest.validity.ttl));
|
||||||
|
} else {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Either notAfter date or TTL must be provided for certificate validity"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const serialNumber = createSerialNumber();
|
||||||
|
const dn = createDistinguishedName({
|
||||||
|
commonName: certificateSubject.common_name,
|
||||||
|
organization: certificateSubject.organization,
|
||||||
|
ou: certificateSubject.organizational_unit,
|
||||||
|
country: certificateSubject.country,
|
||||||
|
province: certificateSubject.state_or_province_name,
|
||||||
|
locality: certificateSubject.locality_name
|
||||||
|
});
|
||||||
|
|
||||||
|
const cert = await x509.X509CertificateGenerator.createSelfSigned({
|
||||||
|
name: dn,
|
||||||
|
serialNumber,
|
||||||
|
notBefore: notBeforeDate,
|
||||||
|
notAfter: notAfterDate,
|
||||||
|
signingAlgorithm: signatureAlgorithmConfig,
|
||||||
|
keys: keyPair,
|
||||||
|
extensions: [
|
||||||
|
new x509.BasicConstraintsExtension(false, undefined, false),
|
||||||
|
...(certificateRequest.keyUsages?.length
|
||||||
|
? [
|
||||||
|
new x509.KeyUsagesExtension(
|
||||||
|
(convertKeyUsageArrayToLegacy(certificateRequest.keyUsages) || []).reduce(
|
||||||
|
// eslint-disable-next-line no-bitwise
|
||||||
|
(acc: number, usage) => acc | x509.KeyUsageFlags[usage],
|
||||||
|
0
|
||||||
|
),
|
||||||
|
false
|
||||||
|
)
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
...(certificateRequest.extendedKeyUsages?.length
|
||||||
|
? [
|
||||||
|
new x509.ExtendedKeyUsageExtension(
|
||||||
|
(convertExtendedKeyUsageArrayToLegacy(certificateRequest.extendedKeyUsages) || []).map(
|
||||||
|
(eku) => x509.ExtendedKeyUsage[eku]
|
||||||
|
),
|
||||||
|
false
|
||||||
|
)
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
...(subjectAlternativeNames
|
||||||
|
? [
|
||||||
|
new x509.SubjectAlternativeNameExtension(
|
||||||
|
certificateRequest.altNames?.map((san) => {
|
||||||
|
switch (san.type) {
|
||||||
|
case CertSubjectAlternativeNameType.DNS_NAME:
|
||||||
|
return { type: "dns" as const, value: san.value };
|
||||||
|
case CertSubjectAlternativeNameType.IP_ADDRESS:
|
||||||
|
return { type: "ip" as const, value: san.value };
|
||||||
|
case CertSubjectAlternativeNameType.EMAIL:
|
||||||
|
return { type: "email" as const, value: san.value };
|
||||||
|
case CertSubjectAlternativeNameType.URI:
|
||||||
|
return { type: "url" as const, value: san.value };
|
||||||
|
default:
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Unsupported Subject Alternative Name type: ${san.type as string}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}) || [],
|
||||||
|
false
|
||||||
|
)
|
||||||
|
]
|
||||||
|
: [])
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
const certificatePem = cert.toString("pem");
|
||||||
|
const privateKeyObj = crypto.nativeCrypto.KeyObject.from(keyPair.privateKey);
|
||||||
|
const privateKeyPem = privateKeyObj.export({ format: "pem", type: "pkcs8" }) as string;
|
||||||
|
|
||||||
|
return {
|
||||||
|
certificate: Buffer.from(certificatePem),
|
||||||
|
privateKey: Buffer.from(privateKeyPem),
|
||||||
|
serialNumber,
|
||||||
|
notBefore: notBeforeDate,
|
||||||
|
notAfter: notAfterDate,
|
||||||
|
certificateSubject,
|
||||||
|
subjectAlternativeNames: certificateRequest.altNames || []
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const calculateFinalRenewBeforeDays = (
|
const calculateFinalRenewBeforeDays = (
|
||||||
profile: { apiConfig?: { autoRenew?: boolean; renewBeforeDays?: number } },
|
profile: { apiConfig?: { autoRenew?: boolean; renewBeforeDays?: number } },
|
||||||
ttl: string,
|
ttl: string,
|
||||||
@@ -348,8 +520,248 @@ const calculateFinalRenewBeforeDays = (
|
|||||||
return isValidRenewalTiming(renewBeforeDays, certificateExpiryDate) ? renewBeforeDays : undefined;
|
return isValidRenewalTiming(renewBeforeDays, certificateExpiryDate) ? renewBeforeDays : undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getEffectiveAlgorithms = (
|
||||||
|
requestSignatureAlgorithm?: CertSignatureAlgorithm,
|
||||||
|
requestKeyAlgorithm?: CertKeyAlgorithm,
|
||||||
|
originalSignatureAlgorithm?: CertSignatureAlgorithm,
|
||||||
|
originalKeyAlgorithm?: CertKeyAlgorithm
|
||||||
|
) => {
|
||||||
|
return {
|
||||||
|
signatureAlgorithm: requestSignatureAlgorithm || originalSignatureAlgorithm || CertSignatureAlgorithm.RSA_SHA256,
|
||||||
|
keyAlgorithm: requestKeyAlgorithm || originalKeyAlgorithm || CertKeyAlgorithm.RSA_2048
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const createSelfSignedCertificateRecord = async ({
|
||||||
|
selfSignedResult,
|
||||||
|
certificateRequest,
|
||||||
|
profile,
|
||||||
|
originalCert,
|
||||||
|
certificateDAL,
|
||||||
|
tx,
|
||||||
|
isRenewal = false
|
||||||
|
}: {
|
||||||
|
selfSignedResult: Awaited<ReturnType<typeof generateSelfSignedCertificate>>;
|
||||||
|
certificateRequest: {
|
||||||
|
commonName?: string;
|
||||||
|
keyUsages?: CertKeyUsageType[];
|
||||||
|
extendedKeyUsages?: CertExtendedKeyUsageType[];
|
||||||
|
};
|
||||||
|
profile?: { id: string; projectId: string } | null;
|
||||||
|
originalCert?: {
|
||||||
|
id: string;
|
||||||
|
friendlyName?: string | null;
|
||||||
|
commonName?: string | null;
|
||||||
|
projectId: string;
|
||||||
|
};
|
||||||
|
certificateDAL: Pick<TCertificateDALFactory, "create" | "updateById">;
|
||||||
|
tx: Parameters<TCertificateDALFactory["create"]>[1];
|
||||||
|
isRenewal?: boolean;
|
||||||
|
}) => {
|
||||||
|
const subjectCommonName =
|
||||||
|
(selfSignedResult.certificateSubject.common_name as string) ||
|
||||||
|
certificateRequest.commonName ||
|
||||||
|
originalCert?.commonName ||
|
||||||
|
"";
|
||||||
|
|
||||||
|
const altNamesList = selfSignedResult.subjectAlternativeNames.map((san) => san.value).join(",");
|
||||||
|
|
||||||
|
const projectId = originalCert?.projectId || profile?.projectId;
|
||||||
|
if (!projectId) {
|
||||||
|
throw new BadRequestError({ message: "Project ID is required for certificate creation" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseRecord = {
|
||||||
|
serialNumber: selfSignedResult.serialNumber,
|
||||||
|
friendlyName: originalCert?.friendlyName || subjectCommonName,
|
||||||
|
commonName: subjectCommonName,
|
||||||
|
altNames: altNamesList,
|
||||||
|
status: CertStatus.ACTIVE,
|
||||||
|
notBefore: selfSignedResult.notBefore,
|
||||||
|
notAfter: selfSignedResult.notAfter,
|
||||||
|
projectId,
|
||||||
|
keyUsages: convertKeyUsageArrayToLegacy(certificateRequest.keyUsages) || [],
|
||||||
|
extendedKeyUsages: convertExtendedKeyUsageArrayToLegacy(certificateRequest.extendedKeyUsages) || [],
|
||||||
|
profileId: profile?.id || null
|
||||||
|
};
|
||||||
|
|
||||||
|
const renewalRecord =
|
||||||
|
isRenewal && originalCert
|
||||||
|
? {
|
||||||
|
renewedFromCertificateId: originalCert.id
|
||||||
|
}
|
||||||
|
: {};
|
||||||
|
|
||||||
|
return certificateDAL.create(
|
||||||
|
{
|
||||||
|
...baseRecord,
|
||||||
|
...renewalRecord
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const createEncryptedCertificateData = async ({
|
||||||
|
certificateId,
|
||||||
|
certificate,
|
||||||
|
privateKey,
|
||||||
|
projectId,
|
||||||
|
certificateBodyDAL,
|
||||||
|
certificateSecretDAL,
|
||||||
|
kmsService,
|
||||||
|
projectDAL,
|
||||||
|
tx
|
||||||
|
}: {
|
||||||
|
certificateId: string;
|
||||||
|
certificate: Buffer;
|
||||||
|
privateKey: Buffer;
|
||||||
|
projectId: string;
|
||||||
|
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
|
||||||
|
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "create">;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "encryptWithKmsKey" | "generateKmsKey">;
|
||||||
|
projectDAL: TProjectDALFactory;
|
||||||
|
tx: Parameters<TCertificateBodyDALFactory["create"]>[1];
|
||||||
|
}) => {
|
||||||
|
const certificateManagerKeyId = await getProjectKmsCertificateKeyId({
|
||||||
|
projectId,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
const kmsEncryptor = await kmsService.encryptWithKmsKey({ kmsId: certificateManagerKeyId });
|
||||||
|
|
||||||
|
const encryptedCertificate = await kmsEncryptor({
|
||||||
|
plainText: certificate
|
||||||
|
});
|
||||||
|
|
||||||
|
await certificateBodyDAL.create(
|
||||||
|
{
|
||||||
|
certId: certificateId,
|
||||||
|
encryptedCertificate: encryptedCertificate.cipherTextBlob
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
const encryptedPrivateKey = await kmsEncryptor({
|
||||||
|
plainText: privateKey
|
||||||
|
});
|
||||||
|
|
||||||
|
await certificateSecretDAL.create(
|
||||||
|
{
|
||||||
|
certId: certificateId,
|
||||||
|
encryptedPrivateKey: encryptedPrivateKey.cipherTextBlob
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const processSelfSignedCertificate = async ({
|
||||||
|
certificateRequest,
|
||||||
|
template,
|
||||||
|
profile,
|
||||||
|
originalCert,
|
||||||
|
effectiveAlgorithms,
|
||||||
|
certificateDAL,
|
||||||
|
certificateBodyDAL,
|
||||||
|
certificateSecretDAL,
|
||||||
|
kmsService,
|
||||||
|
projectDAL,
|
||||||
|
tx,
|
||||||
|
isRenewal = false
|
||||||
|
}: {
|
||||||
|
certificateRequest: {
|
||||||
|
commonName?: string;
|
||||||
|
keyUsages?: CertKeyUsageType[];
|
||||||
|
extendedKeyUsages?: CertExtendedKeyUsageType[];
|
||||||
|
validity: { ttl: string };
|
||||||
|
notBefore?: Date;
|
||||||
|
notAfter?: Date;
|
||||||
|
};
|
||||||
|
template?: {
|
||||||
|
subject?: Array<{
|
||||||
|
type: string;
|
||||||
|
allowed?: string[];
|
||||||
|
required?: string[];
|
||||||
|
denied?: string[];
|
||||||
|
}>;
|
||||||
|
sans?: Array<{
|
||||||
|
type: string;
|
||||||
|
allowed?: string[];
|
||||||
|
required?: string[];
|
||||||
|
denied?: string[];
|
||||||
|
}>;
|
||||||
|
} | null;
|
||||||
|
profile?: { id: string; projectId: string } | null;
|
||||||
|
originalCert?: {
|
||||||
|
id: string;
|
||||||
|
friendlyName?: string | null;
|
||||||
|
commonName?: string | null;
|
||||||
|
projectId: string;
|
||||||
|
};
|
||||||
|
effectiveAlgorithms: {
|
||||||
|
signatureAlgorithm: CertSignatureAlgorithm;
|
||||||
|
keyAlgorithm: CertKeyAlgorithm;
|
||||||
|
};
|
||||||
|
certificateDAL: Pick<TCertificateDALFactory, "create" | "updateById">;
|
||||||
|
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
|
||||||
|
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "create">;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "encryptWithKmsKey" | "generateKmsKey">;
|
||||||
|
projectDAL: TProjectDALFactory;
|
||||||
|
tx: Parameters<TCertificateDALFactory["create"]>[1];
|
||||||
|
isRenewal?: boolean;
|
||||||
|
}) => {
|
||||||
|
const projectId = originalCert?.projectId || profile?.projectId;
|
||||||
|
if (!projectId) {
|
||||||
|
throw new BadRequestError({ message: "Project ID is required for certificate creation" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const selfSignedResult = await generateSelfSignedCertificate({
|
||||||
|
certificateRequest,
|
||||||
|
template,
|
||||||
|
effectiveSignatureAlgorithm: effectiveAlgorithms.signatureAlgorithm,
|
||||||
|
effectiveKeyAlgorithm: effectiveAlgorithms.keyAlgorithm
|
||||||
|
});
|
||||||
|
|
||||||
|
const certificateData = await createSelfSignedCertificateRecord({
|
||||||
|
selfSignedResult,
|
||||||
|
certificateRequest,
|
||||||
|
profile,
|
||||||
|
originalCert,
|
||||||
|
certificateDAL,
|
||||||
|
tx,
|
||||||
|
isRenewal
|
||||||
|
});
|
||||||
|
|
||||||
|
await certificateDAL.updateById(
|
||||||
|
certificateData.id,
|
||||||
|
{
|
||||||
|
signatureAlgorithm: effectiveAlgorithms.signatureAlgorithm,
|
||||||
|
keyAlgorithm: effectiveAlgorithms.keyAlgorithm
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
await createEncryptedCertificateData({
|
||||||
|
certificateId: certificateData.id,
|
||||||
|
certificate: selfSignedResult.certificate,
|
||||||
|
privateKey: selfSignedResult.privateKey,
|
||||||
|
projectId,
|
||||||
|
certificateBodyDAL,
|
||||||
|
certificateSecretDAL,
|
||||||
|
kmsService,
|
||||||
|
projectDAL,
|
||||||
|
tx
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
selfSignedResult,
|
||||||
|
certificateData
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const certificateV3ServiceFactory = ({
|
export const certificateV3ServiceFactory = ({
|
||||||
certificateDAL,
|
certificateDAL,
|
||||||
|
certificateBodyDAL,
|
||||||
certificateSecretDAL,
|
certificateSecretDAL,
|
||||||
certificateAuthorityDAL,
|
certificateAuthorityDAL,
|
||||||
certificateProfileDAL,
|
certificateProfileDAL,
|
||||||
@@ -359,7 +771,9 @@ export const certificateV3ServiceFactory = ({
|
|||||||
permissionService,
|
permissionService,
|
||||||
certificateSyncDAL,
|
certificateSyncDAL,
|
||||||
pkiSyncDAL,
|
pkiSyncDAL,
|
||||||
pkiSyncQueue
|
pkiSyncQueue,
|
||||||
|
kmsService,
|
||||||
|
projectDAL
|
||||||
}: TCertificateV3ServiceFactoryDep) => {
|
}: TCertificateV3ServiceFactoryDep) => {
|
||||||
const issueCertificateFromProfile = async ({
|
const issueCertificateFromProfile = async ({
|
||||||
profileId,
|
profileId,
|
||||||
@@ -416,15 +830,6 @@ export const certificateV3ServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(profile.caId);
|
|
||||||
if (!ca) {
|
|
||||||
throw new NotFoundError({ message: "Certificate Authority not found" });
|
|
||||||
}
|
|
||||||
|
|
||||||
validateCaSupport(ca, "direct certificate issuance");
|
|
||||||
|
|
||||||
validateAlgorithmCompatibility(ca, template);
|
|
||||||
|
|
||||||
const effectiveSignatureAlgorithm = certificateRequest.signatureAlgorithm as CertSignatureAlgorithm | undefined;
|
const effectiveSignatureAlgorithm = certificateRequest.signatureAlgorithm as CertSignatureAlgorithm | undefined;
|
||||||
const effectiveKeyAlgorithm = certificateRequest.keyAlgorithm as CertKeyAlgorithm | undefined;
|
const effectiveKeyAlgorithm = certificateRequest.keyAlgorithm as CertKeyAlgorithm | undefined;
|
||||||
|
|
||||||
@@ -440,12 +845,76 @@ export const certificateV3ServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const certificateSubject = buildCertificateSubjectFromTemplate(certificateRequest, template.subject);
|
const certificateSubject = buildCertificateSubjectFromTemplate(certificateRequest, template?.subject);
|
||||||
const subjectAlternativeNames = buildSubjectAlternativeNamesFromTemplate(
|
const subjectAlternativeNames = buildSubjectAlternativeNamesFromTemplate(
|
||||||
{ subjectAlternativeNames: certificateRequest.altNames },
|
{ subjectAlternativeNames: certificateRequest.altNames },
|
||||||
template.sans
|
template?.sans
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const issuerType = profile?.issuerType || (profile?.caId ? IssuerType.CA : IssuerType.SELF_SIGNED);
|
||||||
|
|
||||||
|
if (issuerType === IssuerType.SELF_SIGNED) {
|
||||||
|
const result = await certificateDAL.transaction(async (tx) => {
|
||||||
|
const effectiveAlgorithms = getEffectiveAlgorithms(effectiveSignatureAlgorithm, effectiveKeyAlgorithm);
|
||||||
|
|
||||||
|
return processSelfSignedCertificate({
|
||||||
|
certificateRequest,
|
||||||
|
template,
|
||||||
|
profile,
|
||||||
|
effectiveAlgorithms,
|
||||||
|
certificateDAL,
|
||||||
|
certificateBodyDAL,
|
||||||
|
certificateSecretDAL,
|
||||||
|
kmsService,
|
||||||
|
projectDAL,
|
||||||
|
tx
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const { selfSignedResult, certificateData } = result;
|
||||||
|
|
||||||
|
const subjectCommonName =
|
||||||
|
(selfSignedResult.certificateSubject.common_name as string) ||
|
||||||
|
certificateRequest.commonName ||
|
||||||
|
"Self-signed Certificate";
|
||||||
|
|
||||||
|
const finalRenewBeforeDays = calculateFinalRenewBeforeDays(
|
||||||
|
profile,
|
||||||
|
certificateRequest.validity.ttl,
|
||||||
|
selfSignedResult.notAfter
|
||||||
|
);
|
||||||
|
|
||||||
|
if (finalRenewBeforeDays !== undefined) {
|
||||||
|
await certificateDAL.updateById(certificateData.id, {
|
||||||
|
renewBeforeDays: finalRenewBeforeDays
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
certificate: selfSignedResult.certificate.toString("utf8"),
|
||||||
|
issuingCaCertificate: "",
|
||||||
|
certificateChain: selfSignedResult.certificate.toString("utf8"),
|
||||||
|
privateKey: selfSignedResult.privateKey.toString("utf8"),
|
||||||
|
serialNumber: selfSignedResult.serialNumber,
|
||||||
|
certificateId: certificateData.id,
|
||||||
|
projectId: profile.projectId,
|
||||||
|
profileName: profile.slug,
|
||||||
|
commonName: subjectCommonName
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!profile.caId) {
|
||||||
|
throw new NotFoundError({ message: "Certificate Authority ID not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(profile.caId);
|
||||||
|
if (!ca) {
|
||||||
|
throw new NotFoundError({ message: "Certificate Authority not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
validateCaSupport(ca, "direct certificate issuance");
|
||||||
|
validateAlgorithmCompatibility(ca, template);
|
||||||
|
|
||||||
const { certificate, certificateChain, issuingCaCertificate, privateKey, serialNumber } =
|
const { certificate, certificateChain, issuingCaCertificate, privateKey, serialNumber } =
|
||||||
await internalCaService.issueCertFromCa({
|
await internalCaService.issueCertFromCa({
|
||||||
caId: ca.id,
|
caId: ca.id,
|
||||||
@@ -477,10 +946,11 @@ export const certificateV3ServiceFactory = ({
|
|||||||
new Date(cert.notAfter)
|
new Date(cert.notAfter)
|
||||||
);
|
);
|
||||||
|
|
||||||
await certificateDAL.updateById(cert.id, {
|
const updateData: { profileId: string; renewBeforeDays?: number } = { profileId };
|
||||||
profileId,
|
if (finalRenewBeforeDays !== undefined) {
|
||||||
renewBeforeDays: finalRenewBeforeDays
|
updateData.renewBeforeDays = finalRenewBeforeDays;
|
||||||
});
|
}
|
||||||
|
await certificateDAL.updateById(cert.id, updateData);
|
||||||
|
|
||||||
let finalCertificateChain = bufferToString(certificateChain);
|
let finalCertificateChain = bufferToString(certificateChain);
|
||||||
if (removeRootsFromChain) {
|
if (removeRootsFromChain) {
|
||||||
@@ -525,6 +995,12 @@ export const certificateV3ServiceFactory = ({
|
|||||||
enrollmentType
|
enrollmentType
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!profile.caId) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Self-signed certificates are not supported for CSR signing"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(profile.caId);
|
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(profile.caId);
|
||||||
if (!ca) {
|
if (!ca) {
|
||||||
throw new NotFoundError({ message: "Certificate Authority not found" });
|
throw new NotFoundError({ message: "Certificate Authority not found" });
|
||||||
@@ -592,10 +1068,11 @@ export const certificateV3ServiceFactory = ({
|
|||||||
|
|
||||||
const finalRenewBeforeDays = calculateFinalRenewBeforeDays(profile, validity.ttl, new Date(cert.notAfter));
|
const finalRenewBeforeDays = calculateFinalRenewBeforeDays(profile, validity.ttl, new Date(cert.notAfter));
|
||||||
|
|
||||||
await certificateDAL.updateById(cert.id, {
|
const updateData2: { profileId: string; renewBeforeDays?: number } = { profileId };
|
||||||
profileId,
|
if (finalRenewBeforeDays !== undefined) {
|
||||||
renewBeforeDays: finalRenewBeforeDays
|
updateData2.renewBeforeDays = finalRenewBeforeDays;
|
||||||
});
|
}
|
||||||
|
await certificateDAL.updateById(cert.id, updateData2);
|
||||||
|
|
||||||
const certificateString = extractCertificateFromBuffer(certificate as unknown as Buffer);
|
const certificateString = extractCertificateFromBuffer(certificate as unknown as Buffer);
|
||||||
let certificateChainString = extractCertificateFromBuffer(certificateChain as unknown as Buffer);
|
let certificateChainString = extractCertificateFromBuffer(certificateChain as unknown as Buffer);
|
||||||
@@ -640,10 +1117,25 @@ export const certificateV3ServiceFactory = ({
|
|||||||
commonName: certificateOrder.commonName,
|
commonName: certificateOrder.commonName,
|
||||||
keyUsages: certificateOrder.keyUsages,
|
keyUsages: certificateOrder.keyUsages,
|
||||||
extendedKeyUsages: certificateOrder.extendedKeyUsages,
|
extendedKeyUsages: certificateOrder.extendedKeyUsages,
|
||||||
subjectAlternativeNames: certificateOrder.altNames.map((san) => ({
|
subjectAlternativeNames: certificateOrder.altNames.map((san) => {
|
||||||
type: san.type === "dns" ? CertSubjectAlternativeNameType.DNS_NAME : CertSubjectAlternativeNameType.IP_ADDRESS,
|
let certType: CertSubjectAlternativeNameType;
|
||||||
value: san.value
|
switch (san.type) {
|
||||||
})),
|
case "dns":
|
||||||
|
certType = CertSubjectAlternativeNameType.DNS_NAME;
|
||||||
|
break;
|
||||||
|
case "ip":
|
||||||
|
certType = CertSubjectAlternativeNameType.IP_ADDRESS;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Unsupported Subject Alternative Name type: ${san.type as string}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type: certType,
|
||||||
|
value: san.value
|
||||||
|
};
|
||||||
|
}),
|
||||||
validity: certificateOrder.validity,
|
validity: certificateOrder.validity,
|
||||||
notBefore: certificateOrder.notBefore,
|
notBefore: certificateOrder.notBefore,
|
||||||
notAfter: certificateOrder.notAfter,
|
notAfter: certificateOrder.notAfter,
|
||||||
@@ -663,6 +1155,12 @@ export const certificateV3ServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!profile.caId) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Self-signed certificates are not supported for certificate ordering"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(profile.caId);
|
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(profile.caId);
|
||||||
if (!ca) {
|
if (!ca) {
|
||||||
throw new NotFoundError({ message: "Certificate Authority not found" });
|
throw new NotFoundError({ message: "Certificate Authority not found" });
|
||||||
@@ -741,15 +1239,19 @@ export const certificateV3ServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const profile = await certificateProfileDAL.findByIdWithConfigs(originalCert.profileId);
|
let profile = null;
|
||||||
if (!profile) {
|
if (originalCert.profileId) {
|
||||||
throw new NotFoundError({ message: "Certificate profile not found" });
|
profile = await certificateProfileDAL.findByIdWithConfigs(originalCert.profileId);
|
||||||
}
|
if (!profile) {
|
||||||
|
throw new NotFoundError({ message: "Certificate profile not found" });
|
||||||
|
}
|
||||||
|
|
||||||
if (profile.enrollmentType !== EnrollmentType.API) {
|
if (profile.enrollmentType !== EnrollmentType.API) {
|
||||||
throw new ForbiddenRequestError({
|
throw new ForbiddenRequestError({
|
||||||
message: "Certificate is not eligible for renewal: EST certificates cannot be renewed through this endpoint"
|
message:
|
||||||
});
|
"Certificate is not eligible for renewal: Only certificates issued from an API enrollment profile can be renewed through this endpoint"
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const certificateSecret = await certificateSecretDAL.findOne({ certId: originalCert.id }, tx);
|
const certificateSecret = await certificateSecretDAL.findOne({ certId: originalCert.id }, tx);
|
||||||
@@ -761,10 +1263,11 @@ export const certificateV3ServiceFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!internal) {
|
if (!internal) {
|
||||||
|
const projectId = profile?.projectId || originalCert.projectId;
|
||||||
const { permission } = await permissionService.getProjectPermission({
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
projectId: profile.projectId,
|
projectId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
actionProjectType: ActionProjectType.CertificateManager
|
actionProjectType: ActionProjectType.CertificateManager
|
||||||
@@ -776,33 +1279,46 @@ export const certificateV3ServiceFactory = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(profile.caId);
|
const issuerType = profile?.issuerType || (originalCert.caId ? IssuerType.CA : IssuerType.SELF_SIGNED);
|
||||||
if (!ca) {
|
|
||||||
throw new NotFoundError({ message: "Certificate Authority not found" });
|
let ca;
|
||||||
|
if (issuerType === IssuerType.CA) {
|
||||||
|
const caId = profile?.caId || originalCert.caId;
|
||||||
|
if (!caId) {
|
||||||
|
throw new NotFoundError({ message: "Certificate Authority ID not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId);
|
||||||
|
if (!ca) {
|
||||||
|
throw new NotFoundError({ message: "Certificate Authority not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const eligibilityCheck = validateRenewalEligibility(originalCert, ca);
|
||||||
|
if (!eligibilityCheck.isEligible) {
|
||||||
|
await certificateDAL.updateById(originalCert.id, {
|
||||||
|
renewalError: `Certificate is not eligible for renewal: ${eligibilityCheck.errors.join(", ")}`
|
||||||
|
});
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Certificate is not eligible for renewal: ${eligibilityCheck.errors.join(", ")}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
validateCaSupport(ca, "direct certificate issuance");
|
||||||
}
|
}
|
||||||
|
|
||||||
const eligibilityCheck = validateRenewalEligibility(originalCert, ca);
|
const templateId = profile?.certificateTemplateId || originalCert.certificateTemplateId;
|
||||||
if (!eligibilityCheck.isEligible) {
|
const template = templateId
|
||||||
await certificateDAL.updateById(originalCert.id, {
|
? await certificateTemplateV2Service.getTemplateV2ById({
|
||||||
renewalError: `Certificate is not eligible for renewal: ${eligibilityCheck.errors.join(", ")}`
|
actor,
|
||||||
});
|
actorId,
|
||||||
throw new BadRequestError({
|
actorAuthMethod,
|
||||||
message: `Certificate is not eligible for renewal: ${eligibilityCheck.errors.join(", ")}`
|
actorOrgId,
|
||||||
});
|
templateId,
|
||||||
}
|
internal
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
|
||||||
validateCaSupport(ca, "direct certificate issuance");
|
if (!template && profile) {
|
||||||
|
|
||||||
const template = await certificateTemplateV2Service.getTemplateV2ById({
|
|
||||||
actor,
|
|
||||||
actorId,
|
|
||||||
actorAuthMethod,
|
|
||||||
actorOrgId,
|
|
||||||
templateId: profile.certificateTemplateId,
|
|
||||||
internal
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!template) {
|
|
||||||
throw new NotFoundError({ message: "Certificate template not found for this profile" });
|
throw new NotFoundError({ message: "Certificate template not found for this profile" });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -857,10 +1373,13 @@ export const certificateV3ServiceFactory = ({
|
|||||||
keyAlgorithm: originalCert.keyAlgorithm || undefined
|
keyAlgorithm: originalCert.keyAlgorithm || undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
const validationResult = await certificateTemplateV2Service.validateCertificateRequest(
|
let validationResult: { isValid: boolean; errors: string[] } = { isValid: true, errors: [] };
|
||||||
profile.certificateTemplateId,
|
if (profile?.certificateTemplateId) {
|
||||||
certificateRequest
|
validationResult = await certificateTemplateV2Service.validateCertificateRequest(
|
||||||
);
|
profile.certificateTemplateId,
|
||||||
|
certificateRequest
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!validationResult.isValid) {
|
if (!validationResult.isValid) {
|
||||||
await certificateDAL.updateById(originalCert.id, {
|
await certificateDAL.updateById(originalCert.id, {
|
||||||
@@ -872,14 +1391,28 @@ export const certificateV3ServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
validateAlgorithmCompatibility(ca, template);
|
|
||||||
const notBefore = new Date();
|
const notBefore = new Date();
|
||||||
const notAfter = new Date(Date.now() + parseTtlToDays(ttl) * 24 * 60 * 60 * 1000);
|
const notAfter = new Date(Date.now() + parseTtlToDays(ttl) * 24 * 60 * 60 * 1000);
|
||||||
|
|
||||||
const finalRenewBeforeDays = calculateFinalRenewBeforeDays(profile, ttl, notAfter);
|
const finalRenewBeforeDays = profile ? calculateFinalRenewBeforeDays(profile, ttl, notAfter) : undefined;
|
||||||
|
|
||||||
const { certificate, certificateChain, issuingCaCertificate, serialNumber } =
|
let certificate: string;
|
||||||
await internalCaService.issueCertFromCa({
|
let certificateChain: string;
|
||||||
|
let issuingCaCertificate: string;
|
||||||
|
let serialNumber: string;
|
||||||
|
let newCert: TCertificates;
|
||||||
|
|
||||||
|
if (issuerType === IssuerType.CA) {
|
||||||
|
// CA-signed certificate renewal
|
||||||
|
if (!ca) {
|
||||||
|
throw new NotFoundError({ message: "Certificate Authority not found for CA-signed certificate renewal" });
|
||||||
|
}
|
||||||
|
|
||||||
|
validateAlgorithmCompatibility(ca, {
|
||||||
|
algorithms: template?.algorithms
|
||||||
|
} as { algorithms?: { signature?: string[] } });
|
||||||
|
|
||||||
|
const caResult = await internalCaService.issueCertFromCa({
|
||||||
caId: ca.id,
|
caId: ca.id,
|
||||||
friendlyName: originalCert.friendlyName || originalCert.commonName || "Renewed Certificate",
|
friendlyName: originalCert.friendlyName || originalCert.commonName || "Renewed Certificate",
|
||||||
commonName: originalCert.commonName || "",
|
commonName: originalCert.commonName || "",
|
||||||
@@ -900,20 +1433,72 @@ export const certificateV3ServiceFactory = ({
|
|||||||
tx
|
tx
|
||||||
});
|
});
|
||||||
|
|
||||||
const newCert = await certificateDAL.findOne({ serialNumber, caId: ca.id }, tx);
|
certificate = caResult.certificate;
|
||||||
|
certificateChain = caResult.certificateChain;
|
||||||
|
issuingCaCertificate = caResult.issuingCaCertificate;
|
||||||
|
serialNumber = caResult.serialNumber;
|
||||||
|
|
||||||
|
const foundCert = await certificateDAL.findOne({ serialNumber, caId: ca.id }, tx);
|
||||||
|
if (!foundCert) {
|
||||||
|
throw new NotFoundError({ message: "Certificate was signed but could not be found in database" });
|
||||||
|
}
|
||||||
|
newCert = foundCert;
|
||||||
|
} else {
|
||||||
|
// Self-signed certificate renewal
|
||||||
|
const effectiveAlgorithms = getEffectiveAlgorithms(
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
originalSignatureAlgorithm,
|
||||||
|
originalKeyAlgorithm
|
||||||
|
);
|
||||||
|
|
||||||
|
const selfSignedRenewalResult = await processSelfSignedCertificate({
|
||||||
|
certificateRequest,
|
||||||
|
template,
|
||||||
|
profile,
|
||||||
|
originalCert,
|
||||||
|
effectiveAlgorithms,
|
||||||
|
certificateDAL,
|
||||||
|
certificateBodyDAL,
|
||||||
|
certificateSecretDAL,
|
||||||
|
kmsService,
|
||||||
|
projectDAL,
|
||||||
|
tx,
|
||||||
|
isRenewal: true
|
||||||
|
});
|
||||||
|
|
||||||
|
certificate = selfSignedRenewalResult.selfSignedResult.certificate.toString("utf8");
|
||||||
|
certificateChain = selfSignedRenewalResult.selfSignedResult.certificate.toString("utf8"); // Self-signed has no chain
|
||||||
|
issuingCaCertificate = ""; // No issuing CA for self-signed
|
||||||
|
serialNumber = selfSignedRenewalResult.selfSignedResult.serialNumber;
|
||||||
|
newCert = selfSignedRenewalResult.certificateData;
|
||||||
|
}
|
||||||
|
|
||||||
if (!newCert) {
|
if (!newCert) {
|
||||||
throw new NotFoundError({ message: "Certificate was signed but could not be found in database" });
|
throw new NotFoundError({ message: "Certificate was signed but could not be found in database" });
|
||||||
}
|
}
|
||||||
|
|
||||||
await certificateDAL.updateById(
|
// For self-signed certificates, we already set the renewal data during creation
|
||||||
newCert.id,
|
// For CA-signed certificates, we need to set it now
|
||||||
{
|
if (issuerType === IssuerType.CA) {
|
||||||
profileId: originalCert.profileId,
|
const renewalUpdateData: {
|
||||||
renewBeforeDays: finalRenewBeforeDays,
|
profileId: string | null;
|
||||||
|
renewedFromCertificateId: string;
|
||||||
|
renewBeforeDays?: number;
|
||||||
|
} = {
|
||||||
|
profileId: originalCert.profileId || null,
|
||||||
renewedFromCertificateId: originalCert.id
|
renewedFromCertificateId: originalCert.id
|
||||||
},
|
};
|
||||||
tx
|
|
||||||
);
|
if (finalRenewBeforeDays !== undefined) {
|
||||||
|
renewalUpdateData.renewBeforeDays = finalRenewBeforeDays;
|
||||||
|
}
|
||||||
|
|
||||||
|
await certificateDAL.updateById(newCert.id, renewalUpdateData, tx);
|
||||||
|
} else if (finalRenewBeforeDays !== undefined) {
|
||||||
|
// For self-signed certificates, just update the renewBeforeDays if needed
|
||||||
|
await certificateDAL.updateById(newCert.id, { renewBeforeDays: finalRenewBeforeDays }, tx);
|
||||||
|
}
|
||||||
|
|
||||||
await certificateDAL.updateById(
|
await certificateDAL.updateById(
|
||||||
originalCert.id,
|
originalCert.id,
|
||||||
@@ -953,8 +1538,8 @@ export const certificateV3ServiceFactory = ({
|
|||||||
certificateChain: finalCertificateChain,
|
certificateChain: finalCertificateChain,
|
||||||
serialNumber: renewalResult.serialNumber,
|
serialNumber: renewalResult.serialNumber,
|
||||||
certificateId: renewalResult.newCert.id,
|
certificateId: renewalResult.newCert.id,
|
||||||
projectId: renewalResult.profile.projectId,
|
projectId: renewalResult.originalCert.projectId,
|
||||||
profileName: renewalResult.profile.slug,
|
profileName: renewalResult.profile?.slug || "Self-signed Certificate",
|
||||||
commonName: renewalResult.originalCert.commonName || ""
|
commonName: renewalResult.originalCert.commonName || ""
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -309,6 +309,14 @@ export const certificateServiceFactory = ({
|
|||||||
|
|
||||||
const certBody = await certificateBodyDAL.findOne({ certId: cert.id });
|
const certBody = await certificateBodyDAL.findOne({ certId: cert.id });
|
||||||
|
|
||||||
|
if (!certBody) {
|
||||||
|
throw new NotFoundError({ message: "Certificate body not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!certBody.encryptedCertificate) {
|
||||||
|
throw new BadRequestError({ message: "Certificate data not available" });
|
||||||
|
}
|
||||||
|
|
||||||
const certificateManagerKeyId = await getProjectKmsCertificateKeyId({
|
const certificateManagerKeyId = await getProjectKmsCertificateKeyId({
|
||||||
projectId: cert.projectId,
|
projectId: cert.projectId,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
@@ -599,6 +607,14 @@ export const certificateServiceFactory = ({
|
|||||||
|
|
||||||
const certBody = await certificateBodyDAL.findOne({ certId: cert.id });
|
const certBody = await certificateBodyDAL.findOne({ certId: cert.id });
|
||||||
|
|
||||||
|
if (!certBody) {
|
||||||
|
throw new NotFoundError({ message: "Certificate body not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!certBody.encryptedCertificate) {
|
||||||
|
throw new BadRequestError({ message: "Certificate data not available" });
|
||||||
|
}
|
||||||
|
|
||||||
const certificateManagerKeyId = await getProjectKmsCertificateKeyId({
|
const certificateManagerKeyId = await getProjectKmsCertificateKeyId({
|
||||||
projectId: cert.projectId,
|
projectId: cert.projectId,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
|
|||||||
@@ -10,4 +10,4 @@ export {
|
|||||||
useGetProfileCertificates,
|
useGetProfileCertificates,
|
||||||
useListCertificateProfiles
|
useListCertificateProfiles
|
||||||
} from "./queries";
|
} from "./queries";
|
||||||
export type * from "./types";
|
export * from "./types";
|
||||||
|
|||||||
@@ -1,11 +1,23 @@
|
|||||||
|
export enum EnrollmentType {
|
||||||
|
API = "api",
|
||||||
|
EST = "est",
|
||||||
|
ACME = "acme"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum IssuerType {
|
||||||
|
CA = "ca",
|
||||||
|
SELF_SIGNED = "self-signed"
|
||||||
|
}
|
||||||
|
|
||||||
export type TCertificateProfile = {
|
export type TCertificateProfile = {
|
||||||
id: string;
|
id: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
caId: string;
|
caId: string | null;
|
||||||
certificateTemplateId: string;
|
certificateTemplateId: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
enrollmentType: "api" | "est" | "acme";
|
enrollmentType: EnrollmentType;
|
||||||
|
issuerType: IssuerType;
|
||||||
estConfigId?: string;
|
estConfigId?: string;
|
||||||
apiConfigId?: string;
|
apiConfigId?: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
@@ -44,11 +56,12 @@ export type TCertificateProfileWithDetails = TCertificateProfile & {
|
|||||||
|
|
||||||
export type TCreateCertificateProfileDTO = {
|
export type TCreateCertificateProfileDTO = {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
caId: string;
|
caId?: string;
|
||||||
certificateTemplateId: string;
|
certificateTemplateId: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
enrollmentType: "api" | "est" | "acme";
|
enrollmentType: EnrollmentType;
|
||||||
|
issuerType: IssuerType;
|
||||||
estConfig?: {
|
estConfig?: {
|
||||||
disableBootstrapCaValidation?: boolean;
|
disableBootstrapCaValidation?: boolean;
|
||||||
passphrase: string;
|
passphrase: string;
|
||||||
@@ -65,6 +78,8 @@ export type TUpdateCertificateProfileDTO = {
|
|||||||
profileId: string;
|
profileId: string;
|
||||||
slug?: string;
|
slug?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
enrollmentType?: EnrollmentType;
|
||||||
|
issuerType?: IssuerType;
|
||||||
estConfig?: {
|
estConfig?: {
|
||||||
disableBootstrapCaValidation?: boolean;
|
disableBootstrapCaValidation?: boolean;
|
||||||
passphrase?: string;
|
passphrase?: string;
|
||||||
@@ -87,7 +102,9 @@ export type TListCertificateProfilesDTO = {
|
|||||||
offset?: number;
|
offset?: number;
|
||||||
search?: string;
|
search?: string;
|
||||||
includeConfigs?: boolean;
|
includeConfigs?: boolean;
|
||||||
enrollmentType?: "api" | "est" | "acme";
|
enrollmentType?: EnrollmentType;
|
||||||
|
issuerType?: IssuerType;
|
||||||
|
caId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TGetCertificateProfileByIdDTO = {
|
export type TGetCertificateProfileByIdDTO = {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import {
|
|||||||
import { useProject } from "@app/context";
|
import { useProject } from "@app/context";
|
||||||
import { useGetCert } from "@app/hooks/api";
|
import { useGetCert } from "@app/hooks/api";
|
||||||
import { useCreateCertificateV3 } from "@app/hooks/api/ca";
|
import { useCreateCertificateV3 } from "@app/hooks/api/ca";
|
||||||
import { useListCertificateProfiles } from "@app/hooks/api/certificateProfiles";
|
import { EnrollmentType, useListCertificateProfiles } from "@app/hooks/api/certificateProfiles";
|
||||||
import { CertExtendedKeyUsage, CertKeyUsage } from "@app/hooks/api/certificates/enums";
|
import { CertExtendedKeyUsage, CertKeyUsage } from "@app/hooks/api/certificates/enums";
|
||||||
import { useGetCertificateTemplateV2ById } from "@app/hooks/api/certificateTemplates/queries";
|
import { useGetCertificateTemplateV2ById } from "@app/hooks/api/certificateTemplates/queries";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
@@ -122,7 +122,7 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
|||||||
|
|
||||||
const { data: profilesData } = useListCertificateProfiles({
|
const { data: profilesData } = useListCertificateProfiles({
|
||||||
projectId: currentProject?.id || "",
|
projectId: currentProject?.id || "",
|
||||||
enrollmentType: "api"
|
enrollmentType: EnrollmentType.API
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutateAsync: createCertificate } = useCreateCertificateV3({
|
const { mutateAsync: createCertificate } = useCreateCertificateV3({
|
||||||
|
|||||||
@@ -416,7 +416,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
{/* Manual renewal action for profile-issued certificates that are not revoked/expired (including failed ones) */}
|
{/* Manual renewal action for profile-issued certificates that are not revoked/expired (including failed ones) */}
|
||||||
{(() => {
|
{(() => {
|
||||||
const canRenew =
|
const canRenew =
|
||||||
certificate.profileId &&
|
(certificate.profileId || certificate.caId) &&
|
||||||
certificate.hasPrivateKey !== false &&
|
certificate.hasPrivateKey !== false &&
|
||||||
!certificate.renewedByCertificateId &&
|
!certificate.renewedByCertificateId &&
|
||||||
!isRevoked &&
|
!isRevoked &&
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ import {
|
|||||||
import { useProject, useSubscription } from "@app/context";
|
import { useProject, useSubscription } from "@app/context";
|
||||||
import { useListCasByProjectId } from "@app/hooks/api/ca/queries";
|
import { useListCasByProjectId } from "@app/hooks/api/ca/queries";
|
||||||
import {
|
import {
|
||||||
|
EnrollmentType,
|
||||||
|
IssuerType,
|
||||||
TCertificateProfileWithDetails,
|
TCertificateProfileWithDetails,
|
||||||
TCreateCertificateProfileDTO,
|
TCreateCertificateProfileDTO,
|
||||||
TUpdateCertificateProfileDTO,
|
TUpdateCertificateProfileDTO,
|
||||||
@@ -46,8 +48,9 @@ const createSchema = z
|
|||||||
.trim()
|
.trim()
|
||||||
.max(1000, "Description must be less than 1000 characters")
|
.max(1000, "Description must be less than 1000 characters")
|
||||||
.optional(),
|
.optional(),
|
||||||
enrollmentType: z.enum(["api", "est", "acme"]),
|
enrollmentType: z.nativeEnum(EnrollmentType),
|
||||||
certificateAuthorityId: z.string().min(1, "Certificate Authority is required"),
|
issuerType: z.nativeEnum(IssuerType),
|
||||||
|
certificateAuthorityId: z.string().nullable().optional(),
|
||||||
certificateTemplateId: z.string().min(1, "Certificate Template is required"),
|
certificateTemplateId: z.string().min(1, "Certificate Template is required"),
|
||||||
estConfig: z
|
estConfig: z
|
||||||
.object({
|
.object({
|
||||||
@@ -78,19 +81,101 @@ const createSchema = z
|
|||||||
})
|
})
|
||||||
.refine(
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
if (data.enrollmentType === "est" && !data.estConfig) {
|
if (data.enrollmentType === EnrollmentType.EST) {
|
||||||
return false;
|
return !!data.estConfig;
|
||||||
}
|
|
||||||
if (data.enrollmentType === "api" && !data.apiConfig) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (data.enrollmentType === "acme" && !data.acmeConfig) {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
message: "Configuration is required for selected enrollment type"
|
message: "EST enrollment type requires EST configuration"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.enrollmentType === EnrollmentType.API) {
|
||||||
|
return !!data.apiConfig;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "API enrollment type requires API configuration"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.enrollmentType === EnrollmentType.ACME) {
|
||||||
|
return !!data.acmeConfig;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "ACME enrollment type requires ACME configuration"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.enrollmentType === EnrollmentType.EST) {
|
||||||
|
return !data.apiConfig && !data.acmeConfig;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "EST enrollment type cannot have API or ACME configuration"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.enrollmentType === EnrollmentType.API) {
|
||||||
|
return !data.estConfig && !data.acmeConfig;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "API enrollment type cannot have EST or ACME configuration"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.enrollmentType === EnrollmentType.ACME) {
|
||||||
|
return !data.estConfig && !data.apiConfig;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "ACME enrollment type cannot have EST or API configuration"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.issuerType === IssuerType.CA) {
|
||||||
|
return !!data.certificateAuthorityId;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "CA issuer type requires a certificate authority"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.issuerType === IssuerType.SELF_SIGNED) {
|
||||||
|
return !data.certificateAuthorityId;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "Self-signed issuer type cannot have a certificate authority"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.issuerType === IssuerType.SELF_SIGNED) {
|
||||||
|
return data.enrollmentType === EnrollmentType.API;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "Self-signed issuer type only supports API enrollment"
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -110,8 +195,9 @@ const editSchema = z
|
|||||||
.trim()
|
.trim()
|
||||||
.max(1000, "Description must be less than 1000 characters")
|
.max(1000, "Description must be less than 1000 characters")
|
||||||
.optional(),
|
.optional(),
|
||||||
enrollmentType: z.enum(["api", "est", "acme"]),
|
enrollmentType: z.nativeEnum(EnrollmentType),
|
||||||
certificateAuthorityId: z.string().optional(),
|
issuerType: z.nativeEnum(IssuerType),
|
||||||
|
certificateAuthorityId: z.string().nullable().optional(),
|
||||||
certificateTemplateId: z.string().optional(),
|
certificateTemplateId: z.string().optional(),
|
||||||
estConfig: z
|
estConfig: z
|
||||||
.object({
|
.object({
|
||||||
@@ -130,19 +216,101 @@ const editSchema = z
|
|||||||
})
|
})
|
||||||
.refine(
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
if (data.enrollmentType === "est" && !data.estConfig) {
|
if (data.enrollmentType === EnrollmentType.EST) {
|
||||||
return false;
|
return !!data.estConfig;
|
||||||
}
|
|
||||||
if (data.enrollmentType === "api" && !data.apiConfig) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (data.enrollmentType === "acme" && !data.acmeConfig) {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
message: "Configuration is required for selected enrollment type"
|
message: "EST enrollment type requires EST configuration"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.enrollmentType === EnrollmentType.API) {
|
||||||
|
return !!data.apiConfig;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "API enrollment type requires API configuration"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.enrollmentType === EnrollmentType.ACME) {
|
||||||
|
return !!data.acmeConfig;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "ACME enrollment type requires ACME configuration"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.enrollmentType === EnrollmentType.EST) {
|
||||||
|
return !data.apiConfig && !data.acmeConfig;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "EST enrollment type cannot have API or ACME configuration"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.enrollmentType === EnrollmentType.API) {
|
||||||
|
return !data.estConfig && !data.acmeConfig;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "API enrollment type cannot have EST or ACME configuration"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.enrollmentType === EnrollmentType.ACME) {
|
||||||
|
return !data.estConfig && !data.apiConfig;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "ACME enrollment type cannot have EST or API configuration"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.issuerType === IssuerType.CA) {
|
||||||
|
return !!data.certificateAuthorityId;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "CA issuer type requires a certificate authority"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.issuerType === IssuerType.SELF_SIGNED) {
|
||||||
|
return !data.certificateAuthorityId;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "Self-signed issuer type cannot have a certificate authority"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.issuerType === IssuerType.SELF_SIGNED) {
|
||||||
|
return data.enrollmentType === EnrollmentType.API;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "Self-signed issuer type only supports API enrollment"
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -193,10 +361,11 @@ export const CreateProfileModal = ({
|
|||||||
slug: profile.slug,
|
slug: profile.slug,
|
||||||
description: profile.description || "",
|
description: profile.description || "",
|
||||||
enrollmentType: profile.enrollmentType,
|
enrollmentType: profile.enrollmentType,
|
||||||
certificateAuthorityId: profile.caId,
|
issuerType: profile.issuerType,
|
||||||
|
certificateAuthorityId: profile.caId || undefined,
|
||||||
certificateTemplateId: profile.certificateTemplateId,
|
certificateTemplateId: profile.certificateTemplateId,
|
||||||
estConfig:
|
estConfig:
|
||||||
profile.enrollmentType === "est"
|
profile.enrollmentType === EnrollmentType.EST
|
||||||
? {
|
? {
|
||||||
disableBootstrapCaValidation:
|
disableBootstrapCaValidation:
|
||||||
profile.estConfig?.disableBootstrapCaValidation || false,
|
profile.estConfig?.disableBootstrapCaValidation || false,
|
||||||
@@ -205,18 +374,19 @@ export const CreateProfileModal = ({
|
|||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
apiConfig:
|
apiConfig:
|
||||||
profile.enrollmentType === "api"
|
profile.enrollmentType === EnrollmentType.API
|
||||||
? {
|
? {
|
||||||
autoRenew: profile.apiConfig?.autoRenew || false,
|
autoRenew: profile.apiConfig?.autoRenew || false,
|
||||||
renewBeforeDays: profile.apiConfig?.renewBeforeDays || 30
|
renewBeforeDays: profile.apiConfig?.renewBeforeDays || 30
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
acmeConfig: profile.enrollmentType === "acme" ? {} : undefined
|
acmeConfig: profile.enrollmentType === EnrollmentType.ACME ? {} : undefined
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
slug: "",
|
slug: "",
|
||||||
description: "",
|
description: "",
|
||||||
enrollmentType: "api",
|
enrollmentType: EnrollmentType.API,
|
||||||
|
issuerType: IssuerType.CA,
|
||||||
certificateAuthorityId: "",
|
certificateAuthorityId: "",
|
||||||
certificateTemplateId: "",
|
certificateTemplateId: "",
|
||||||
apiConfig: {
|
apiConfig: {
|
||||||
@@ -228,6 +398,7 @@ export const CreateProfileModal = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const watchedEnrollmentType = watch("enrollmentType");
|
const watchedEnrollmentType = watch("enrollmentType");
|
||||||
|
const watchedIssuerType = watch("issuerType");
|
||||||
const watchedDisableBootstrapValidation = watch("estConfig.disableBootstrapCaValidation");
|
const watchedDisableBootstrapValidation = watch("estConfig.disableBootstrapCaValidation");
|
||||||
const watchedAutoRenew = watch("apiConfig.autoRenew");
|
const watchedAutoRenew = watch("apiConfig.autoRenew");
|
||||||
|
|
||||||
@@ -237,7 +408,8 @@ export const CreateProfileModal = ({
|
|||||||
slug: profile.slug,
|
slug: profile.slug,
|
||||||
description: profile.description || "",
|
description: profile.description || "",
|
||||||
enrollmentType: profile.enrollmentType,
|
enrollmentType: profile.enrollmentType,
|
||||||
certificateAuthorityId: profile.caId,
|
issuerType: profile.issuerType,
|
||||||
|
certificateAuthorityId: profile.caId || undefined,
|
||||||
certificateTemplateId: profile.certificateTemplateId,
|
certificateTemplateId: profile.certificateTemplateId,
|
||||||
estConfig:
|
estConfig:
|
||||||
profile.enrollmentType === "est"
|
profile.enrollmentType === "est"
|
||||||
@@ -255,13 +427,13 @@ export const CreateProfileModal = ({
|
|||||||
renewBeforeDays: profile.apiConfig?.renewBeforeDays || 30
|
renewBeforeDays: profile.apiConfig?.renewBeforeDays || 30
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
acmeConfig: profile.enrollmentType === "acme" ? {} : undefined
|
acmeConfig: profile.enrollmentType === EnrollmentType.ACME ? {} : undefined
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [isEdit, profile, reset]);
|
}, [isEdit, profile, reset]);
|
||||||
|
|
||||||
const onFormSubmit = async (data: FormData) => {
|
const onFormSubmit = async (data: FormData) => {
|
||||||
if (!isEdit && !subscription?.pkiAcme && data.enrollmentType === "acme") {
|
if (!isEdit && !subscription?.pkiAcme && data.enrollmentType === EnrollmentType.ACME) {
|
||||||
reset();
|
reset();
|
||||||
onClose();
|
onClose();
|
||||||
handlePopUpOpen("upgradePlan", {
|
handlePopUpOpen("upgradePlan", {
|
||||||
@@ -276,14 +448,15 @@ export const CreateProfileModal = ({
|
|||||||
const updateData: TUpdateCertificateProfileDTO = {
|
const updateData: TUpdateCertificateProfileDTO = {
|
||||||
profileId: profile.id,
|
profileId: profile.id,
|
||||||
slug: data.slug,
|
slug: data.slug,
|
||||||
description: data.description
|
description: data.description,
|
||||||
|
issuerType: data.issuerType
|
||||||
};
|
};
|
||||||
|
|
||||||
if (data.enrollmentType === "est" && data.estConfig) {
|
if (data.enrollmentType === EnrollmentType.EST && data.estConfig) {
|
||||||
updateData.estConfig = data.estConfig;
|
updateData.estConfig = data.estConfig;
|
||||||
} else if (data.enrollmentType === "api" && data.apiConfig) {
|
} else if (data.enrollmentType === EnrollmentType.API && data.apiConfig) {
|
||||||
updateData.apiConfig = data.apiConfig;
|
updateData.apiConfig = data.apiConfig;
|
||||||
} else if (data.enrollmentType === "acme" && data.acmeConfig) {
|
} else if (data.enrollmentType === EnrollmentType.ACME && data.acmeConfig) {
|
||||||
updateData.acmeConfig = data.acmeConfig;
|
updateData.acmeConfig = data.acmeConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,19 +471,23 @@ export const CreateProfileModal = ({
|
|||||||
slug: data.slug,
|
slug: data.slug,
|
||||||
description: data.description,
|
description: data.description,
|
||||||
enrollmentType: data.enrollmentType,
|
enrollmentType: data.enrollmentType,
|
||||||
caId: data.certificateAuthorityId,
|
issuerType: data.issuerType,
|
||||||
|
caId:
|
||||||
|
data.issuerType === IssuerType.SELF_SIGNED
|
||||||
|
? undefined
|
||||||
|
: data.certificateAuthorityId || undefined,
|
||||||
certificateTemplateId: data.certificateTemplateId
|
certificateTemplateId: data.certificateTemplateId
|
||||||
};
|
};
|
||||||
|
|
||||||
if (data.enrollmentType === "est" && data.estConfig) {
|
if (data.enrollmentType === EnrollmentType.EST && data.estConfig) {
|
||||||
createData.estConfig = {
|
createData.estConfig = {
|
||||||
passphrase: data.estConfig.passphrase,
|
passphrase: data.estConfig.passphrase,
|
||||||
caChain: data.estConfig.caChain || undefined,
|
caChain: data.estConfig.caChain || undefined,
|
||||||
disableBootstrapCaValidation: data.estConfig.disableBootstrapCaValidation
|
disableBootstrapCaValidation: data.estConfig.disableBootstrapCaValidation
|
||||||
};
|
};
|
||||||
} else if (data.enrollmentType === "api" && data.apiConfig) {
|
} else if (data.enrollmentType === EnrollmentType.API && data.apiConfig) {
|
||||||
createData.apiConfig = data.apiConfig;
|
createData.apiConfig = data.apiConfig;
|
||||||
} else if (data.enrollmentType === "acme" && data.acmeConfig) {
|
} else if (data.enrollmentType === EnrollmentType.ACME && data.acmeConfig) {
|
||||||
createData.acmeConfig = data.acmeConfig;
|
createData.acmeConfig = data.acmeConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,34 +549,73 @@ export const CreateProfileModal = ({
|
|||||||
|
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="certificateAuthorityId"
|
name="issuerType"
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
<FormControl
|
<FormControl
|
||||||
label="Issuing CA"
|
label="Issuer Type"
|
||||||
isRequired
|
isRequired
|
||||||
isError={Boolean(error)}
|
isError={Boolean(error)}
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
{...field}
|
{...field}
|
||||||
onValueChange={onChange}
|
onValueChange={(value) => {
|
||||||
placeholder="Select a certificate authority"
|
if (value === "self-signed") {
|
||||||
|
setValue("certificateAuthorityId", "");
|
||||||
|
setValue("enrollmentType", EnrollmentType.API);
|
||||||
|
setValue("apiConfig", {
|
||||||
|
autoRenew: false,
|
||||||
|
renewBeforeDays: 30
|
||||||
|
});
|
||||||
|
setValue("estConfig", undefined);
|
||||||
|
setValue("acmeConfig", undefined);
|
||||||
|
}
|
||||||
|
onChange(value);
|
||||||
|
}}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
position="popper"
|
position="popper"
|
||||||
isDisabled={Boolean(isEdit)}
|
isDisabled={Boolean(isEdit)}
|
||||||
>
|
>
|
||||||
{certificateAuthorities.map((ca) => (
|
<SelectItem value="ca">Certificate Authority</SelectItem>
|
||||||
<SelectItem key={ca.id} value={ca.id}>
|
<SelectItem value="self-signed">Self-signed</SelectItem>
|
||||||
{ca.type === "internal" && ca.configuration.friendlyName
|
|
||||||
? ca.configuration.friendlyName
|
|
||||||
: ca.name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{watchedIssuerType === "ca" && (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="certificateAuthorityId"
|
||||||
|
render={({ field: { onChange, value, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Issuing CA"
|
||||||
|
isRequired
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
{...field}
|
||||||
|
value={value || undefined}
|
||||||
|
onValueChange={onChange}
|
||||||
|
placeholder="Select a certificate authority"
|
||||||
|
className="w-full"
|
||||||
|
position="popper"
|
||||||
|
isDisabled={Boolean(isEdit)}
|
||||||
|
>
|
||||||
|
{certificateAuthorities.map((ca) => (
|
||||||
|
<SelectItem key={ca.id} value={ca.id}>
|
||||||
|
{ca.type === "internal" && ca.configuration.friendlyName
|
||||||
|
? ca.configuration.friendlyName
|
||||||
|
: ca.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="certificateTemplateId"
|
name="certificateTemplateId"
|
||||||
@@ -488,8 +704,12 @@ export const CreateProfileModal = ({
|
|||||||
isDisabled={Boolean(isEdit)}
|
isDisabled={Boolean(isEdit)}
|
||||||
>
|
>
|
||||||
<SelectItem value="api">API</SelectItem>
|
<SelectItem value="api">API</SelectItem>
|
||||||
<SelectItem value="est">EST</SelectItem>
|
{watchedIssuerType !== IssuerType.SELF_SIGNED && (
|
||||||
<SelectItem value="acme">ACME</SelectItem>
|
<SelectItem value="est">EST</SelectItem>
|
||||||
|
)}
|
||||||
|
{watchedIssuerType !== IssuerType.SELF_SIGNED && (
|
||||||
|
<SelectItem value="acme">ACME</SelectItem>
|
||||||
|
)}
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export const ProfileList = ({
|
|||||||
</THead>
|
</THead>
|
||||||
<TBody>
|
<TBody>
|
||||||
<Tr>
|
<Tr>
|
||||||
<Td colSpan={6}>
|
<Td colSpan={5}>
|
||||||
<EmptyState title="No Project Selected" />
|
<EmptyState title="No Project Selected" />
|
||||||
</Td>
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
@@ -77,10 +77,10 @@ export const ProfileList = ({
|
|||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
</THead>
|
||||||
<TBody>
|
<TBody>
|
||||||
{isLoading && <TableSkeleton columns={6} innerKey="certificate-profiles" />}
|
{isLoading && <TableSkeleton columns={5} innerKey="certificate-profiles" />}
|
||||||
{!isLoading && (!profiles || profiles.length === 0) && (
|
{!isLoading && (!profiles || profiles.length === 0) && (
|
||||||
<Tr>
|
<Tr>
|
||||||
<Td colSpan={6}>
|
<Td colSpan={5}>
|
||||||
<EmptyState title="No Certificate Profiles" />
|
<EmptyState title="No Certificate Profiles" />
|
||||||
</Td>
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import {
|
|||||||
} from "@app/context/ProjectPermissionContext/types";
|
} from "@app/context/ProjectPermissionContext/types";
|
||||||
import { usePopUp, useToggle } from "@app/hooks";
|
import { usePopUp, useToggle } from "@app/hooks";
|
||||||
import { useGetCaById } from "@app/hooks/api/ca/queries";
|
import { useGetCaById } from "@app/hooks/api/ca/queries";
|
||||||
import { TCertificateProfile } from "@app/hooks/api/certificateProfiles";
|
import { IssuerType, TCertificateProfile } from "@app/hooks/api/certificateProfiles";
|
||||||
import { useGetCertificateTemplateV2ById } from "@app/hooks/api/certificateTemplates/queries";
|
import { useGetCertificateTemplateV2ById } from "@app/hooks/api/certificateTemplates/queries";
|
||||||
import { CertificateIssuanceModal } from "@app/pages/cert-manager/CertificatesPage/components/CertificateIssuanceModal";
|
import { CertificateIssuanceModal } from "@app/pages/cert-manager/CertificatesPage/components/CertificateIssuanceModal";
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ export const ProfileRow = ({
|
|||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { permission } = useProjectPermission();
|
const { permission } = useProjectPermission();
|
||||||
|
|
||||||
const { data: caData } = useGetCaById(profile.caId);
|
const { data: caData } = useGetCaById(profile.caId ?? "");
|
||||||
|
|
||||||
const { popUp, handlePopUpToggle } = usePopUp(["issueCertificate"] as const);
|
const { popUp, handlePopUpToggle } = usePopUp(["issueCertificate"] as const);
|
||||||
|
|
||||||
@@ -121,7 +121,9 @@ export const ProfileRow = ({
|
|||||||
<Td className="text-start">{getEnrollmentTypeBadge(profile.enrollmentType)}</Td>
|
<Td className="text-start">{getEnrollmentTypeBadge(profile.enrollmentType)}</Td>
|
||||||
<Td className="text-start">
|
<Td className="text-start">
|
||||||
<span className="text-sm text-mineshaft-300">
|
<span className="text-sm text-mineshaft-300">
|
||||||
{caData?.friendlyName || caData?.commonName || profile.caId}
|
{profile.issuerType === IssuerType.SELF_SIGNED
|
||||||
|
? "Self-signed"
|
||||||
|
: caData?.friendlyName || caData?.commonName || profile.caId}
|
||||||
</span>
|
</span>
|
||||||
</Td>
|
</Td>
|
||||||
<Td>
|
<Td>
|
||||||
|
|||||||
Reference in New Issue
Block a user