PKI revamp, last changes and improvements on API and UI

This commit is contained in:
Carlos Monastyrski
2025-10-16 12:29:12 -03:00
parent 42800fdfe5
commit 5684127ce0
28 changed files with 2482 additions and 2257 deletions
@@ -10,20 +10,19 @@ export async function up(knex: Knex): Promise<void> {
t.string("projectId").notNullable();
t.foreign("projectId").references("id").inTable(TableName.Project);
t.string("slug").notNullable();
t.string("name").notNullable();
t.string("description");
t.jsonb("attributes");
t.jsonb("subject");
t.jsonb("sans");
t.jsonb("keyUsages");
t.jsonb("extendedKeyUsages");
t.jsonb("subjectAlternativeNames");
t.jsonb("algorithms");
t.jsonb("validity");
t.jsonb("signatureAlgorithm");
t.jsonb("keyAlgorithm");
t.timestamps(true, true, true);
t.unique(["slug", "projectId"]);
t.unique(["name", "projectId"]);
});
await createOnUpdateTrigger(knex, TableName.CertificateTemplateV2);
@@ -10,15 +10,14 @@ import { TImmutableDBKeys } from "./models";
export const CertificateTemplatesV2Schema = z.object({
id: z.string().uuid(),
projectId: z.string(),
slug: z.string(),
name: z.string(),
description: z.string().nullable().optional(),
attributes: z.unknown().nullable().optional(),
subject: z.unknown().nullable().optional(),
sans: z.unknown().nullable().optional(),
keyUsages: z.unknown().nullable().optional(),
extendedKeyUsages: z.unknown().nullable().optional(),
subjectAlternativeNames: z.unknown().nullable().optional(),
algorithms: z.unknown().nullable().optional(),
validity: z.unknown().nullable().optional(),
signatureAlgorithm: z.unknown().nullable().optional(),
keyAlgorithm: z.unknown().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date()
});
+1 -1
View File
@@ -11,8 +11,8 @@ import { registerAuthRoutes } from "./auth-router";
import { registerProjectBotRouter } from "./bot-router";
import { registerCaRouter } from "./certificate-authority-router";
import { CERTIFICATE_AUTHORITY_REGISTER_ROUTER_MAP } from "./certificate-authority-routers";
import { registerCertRouter } from "./certificate-router";
import { registerCertificateProfilesRouter } from "./certificate-profiles-router";
import { registerCertRouter } from "./certificate-router";
import { registerCertificateTemplateRouter } from "./certificate-template-router";
import { registerDeprecatedProjectEnvRouter } from "./deprecated-project-env-router";
import { registerDeprecatedProjectMembershipRouter } from "./deprecated-project-membership-router";
@@ -1,110 +1,133 @@
import { z } from "zod";
import { CertificateTemplatesV2Schema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import {
CertDurationUnit,
CertExtendedKeyUsageType,
CertIncludeType,
CertKeyUsageType,
CertSubjectAlternativeNameType,
CertSubjectAttributeType
} from "@app/services/certificate-common/certificate-constants";
import { certificateTemplateV2ResponseSchema } from "@app/services/certificate-template-v2/certificate-template-v2-schemas";
const attributeTypeSchema = z.nativeEnum(CertSubjectAttributeType);
const sanTypeSchema = z.nativeEnum(CertSubjectAlternativeNameType);
const templateV2SubjectSchema = z
.object({
type: attributeTypeSchema,
allowed: z.array(z.string()).optional(),
required: z.array(z.string()).optional(),
denied: z.array(z.string()).optional()
})
.refine(
(data) => {
if (!data.allowed && !data.required && !data.denied) {
return false;
}
return true;
},
{
message: "Subject attribute must have at least one allowed, required, or denied value"
}
);
const templateV2KeyUsagesSchema = z
.object({
allowed: z.array(z.nativeEnum(CertKeyUsageType)).optional(),
required: z.array(z.nativeEnum(CertKeyUsageType)).optional(),
denied: z.array(z.nativeEnum(CertKeyUsageType)).optional()
})
.refine(
(data) => {
if (!data.allowed && !data.required && !data.denied) {
return false;
}
return true;
},
{
message: "Key usages must have at least one allowed, required, or denied value"
}
);
const templateV2ExtendedKeyUsagesSchema = z
.object({
allowed: z.array(z.nativeEnum(CertExtendedKeyUsageType)).optional(),
required: z.array(z.nativeEnum(CertExtendedKeyUsageType)).optional(),
denied: z.array(z.nativeEnum(CertExtendedKeyUsageType)).optional()
})
.refine(
(data) => {
if (!data.allowed && !data.required && !data.denied) {
return false;
}
return true;
},
{
message: "Extended key usages must have at least one allowed, required, or denied value"
}
);
const templateV2SanSchema = z
.object({
type: sanTypeSchema,
allowed: z.array(z.string()).optional(),
required: z.array(z.string()).optional(),
denied: z.array(z.string()).optional()
})
.refine(
(data) => {
if (!data.allowed && !data.required && !data.denied) {
return false;
}
return true;
},
{
message: "SAN must have at least one allowed, required, or denied value"
}
);
const templateV2ValiditySchema = z.object({
max: z
.string()
.regex(/^\d+[dhmy]$/, {
message: "Max validity must be in format like '365d', '12m', '1y', or '24h'"
})
.optional()
});
const templateV2AlgorithmsSchema = z.object({
signature: z.array(z.string()).min(1, "At least one signature algorithm must be provided").optional(),
keyAlgorithm: z.array(z.string()).min(1, "At least one key algorithm must be provided").optional()
});
const createCertificateTemplateV2Schema = z.object({
projectId: z.string().min(1),
name: z.string().min(1).max(255, "Name must be between 1 and 255 characters"),
description: z.string().max(1000).optional(),
subject: z.array(templateV2SubjectSchema).optional(),
sans: z.array(templateV2SanSchema).optional(),
keyUsages: templateV2KeyUsagesSchema.optional(),
extendedKeyUsages: templateV2ExtendedKeyUsagesSchema.optional(),
algorithms: templateV2AlgorithmsSchema.optional(),
validity: templateV2ValiditySchema.optional()
});
const updateCertificateTemplateV2Schema = z.object({
name: z.string().min(1).max(255, "Name must be between 1 and 255 characters").optional(),
description: z.string().max(1000).optional(),
subject: z.array(templateV2SubjectSchema).optional(),
sans: z.array(templateV2SanSchema).optional(),
keyUsages: templateV2KeyUsagesSchema.optional(),
extendedKeyUsages: templateV2ExtendedKeyUsagesSchema.optional(),
algorithms: templateV2AlgorithmsSchema.optional(),
validity: templateV2ValiditySchema.optional()
});
export const registerCertificateTemplatesV2Router = async (server: FastifyZodProvider) => {
const templateV2AttributeSchema = z
.object({
type: z.nativeEnum(CertSubjectAttributeType),
include: z.nativeEnum(CertIncludeType),
value: z.array(z.string()).optional()
})
.refine(
(data) => {
if (data.type === CertSubjectAttributeType.COMMON_NAME && data.value && data.value.length > 1) {
return false;
}
if (data.include === CertIncludeType.MANDATORY && (!data.value || data.value.length > 1)) {
return false;
}
return true;
},
{
message: "Common name can only have one value. Mandatory attributes can only have one value or no value (empty)"
}
);
const templateV2KeyUsagesSchema = z.object({
requiredUsages: z
.object({
all: z.array(z.nativeEnum(CertKeyUsageType))
})
.optional(),
optionalUsages: z
.object({
all: z.array(z.nativeEnum(CertKeyUsageType))
})
.optional()
});
const templateV2ExtendedKeyUsagesSchema = z.object({
requiredUsages: z
.object({
all: z.array(z.nativeEnum(CertExtendedKeyUsageType))
})
.optional(),
optionalUsages: z
.object({
all: z.array(z.nativeEnum(CertExtendedKeyUsageType))
})
.optional()
});
const templateV2SanSchema = z
.object({
type: z.nativeEnum(CertSubjectAlternativeNameType),
include: z.nativeEnum(CertIncludeType),
value: z.array(z.string()).optional()
})
.refine(
(data) => {
if (data.include === CertIncludeType.MANDATORY && (!data.value || data.value.length > 1)) {
return false;
}
return true;
},
{
message: "Mandatory SANs can only have one value or no value (empty)"
}
);
const templateV2ValiditySchema = z.object({
maxDuration: z.object({
value: z.number().positive(),
unit: z.nativeEnum(CertDurationUnit)
}),
minDuration: z
.object({
value: z.number().positive(),
unit: z.nativeEnum(CertDurationUnit)
})
.optional()
});
const templateV2SignatureAlgorithmSchema = z.object({
allowedAlgorithms: z.array(z.string()).min(1),
defaultAlgorithm: z.string()
});
const templateV2KeyAlgorithmSchema = z.object({
allowedKeyTypes: z.array(z.string()).min(1),
defaultKeyType: z.string()
});
server.route({
method: "POST",
url: "/",
@@ -114,39 +137,10 @@ export const registerCertificateTemplatesV2Router = async (server: FastifyZodPro
schema: {
hide: false,
tags: [ApiDocsTags.PkiCertificateTemplates],
body: z
.object({
projectId: z.string().min(1),
slug: slugSchema({ min: 1, max: 255 }),
description: z.string().max(1000).optional(),
attributes: z.array(templateV2AttributeSchema).optional(),
keyUsages: templateV2KeyUsagesSchema.optional(),
extendedKeyUsages: templateV2ExtendedKeyUsagesSchema.optional(),
subjectAlternativeNames: z.array(templateV2SanSchema).optional(),
validity: templateV2ValiditySchema.optional(),
signatureAlgorithm: templateV2SignatureAlgorithmSchema.optional(),
keyAlgorithm: templateV2KeyAlgorithmSchema.optional()
})
.refine(
(data) => {
const hasConstraints =
(data.attributes && data.attributes.length > 0) ||
(data.subjectAlternativeNames && data.subjectAlternativeNames.length > 0) ||
data.keyUsages ||
data.extendedKeyUsages ||
data.validity ||
data.signatureAlgorithm ||
data.keyAlgorithm;
return hasConstraints;
},
{
message:
"Certificate template must define at least one constraint (attributes, SANs, key usages, validity, or algorithms)"
}
),
body: createCertificateTemplateV2Schema,
response: {
200: z.object({
certificateTemplate: CertificateTemplatesV2Schema
certificateTemplate: certificateTemplateV2ResponseSchema
})
}
},
@@ -169,7 +163,7 @@ export const registerCertificateTemplatesV2Router = async (server: FastifyZodPro
type: EventType.CREATE_CERTIFICATE_TEMPLATE,
metadata: {
certificateTemplateId: certificateTemplate.id,
name: certificateTemplate.slug,
name: certificateTemplate.name,
projectId: certificateTemplate.projectId
}
}
@@ -196,7 +190,7 @@ export const registerCertificateTemplatesV2Router = async (server: FastifyZodPro
}),
response: {
200: z.object({
certificateTemplates: CertificateTemplatesV2Schema.array(),
certificateTemplates: certificateTemplateV2ResponseSchema.array(),
totalCount: z.number()
})
}
@@ -240,7 +234,7 @@ export const registerCertificateTemplatesV2Router = async (server: FastifyZodPro
}),
response: {
200: z.object({
certificateTemplate: CertificateTemplatesV2Schema
certificateTemplate: certificateTemplateV2ResponseSchema
})
}
},
@@ -261,7 +255,7 @@ export const registerCertificateTemplatesV2Router = async (server: FastifyZodPro
type: EventType.GET_CERTIFICATE_TEMPLATE,
metadata: {
certificateTemplateId: certificateTemplate.id,
name: certificateTemplate.slug
name: certificateTemplate.name
}
}
});
@@ -282,20 +276,10 @@ export const registerCertificateTemplatesV2Router = async (server: FastifyZodPro
params: z.object({
id: z.string().uuid()
}),
body: z.object({
slug: slugSchema({ min: 1, max: 255 }).optional(),
description: z.string().max(1000).optional(),
attributes: z.array(templateV2AttributeSchema).optional(),
keyUsages: templateV2KeyUsagesSchema.optional(),
extendedKeyUsages: templateV2ExtendedKeyUsagesSchema.optional(),
subjectAlternativeNames: z.array(templateV2SanSchema).optional(),
validity: templateV2ValiditySchema.optional(),
signatureAlgorithm: templateV2SignatureAlgorithmSchema.optional(),
keyAlgorithm: templateV2KeyAlgorithmSchema.optional()
}),
body: updateCertificateTemplateV2Schema,
response: {
200: z.object({
certificateTemplate: CertificateTemplatesV2Schema
certificateTemplate: certificateTemplateV2ResponseSchema
})
}
},
@@ -317,7 +301,7 @@ export const registerCertificateTemplatesV2Router = async (server: FastifyZodPro
type: EventType.UPDATE_CERTIFICATE_TEMPLATE,
metadata: {
certificateTemplateId: certificateTemplate.id,
name: certificateTemplate.slug
name: certificateTemplate.name
}
}
});
@@ -340,7 +324,7 @@ export const registerCertificateTemplatesV2Router = async (server: FastifyZodPro
}),
response: {
200: z.object({
certificateTemplate: CertificateTemplatesV2Schema
certificateTemplate: certificateTemplateV2ResponseSchema
})
}
},
@@ -361,7 +345,7 @@ export const registerCertificateTemplatesV2Router = async (server: FastifyZodPro
type: EventType.DELETE_CERTIFICATE_TEMPLATE,
metadata: {
certificateTemplateId: certificateTemplate.id,
name: certificateTemplate.slug
name: certificateTemplate.name
}
}
});
@@ -32,6 +32,17 @@ export enum CertIncludeType {
PROHIBIT = "prohibit"
}
export enum CertAttributeRule {
ALLOW = "allow",
DENY = "deny"
}
export enum CertSanEffect {
ALLOW = "allow",
DENY = "deny",
REQUIRE = "require"
}
export enum CertDurationUnit {
DAYS = "days",
MONTHS = "months",
@@ -39,7 +50,9 @@ export enum CertDurationUnit {
}
export enum CertSubjectAttributeType {
COMMON_NAME = "common_name"
COMMON_NAME = "common_name",
ORGANIZATION = "organization",
COUNTRY = "country"
}
export const mapSANTypeToLegacy = (type: CertSubjectAlternativeNameType): string => {
@@ -184,3 +197,5 @@ export const EXTENDED_KEY_USAGE_OPTIONS = Object.values(CertExtendedKeyUsageType
export const INCLUDE_TYPE_OPTIONS = Object.values(CertIncludeType);
export const DURATION_UNIT_OPTIONS = Object.values(CertDurationUnit);
export const SUBJECT_ATTRIBUTE_TYPE_OPTIONS = Object.values(CertSubjectAttributeType);
export const ATTRIBUTE_RULE_OPTIONS = Object.values(CertAttributeRule);
export const SAN_EFFECT_OPTIONS = Object.values(CertSanEffect);
@@ -50,43 +50,27 @@ export const buildCertificateSubjectFromTemplate = (
request: Record<string, unknown>,
templateAttributes?: Array<{
type: string;
include: "mandatory" | "optional" | "prohibit";
value?: string[];
allowed?: string[];
required?: string[];
denied?: string[];
}>
): Record<string, string | undefined> => {
const subject: Record<string, string> = {};
const attributeMap: Record<string, string> = {
common_name: "commonName"
common_name: "commonName",
organization: "organization",
country: "country"
};
if (!templateAttributes || templateAttributes.length === 0) {
throw new Error(
"Template must define allowed certificate attributes. Cannot issue certificate without template attribute constraints."
);
return subject;
}
const allowedAttributes = new Set(templateAttributes.map((attr) => attributeMap[attr.type]));
Object.keys(attributeMap).forEach((templateType) => {
const requestKey = attributeMap[templateType];
const value = request[requestKey];
if (value && !allowedAttributes.has(requestKey)) {
throw new Error(
`Certificate attribute '${requestKey}' is not allowed by the template. Template must define constraints for all requested attributes.`
);
}
});
templateAttributes.forEach((attr) => {
if (attr.include === "prohibit") {
return;
}
const requestKey = attributeMap[attr.type];
const value = request[requestKey];
if (value && typeof value === "string") {
if (value && typeof value === "string" && (attr.allowed || attr.required)) {
subject[attr.type] = value;
}
});
@@ -98,8 +82,9 @@ export const buildSubjectAlternativeNamesFromTemplate = (
request: { subjectAlternativeNames?: Array<{ type: string; value: string }> },
templateSans?: Array<{
type: string;
include: "mandatory" | "optional" | "prohibit";
value?: string[];
allowed?: string[];
required?: string[];
denied?: string[];
}>
): string => {
if (!request.subjectAlternativeNames || request.subjectAlternativeNames.length === 0) {
@@ -107,33 +92,13 @@ export const buildSubjectAlternativeNamesFromTemplate = (
}
if (!templateSans || templateSans.length === 0) {
if (request.subjectAlternativeNames.length > 0) {
throw new Error(
"Template must define allowed subject alternative names. Cannot issue certificate with SANs when template has no SAN constraints."
);
return request.subjectAlternativeNames.map((san) => san.value).join(",");
}
return "";
}
const templateSanTypes = new Set(templateSans.map((san) => san.type));
const prohibitedTypes = new Set(templateSans.filter((san) => san.include === "prohibit").map((san) => san.type));
request.subjectAlternativeNames.forEach((san) => {
const sanType = san.type === "dns_name" ? "dns_name" : san.type;
if (!templateSanTypes.has(sanType)) {
throw new Error(
`Subject Alternative Name type '${sanType}' is not allowed by the template. Template must define constraints for all requested SAN types.`
);
}
});
const allowedSans: string[] = [];
request.subjectAlternativeNames.forEach((san) => {
const sanType = san.type === "dns_name" ? "dns_name" : san.type;
if (!prohibitedTypes.has(sanType)) {
allowedSans.push(san.value);
}
});
return allowedSans.join(",");
@@ -68,7 +68,7 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
(tx || db).ref("name").withSchema(TableName.CertificateAuthority).as("caName"),
(tx || db).ref("id").withSchema(TableName.CertificateTemplateV2).as("templateId"),
(tx || db).ref("projectId").withSchema(TableName.CertificateTemplateV2).as("templateProjectId"),
(tx || db).ref("slug").withSchema(TableName.CertificateTemplateV2).as("templateName"),
(tx || db).ref("name").withSchema(TableName.CertificateTemplateV2).as("templateName"),
(tx || db).ref("description").withSchema(TableName.CertificateTemplateV2).as("templateDescription"),
(tx || db).ref("id").withSchema(TableName.PkiEstEnrollmentConfig).as("estConfigId"),
(tx || db)
@@ -23,19 +23,12 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => {
const serializeJsonFields = (data: TCertificateTemplateV2Insert | TCertificateTemplateV2Update) => {
const serialized = { ...data } as Record<string, unknown>;
const jsonFields = [
"attributes",
"keyUsages",
"extendedKeyUsages",
"subjectAlternativeNames",
"validity",
"signatureAlgorithm",
"keyAlgorithm"
];
const jsonFields = ["subject", "sans", "keyUsages", "extendedKeyUsages", "algorithms", "validity"];
jsonFields.forEach((field) => {
const value = (data as Record<string, unknown>)[field];
if (value !== undefined) {
const value = serialized[field];
if (value !== undefined && typeof value !== "string") {
serialized[field] = JSON.stringify(value);
}
});
@@ -44,21 +37,15 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => {
};
const parseJsonFields = (raw: Record<string, unknown>): TCertificateTemplateV2 => {
const jsonFields = [
"attributes",
"keyUsages",
"extendedKeyUsages",
"subjectAlternativeNames",
"validity",
"signatureAlgorithm",
"keyAlgorithm"
];
const parsed = { ...raw };
const jsonFields = ["subject", "sans", "keyUsages", "extendedKeyUsages", "algorithms", "validity"];
const parsed = { ...raw } as Record<string, unknown>;
jsonFields.forEach((field) => {
const value = raw[field];
if (value) {
if (value !== null && value !== undefined) {
parsed[field] = typeof value === "string" ? JSON.parse(value) : value;
} else {
parsed[field] = undefined;
}
});
@@ -143,7 +130,7 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => {
if (search) {
query = query.where((builder) => {
void builder.whereILike("slug", `%${search}%`).orWhereILike("description", `%${search}%`);
void builder.whereILike("name", `%${search}%`).orWhereILike("description", `%${search}%`);
});
}
@@ -169,7 +156,7 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => {
if (search) {
query = query.where((builder) => {
void builder.whereILike("slug", `%${search}%`).orWhereILike("description", `%${search}%`);
void builder.whereILike("name", `%${search}%`).orWhereILike("description", `%${search}%`);
});
}
@@ -180,10 +167,10 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => {
}
};
const findBySlugAndProjectId = async (slug: string, projectId: string, tx?: Knex) => {
const findByNameAndProjectId = async (name: string, projectId: string, tx?: Knex) => {
try {
const certificateTemplateV2 = await (tx || db)(TableName.CertificateTemplateV2)
.where({ slug, projectId })
.where({ name, projectId })
.first();
if (!certificateTemplateV2) {
@@ -192,7 +179,7 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => {
return parseJsonFields(certificateTemplateV2);
} catch (error) {
throw new DatabaseError({ error, name: "Find certificate template v2 by slug and project id" });
throw new DatabaseError({ error, name: "Find certificate template v2 by name and project id" });
}
};
@@ -238,7 +225,7 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => {
findById,
findByProjectId,
countByProjectId,
findBySlugAndProjectId,
findByNameAndProjectId,
isTemplateInUse,
getProfilesUsingTemplate
};
@@ -1,166 +1,123 @@
import { z } from "zod";
import { slugSchema } from "@app/server/lib/schemas";
import {
CertDurationUnit,
CertExtendedKeyUsageType,
CertIncludeType,
CertKeyUsageType,
CertSubjectAlternativeNameType,
CertSubjectAttributeType
} from "@app/services/certificate-common/certificate-constants";
const attributeTypeSchema = z.nativeEnum(CertSubjectAttributeType);
const includeTypeSchema = z.nativeEnum(CertIncludeType);
const sanTypeSchema = z.nativeEnum(CertSubjectAlternativeNameType);
const durationUnitSchema = z.nativeEnum(CertDurationUnit);
export const templateV2AttributeSchema = z
const templateV2SubjectSchema = z
.object({
type: attributeTypeSchema,
include: includeTypeSchema,
value: z.array(z.string()).optional()
allowed: z.array(z.string()).optional(),
required: z.array(z.string()).optional(),
denied: z.array(z.string()).optional()
})
.refine(
(data) => {
if (data.type === "common_name" && data.value && data.value.length > 1) {
return false;
}
if (data.include === "mandatory" && (!data.value || data.value.length > 1)) {
if (!data.allowed && !data.required && !data.denied) {
return false;
}
return true;
},
{
message: "Common name can only have one value. Mandatory attributes can only have one value or no value (empty)"
message: "Subject attribute must have at least one allowed, required, or denied value"
}
);
export const templateV2KeyUsagesSchema = z.object({
requiredUsages: z
const templateV2KeyUsagesSchema = z
.object({
all: z.array(z.nativeEnum(CertKeyUsageType))
allowed: z.array(z.nativeEnum(CertKeyUsageType)).optional(),
required: z.array(z.nativeEnum(CertKeyUsageType)).optional(),
denied: z.array(z.nativeEnum(CertKeyUsageType)).optional()
})
.optional(),
optionalUsages: z
.object({
all: z.array(z.nativeEnum(CertKeyUsageType))
})
.optional()
});
.refine(
(data) => {
if (!data.allowed && !data.required && !data.denied) {
return false;
}
return true;
},
{
message: "Key usages must have at least one allowed, required, or denied value"
}
);
export const templateV2ExtendedKeyUsagesSchema = z.object({
requiredUsages: z
const templateV2ExtendedKeyUsagesSchema = z
.object({
all: z.array(z.nativeEnum(CertExtendedKeyUsageType))
allowed: z.array(z.nativeEnum(CertExtendedKeyUsageType)).optional(),
required: z.array(z.nativeEnum(CertExtendedKeyUsageType)).optional(),
denied: z.array(z.nativeEnum(CertExtendedKeyUsageType)).optional()
})
.optional(),
optionalUsages: z
.object({
all: z.array(z.nativeEnum(CertExtendedKeyUsageType))
})
.optional()
});
.refine(
(data) => {
if (!data.allowed && !data.required && !data.denied) {
return false;
}
return true;
},
{
message: "Extended key usages must have at least one allowed, required, or denied value"
}
);
export const templateV2SanSchema = z
const templateV2SanSchema = z
.object({
type: sanTypeSchema,
include: includeTypeSchema,
value: z.array(z.string()).optional()
allowed: z.array(z.string()).optional(),
required: z.array(z.string()).optional(),
denied: z.array(z.string()).optional()
})
.refine(
(data) => {
if (data.include === "mandatory" && (!data.value || data.value.length > 1)) {
if (!data.allowed && !data.required && !data.denied) {
return false;
}
return true;
},
{
message: "Mandatory SANs can only have one value or no value (empty)"
message: "SAN must have at least one allowed, required, or denied value"
}
);
export const templateV2ValiditySchema = z.object({
maxDuration: z.object({
value: z.number().positive(),
unit: durationUnitSchema
}),
minDuration: z
.object({
value: z.number().positive(),
unit: durationUnitSchema
const templateV2ValiditySchema = z.object({
max: z
.string()
.regex(/^\d+[dhmy]$/, {
message: "Max validity must be in format like '365d', '12m', '1y', or '24h'"
})
.optional()
});
export const templateV2SignatureAlgorithmSchema = z
.object({
allowedAlgorithms: z.array(z.string()).min(1),
defaultAlgorithm: z.string()
})
.refine((data) => data.allowedAlgorithms.includes(data.defaultAlgorithm), {
message: "Default signature algorithm must be included in the allowed algorithms list"
});
export const templateV2KeyAlgorithmSchema = z
.object({
allowedKeyTypes: z.array(z.string()).min(1),
defaultKeyType: z.string()
})
.refine((data) => data.allowedKeyTypes.includes(data.defaultKeyType), {
message: "Default key algorithm must be included in the allowed key types list"
});
export const createCertificateTemplateV2Schema = z.object({
projectId: z.string().min(1),
slug: slugSchema({ min: 1, max: 255 }),
description: z.string().max(1000).optional(),
attributes: z.array(templateV2AttributeSchema).min(1),
keyUsages: templateV2KeyUsagesSchema,
extendedKeyUsages: templateV2ExtendedKeyUsagesSchema.optional(),
subjectAlternativeNames: z.array(templateV2SanSchema).optional(),
validity: templateV2ValiditySchema.optional(),
signatureAlgorithm: templateV2SignatureAlgorithmSchema.optional(),
keyAlgorithm: templateV2KeyAlgorithmSchema.optional()
const templateV2AlgorithmsSchema = z.object({
signature: z.array(z.string()).min(1, "At least one signature algorithm must be provided").optional(),
keyAlgorithm: z.array(z.string()).min(1, "At least one key algorithm must be provided").optional()
});
export const updateCertificateTemplateV2Schema = z.object({
slug: slugSchema({ min: 1, max: 255 }).optional(),
description: z.string().max(1000).optional(),
attributes: z.array(templateV2AttributeSchema).optional(),
export const certificateTemplateV2ResponseSchema = z.object({
id: z.string().uuid(),
projectId: z.string(),
name: z.string(),
description: z.string().nullable().optional(),
subject: z.array(templateV2SubjectSchema).optional(),
sans: z.array(templateV2SanSchema).optional(),
keyUsages: templateV2KeyUsagesSchema.optional(),
extendedKeyUsages: templateV2ExtendedKeyUsagesSchema.optional(),
subjectAlternativeNames: z.array(templateV2SanSchema).optional(),
algorithms: templateV2AlgorithmsSchema.optional(),
validity: templateV2ValiditySchema.optional(),
signatureAlgorithm: templateV2SignatureAlgorithmSchema.optional(),
keyAlgorithm: templateV2KeyAlgorithmSchema.optional()
});
export const getCertificateTemplateV2ByIdSchema = z.object({
id: z.string().uuid()
});
export const getCertificateTemplateV2BySlugSchema = z.object({
projectId: z.string().min(1),
slug: slugSchema()
});
export const listCertificateTemplatesV2Schema = z.object({
projectId: z.string().min(1),
offset: z.coerce.number().min(0).default(0),
limit: z.coerce.number().min(1).max(100).default(20),
search: z.string().optional()
});
export const deleteCertificateTemplateV2Schema = z.object({
id: z.string().uuid()
createdAt: z.date(),
updatedAt: z.date()
});
export const certificateRequestSchema = z.object({
commonName: z.string().optional(),
organization: z.string().optional(),
organizationName: z.string().optional(),
country: z.string().optional(),
keyUsages: z.array(z.nativeEnum(CertKeyUsageType)).optional(),
extendedKeyUsages: z.array(z.nativeEnum(CertExtendedKeyUsageType)).optional(),
subjectAlternativeNames: z
File diff suppressed because it is too large Load Diff
@@ -12,7 +12,7 @@ import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
import { CertIncludeType, CertSubjectAttributeType } from "../certificate-common/certificate-constants";
import { CertSubjectAttributeType } from "../certificate-common/certificate-constants";
import { TCertificateTemplateV2DALFactory } from "./certificate-template-v2-dal";
import {
TCertificateRequest,
@@ -57,75 +57,86 @@ export const certificateTemplateV2ServiceFactory = ({
}
};
const convertToMilliseconds = (value: number, unit: "days" | "months" | "years"): number => {
switch (unit) {
case "days":
return value * 24 * 60 * 60 * 1000;
case "months":
return value * 30 * 24 * 60 * 60 * 1000;
case "years":
return value * 365 * 24 * 60 * 60 * 1000;
default:
throw new Error(`Unsupported duration unit: ${unit as string}`);
}
};
const validateSubjectAttributePolicy = (
subject: Array<{ type: string; allowed?: string[]; required?: string[]; denied?: string[] }>
) => {
if (!subject || subject.length === 0) return;
const validateSubjectAttributePolicy = (attributes: Array<{ type: string; include: string; value?: string[] }>) => {
if (!attributes || attributes.length === 0) return;
const attributesByType = attributes.reduce(
(acc, attr) => {
if (!acc[attr.type]) acc[attr.type] = [];
acc[attr.type].push(attr);
return acc;
},
{} as Record<string, typeof attributes>
);
for (const [type, attrs] of Object.entries(attributesByType)) {
const mandatoryAttrs = attrs.filter((attr) => attr.include === CertIncludeType.MANDATORY);
if (mandatoryAttrs.length > 1) {
// Validate each subject attribute policy
for (const attr of subject) {
// Ensure at least one field is provided
if (!attr.allowed && !attr.required && !attr.denied) {
throw new ForbiddenRequestError({
message: `Multiple mandatory values found for subject attribute type '${type}'. Only one mandatory value is allowed per attribute type.`
message: `Subject attribute type '${attr.type}' must have at least one allowed, required, or denied value`
});
}
if (mandatoryAttrs.length === 1 && attrs.length > 1) {
// Check for duplicate values within arrays
const arrays = [
{ name: "allowed", values: attr.allowed },
{ name: "required", values: attr.required },
{ name: "denied", values: attr.denied }
];
for (const { name, values } of arrays) {
if (values && values.length > 0) {
const uniqueValues = new Set(values);
if (uniqueValues.size !== values.length) {
throw new ForbiddenRequestError({
message: `When a mandatory value exists for subject attribute type '${type}', no other values (optional or forbidden) are allowed for that attribute type.`
message: `Duplicate values found in ${name} list for subject attribute type '${attr.type}'`
});
}
}
};
const getRequestAttributeValue = (
request: TCertificateRequest,
attrType: CertSubjectAttributeType | string
): string | undefined => {
switch (attrType) {
case CertSubjectAttributeType.COMMON_NAME:
case "common_name":
return request.commonName;
default:
return undefined;
}
}
};
const generateTemplateSlug = (baseSlug?: string): string => {
if (baseSlug) {
return slugify(baseSlug);
const validateSanPolicy = (
sans: Array<{ type: string; allowed?: string[]; required?: string[]; denied?: string[] }>
) => {
if (!sans || sans.length === 0) return;
// Validate each SAN policy
for (const san of sans) {
if (!san.allowed && !san.required && !san.denied) {
throw new ForbiddenRequestError({
message: `SAN type '${san.type}' must have at least one allowed, required, or denied value`
});
}
const arrays = [
{ name: "allowed", values: san.allowed },
{ name: "required", values: san.required },
{ name: "denied", values: san.denied }
];
for (const { name, values } of arrays) {
if (values && values.length > 0) {
const uniqueValues = new Set(values);
if (uniqueValues.size !== values.length) {
throw new ForbiddenRequestError({
message: `Duplicate values found in ${name} list for SAN type '${san.type}'`
});
}
}
}
}
};
const generateTemplateSlug = (baseName?: string): string => {
if (baseName) {
return slugify(baseName);
}
return slugify(alphaNumericNanoId(12));
};
const ensureUniqueSlug = async (projectId: string, desiredSlug: string, templateId?: string): Promise<string> => {
const existingTemplate = await certificateTemplateV2DAL.findBySlugAndProjectId(desiredSlug, projectId);
const existingTemplate = await certificateTemplateV2DAL.findByNameAndProjectId(desiredSlug, projectId);
if (!existingTemplate || (templateId && existingTemplate.id === templateId)) {
return desiredSlug;
}
const alternativeSlug = `${desiredSlug}-${alphaNumericNanoId(8)}`;
const existingAlternative = await certificateTemplateV2DAL.findBySlugAndProjectId(alternativeSlug, projectId);
const existingAlternative = await certificateTemplateV2DAL.findByNameAndProjectId(alternativeSlug, projectId);
if (!existingAlternative) {
return alternativeSlug;
}
@@ -139,8 +150,12 @@ export const certificateTemplateV2ServiceFactory = ({
};
const createWildcardRegex = (pattern: string): RegExp => {
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
const regexPattern = escaped.replace(/\*/g, ".*");
const wildcardRegex = new RE2(/\*/g);
const withPlaceholder = pattern.replace(wildcardRegex, "__WILDCARD__");
const escapeRegex = new RE2(/[.+?^${}()|[\]\\]/g);
const escaped = withPlaceholder.replace(escapeRegex, "\\$&");
const placeholderRegex = new RE2(/__WILDCARD__/g);
const regexPattern = escaped.replace(placeholderRegex, ".*");
return new RE2(`^${regexPattern}$`);
};
@@ -166,6 +181,64 @@ export const certificateTemplateV2ServiceFactory = ({
return mapping[templateFormat] || templateFormat;
};
const validateKeyUsagePolicy = (keyUsages: { allowed?: string[]; required?: string[]; denied?: string[] }) => {
if (!keyUsages) return;
if (!keyUsages.allowed && !keyUsages.required && !keyUsages.denied) {
throw new ForbiddenRequestError({
message: "Key usages must have at least one allowed, required, or denied value"
});
}
const arrays = [
{ name: "allowed", values: keyUsages.allowed },
{ name: "required", values: keyUsages.required },
{ name: "denied", values: keyUsages.denied }
];
for (const { name, values } of arrays) {
if (values && values.length > 0) {
const uniqueValues = new Set(values);
if (uniqueValues.size !== values.length) {
throw new ForbiddenRequestError({
message: `Duplicate values found in ${name} key usages list`
});
}
}
}
};
const validateExtendedKeyUsagePolicy = (extendedKeyUsages: {
allowed?: string[];
required?: string[];
denied?: string[];
}) => {
if (!extendedKeyUsages) return;
if (!extendedKeyUsages.allowed && !extendedKeyUsages.required && !extendedKeyUsages.denied) {
throw new ForbiddenRequestError({
message: "Extended key usages must have at least one allowed, required, or denied value"
});
}
const arrays = [
{ name: "allowed", values: extendedKeyUsages.allowed },
{ name: "required", values: extendedKeyUsages.required },
{ name: "denied", values: extendedKeyUsages.denied }
];
for (const { name, values } of arrays) {
if (values && values.length > 0) {
const uniqueValues = new Set(values);
if (uniqueValues.size !== values.length) {
throw new ForbiddenRequestError({
message: `Duplicate values found in ${name} extended key usages list`
});
}
}
}
};
const validateValueAgainstConstraints = (
value: string,
allowedValues: string[],
@@ -184,7 +257,7 @@ export const certificateTemplateV2ServiceFactory = ({
if (regex.test(value)) {
return { isValid: true };
}
} catch {
} catch (error) {
if (allowedValue === value) {
return { isValid: true };
}
@@ -213,44 +286,128 @@ export const certificateTemplateV2ServiceFactory = ({
const errors: string[] = [];
const warnings: string[] = [];
const templateAttributeTypes = new Set(template.attributes?.map((attr) => attr.type) || []);
// Validate subject attributes
const subjectPolicies = template.subject;
const requestAttributes = new Map<string, string>();
if (request.commonName) requestAttributes.set(CertSubjectAttributeType.COMMON_NAME, request.commonName);
if (request.organization || request.organizationName) {
requestAttributes.set(CertSubjectAttributeType.ORGANIZATION, request.organization || request.organizationName!);
}
if (request.country) requestAttributes.set(CertSubjectAttributeType.COUNTRY, request.country);
const attributePoliciesByType = new Map<string, typeof template.attributes>();
template.attributes?.forEach((attrPolicy) => {
const existing = attributePoliciesByType.get(attrPolicy.type) || [];
attributePoliciesByType.set(attrPolicy.type, [...existing, attrPolicy]);
if (subjectPolicies && subjectPolicies.length > 0) {
// Validate each template subject attribute policy
for (const attrPolicy of subjectPolicies) {
const requestValue = requestAttributes.get(attrPolicy.type);
// Check denied values first
if (requestValue && attrPolicy.denied && attrPolicy.denied.length > 0) {
const validation = validateValueAgainstConstraints(requestValue, attrPolicy.denied, attrPolicy.type);
if (validation.isValid) {
errors.push(`${attrPolicy.type} value '${requestValue}' is denied by template policy`);
// Skip further validation for this attribute if it's denied
} else if (requestValue && attrPolicy.allowed && attrPolicy.allowed.length > 0) {
// Check allowed values if present and not denied
const allowedValidation = validateValueAgainstConstraints(
requestValue,
attrPolicy.allowed,
attrPolicy.type
);
if (!allowedValidation.isValid && allowedValidation.error) {
errors.push(allowedValidation.error);
}
}
} else if (requestValue && attrPolicy.allowed && attrPolicy.allowed.length > 0) {
// Check allowed values if present and not denied
const allowedValidation = validateValueAgainstConstraints(requestValue, attrPolicy.allowed, attrPolicy.type);
if (!allowedValidation.isValid && allowedValidation.error) {
errors.push(allowedValidation.error);
}
}
}
// Check for required subject attributes
for (const attrPolicy of subjectPolicies) {
if (attrPolicy.required && attrPolicy.required.length > 0) {
const requestValue = requestAttributes.get(attrPolicy.type);
if (!requestValue) {
errors.push(`Missing required ${attrPolicy.type} attribute`);
} else {
// Validate that the request value matches at least one required pattern
const hasMatchingRequired = attrPolicy.required.some((requiredValue) => {
const validation = validateValueAgainstConstraints(requestValue, [requiredValue], attrPolicy.type);
return validation.isValid;
});
if (!hasMatchingRequired) {
errors.push(
`${attrPolicy.type} value '${requestValue}' does not match any required patterns: ${attrPolicy.required.join(", ")}`
);
}
}
}
}
// Check if any request attributes are not covered by template policies
for (const [attrType] of requestAttributes) {
const hasPolicy = subjectPolicies.some((policy) => policy.type === attrType);
if (!hasPolicy) {
errors.push(`${attrType} is not allowed by template policy (not defined in template)`);
}
}
} else if (requestAttributes.size > 0) {
// No subject policies defined but request has subject attributes - deny all
for (const [attrType] of requestAttributes) {
errors.push(`${attrType} is not allowed by template policy (no subject policies defined)`);
}
}
// Validate Subject Alternative Names
const sansPolicies = template.sans;
if (sansPolicies && sansPolicies.length > 0) {
const requestSansByType = new Map<string, string[]>();
// Group request SANs by type
if (request.subjectAlternativeNames) {
for (const san of request.subjectAlternativeNames) {
if (!requestSansByType.has(san.type)) {
requestSansByType.set(san.type, []);
}
requestSansByType.get(san.type)!.push(san.value);
}
}
// Validate each SAN policy
for (const sanPolicy of sansPolicies) {
const requestSans = requestSansByType.get(sanPolicy.type) || [];
// Check REQUIRED values - at least one SAN must match each required pattern
if (sanPolicy.required && sanPolicy.required.length > 0) {
for (const requiredValue of sanPolicy.required) {
const hasMatchingRequiredSan = requestSans.some((sanValue) => {
const validation = validateValueAgainstConstraints(sanValue, [requiredValue], `${sanPolicy.type} SAN`);
return validation.isValid;
});
for (const [attrType, policies] of attributePoliciesByType) {
const requestValue = getRequestAttributeValue(request, attrType);
const hasMandatory = policies.some((p) => p.include === CertIncludeType.MANDATORY);
const hasProhibit = policies.some((p) => p.include === CertIncludeType.PROHIBIT);
if (hasProhibit && requestValue) {
errors.push(`${attrType} is prohibited by template policy`);
// eslint-disable-next-line no-continue
continue;
if (!hasMatchingRequiredSan) {
errors.push(`Required ${sanPolicy.type} SAN matching pattern '${requiredValue}' not found in request`);
}
}
}
if (hasMandatory && !requestValue) {
errors.push(`${attrType} is mandatory but not provided in request`);
// eslint-disable-next-line no-continue
continue;
// Check DENIED values - no SAN should match denied patterns
if (sanPolicy.denied && sanPolicy.denied.length > 0) {
for (const sanValue of requestSans) {
const validation = validateValueAgainstConstraints(sanValue, sanPolicy.denied, `${sanPolicy.type} SAN`);
if (validation.isValid) {
errors.push(`${sanPolicy.type} SAN matching denied pattern '${sanValue}' found in request`);
}
}
}
if (requestValue) {
const policiesWithValues = policies.filter(
(p) =>
p.value &&
p.value.length > 0 &&
(p.include === CertIncludeType.MANDATORY || p.include === CertIncludeType.OPTIONAL)
);
if (policiesWithValues.length > 0) {
const allAllowedValues = policiesWithValues.flatMap((p) => p.value || []);
const validation = validateValueAgainstConstraints(requestValue, allAllowedValues, attrType);
// Check ALLOWED values - if present, all SANs must match at least one allowed pattern
if (sanPolicy.allowed && sanPolicy.allowed.length > 0 && requestSans.length > 0) {
for (const sanValue of requestSans) {
const validation = validateValueAgainstConstraints(sanValue, sanPolicy.allowed, `${sanPolicy.type} SAN`);
if (!validation.isValid && validation.error) {
errors.push(validation.error);
}
@@ -258,44 +415,57 @@ export const certificateTemplateV2ServiceFactory = ({
}
}
const requestAttributeTypes: CertSubjectAttributeType[] = [];
if (request.commonName) requestAttributeTypes.push(CertSubjectAttributeType.COMMON_NAME);
// Check if any request SANs are for types not covered by template policies
for (const [requestSanType] of requestSansByType) {
const hasPolicy = sansPolicies.some((policy) => policy.type === requestSanType);
if (!hasPolicy) {
errors.push(`${requestSanType} SAN is not allowed by template policy (not defined in template)`);
}
}
} else if (request.subjectAlternativeNames && request.subjectAlternativeNames.length > 0) {
// No SAN policies defined but request has SANs - deny all
for (const san of request.subjectAlternativeNames) {
errors.push(`${san.type} SAN is not allowed by template policy (no SAN policies defined)`);
}
}
for (const requestAttrType of requestAttributeTypes) {
if (!templateAttributeTypes.has(requestAttrType)) {
errors.push(`${requestAttrType} is not allowed by template policy (not defined in template)`);
}
}
if (template.keyUsages) {
if (template.keyUsages.requiredUsages && template.keyUsages.requiredUsages.all.length > 0) {
const missingRequired = template.keyUsages.requiredUsages.all.filter(
(usage) => !request.keyUsages?.includes(usage)
);
// Validate key usages
const keyUsagePolicy = template.keyUsages;
if (keyUsagePolicy) {
// Check REQUIRED key usages - must have all required usages
if (keyUsagePolicy.required && keyUsagePolicy.required.length > 0) {
const missingRequired = keyUsagePolicy.required.filter((usage) => !request.keyUsages?.includes(usage));
if (missingRequired.length > 0) {
errors.push(`Missing required key usages: ${missingRequired.join(", ")}`);
}
}
if (request.keyUsages && (template.keyUsages.requiredUsages || template.keyUsages.optionalUsages)) {
const allAllowedUsages = [
...(template.keyUsages.requiredUsages?.all || []),
...(template.keyUsages.optionalUsages?.all || [])
];
// Check DENIED key usages - must not have any denied usages
if (request.keyUsages && keyUsagePolicy.denied && keyUsagePolicy.denied.length > 0) {
const deniedUsages = request.keyUsages.filter((usage) => keyUsagePolicy?.denied?.includes(usage));
if (deniedUsages.length > 0) {
errors.push(`Denied key usages found in request: ${deniedUsages.join(", ")}`);
}
}
if (allAllowedUsages.length > 0) {
// Check ALLOWED key usages - if present, all usages must be in allowed list
if (request.keyUsages && keyUsagePolicy && keyUsagePolicy.allowed && keyUsagePolicy.allowed.length > 0) {
const allAllowedUsages = [...(keyUsagePolicy.required || []), ...(keyUsagePolicy.allowed || [])];
const invalidUsages = request.keyUsages.filter((usage) => !allAllowedUsages.includes(usage));
if (invalidUsages.length > 0) {
errors.push(`Invalid key usages: ${invalidUsages.join(", ")}`);
}
}
}
} else if (request.keyUsages && request.keyUsages.length > 0) {
errors.push(`Key usages are not allowed by template policy (not defined in template)`);
}
if (template.extendedKeyUsages) {
if (template.extendedKeyUsages.requiredUsages && template.extendedKeyUsages.requiredUsages.all.length > 0) {
const missingRequired = template.extendedKeyUsages.requiredUsages.all.filter(
// Validate extended key usages
const extendedKeyUsagePolicy = template.extendedKeyUsages;
if (extendedKeyUsagePolicy) {
// Check REQUIRED extended key usages - must have all required usages
if (extendedKeyUsagePolicy.required && extendedKeyUsagePolicy.required.length > 0) {
const missingRequired = extendedKeyUsagePolicy.required.filter(
(usage) => !request.extendedKeyUsages?.includes(usage)
);
if (missingRequired.length > 0) {
@@ -303,89 +473,46 @@ export const certificateTemplateV2ServiceFactory = ({
}
}
// Check DENIED extended key usages - must not have any denied usages
if (request.extendedKeyUsages && extendedKeyUsagePolicy.denied && extendedKeyUsagePolicy.denied.length > 0) {
const deniedUsages = request.extendedKeyUsages.filter((usage) =>
extendedKeyUsagePolicy?.denied?.includes(usage)
);
if (deniedUsages.length > 0) {
errors.push(`Denied extended key usages found in request: ${deniedUsages.join(", ")}`);
}
}
// Check ALLOWED extended key usages - if present, all usages must be in allowed list
if (
request.extendedKeyUsages &&
(template.extendedKeyUsages.requiredUsages || template.extendedKeyUsages.optionalUsages)
extendedKeyUsagePolicy &&
extendedKeyUsagePolicy.allowed &&
extendedKeyUsagePolicy.allowed.length > 0
) {
const allAllowedUsages = [
...(template.extendedKeyUsages.requiredUsages?.all || []),
...(template.extendedKeyUsages.optionalUsages?.all || [])
const allAllowedExtendedUsages = [
...(extendedKeyUsagePolicy.required || []),
...(extendedKeyUsagePolicy.allowed || [])
];
if (allAllowedUsages.length > 0) {
const invalidUsages = request.extendedKeyUsages.filter((usage) => !allAllowedUsages.includes(usage));
if (invalidUsages.length > 0) {
errors.push(`Invalid extended key usages: ${invalidUsages.join(", ")}`);
}
const invalidExtendedUsages = request.extendedKeyUsages.filter(
(usage) => !allAllowedExtendedUsages.includes(usage)
);
if (invalidExtendedUsages.length > 0) {
errors.push(`Invalid extended key usages: ${invalidExtendedUsages.join(", ")}`);
}
}
} else if (request.extendedKeyUsages && request.extendedKeyUsages.length > 0) {
errors.push(`Extended key usages are not allowed by template policy (not defined in template)`);
}
const templateSanTypes = new Set(template.subjectAlternativeNames?.map((san) => san.type) || []);
const sanPoliciesByType = new Map<string, typeof template.subjectAlternativeNames>();
template.subjectAlternativeNames?.forEach((sanPolicy) => {
const existing = sanPoliciesByType.get(sanPolicy.type) || [];
sanPoliciesByType.set(sanPolicy.type, [...existing, sanPolicy]);
});
for (const [sanType, policies] of sanPoliciesByType) {
const requestSans = request.subjectAlternativeNames?.filter((san) => san.type === sanType) || [];
const hasMandatory = policies.some((p) => p.include === CertIncludeType.MANDATORY);
const hasProhibit = policies.some((p) => p.include === CertIncludeType.PROHIBIT);
if (hasProhibit && requestSans.length > 0) {
errors.push(`${sanType} SAN is prohibited by template policy`);
// eslint-disable-next-line no-continue
continue;
}
if (hasMandatory && requestSans.length === 0) {
errors.push(`${sanType} SAN is mandatory but not provided in request`);
// eslint-disable-next-line no-continue
continue;
}
if (requestSans.length > 0) {
const policiesWithValues = policies.filter(
(p) =>
p.value &&
p.value.length > 0 &&
(p.include === CertIncludeType.MANDATORY || p.include === CertIncludeType.OPTIONAL)
);
if (policiesWithValues.length > 0) {
const allAllowedValues = policiesWithValues.flatMap((p) => p.value || []);
requestSans.forEach((san) => {
const validation = validateValueAgainstConstraints(san.value, allAllowedValues, `${sanType} SAN`);
if (!validation.isValid && validation.error) {
errors.push(validation.error);
}
});
}
}
}
const requestSanTypes = new Set(request.subjectAlternativeNames?.map((san) => san.type) || []);
for (const requestSanType of requestSanTypes) {
if (!templateSanTypes.has(requestSanType)) {
errors.push(`${requestSanType} SAN is not allowed by template policy (not defined in template)`);
}
}
// Validate algorithms with new structure
if (request.signatureAlgorithm) {
if (template.signatureAlgorithm && template.signatureAlgorithm.allowedAlgorithms) {
const mappedTemplateAlgorithms = template.signatureAlgorithm.allowedAlgorithms.map(
mapTemplateSignatureAlgorithmToApi
);
if (template.algorithms?.signature && template.algorithms.signature.length > 0) {
const mappedTemplateAlgorithms = template.algorithms.signature.map(mapTemplateSignatureAlgorithmToApi);
if (!mappedTemplateAlgorithms.includes(request.signatureAlgorithm)) {
errors.push(`Signature algorithm '${request.signatureAlgorithm}' is not allowed by template policy`);
}
} else if (!template.signatureAlgorithm) {
} else if (!template.algorithms?.signature) {
errors.push(
`Signature algorithm '${request.signatureAlgorithm}' is not allowed by template policy (not defined in template)`
);
@@ -393,40 +520,19 @@ export const certificateTemplateV2ServiceFactory = ({
}
if (request.keyAlgorithm) {
if (template.keyAlgorithm && template.keyAlgorithm.allowedKeyTypes) {
const mappedTemplateKeyTypes = template.keyAlgorithm.allowedKeyTypes.map(mapTemplateKeyAlgorithmToApi);
if (template.algorithms?.keyAlgorithm && template.algorithms.keyAlgorithm.length > 0) {
const mappedTemplateKeyTypes = template.algorithms.keyAlgorithm.map(mapTemplateKeyAlgorithmToApi);
if (!mappedTemplateKeyTypes.includes(request.keyAlgorithm)) {
errors.push(`Key algorithm '${request.keyAlgorithm}' is not allowed by template policy`);
}
} else if (!template.keyAlgorithm) {
} else if (!template.algorithms?.keyAlgorithm) {
errors.push(
`Key algorithm '${request.keyAlgorithm}' is not allowed by template policy (not defined in template)`
);
}
}
if (request.validity?.ttl && template.validity) {
const requestDuration = parseTTL(request.validity.ttl);
const maxDuration = convertToMilliseconds(
template.validity.maxDuration.value,
template.validity.maxDuration.unit
);
if (requestDuration > maxDuration) {
errors.push(`Requested validity period exceeds maximum allowed duration`);
}
if (template.validity.minDuration) {
const minDuration = convertToMilliseconds(
template.validity.minDuration.value,
template.validity.minDuration.unit
);
if (requestDuration < minDuration) {
errors.push(`Requested validity period is below minimum required duration`);
}
}
}
// Validate validity with new structure
if (request.validity?.ttl && (request.notBefore || request.notAfter)) {
errors.push(
"Cannot specify both TTL and notBefore/notAfter. Use either TTL for duration-based validity or notBefore/notAfter for explicit date range."
@@ -437,30 +543,32 @@ export const certificateTemplateV2ServiceFactory = ({
errors.push("notBefore must be earlier than notAfter");
}
if ((request.notBefore || request.notAfter) && template.validity) {
// Validate TTL against template validity constraints
if (request.validity?.ttl && template.validity) {
const requestDurationMs = parseTTL(request.validity.ttl);
// Check maximum duration using max field
if (template.validity.max) {
const maxDurationMs = parseTTL(template.validity.max);
if (requestDurationMs > maxDurationMs) {
errors.push("Requested validity period exceeds maximum allowed duration");
}
}
}
// Validate explicit date range against max duration
if ((request.notBefore || request.notAfter) && template.validity?.max) {
const notBefore = request.notBefore || new Date();
const { notAfter } = request;
if (notAfter && notBefore && notAfter instanceof Date && notBefore instanceof Date) {
const requestDuration = notAfter.getTime() - notBefore.getTime();
const maxDurationMs = parseTTL(template.validity.max);
const maxDuration = convertToMilliseconds(
template.validity.maxDuration.value,
template.validity.maxDuration.unit
if (requestDuration > maxDurationMs) {
errors.push(
`Requested validity period (notBefore to notAfter) exceeds maximum allowed duration of ${template.validity.max}`
);
if (requestDuration > maxDuration) {
errors.push(`Requested validity period (notBefore to notAfter) exceeds maximum allowed duration`);
}
if (template.validity.minDuration) {
const minDuration = convertToMilliseconds(
template.validity.minDuration.value,
template.validity.minDuration.unit
);
if (requestDuration < minDuration) {
errors.push(`Requested validity period (notBefore to notAfter) is below minimum required duration`);
}
}
}
}
@@ -505,16 +613,33 @@ export const certificateTemplateV2ServiceFactory = ({
throw new Error("Template data is required");
}
if (data.attributes) {
validateSubjectAttributePolicy(data.attributes);
if (data.subject) {
validateSubjectAttributePolicy(data.subject);
}
const slug = data.slug || generateTemplateSlug();
if (data.sans) {
validateSanPolicy(data.sans);
}
if (data.keyUsages) {
validateKeyUsagePolicy(data.keyUsages);
}
if (data.extendedKeyUsages) {
validateExtendedKeyUsagePolicy(data.extendedKeyUsages);
}
// Generate slug from name and ensure it's unique within project
if (!data.name) {
throw new ForbiddenRequestError({ message: "Template name is required" });
}
const slug = generateTemplateSlug(data.name);
const uniqueSlug = await ensureUniqueSlug(projectId, slug);
const template = await certificateTemplateV2DAL.create({
...data,
slug: uniqueSlug,
name: uniqueSlug,
projectId
});
@@ -555,14 +680,29 @@ export const certificateTemplateV2ServiceFactory = ({
ProjectPermissionSub.CertificateTemplates
);
if (data.attributes) {
validateSubjectAttributePolicy(data.attributes);
if (data.subject) {
validateSubjectAttributePolicy(data.subject);
}
if (data.sans) {
validateSanPolicy(data.sans);
}
if (data.keyUsages) {
validateKeyUsagePolicy(data.keyUsages);
}
if (data.extendedKeyUsages) {
validateExtendedKeyUsagePolicy(data.extendedKeyUsages);
}
const updateData = { ...data };
if (data.slug && typeof data.slug === "string" && data.slug !== existingTemplate.slug) {
const uniqueSlug = await ensureUniqueSlug(existingTemplate.projectId, data.slug, templateId);
updateData.slug = uniqueSlug;
if (data.name && typeof data.name === "string") {
const newSlug = generateTemplateSlug(data.name);
if (newSlug !== existingTemplate.name) {
const uniqueSlug = await ensureUniqueSlug(existingTemplate.projectId, newSlug, templateId);
updateData.name = uniqueSlug;
}
}
const updatedTemplate = await certificateTemplateV2DAL.updateById(templateId, updateData);
@@ -636,7 +776,7 @@ export const certificateTemplateV2ServiceFactory = ({
ProjectPermissionSub.CertificateTemplates
);
const template = await certificateTemplateV2DAL.findBySlugAndProjectId(slug, projectId);
const template = await certificateTemplateV2DAL.findByNameAndProjectId(slug, projectId);
if (!template) {
throw new NotFoundError({ message: "Certificate template not found" });
}
@@ -734,8 +874,8 @@ export const certificateTemplateV2ServiceFactory = ({
throw new ForbiddenRequestError({
message:
profilesUsingTemplate.length > 0
? `Cannot delete template '${template.slug}' as it is currently in use by the following certificate profiles: ${profileNames}. Please remove this template from these profiles before deleting it.`
: `Cannot delete template '${template.slug}' as it is currently in use by one or more certificates. Please ensure no certificates are using this template before deleting it.`
? `Cannot delete template '${template.name}' as it is currently in use by the following certificate profiles: ${profileNames}. Please remove this template from these profiles before deleting it.`
: `Cannot delete template '${template.name}' as it is currently in use by one or more certificates. Please ensure no certificates are using this template before deleting it.`
});
}
@@ -1,101 +1,71 @@
import { TCertificateTemplatesV2, TCertificateTemplatesV2Insert } from "@app/db/schemas/certificate-templates-v2";
import {
CertDurationUnit,
CertExtendedKeyUsageType,
CertIncludeType,
CertKeyUsageType,
CertSubjectAlternativeNameType,
CertSubjectAttributeType
} from "@app/services/certificate-common/certificate-constants";
export interface TTemplateV2Policy {
attributes: Array<{
subject?: Array<{
type: CertSubjectAttributeType;
include: CertIncludeType;
value?: string[];
allowed?: string[];
required?: string[];
denied?: string[];
}>;
keyUsages: {
requiredUsages?: { all: CertKeyUsageType[] };
optionalUsages?: { all: CertKeyUsageType[] };
};
extendedKeyUsages: {
requiredUsages?: { all: CertExtendedKeyUsageType[] };
optionalUsages?: { all: CertExtendedKeyUsageType[] };
};
subjectAlternativeNames: Array<{
sans?: Array<{
type: CertSubjectAlternativeNameType;
include: CertIncludeType;
value?: string[];
allowed?: string[];
required?: string[];
denied?: string[];
}>;
validity: {
maxDuration: { value: number; unit: CertDurationUnit };
minDuration?: { value: number; unit: CertDurationUnit };
keyUsages?: {
allowed?: CertKeyUsageType[];
required?: CertKeyUsageType[];
denied?: CertKeyUsageType[];
};
signatureAlgorithm: {
allowedAlgorithms: string[];
defaultAlgorithm: string;
extendedKeyUsages?: {
allowed?: CertExtendedKeyUsageType[];
required?: CertExtendedKeyUsageType[];
denied?: CertExtendedKeyUsageType[];
};
keyAlgorithm: {
allowedKeyTypes: string[];
defaultKeyType: string;
algorithms?: {
signature?: string[];
keyAlgorithm?: string[];
};
validity?: {
max?: string;
};
}
export type TCertificateTemplateV2 = Omit<
TCertificateTemplatesV2,
| "attributes"
| "keyUsages"
| "extendedKeyUsages"
| "subjectAlternativeNames"
| "validity"
| "signatureAlgorithm"
| "keyAlgorithm"
> & {
attributes: TTemplateV2Policy["attributes"];
keyUsages: TTemplateV2Policy["keyUsages"];
extendedKeyUsages: TTemplateV2Policy["extendedKeyUsages"];
subjectAlternativeNames: TTemplateV2Policy["subjectAlternativeNames"];
validity: TTemplateV2Policy["validity"];
signatureAlgorithm: TTemplateV2Policy["signatureAlgorithm"];
keyAlgorithm: TTemplateV2Policy["keyAlgorithm"];
};
export type TCertificateTemplateV2Insert = Omit<
TCertificateTemplatesV2Insert,
| "attributes"
| "keyUsages"
| "extendedKeyUsages"
| "subjectAlternativeNames"
| "validity"
| "signatureAlgorithm"
| "keyAlgorithm"
> & {
attributes?: TTemplateV2Policy["attributes"];
export type TCertificateTemplateV2 = TCertificateTemplatesV2 & {
subject?: TTemplateV2Policy["subject"];
sans?: TTemplateV2Policy["sans"];
keyUsages?: TTemplateV2Policy["keyUsages"];
extendedKeyUsages?: TTemplateV2Policy["extendedKeyUsages"];
subjectAlternativeNames?: TTemplateV2Policy["subjectAlternativeNames"];
algorithms?: TTemplateV2Policy["algorithms"];
validity?: TTemplateV2Policy["validity"];
};
export type TCertificateTemplateV2Insert = TCertificateTemplatesV2Insert & {
subject?: TTemplateV2Policy["subject"];
sans?: TTemplateV2Policy["sans"];
keyUsages?: TTemplateV2Policy["keyUsages"];
extendedKeyUsages?: TTemplateV2Policy["extendedKeyUsages"];
algorithms?: TTemplateV2Policy["algorithms"];
validity?: TTemplateV2Policy["validity"];
signatureAlgorithm?: TTemplateV2Policy["signatureAlgorithm"];
keyAlgorithm?: TTemplateV2Policy["keyAlgorithm"];
};
export type TCertificateTemplateV2Update = Partial<
Pick<
TCertificateTemplateV2,
| "slug"
| "description"
| "attributes"
| "keyUsages"
| "extendedKeyUsages"
| "subjectAlternativeNames"
| "validity"
| "signatureAlgorithm"
| "keyAlgorithm"
"name" | "description" | "subject" | "sans" | "keyUsages" | "extendedKeyUsages" | "algorithms" | "validity"
>
>;
export interface TCertificateRequest {
commonName?: string;
organization?: string;
organizationName?: string;
organizationUnit?: string;
locality?: string;
@@ -102,12 +102,12 @@ const validateCaSupport = (ca: TCertificateAuthorityWithAssociatedCa, operation:
const validateAlgorithmCompatibility = (
ca: TCertificateAuthorityWithAssociatedCa,
template: {
signatureAlgorithm?: {
allowedAlgorithms?: string[];
algorithms?: {
signature?: string[];
};
}
) => {
if (!template.signatureAlgorithm || !template.signatureAlgorithm.allowedAlgorithms) {
if (!template.algorithms?.signature || template.algorithms.signature.length === 0) {
return;
}
@@ -116,7 +116,7 @@ const validateAlgorithmCompatibility = (
throw new BadRequestError({ message: "CA key algorithm not found" });
}
const compatibleAlgorithms = template.signatureAlgorithm.allowedAlgorithms.filter((sigAlg: string) => {
const compatibleAlgorithms = template.algorithms.signature.filter((sigAlg: string) => {
const parts = sigAlg.split("-");
const keyType = parts[parts.length - 1];
@@ -133,7 +133,7 @@ const validateAlgorithmCompatibility = (
if (compatibleAlgorithms.length === 0) {
throw new BadRequestError({
message: `Template signature algorithms (${template.signatureAlgorithm.allowedAlgorithms.join(", ")}) are not compatible with CA key algorithm (${caKeyAlgorithm})`
message: `Template signature algorithms (${template.algorithms.signature.join(", ")}) are not compatible with CA key algorithm (${caKeyAlgorithm})`
});
}
};
@@ -180,7 +180,10 @@ export const certificateV3ServiceFactory = ({
});
}
const mappedCertificateRequest = mapEnumsForValidation(certificateRequest);
const mappedCertificateRequest = mapEnumsForValidation({
...certificateRequest,
subjectAlternativeNames: certificateRequest.altNames
});
const validationResult = await certificateTemplateV2Service.validateCertificateRequest(
profile.certificateTemplateId,
mappedCertificateRequest
@@ -217,26 +220,25 @@ export const certificateV3ServiceFactory = ({
validateAlgorithmCompatibility(ca, template);
const effectiveSignatureAlgorithm =
certificateRequest.signatureAlgorithm || template.signatureAlgorithm?.defaultAlgorithm;
const effectiveKeyAlgorithm = certificateRequest.keyAlgorithm || template.keyAlgorithm?.defaultKeyType;
const effectiveSignatureAlgorithm = certificateRequest.signatureAlgorithm;
const effectiveKeyAlgorithm = certificateRequest.keyAlgorithm;
if (template.keyAlgorithm?.allowedKeyTypes && !effectiveKeyAlgorithm) {
if (template.algorithms?.keyAlgorithm && !effectiveKeyAlgorithm) {
throw new BadRequestError({
message: "Key algorithm is required by template policy but not provided in request or template default"
message: "Key algorithm is required by template policy but not provided in request"
});
}
if (template.signatureAlgorithm?.allowedAlgorithms && !effectiveSignatureAlgorithm) {
if (template.algorithms?.signature && !effectiveSignatureAlgorithm) {
throw new BadRequestError({
message: "Signature algorithm is required by template policy but not provided in request or template default"
message: "Signature algorithm is required by template policy but not provided in request"
});
}
const certificateSubject = buildCertificateSubjectFromTemplate(certificateRequest, template.attributes);
const certificateSubject = buildCertificateSubjectFromTemplate(certificateRequest, template.subject);
const subjectAlternativeNames = buildSubjectAlternativeNamesFromTemplate(
{ subjectAlternativeNames: certificateRequest.altNames },
template.subjectAlternativeNames
template.sans
);
const { certificate, certificateChain, issuingCaCertificate, privateKey, serialNumber } =
@@ -326,18 +328,18 @@ export const certificateV3ServiceFactory = ({
validateAlgorithmCompatibility(ca, template);
const effectiveSignatureAlgorithm = signatureAlgorithm || template.signatureAlgorithm?.defaultAlgorithm;
const effectiveKeyAlgorithm = keyAlgorithm || template.keyAlgorithm?.defaultKeyType;
const effectiveSignatureAlgorithm = signatureAlgorithm;
const effectiveKeyAlgorithm = keyAlgorithm;
if (template.keyAlgorithm?.allowedKeyTypes && !effectiveKeyAlgorithm) {
if (template.algorithms?.keyAlgorithm && !effectiveKeyAlgorithm) {
throw new BadRequestError({
message: "Key algorithm is required by template policy but not provided in request or template default"
message: "Key algorithm is required by template policy but not provided in request"
});
}
if (template.signatureAlgorithm?.allowedAlgorithms && !effectiveSignatureAlgorithm) {
if (template.algorithms?.signature && !effectiveSignatureAlgorithm) {
throw new BadRequestError({
message: "Signature algorithm is required by template policy but not provided in request or template default"
message: "Signature algorithm is required by template policy but not provided in request"
});
}
+1 -1
View File
@@ -1,5 +1,4 @@
export { AcmeDnsProvider, CaRenewalType, CaStatus, CaType, InternalCaType } from "./enums";
export type { TOrderCertificateDTO, TOrderCertificateResponse } from "./types";
export {
useCreateCa,
useCreateCertificate,
@@ -24,3 +23,4 @@ export {
useListCasByTypeAndProjectId,
useListExternalCasByProjectId
} from "./queries";
export type { TOrderCertificateDTO, TOrderCertificateResponse } from "./types";
@@ -123,78 +123,74 @@ export type TListCertificateTemplatesDTO = {
};
export type TCertificateTemplateV2Policy = {
attributes: Array<{
type: "common_name";
include: "mandatory" | "optional" | "prohibit";
value?: string[];
subject?: Array<{
type: "common_name" | "organization" | "country";
allowed?: string[];
required?: string[];
denied?: string[];
}>;
keyUsages: {
requiredUsages: { all: string[] };
optionalUsages: { all: string[] };
};
extendedKeyUsages: {
requiredUsages: { all: string[] };
optionalUsages: { all: string[] };
};
subjectAlternativeNames: Array<{
sans?: Array<{
type: "dns_name" | "ip_address" | "email" | "uri";
include: "mandatory" | "optional" | "prohibit";
value?: string[];
allowed?: string[];
required?: string[];
denied?: string[];
}>;
validity: {
maxDuration: { value: number; unit: "days" | "months" | "years" };
minDuration?: { value: number; unit: "days" | "months" | "years" };
keyUsages?: {
allowed?: string[];
required?: string[];
denied?: string[];
};
signatureAlgorithm: {
allowedAlgorithms: string[];
defaultAlgorithm: string;
extendedKeyUsages?: {
allowed?: string[];
required?: string[];
denied?: string[];
};
keyAlgorithm: {
allowedKeyTypes: string[];
defaultKeyType: string;
algorithms?: {
signature?: string[];
keyAlgorithm?: string[];
};
validity?: {
max?: string;
};
};
export type TCertificateTemplateV2New = {
id: string;
projectId: string;
slug: string;
name: string;
description?: string;
attributes: any;
keyUsages: any;
extendedKeyUsages: any;
subjectAlternativeNames: any;
validity: any;
signatureAlgorithm: any;
keyAlgorithm: any;
subject?: TCertificateTemplateV2Policy["subject"];
sans?: TCertificateTemplateV2Policy["sans"];
keyUsages?: TCertificateTemplateV2Policy["keyUsages"];
extendedKeyUsages?: TCertificateTemplateV2Policy["extendedKeyUsages"];
algorithms?: TCertificateTemplateV2Policy["algorithms"];
validity?: TCertificateTemplateV2Policy["validity"];
createdAt: string;
updatedAt: string;
};
export type TCreateCertificateTemplateV2NewDTO = {
projectId: string;
slug: string;
name: string;
description?: string;
attributes: TCertificateTemplateV2Policy["attributes"];
keyUsages: TCertificateTemplateV2Policy["keyUsages"];
extendedKeyUsages: TCertificateTemplateV2Policy["extendedKeyUsages"];
subjectAlternativeNames: TCertificateTemplateV2Policy["subjectAlternativeNames"];
validity: TCertificateTemplateV2Policy["validity"];
signatureAlgorithm: TCertificateTemplateV2Policy["signatureAlgorithm"];
keyAlgorithm: TCertificateTemplateV2Policy["keyAlgorithm"];
subject?: TCertificateTemplateV2Policy["subject"];
sans?: TCertificateTemplateV2Policy["sans"];
keyUsages?: TCertificateTemplateV2Policy["keyUsages"];
extendedKeyUsages?: TCertificateTemplateV2Policy["extendedKeyUsages"];
algorithms?: TCertificateTemplateV2Policy["algorithms"];
validity?: TCertificateTemplateV2Policy["validity"];
};
export type TUpdateCertificateTemplateV2NewDTO = {
templateId: string;
slug?: string;
name?: string;
description?: string;
attributes?: TCertificateTemplateV2Policy["attributes"];
subject?: TCertificateTemplateV2Policy["subject"];
sans?: TCertificateTemplateV2Policy["sans"];
keyUsages?: TCertificateTemplateV2Policy["keyUsages"];
extendedKeyUsages?: TCertificateTemplateV2Policy["extendedKeyUsages"];
subjectAlternativeNames?: TCertificateTemplateV2Policy["subjectAlternativeNames"];
algorithms?: TCertificateTemplateV2Policy["algorithms"];
validity?: TCertificateTemplateV2Policy["validity"];
signatureAlgorithm?: TCertificateTemplateV2Policy["signatureAlgorithm"];
keyAlgorithm?: TCertificateTemplateV2Policy["keyAlgorithm"];
};
export type TDeleteCertificateTemplateV2NewDTO = {
@@ -53,10 +53,10 @@ export const PkiManagerLayout = () => {
animate={{ x: 0 }}
exit={{ x: -150 }}
transition={{ duration: 0.2 }}
className="border-mineshaft-600 bg-linear-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 dark w-full border-r md:w-60"
className="dark w-full border-r border-mineshaft-600 bg-linear-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60"
>
<nav className="items-between dark:scheme-dark flex h-full flex-col overflow-y-auto">
<div className="border-mineshaft-600 flex items-center gap-3 border-b px-4 py-3.5 text-lg text-white">
<nav className="items-between flex h-full flex-col overflow-y-auto dark:scheme-dark">
<div className="flex items-center gap-3 border-b border-mineshaft-600 px-4 py-3.5 text-lg text-white">
<Lottie className="inline-block h-5 w-5 shrink-0" icon="note" />
PKI Manager
</div>
@@ -267,7 +267,7 @@ export const PkiManagerLayout = () => {
<Menu>
<Link to="/organization/projects">
<MenuItem
className="text-mineshaft-400 hover:text-mineshaft-300 relative flex items-center gap-2 overflow-hidden text-sm"
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300"
leftIcon={
<div className="w-6">
<FontAwesomeIcon className="mx-1 inline-block shrink-0" icon={faHome} />
@@ -281,13 +281,13 @@ export const PkiManagerLayout = () => {
</div>
</nav>
</motion.div>
<div className="bg-bunker-800 flex-1 overflow-y-auto overflow-x-hidden p-4 pt-8">
<div className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 p-4 pt-8">
{assumedPrivilegeDetails && <AssumePrivilegeModeBanner />}
<Outlet />
</div>
</div>
</div>
<div className="z-200 bg-bunker-800 flex h-screen w-screen flex-col items-center justify-center md:hidden">
<div className="z-200 flex h-screen w-screen flex-col items-center justify-center bg-bunker-800 md:hidden">
<FontAwesomeIcon icon={faMobile} className="mb-8 text-7xl text-gray-300" />
<p className="max-w-sm px-6 text-center text-lg text-gray-200">
{` ${t("common.no-mobile")} `}
@@ -1,9 +1,6 @@
/* eslint-disable react/no-array-index-key */
/* eslint-disable no-nested-ternary */
import { useEffect, useState } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { faQuestionCircle } from "@fortawesome/free-regular-svg-icons";
import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
import { faPlus, faQuestionCircle, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
@@ -35,13 +32,13 @@ import {
KEY_USAGES_OPTIONS,
SIGNATURE_ALGORITHMS_OPTIONS
} from "@app/hooks/api/certificates/constants";
import {
CertExtendedKeyUsage,
CertKeyAlgorithm,
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 { UsePopUpState } from "@app/hooks/usePopUp";
import {
mapTemplateKeyAlgorithmToApi,
mapTemplateSignatureAlgorithmToApi
} from "@app/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/shared/certificate-constants";
import { CertificateContent } from "./CertificateContent";
@@ -109,9 +106,12 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
const [certificateDetails, setCertificateDetails] = useState<TCertificateDetails | null>(null);
const [allowedKeyUsages, setAllowedKeyUsages] = useState<string[]>([]);
const [allowedExtendedKeyUsages, setAllowedExtendedKeyUsages] = useState<string[]>([]);
const [requiredKeyUsages, setRequiredKeyUsages] = useState<string[]>([]);
const [requiredExtendedKeyUsages, setRequiredExtendedKeyUsages] = useState<string[]>([]);
const [allowedSignatureAlgorithms, setAllowedSignatureAlgorithms] = useState<string[]>([]);
const [allowedKeyAlgorithms, setAllowedKeyAlgorithms] = useState<string[]>([]);
const { currentProject } = useProject();
const { data: cert } = useGetCert(
(popUp?.certificateIssuance?.data as { serialNumber: string })?.serialNumber || ""
);
@@ -133,7 +133,7 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
} = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: {
profileId: profileId ? profileId : "",
profileId: profileId || "",
subjectAttributes: [{ type: "common_name", value: "" }],
subjectAltNames: [],
ttl: "30d",
@@ -144,276 +144,122 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
}
});
const resetAllState = useCallback(() => {
setCertificateDetails(null);
setAllowedKeyUsages([]);
setAllowedExtendedKeyUsages([]);
setRequiredKeyUsages([]);
setRequiredExtendedKeyUsages([]);
setAllowedSignatureAlgorithms([]);
setAllowedKeyAlgorithms([]);
reset();
}, [reset]);
const selectedProfileId = watch("profileId");
const selectedProfile = profilesData?.certificateProfiles?.find(
(p) => p.id === selectedProfileId
const selectedProfile = useMemo(
() => profilesData?.certificateProfiles?.find((p) => p.id === selectedProfileId),
[profilesData?.certificateProfiles, selectedProfileId]
);
const { data: templateData } = useGetCertificateTemplateV2ById({
templateId: selectedProfile?.certificateTemplateId || ""
});
useEffect(() => {
if (templateData && selectedProfile) {
if (templateData.signatureAlgorithm?.allowedAlgorithms && templateData.signatureAlgorithm.allowedAlgorithms.length > 0) {
const sigAlgMap: Record<string, string> = {
"SHA256-RSA": "RSA-SHA256",
"SHA384-RSA": "RSA-SHA384",
"SHA512-RSA": "RSA-SHA512",
"SHA256-ECDSA": "ECDSA-SHA256",
"SHA384-ECDSA": "ECDSA-SHA384",
"SHA512-ECDSA": "ECDSA-SHA512"
};
const filteredKeyUsages = useMemo(() => {
if (allowedKeyUsages.length === 0) return KEY_USAGES_OPTIONS;
return KEY_USAGES_OPTIONS.filter(({ value }) => allowedKeyUsages.includes(value));
}, [allowedKeyUsages]);
let defaultValue = templateData.signatureAlgorithm.defaultAlgorithm;
if (defaultValue && sigAlgMap[defaultValue]) {
defaultValue = sigAlgMap[defaultValue];
}
const allowedValues = templateData.signatureAlgorithm.allowedAlgorithms.map((alg: string) => sigAlgMap[alg] || alg);
if (defaultValue && allowedValues.includes(defaultValue)) {
setValue("signatureAlgorithm", defaultValue);
} else if (allowedValues.length > 0) {
setValue("signatureAlgorithm", allowedValues[0]);
}
}
if (templateData.keyAlgorithm?.allowedKeyTypes && templateData.keyAlgorithm.allowedKeyTypes.length > 0) {
const keyAlgMap: Record<string, string> = {
"RSA-2048": CertKeyAlgorithm.RSA_2048,
"RSA-3072": CertKeyAlgorithm.RSA_3072,
"RSA-4096": CertKeyAlgorithm.RSA_4096,
"ECDSA-P256": CertKeyAlgorithm.ECDSA_P256,
"ECDSA-P384": CertKeyAlgorithm.ECDSA_P384,
[CertKeyAlgorithm.ECDSA_P256]: CertKeyAlgorithm.ECDSA_P256,
[CertKeyAlgorithm.ECDSA_P384]: CertKeyAlgorithm.ECDSA_P384
};
let defaultValue = templateData.keyAlgorithm.defaultKeyType;
if (defaultValue && keyAlgMap[defaultValue]) {
defaultValue = keyAlgMap[defaultValue];
}
const allowedValues = templateData.keyAlgorithm.allowedKeyTypes.map((alg: string) => keyAlgMap[alg] || alg);
if (defaultValue && allowedValues.includes(defaultValue)) {
setValue("keyAlgorithm", defaultValue);
} else if (allowedValues.length > 0) {
setValue("keyAlgorithm", allowedValues[0]);
}
}
if (templateData.validity?.maxDuration) {
const { value, unit } = templateData.validity.maxDuration;
let ttlValue = "";
switch (unit) {
case "days":
ttlValue = `${value}d`;
break;
case "months":
ttlValue = `${value}m`;
break;
case "years":
ttlValue = `${value}y`;
break;
default:
ttlValue = `${value}d`;
}
setValue("ttl", ttlValue);
}
if (templateData.signatureAlgorithm?.allowedAlgorithms) {
const mappedSigAlgs = templateData.signatureAlgorithm.allowedAlgorithms.map(
(alg: string) => {
const sigAlgMap: Record<string, string> = {
"SHA256-RSA": "RSA-SHA256",
"SHA384-RSA": "RSA-SHA384",
"SHA512-RSA": "RSA-SHA512",
"SHA256-ECDSA": "ECDSA-SHA256",
"SHA384-ECDSA": "ECDSA-SHA384",
"SHA512-ECDSA": "ECDSA-SHA512"
};
return sigAlgMap[alg] || alg;
}
const filteredExtendedKeyUsages = useMemo(() => {
if (allowedExtendedKeyUsages.length === 0) return EXTENDED_KEY_USAGES_OPTIONS;
return EXTENDED_KEY_USAGES_OPTIONS.filter(({ value }) =>
allowedExtendedKeyUsages.includes(value)
);
setAllowedSignatureAlgorithms(mappedSigAlgs);
}
}, [allowedExtendedKeyUsages]);
if (templateData.keyAlgorithm?.allowedKeyTypes) {
const mappedKeyAlgs = templateData.keyAlgorithm.allowedKeyTypes.map((alg: string) => {
const keyAlgMap: Record<string, string> = {
"RSA-2048": CertKeyAlgorithm.RSA_2048,
"RSA-3072": CertKeyAlgorithm.RSA_3072,
"RSA-4096": CertKeyAlgorithm.RSA_4096,
"ECDSA-P256": CertKeyAlgorithm.ECDSA_P256,
"ECDSA-P384": CertKeyAlgorithm.ECDSA_P384
const availableSignatureAlgorithms = useMemo(() => {
if (allowedSignatureAlgorithms.length === 0) {
return SIGNATURE_ALGORITHMS_OPTIONS;
}
return allowedSignatureAlgorithms.map((templateAlgorithm) => {
const apiAlgorithm = mapTemplateSignatureAlgorithmToApi(templateAlgorithm);
return {
value: apiAlgorithm,
label: apiAlgorithm
};
return keyAlgMap[alg] || alg;
});
setAllowedKeyAlgorithms(mappedKeyAlgs);
}
}, [allowedSignatureAlgorithms]);
const allAllowedKeyUsages: string[] = [];
if (templateData.keyUsages?.requiredUsages?.all) {
allAllowedKeyUsages.push(...templateData.keyUsages.requiredUsages.all);
}
if (templateData.keyUsages?.optionalUsages?.all) {
allAllowedKeyUsages.push(...templateData.keyUsages.optionalUsages.all);
}
setAllowedKeyUsages([...new Set(allAllowedKeyUsages)]);
const allAllowedExtendedKeyUsages: string[] = [];
if (templateData.extendedKeyUsages?.requiredUsages?.all) {
allAllowedExtendedKeyUsages.push(...templateData.extendedKeyUsages.requiredUsages.all);
}
if (templateData.extendedKeyUsages?.optionalUsages?.all) {
allAllowedExtendedKeyUsages.push(...templateData.extendedKeyUsages.optionalUsages.all);
}
setAllowedExtendedKeyUsages([...new Set(allAllowedExtendedKeyUsages)]);
if (templateData.attributes && Array.isArray(templateData.attributes)) {
const subjectAttrs: Array<{
type: "common_name";
value: string;
}> = [];
templateData.attributes.forEach((attr) => {
if (
(attr.include === "mandatory" ||
attr.include === "optional" ||
attr.include === "prohibit") &&
attr.value &&
attr.value.length > 0
) {
attr.value.forEach((val: string) => {
subjectAttrs.push({ type: attr.type as any, value: val });
});
const availableKeyAlgorithms = useMemo(() => {
if (allowedKeyAlgorithms.length === 0) {
return certKeyAlgorithms;
}
return allowedKeyAlgorithms.map((templateAlgorithm) => {
const apiAlgorithm = mapTemplateKeyAlgorithmToApi(templateAlgorithm);
return {
value: apiAlgorithm,
label: apiAlgorithm
};
});
}, [allowedKeyAlgorithms]);
if (subjectAttrs.length > 0) {
setValue("subjectAttributes", subjectAttrs);
useEffect(() => {
if (templateData && selectedProfile && popUp?.certificateIssuance?.isOpen) {
if (templateData.algorithms?.signature && templateData.algorithms.signature.length > 0) {
setAllowedSignatureAlgorithms(templateData.algorithms.signature);
} else {
setValue("subjectAttributes", [{ type: "common_name", value: "" }]);
}
setAllowedSignatureAlgorithms([]);
}
if (
templateData.subjectAlternativeNames &&
Array.isArray(templateData.subjectAlternativeNames)
templateData.algorithms?.keyAlgorithm &&
templateData.algorithms.keyAlgorithm.length > 0
) {
const templateSans: Array<{ type: "dns" | "ip" | "email" | "uri"; value: string }> = [];
templateData.subjectAlternativeNames.forEach((sanPolicy) => {
if (
(sanPolicy.include === "mandatory" ||
sanPolicy.include === "optional" ||
sanPolicy.include === "prohibit") &&
sanPolicy.value &&
sanPolicy.value.length > 0
) {
const typeMapping: Record<string, "dns" | "ip" | "email" | "uri"> = {
dns_name: "dns",
ip_address: "ip",
email: "email",
uri: "uri"
};
const mappedType = typeMapping[sanPolicy.type];
if (mappedType) {
sanPolicy.value.forEach((val: string) => {
templateSans.push({ type: mappedType, value: val });
});
setAllowedKeyAlgorithms(templateData.algorithms.keyAlgorithm);
} else {
setAllowedKeyAlgorithms([]);
}
if (templateData.validity?.max) {
setValue("ttl", templateData.validity.max);
}
const keyUsages: string[] = [];
if (templateData.keyUsages?.required) {
keyUsages.push(...templateData.keyUsages.required);
}
if (templateData.keyUsages?.allowed) {
keyUsages.push(...templateData.keyUsages.allowed);
}
setAllowedKeyUsages(keyUsages);
const extendedKeyUsages: string[] = [];
if (templateData.extendedKeyUsages?.required) {
extendedKeyUsages.push(...templateData.extendedKeyUsages.required);
}
if (templateData.extendedKeyUsages?.allowed) {
extendedKeyUsages.push(...templateData.extendedKeyUsages.allowed);
}
setAllowedExtendedKeyUsages(extendedKeyUsages);
setRequiredKeyUsages(templateData.keyUsages?.required || []);
setRequiredExtendedKeyUsages(templateData.extendedKeyUsages?.required || []);
const initialKeyUsages: Record<string, boolean> = {};
const initialExtendedKeyUsages: Record<string, boolean> = {};
(templateData.keyUsages?.required || []).forEach((usage: string) => {
initialKeyUsages[usage] = true;
});
if (templateSans.length > 0) {
setValue("subjectAltNames", templateSans);
}
}
const resetKeyUsages = {
[CertKeyUsage.DIGITAL_SIGNATURE]: false,
[CertKeyUsage.KEY_ENCIPHERMENT]: false,
[CertKeyUsage.NON_REPUDIATION]: false,
[CertKeyUsage.DATA_ENCIPHERMENT]: false,
[CertKeyUsage.KEY_AGREEMENT]: false,
[CertKeyUsage.KEY_CERT_SIGN]: false,
[CertKeyUsage.CRL_SIGN]: false,
[CertKeyUsage.ENCIPHER_ONLY]: false,
[CertKeyUsage.DECIPHER_ONLY]: false
};
const resetExtendedKeyUsages = {
[CertExtendedKeyUsage.CLIENT_AUTH]: false,
[CertExtendedKeyUsage.CODE_SIGNING]: false,
[CertExtendedKeyUsage.EMAIL_PROTECTION]: false,
[CertExtendedKeyUsage.OCSP_SIGNING]: false,
[CertExtendedKeyUsage.SERVER_AUTH]: false,
[CertExtendedKeyUsage.TIMESTAMPING]: false
};
const templateToEnumMap = {
digital_signature: CertKeyUsage.DIGITAL_SIGNATURE,
digitalSignature: CertKeyUsage.DIGITAL_SIGNATURE,
key_encipherment: CertKeyUsage.KEY_ENCIPHERMENT,
keyEncipherment: CertKeyUsage.KEY_ENCIPHERMENT,
non_repudiation: CertKeyUsage.NON_REPUDIATION,
nonRepudiation: CertKeyUsage.NON_REPUDIATION,
data_encipherment: CertKeyUsage.DATA_ENCIPHERMENT,
dataEncipherment: CertKeyUsage.DATA_ENCIPHERMENT,
key_agreement: CertKeyUsage.KEY_AGREEMENT,
keyAgreement: CertKeyUsage.KEY_AGREEMENT,
key_cert_sign: CertKeyUsage.KEY_CERT_SIGN,
keyCertSign: CertKeyUsage.KEY_CERT_SIGN,
crl_sign: CertKeyUsage.CRL_SIGN,
cRLSign: CertKeyUsage.CRL_SIGN,
encipher_only: CertKeyUsage.ENCIPHER_ONLY,
encipherOnly: CertKeyUsage.ENCIPHER_ONLY,
decipher_only: CertKeyUsage.DECIPHER_ONLY,
decipherOnly: CertKeyUsage.DECIPHER_ONLY,
client_auth: CertExtendedKeyUsage.CLIENT_AUTH,
clientAuth: CertExtendedKeyUsage.CLIENT_AUTH,
server_auth: CertExtendedKeyUsage.SERVER_AUTH,
serverAuth: CertExtendedKeyUsage.SERVER_AUTH,
code_signing: CertExtendedKeyUsage.CODE_SIGNING,
codeSigning: CertExtendedKeyUsage.CODE_SIGNING,
email_protection: CertExtendedKeyUsage.EMAIL_PROTECTION,
emailProtection: CertExtendedKeyUsage.EMAIL_PROTECTION,
ocsp_signing: CertExtendedKeyUsage.OCSP_SIGNING,
ocspSigning: CertExtendedKeyUsage.OCSP_SIGNING,
time_stamping: CertExtendedKeyUsage.TIMESTAMPING,
timestamping: CertExtendedKeyUsage.TIMESTAMPING,
timeStamping: CertExtendedKeyUsage.TIMESTAMPING
};
const currentKeyUsages = { ...resetKeyUsages };
if (templateData.keyUsages?.requiredUsages?.all) {
templateData.keyUsages.requiredUsages.all.forEach((usage: string) => {
const enumValue = (templateToEnumMap as any)[usage];
if (enumValue && enumValue in currentKeyUsages) {
(currentKeyUsages as any)[enumValue] = true;
}
(templateData.extendedKeyUsages?.required || []).forEach((usage: string) => {
initialExtendedKeyUsages[usage] = true;
});
}
const currentExtendedKeyUsages = { ...resetExtendedKeyUsages };
if (templateData.extendedKeyUsages?.requiredUsages?.all) {
templateData.extendedKeyUsages.requiredUsages.all.forEach((usage: string) => {
const enumValue = (templateToEnumMap as any)[usage];
if (enumValue && enumValue in currentExtendedKeyUsages) {
(currentExtendedKeyUsages as any)[enumValue] = true;
setValue("keyUsages", initialKeyUsages);
setValue("extendedKeyUsages", initialExtendedKeyUsages);
}
});
}
setValue("keyUsages", currentKeyUsages);
setValue("extendedKeyUsages", currentExtendedKeyUsages);
}
}, [templateData, selectedProfile, setValue]);
}, [templateData, selectedProfile, setValue, popUp?.certificateIssuance?.isOpen]);
useEffect(() => {
if (cert) {
@@ -451,8 +297,33 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
}
}, [popUp?.certificateIssuance?.isOpen, profileId, cert, setValue]);
const onFormSubmit = async ({
profileId,
const getAttributeValue = useCallback(
(subjectAttributes: typeof schema._type.subjectAttributes, type: string) => {
const foundAttr = subjectAttributes.find((attr) => attr.type === type);
return foundAttr?.value || "";
},
[]
);
const formatSubjectAltNames = useCallback(
(subjectAltNames: typeof schema._type.subjectAltNames) => {
return subjectAltNames
.filter((san) => san.value.trim())
.map((san) => san.value.trim())
.join(", ");
},
[]
);
const filterUsages = useCallback(<T extends Record<string, boolean>>(usages: T) => {
return Object.entries(usages)
.filter(([, value]) => value)
.map(([key]) => key);
}, []);
const onFormSubmit = useCallback(
async ({
profileId: formProfileId,
subjectAttributes,
subjectAltNames,
ttl,
@@ -462,54 +333,44 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
extendedKeyUsages
}: FormData) => {
try {
if (!currentProject?.slug) return;
const getAttributeValue = (type: string) => {
const foundAttr = subjectAttributes.find((attr) => attr.type === type);
return foundAttr?.value || "";
};
const { serialNumber, certificate, certificateChain, privateKey } = await createCertificate({
profileId,
projectSlug: currentProject.slug,
commonName: getAttributeValue("common_name"),
subjectAltNames: subjectAltNames
.filter((san) => san.value.trim())
.map((san) => san.value.trim())
.join(", "),
ttl,
signatureAlgorithm: (() => {
const frontendToBackendSigAlg: Record<string, string> = {
"RSA-SHA256": "RSA-SHA256",
"RSA-SHA384": "RSA-SHA384",
"RSA-SHA512": "RSA-SHA512",
"ECDSA-SHA256": "ECDSA-SHA256",
"ECDSA-SHA384": "ECDSA-SHA384",
"ECDSA-SHA512": "ECDSA-SHA512"
};
return signatureAlgorithm
? frontendToBackendSigAlg[signatureAlgorithm] || signatureAlgorithm
: undefined;
})(),
keyAlgorithm: (() => {
const frontendToBackendKeyAlg: Record<string, string> = {
RSA_2048: "RSA_2048",
RSA_3072: "RSA_3072",
RSA_4096: "RSA_4096",
EC_prime256v1: "EC_prime256v1",
EC_secp384r1: "EC_secp384r1"
};
return keyAlgorithm ? frontendToBackendKeyAlg[keyAlgorithm] || keyAlgorithm : undefined;
})(),
keyUsages: Object.entries(keyUsages)
.filter(([, value]) => value)
.map(([key]) => key as CertKeyUsage),
extendedKeyUsages: Object.entries(extendedKeyUsages)
.filter(([, value]) => value)
.map(([key]) => key as CertExtendedKeyUsage)
if (!currentProject?.slug) {
createNotification({
text: "Project not found. Please refresh and try again.",
type: "error"
});
return;
}
reset();
if (!formProfileId) {
createNotification({
text: "Please select a certificate profile.",
type: "error"
});
return;
}
const commonName = getAttributeValue(subjectAttributes, "common_name");
if (!commonName.trim()) {
createNotification({
text: "Common name is required.",
type: "error"
});
return;
}
const { serialNumber, certificate, certificateChain, privateKey } = await createCertificate(
{
profileId: formProfileId,
projectSlug: currentProject.slug,
commonName,
subjectAltNames: formatSubjectAltNames(subjectAltNames),
ttl,
signatureAlgorithm,
keyAlgorithm,
keyUsages: filterUsages(keyUsages) as CertKeyUsage[],
extendedKeyUsages: filterUsages(extendedKeyUsages) as CertExtendedKeyUsage[]
}
);
setCertificateDetails({
serialNumber,
@@ -523,12 +384,52 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
type: "success"
});
} catch (err) {
console.error(err);
console.error("Certificate creation failed:", err);
const errorMessage =
err instanceof Error
? err.message
: "An unexpected error occurred while creating the certificate";
createNotification({
text: "Failed to create certificate",
text: `Failed to create certificate: ${errorMessage}`,
type: "error"
});
}
},
[
currentProject?.slug,
createCertificate,
reset,
getAttributeValue,
formatSubjectAltNames,
filterUsages
]
);
const getModalTitle = () => {
if (certificateDetails) return "Certificate Created Successfully";
if (cert) return "Certificate Details";
return "Issue New Certificate";
};
const getModalSubTitle = () => {
if (certificateDetails) return "Certificate has been successfully created and is ready for use";
if (cert) return "View certificate information";
return "Issue a new certificate using a certificate profile";
};
const getSanPlaceholder = (sanType: string) => {
switch (sanType) {
case "dns":
return "example.com or *.example.com";
case "ip":
return "192.168.1.1";
case "email":
return "[email protected]";
case "uri":
return "https://example.com";
default:
return "Enter value";
}
};
return (
@@ -536,26 +437,12 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
isOpen={popUp?.certificateIssuance?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("certificateIssuance", isOpen);
setCertificateDetails(null);
reset();
if (!isOpen) {
resetAllState();
}
}}
>
<ModalContent
title={
certificateDetails
? "Certificate Created Successfully"
: cert
? "Certificate Details"
: "Issue New Certificate"
}
subTitle={
certificateDetails
? "Certificate has been successfully created and is ready for use"
: cert
? "View certificate information"
: "Issue a new certificate using a certificate profile"
}
>
<ModalContent title={getModalTitle()} subTitle={getModalSubTitle()}>
{certificateDetails && (
<CertificateContent
serialNumber={certificateDetails.serialNumber}
@@ -567,10 +454,10 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
{cert && (
<div className="space-y-4">
<div>
<h4 className="text-mineshaft-300 text-sm font-medium">Certificate Details</h4>
<p className="text-mineshaft-400 text-sm">Serial Number: {cert.serialNumber}</p>
<p className="text-mineshaft-400 text-sm">Common Name: {cert.commonName}</p>
<p className="text-mineshaft-400 text-sm">Status: {cert.status}</p>
<h4 className="text-sm font-medium text-mineshaft-300">Certificate Details</h4>
<p className="text-sm text-mineshaft-400">Serial Number: {cert.serialNumber}</p>
<p className="text-sm text-mineshaft-400">Common Name: {cert.commonName}</p>
<p className="text-sm text-mineshaft-400">Status: {cert.status}</p>
</div>
</div>
)}
@@ -592,9 +479,9 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
className="text-center"
content={
<span>
Certificate profiles define the policies and enrollment methods for
certificate issuance. The selected profile will enforce validation
rules and determine the CA used for signing.
Certificate profiles define the policies and enrollment methods
for certificate issuance. The selected profile will enforce
validation rules and determine the CA used for signing.
</span>
}
>
@@ -629,7 +516,6 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
{(selectedProfile || profileId) && (
<>
<Controller
control={control}
name="subjectAttributes"
@@ -642,7 +528,8 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
>
<div className="space-y-2">
{value.map((attr, index) => (
<div key={`attr-${index}`} className="flex items-start gap-2">
// eslint-disable-next-line react/no-array-index-key
<div key={`subject-attr-${index}`} className="flex items-center gap-2">
<Select
value={attr.type}
onValueChange={(newType) => {
@@ -710,7 +597,11 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
>
<div className="space-y-2">
{value.map((san, index) => (
<div key={`san-${index}`} className="flex items-start gap-2">
// eslint-disable-next-line react/no-array-index-key
<div
key={`subject-alt-name-${index}`}
className="flex items-center gap-2"
>
<Select
value={san.type}
onValueChange={(newType) => {
@@ -735,15 +626,7 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
newValue[index] = { ...san, value: e.target.value };
onChange(newValue);
}}
placeholder={
san.type === "dns"
? "example.com or *.example.com"
: san.type === "ip"
? "192.168.1.1"
: san.type === "email"
? "[email protected]"
: "https://example.com"
}
placeholder={getSanPlaceholder(san.type)}
className="flex-1"
/>
<IconButton
@@ -807,13 +690,14 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
placeholder="Use template default"
placeholder={
availableSignatureAlgorithms.length > 0
? "Select signature algorithm"
: "No algorithms available"
}
position="popper"
>
{SIGNATURE_ALGORITHMS_OPTIONS.filter((algorithm) => {
if (allowedSignatureAlgorithms.length === 0) return true;
return allowedSignatureAlgorithms.includes(algorithm.value);
}).map((algorithm) => (
{availableSignatureAlgorithms.map((algorithm) => (
<SelectItem key={algorithm.value} value={algorithm.value}>
{algorithm.label}
</SelectItem>
@@ -839,15 +723,14 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
placeholder="Use template default"
placeholder={
availableKeyAlgorithms.length > 0
? "Select key algorithm"
: "No algorithms available"
}
position="popper"
>
{certKeyAlgorithms
.filter((algorithm) => {
if (allowedKeyAlgorithms.length === 0) return true;
return allowedKeyAlgorithms.includes(algorithm.value);
})
.map((algorithm) => (
{availableKeyAlgorithms.map((algorithm) => (
<SelectItem key={algorithm.value} value={algorithm.value}>
{algorithm.label}
</SelectItem>
@@ -864,23 +747,9 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
<AccordionTrigger>Key Usages</AccordionTrigger>
<AccordionContent>
<div className="grid grid-cols-2 gap-2 pl-2">
{KEY_USAGES_OPTIONS.filter(({ value }) => {
if (allowedKeyUsages.length === 0) return true;
const templateToEnumMap = {
digital_signature: CertKeyUsage.DIGITAL_SIGNATURE,
key_encipherment: CertKeyUsage.KEY_ENCIPHERMENT,
non_repudiation: CertKeyUsage.NON_REPUDIATION,
data_encipherment: CertKeyUsage.DATA_ENCIPHERMENT,
key_agreement: CertKeyUsage.KEY_AGREEMENT,
key_cert_sign: CertKeyUsage.KEY_CERT_SIGN,
crl_sign: CertKeyUsage.CRL_SIGN,
encipher_only: CertKeyUsage.ENCIPHER_ONLY,
decipher_only: CertKeyUsage.DECIPHER_ONLY
};
return allowedKeyUsages.some(
(allowedUsage) => (templateToEnumMap as any)[allowedUsage] === value
);
}).map(({ label, value }) => (
{filteredKeyUsages.map(({ label, value }) => {
const isRequired = requiredKeyUsages.includes(value);
return (
<Controller
key={label}
control={control}
@@ -890,17 +759,26 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
<Checkbox
id={`key-usage-${value}`}
isChecked={field.value || false}
onCheckedChange={(checked) => field.onChange(checked)}
onCheckedChange={(checked) => {
if (!isRequired) {
field.onChange(checked);
}
}}
isDisabled={isRequired}
/>
<div className="flex items-center gap-2">
<FormLabel
id={`key-usage-${value}`}
className="text-mineshaft-300 cursor-pointer text-sm"
className={`text-sm ${isRequired ? "text-mineshaft-200" : "cursor-pointer text-mineshaft-300"}`}
label={label}
/>
{isRequired && <span className="text-xs">(Required)</span>}
</div>
</div>
)}
/>
))}
);
})}
</div>
</AccordionContent>
</AccordionItem>
@@ -909,21 +787,9 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
<AccordionTrigger>Extended Key Usages</AccordionTrigger>
<AccordionContent>
<div className="grid grid-cols-2 gap-2 pl-2">
{EXTENDED_KEY_USAGES_OPTIONS.filter(({ value }) => {
if (allowedExtendedKeyUsages.length === 0) return true;
const templateToEnumMap = {
client_auth: CertExtendedKeyUsage.CLIENT_AUTH,
server_auth: CertExtendedKeyUsage.SERVER_AUTH,
code_signing: CertExtendedKeyUsage.CODE_SIGNING,
email_protection: CertExtendedKeyUsage.EMAIL_PROTECTION,
ocsp_signing: CertExtendedKeyUsage.OCSP_SIGNING,
time_stamping: CertExtendedKeyUsage.TIMESTAMPING,
timestamping: CertExtendedKeyUsage.TIMESTAMPING
};
return allowedExtendedKeyUsages.some(
(allowedUsage) => (templateToEnumMap as any)[allowedUsage] === value
);
}).map(({ label, value }) => (
{filteredExtendedKeyUsages.map(({ label, value }) => {
const isRequired = requiredExtendedKeyUsages.includes(value);
return (
<Controller
key={label}
control={control}
@@ -933,17 +799,26 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
<Checkbox
id={`ext-key-usage-${value}`}
isChecked={field.value || false}
onCheckedChange={(checked) => field.onChange(checked)}
onCheckedChange={(checked) => {
if (!isRequired) {
field.onChange(checked);
}
}}
isDisabled={isRequired}
/>
<div className="flex items-center gap-2">
<FormLabel
id={`ext-key-usage-${value}`}
className="text-mineshaft-300 cursor-pointer text-sm"
className={`text-sm ${isRequired ? "text-mineshaft-200" : "cursor-pointer text-mineshaft-300"}`}
label={label}
/>
{isRequired && <span className="text-xs">(Required)</span>}
</div>
</div>
)}
/>
))}
);
})}
</div>
</AccordionContent>
</AccordionItem>
@@ -964,7 +839,9 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("certificateIssuance", false)}
onClick={() => {
handlePopUpToggle("certificateIssuance", false);
}}
>
Cancel
</Button>
@@ -87,16 +87,16 @@ export const PkiSubscriberSection = () => {
const subscriberName = subscriberStatusData?.subscriberName || "";
return (
<div className="border-mineshaft-600 bg-mineshaft-900 mb-6 rounded-lg border p-4">
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex justify-between">
<p className="text-mineshaft-100 text-xl font-medium">Subscribers</p>
<p className="text-xl font-medium text-mineshaft-100">Subscribers</p>
<div className="flex w-full justify-end">
<a
target="_blank"
rel="noopener noreferrer"
href="https://infisical.com/docs/documentation/platform/pki/subscribers"
>
<span className="border-mineshaft-500 bg-mineshaft-600 text-mineshaft-200 hover:border-primary/40 hover:bg-primary/10 flex w-max cursor-pointer items-center rounded-md border px-4 py-2 duration-200 hover:text-white">
<span className="flex w-max cursor-pointer items-center rounded-md border border-mineshaft-500 bg-mineshaft-600 px-4 py-2 text-mineshaft-200 duration-200 hover:border-primary/40 hover:bg-primary/10 hover:text-white">
Documentation{" "}
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
@@ -112,8 +112,7 @@ export const PkiTemplateListPage = () => {
/>
</div>
<div className="container mx-auto mb-6 max-w-7xl rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
{
subscription?.pkiLegacyTemplates && (
{subscription?.pkiLegacyTemplates && (
<div className="mb-4 flex justify-between">
<p className="text-xl font-medium text-mineshaft-100">Templates</p>
<div className="flex w-full justify-end">
@@ -136,8 +135,7 @@ export const PkiTemplateListPage = () => {
</ProjectPermissionCan>
</div>
</div>
)
}
)}
<TableContainer>
<Table>
<THead>
@@ -165,7 +165,7 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
if (isEdit) {
const updateData: any = {
profileId: profile.id,
name: data.slug,
slug: data.slug,
description: data.description
};
@@ -240,7 +240,7 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="your-profile-name" isDisabled={Boolean(isEdit)} />
<Input {...field} placeholder="your-profile-name" />
</FormControl>
)}
/>
@@ -319,7 +319,7 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
>
{certificateTemplates.map((template) => (
<SelectItem key={template.id} value={template.id}>
{template.slug}
{template.name}
</SelectItem>
))}
</Select>
@@ -376,17 +376,17 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
name="estConfig.disableBootstrapCaValidation"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<div className="border-mineshaft-600 bg-mineshaft-900 flex items-center gap-3 rounded-md border p-4">
<div className="flex items-center gap-3 rounded-md border border-mineshaft-600 bg-mineshaft-900 p-4">
<Checkbox
id="disableBootstrapCaValidation"
isChecked={value}
onCheckedChange={onChange}
/>
<div className="space-y-1">
<span className="text-mineshaft-100 text-sm font-medium">
<span className="text-sm font-medium text-mineshaft-100">
Disable Bootstrap CA Validation
</span>
<p className="text-bunker-300 text-xs">
<p className="text-xs text-bunker-300">
Skip CA certificate validation during EST bootstrap phase
</p>
</div>
@@ -433,7 +433,7 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
rows={6}
className="w-full font-mono text-xs"
/>
<p className="text-bunker-400 text-xs">
<p className="text-xs text-bunker-400">
Paste the complete CA certificate chain in PEM format
</p>
</div>
@@ -1,7 +1,17 @@
/* eslint-disable no-nested-ternary */
import { faCheck, faCircleInfo, faCopy, faEdit, faEllipsis, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
import { useCallback } from "react";
import {
faCheck,
faCircleInfo,
faCopy,
faEdit,
faEllipsis,
faPlus,
faTrash
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import {
Badge,
DropdownMenu,
@@ -18,12 +28,10 @@ import {
ProjectPermissionCertificateProfileActions,
ProjectPermissionSub
} from "@app/context/ProjectPermissionContext/types";
import { usePopUp, useToggle } from "@app/hooks";
import { useGetCaById } from "@app/hooks/api/ca/queries";
import { TCertificateProfile } from "@app/hooks/api/certificateProfiles";
import { useGetCertificateTemplateV2ById } from "@app/hooks/api/certificateTemplates/queries";
import { usePopUp, useToggle } from "@app/hooks";
import { createNotification } from "@app/components/notifications";
import { useCallback } from "react";
import { CertificateIssuanceModal } from "@app/pages/cert-manager/CertificatesPage/components/CertificateIssuanceModal";
interface Props {
@@ -37,9 +45,7 @@ export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) =
const { data: caData } = useGetCaById(profile.caId);
const { popUp, handlePopUpToggle } = usePopUp([
"certificateIssuance"
] as const);
const { popUp, handlePopUpToggle } = usePopUp(["certificateIssuance"] as const);
const [isIdCopied, setIsIdCopied] = useToggle(false);
@@ -100,15 +106,15 @@ export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) =
)}
</div>
</Td>
<Td className="text-center">{getEnrollmentTypeBadge(profile.enrollmentType)}</Td>
<Td className="text-center">
<Td className="text-start">{getEnrollmentTypeBadge(profile.enrollmentType)}</Td>
<Td className="text-start">
<span className="text-sm text-mineshaft-300">
{caData?.friendlyName || caData?.commonName || profile.caId}
</span>
</Td>
<Td>
<span className="text-sm text-mineshaft-300">
{templateData?.slug || profile.certificateTemplateId}
{templateData?.name || profile.certificateTemplateId}
</span>
</Td>
<Td>
@@ -176,8 +182,7 @@ export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) =
Edit Profile
</DropdownMenuItem>
)}
{
canIssueCertificate && (
{canIssueCertificate && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
@@ -187,8 +192,7 @@ export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) =
>
Issue Certificate
</DropdownMenuItem>
)
}
)}
{canDeleteProfile && (
<DropdownMenuItem
onClick={(e) => {
@@ -202,7 +206,11 @@ export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) =
)}
</DropdownMenuContent>
</DropdownMenu>
<CertificateIssuanceModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} profileId={profile.id}/>
<CertificateIssuanceModal
popUp={popUp}
handlePopUpToggle={handlePopUpToggle}
profileId={profile.id}
/>
</Td>
</Tr>
);
@@ -54,7 +54,7 @@ export const CertificateTemplatesV2Tab = () => {
setIsDeleteModalOpen(false);
setSelectedTemplate(null);
createNotification({
text: `Certificate template "${selectedTemplate.slug}" deleted successfully`,
text: `Certificate template "${selectedTemplate.name}" deleted successfully`,
type: "success"
});
} catch (error: any) {
@@ -103,12 +103,12 @@ export const CertificateTemplatesV2Tab = () => {
<DeleteActionModal
isOpen={isDeleteModalOpen}
title={`Delete Certificate Template ${selectedTemplate.slug}?`}
title={`Delete Certificate Template ${selectedTemplate.name}?`}
onChange={(isOpen) => {
setIsDeleteModalOpen(isOpen);
if (!isOpen) setSelectedTemplate(null);
}}
deleteKey={selectedTemplate.slug}
deleteKey={selectedTemplate.name}
onDeleteApproved={handleDeleteConfirm}
/>
</>
@@ -1,6 +1,7 @@
import { useMemo } from "react";
/* eslint-disable no-nested-ternary */
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { faExclamationTriangle, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -19,8 +20,7 @@ import {
ModalContent,
Select,
SelectItem,
TextArea,
Tooltip
TextArea
} from "@app/components/v2";
import { useProject } from "@app/context";
import {
@@ -29,7 +29,15 @@ import {
} from "@app/hooks/api/certificateTemplates/mutations";
import { TCertificateTemplateV2New } from "@app/hooks/api/certificateTemplates/types";
import { INCLUDE_TYPE_OPTIONS, SAN_TYPE_OPTIONS, SUBJECT_ATTRIBUTE_TYPE_OPTIONS } from "./shared/certificate-constants";
import {
CertDurationUnit,
CertExtendedKeyUsageType,
CertKeyUsageType,
SAN_INCLUDE_OPTIONS,
SAN_TYPE_OPTIONS,
SUBJECT_ATTRIBUTE_INCLUDE_OPTIONS,
SUBJECT_ATTRIBUTE_TYPE_OPTIONS
} from "./shared/certificate-constants";
import { KeyUsagesSection, TemplateFormData, templateSchema } from "./shared";
export type FormData = TemplateFormData;
@@ -42,7 +50,9 @@ interface Props {
}
const ATTRIBUTE_TYPE_LABELS: Record<(typeof SUBJECT_ATTRIBUTE_TYPE_OPTIONS)[number], string> = {
common_name: "Common Name (CN)"
common_name: "Common Name (CN)",
organization: "Organization (O)",
country: "Country (C)"
};
const SAN_TYPE_LABELS: Record<(typeof SAN_TYPE_OPTIONS)[number], string> = {
@@ -52,10 +62,16 @@ const SAN_TYPE_LABELS: Record<(typeof SAN_TYPE_OPTIONS)[number], string> = {
uri: "URI"
};
const INCLUDE_TYPE_LABELS: Record<(typeof INCLUDE_TYPE_OPTIONS)[number], string> = {
mandatory: "Mandatory",
optional: "Optional",
prohibit: "Prohibited"
const SUBJECT_ATTRIBUTE_LABELS: Record<(typeof SUBJECT_ATTRIBUTE_INCLUDE_OPTIONS)[number], string> =
{
optional: "Allow",
prohibit: "Deny"
};
const SAN_INCLUDE_LABELS: Record<(typeof SAN_INCLUDE_OPTIONS)[number], string> = {
mandatory: "Require",
optional: "Allow",
prohibit: "Deny"
};
const SIGNATURE_ALGORITHMS = [
@@ -83,62 +99,122 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
const isEdit = mode === "edit" && template;
const validateAttributeRules = (attributes: FormData["attributes"]) => {
if (!attributes) return { isValid: true, warnings: [], invalidIndices: [] };
const warnings: string[] = [];
const invalidIndices: number[] = [];
const attributesByType = attributes.reduce((acc, attr, index) => {
if (!acc[attr.type]) acc[attr.type] = [];
acc[attr.type].push({ ...attr, index });
return acc;
}, {} as Record<string, Array<(typeof attributes[0] & { index: number })>>);
Object.entries(attributesByType).forEach(([type, attrs]) => {
const mandatoryAttrs = attrs.filter(attr => attr.include === 'mandatory');
if (mandatoryAttrs.length > 1) {
mandatoryAttrs.forEach(attr => invalidIndices.push(attr.index));
warnings.push(`Multiple mandatory values found for ${ATTRIBUTE_TYPE_LABELS[type as keyof typeof ATTRIBUTE_TYPE_LABELS]}. Only one mandatory value is allowed per attribute type.`);
const convertApiToUiFormat = (templateData: TCertificateTemplateV2New): FormData => {
const attributes: any[] = [];
if (templateData.subject && Array.isArray(templateData.subject)) {
templateData.subject.forEach((subj) => {
if (subj.allowed && Array.isArray(subj.allowed)) {
subj.allowed.forEach((allowedValue) => {
attributes.push({
type: subj.type,
include: "optional",
value: [allowedValue]
});
});
}
if (mandatoryAttrs.length === 1 && attrs.length > 1) {
attrs.forEach(attr => invalidIndices.push(attr.index));
warnings.push(`When a mandatory value exists for ${ATTRIBUTE_TYPE_LABELS[type as keyof typeof ATTRIBUTE_TYPE_LABELS]}, no other values (optional or forbidden) are allowed for that attribute type.`);
if (subj.denied && Array.isArray(subj.denied)) {
subj.denied.forEach((deniedValue) => {
attributes.push({
type: subj.type,
include: "prohibit",
value: [deniedValue]
});
});
}
});
}
return { isValid: warnings.length === 0, warnings, invalidIndices };
const subjectAlternativeNames: any[] = [];
if (templateData.sans && Array.isArray(templateData.sans)) {
templateData.sans.forEach((san) => {
if (san.required && Array.isArray(san.required)) {
san.required.forEach((requiredValue) => {
subjectAlternativeNames.push({
type: san.type,
include: "mandatory",
value: [requiredValue]
});
});
}
if (san.allowed && Array.isArray(san.allowed)) {
san.allowed.forEach((allowedValue) => {
subjectAlternativeNames.push({
type: san.type,
include: "optional",
value: [allowedValue]
});
});
}
if (san.denied && Array.isArray(san.denied)) {
san.denied.forEach((deniedValue) => {
subjectAlternativeNames.push({
type: san.type,
include: "prohibit",
value: [deniedValue]
});
});
}
});
}
const keyUsages = {
requiredUsages: (templateData.keyUsages?.required || []) as CertKeyUsageType[],
optionalUsages: (templateData.keyUsages?.allowed || []) as CertKeyUsageType[]
};
const { control, handleSubmit, reset, watch, setValue, formState } = useForm<FormData>({
resolver: zodResolver(templateSchema),
defaultValues: isEdit
? {
slug: template.slug,
description: template.description || "",
attributes: template.attributes || [],
keyUsages: {
requiredUsages: template.keyUsages?.requiredUsages?.all || [],
optionalUsages: template.keyUsages?.optionalUsages?.all || []
},
extendedKeyUsages: {
requiredUsages: template.extendedKeyUsages?.requiredUsages?.all || [],
optionalUsages: template.extendedKeyUsages?.optionalUsages?.all || []
},
subjectAlternativeNames: template.subjectAlternativeNames || [],
validity: template.validity || { maxDuration: { value: 365, unit: "days" } },
signatureAlgorithm: template.signatureAlgorithm || {
allowedAlgorithms: ["SHA256-RSA"],
defaultAlgorithm: "SHA256-RSA"
},
keyAlgorithm: template.keyAlgorithm || {
allowedKeyTypes: ["RSA-2048"],
defaultKeyType: "RSA-2048"
const extendedKeyUsages = {
requiredUsages: (templateData.extendedKeyUsages?.required ||
[]) as CertExtendedKeyUsageType[],
optionalUsages: (templateData.extendedKeyUsages?.allowed || []) as CertExtendedKeyUsageType[]
};
const validity = templateData.validity?.max
? (() => {
const maxValue = templateData.validity.max;
const match = maxValue.match(/^(\d+)([dmy])$/);
if (match) {
const value = parseInt(match[1], 10);
const unitChar = match[2];
let unit: CertDurationUnit = CertDurationUnit.DAYS;
if (unitChar === "d") {
unit = CertDurationUnit.DAYS;
} else if (unitChar === "m") {
unit = CertDurationUnit.MONTHS;
} else {
unit = CertDurationUnit.YEARS;
}
return {
maxDuration: { value, unit }
};
}
: {
return { maxDuration: { value: 365, unit: CertDurationUnit.DAYS } };
})()
: { maxDuration: { value: 365, unit: CertDurationUnit.DAYS } };
const signatureAlgorithm = {
allowedAlgorithms: templateData.algorithms?.signature || [],
defaultAlgorithm: ""
};
const keyAlgorithm = {
allowedKeyTypes: templateData.algorithms?.keyAlgorithm || [],
defaultKeyType: ""
};
return {
slug: templateData.name || "",
description: templateData.description || "",
attributes,
subjectAlternativeNames,
keyUsages,
extendedKeyUsages,
validity,
signatureAlgorithm,
keyAlgorithm
};
};
const getDefaultValues = (): FormData => ({
slug: "",
description: "",
attributes: [],
@@ -146,47 +222,137 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
extendedKeyUsages: { requiredUsages: [], optionalUsages: [] },
subjectAlternativeNames: [],
validity: {
maxDuration: { value: 365, unit: "days" }
maxDuration: { value: 365, unit: CertDurationUnit.DAYS }
},
signatureAlgorithm: {
allowedAlgorithms: ["SHA256-RSA"],
defaultAlgorithm: "SHA256-RSA"
allowedAlgorithms: [],
defaultAlgorithm: ""
},
keyAlgorithm: {
allowedKeyTypes: ["RSA-2048"],
defaultKeyType: "RSA-2048"
}
allowedKeyTypes: [],
defaultKeyType: ""
}
});
const { control, handleSubmit, reset, watch, setValue, formState } = useForm<FormData>({
resolver: zodResolver(templateSchema),
defaultValues: getDefaultValues()
});
useEffect(() => {
if (isEdit && template) {
const convertedData = convertApiToUiFormat(template);
reset(convertedData);
} else if (!isEdit) {
reset(getDefaultValues());
}
}, [isEdit, template, reset]);
const watchedAttributes = watch("attributes") || [];
const watchedSans = watch("subjectAlternativeNames") || [];
const watchedKeyUsages = watch("keyUsages") || { requiredUsages: [], optionalUsages: [] };
const watchedExtendedKeyUsages = watch("extendedKeyUsages") || { requiredUsages: [], optionalUsages: [] };
const watchedExtendedKeyUsages = watch("extendedKeyUsages") || {
requiredUsages: [],
optionalUsages: []
};
const attributeValidation = useMemo(() =>
validateAttributeRules(watchedAttributes),
[watchedAttributes]
);
const transformToNewApiFormat = (data: FormData) => {
const subject =
data.attributes?.map((attr) => {
const result: any = { type: attr.type };
if (attr.include === "optional" && attr.value && attr.value.length > 0) {
result.allowed = attr.value;
} else if (attr.include === "prohibit" && attr.value && attr.value.length > 0) {
result.denied = attr.value;
}
return result;
}) || [];
const sans =
data.subjectAlternativeNames?.map((san) => {
const result: any = { type: san.type };
if (san.include === "mandatory" && san.value && san.value.length > 0) {
result.required = san.value;
} else if (san.include === "optional" && san.value && san.value.length > 0) {
result.allowed = san.value;
} else if (san.include === "prohibit" && san.value && san.value.length > 0) {
result.denied = san.value;
}
return result;
}) || [];
const keyUsages: any = {};
if (data.keyUsages?.requiredUsages && data.keyUsages.requiredUsages.length > 0) {
keyUsages.required = data.keyUsages.requiredUsages;
}
if (data.keyUsages?.optionalUsages && data.keyUsages.optionalUsages.length > 0) {
keyUsages.allowed = data.keyUsages.optionalUsages;
}
const extendedKeyUsages: any = {};
if (
data.extendedKeyUsages?.requiredUsages &&
data.extendedKeyUsages.requiredUsages.length > 0
) {
extendedKeyUsages.required = data.extendedKeyUsages.requiredUsages;
}
if (
data.extendedKeyUsages?.optionalUsages &&
data.extendedKeyUsages.optionalUsages.length > 0
) {
extendedKeyUsages.allowed = data.extendedKeyUsages.optionalUsages;
}
const algorithms: any = {};
if (
data.signatureAlgorithm?.allowedAlgorithms &&
data.signatureAlgorithm.allowedAlgorithms.length > 0
) {
algorithms.signature = data.signatureAlgorithm.allowedAlgorithms;
}
if (data.keyAlgorithm?.allowedKeyTypes && data.keyAlgorithm.allowedKeyTypes.length > 0) {
algorithms.keyAlgorithm = data.keyAlgorithm.allowedKeyTypes;
}
const validity: any = {};
if (data.validity?.maxDuration) {
let unit = "d";
if (data.validity.maxDuration.unit === CertDurationUnit.DAYS) {
unit = "d";
} else if (data.validity.maxDuration.unit === CertDurationUnit.MONTHS) {
unit = "m";
} else {
unit = "y";
}
validity.max = `${data.validity.maxDuration.value}${unit}`;
}
return {
name: data.slug,
description: data.description,
subject: subject.length > 0 ? subject : undefined,
sans: sans.length > 0 ? sans : undefined,
keyUsages: Object.keys(keyUsages).length > 0 ? keyUsages : undefined,
extendedKeyUsages: Object.keys(extendedKeyUsages).length > 0 ? extendedKeyUsages : undefined,
algorithms: Object.keys(algorithms).length > 0 ? algorithms : undefined,
validity: Object.keys(validity).length > 0 ? validity : undefined
};
};
const onFormSubmit = async (data: FormData) => {
try {
if (!currentProject?.id && !isEdit) return;
if (!attributeValidation.isValid) {
createNotification({
text: "Please fix validation errors before submitting",
type: "error"
});
return;
}
const hasEmptyAttributeValues = data.attributes?.some(attr =>
!attr.value || attr.value.length === 0 || attr.value.some(v => !v.trim())
const hasEmptyAttributeValues = data.attributes?.some(
(attr) => !attr.value || attr.value.length === 0 || attr.value.some((v) => !v.trim())
);
const hasEmptySanValues = data.subjectAlternativeNames?.some(san =>
!san.value || san.value.length === 0 || san.value.some(v => !v.trim())
const hasEmptySanValues = data.subjectAlternativeNames?.some(
(san) => !san.value || san.value.length === 0 || san.value.some((v) => !v.trim())
);
if (hasEmptyAttributeValues || hasEmptySanValues) {
@@ -197,60 +363,18 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
return;
}
const transformedData = transformToNewApiFormat(data);
if (isEdit) {
const updateData = {
templateId: template.id,
name: data.slug,
description: data.description,
attributes: data.attributes || [],
keyUsages: {
requiredUsages: { all: data.keyUsages?.requiredUsages || [] },
optionalUsages: { all: data.keyUsages?.optionalUsages || [] }
},
extendedKeyUsages: {
requiredUsages: { all: data.extendedKeyUsages?.requiredUsages || [] },
optionalUsages: { all: data.extendedKeyUsages?.optionalUsages || [] }
},
subjectAlternativeNames: data.subjectAlternativeNames || [],
validity: {
maxDuration: data.validity?.maxDuration || { value: 365, unit: "days" as const }
},
signatureAlgorithm: {
allowedAlgorithms: data.signatureAlgorithm?.allowedAlgorithms || ["SHA256-RSA"],
defaultAlgorithm: data.signatureAlgorithm?.defaultAlgorithm || "SHA256-RSA"
},
keyAlgorithm: {
allowedKeyTypes: data.keyAlgorithm?.allowedKeyTypes || ["RSA-2048"],
defaultKeyType: data.keyAlgorithm?.defaultKeyType || "RSA-2048"
}
...transformedData
};
await updateTemplate.mutateAsync(updateData);
} else {
const createData = {
projectId: currentProject!.id,
slug: data.slug,
description: data.description,
attributes: data.attributes || [],
keyUsages: {
requiredUsages: { all: data.keyUsages?.requiredUsages || [] },
optionalUsages: { all: data.keyUsages?.optionalUsages || [] }
},
extendedKeyUsages: {
requiredUsages: { all: data.extendedKeyUsages?.requiredUsages || [] },
optionalUsages: { all: data.extendedKeyUsages?.optionalUsages || [] }
},
subjectAlternativeNames: data.subjectAlternativeNames || [],
validity: {
maxDuration: data.validity?.maxDuration || { value: 365, unit: "days" as const }
},
signatureAlgorithm: {
allowedAlgorithms: data.signatureAlgorithm?.allowedAlgorithms || ["SHA256-RSA"],
defaultAlgorithm: data.signatureAlgorithm?.defaultAlgorithm || "SHA256-RSA"
},
keyAlgorithm: {
allowedKeyTypes: data.keyAlgorithm?.allowedKeyTypes || ["RSA-2048"],
defaultKeyType: data.keyAlgorithm?.defaultKeyType || "RSA-2048"
}
...transformedData
};
await createTemplate.mutateAsync(createData);
}
@@ -274,7 +398,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
const addAttribute = () => {
const newAttribute = {
type: SUBJECT_ATTRIBUTE_TYPE_OPTIONS[0],
include: INCLUDE_TYPE_OPTIONS[1],
include: SUBJECT_ATTRIBUTE_INCLUDE_OPTIONS[0],
value: ["*"]
};
setValue("attributes", [...watchedAttributes, newAttribute]);
@@ -288,7 +412,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
const addSan = () => {
const newSan = {
type: SAN_TYPE_OPTIONS[0],
include: INCLUDE_TYPE_OPTIONS[1],
include: SAN_INCLUDE_OPTIONS[1],
value: ["*"]
};
setValue("subjectAlternativeNames", [...watchedSans, newSan]);
@@ -299,14 +423,20 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
setValue("subjectAlternativeNames", newSans);
};
const handleKeyUsagesChange = (usages: { requiredUsages: string[]; optionalUsages: string[] }) => {
const handleKeyUsagesChange = (usages: {
requiredUsages: string[];
optionalUsages: string[];
}) => {
setValue("keyUsages", {
requiredUsages: usages.requiredUsages as any,
optionalUsages: usages.optionalUsages as any
});
};
const handleExtendedKeyUsagesChange = (usages: { requiredUsages: string[]; optionalUsages: string[] }) => {
const handleExtendedKeyUsagesChange = (usages: {
requiredUsages: string[];
optionalUsages: string[];
}) => {
setValue("extendedKeyUsages", {
requiredUsages: usages.requiredUsages as any,
optionalUsages: usages.optionalUsages as any
@@ -328,7 +458,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
title={isEdit ? "Edit Certificate Template" : "Create Certificate Template"}
subTitle={
isEdit
? `Update configuration for ${template?.slug}`
? `Update configuration for ${template?.name}`
: "Define comprehensive certificate policies, validation rules, and constraints"
}
>
@@ -380,93 +510,20 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
</Button>
</div>
{/* Validation warnings */}
{!attributeValidation.isValid && attributeValidation.warnings.length > 0 && (
<div className="bg-yellow-500/10 border border-yellow-500/20 rounded-md p-3">
<div className="flex items-start gap-2">
<FontAwesomeIcon icon={faExclamationTriangle} className="text-yellow-500 mt-0.5" />
<div className="flex-1">
<h4 className="text-yellow-500 font-medium text-sm">Validation Warnings</h4>
<ul className="text-yellow-400 text-sm mt-1 space-y-1">
{attributeValidation.warnings.map((warning, index) => (
<li key={index}>• {warning}</li>
))}
</ul>
</div>
</div>
</div>
)}
<div className="space-y-2">
{watchedAttributes.length === 0 ? (
<div className="text-bunker-300 py-8 text-center">
<div className="py-8 text-center text-bunker-300">
No subject attributes configured yet. Click &quot;Add Attribute&quot; to get
started.
</div>
) : (
watchedAttributes.map((attr, index) => {
const isInvalid = attributeValidation.invalidIndices.includes(index);
const errorClass = isInvalid ? "border-red-500 focus:border-red-500" : "";
return (
<div key={`attr-${attr.type}-${attr.include}-${index}`} className="flex items-start gap-2">
{isInvalid ? (
<Tooltip content="This attribute has validation errors. Check the warnings above for details.">
<div className="flex items-start gap-2 flex-1">
<Select
value={attr.type}
onValueChange={(value) => {
const newAttributes = [...watchedAttributes];
newAttributes[index] = { ...attr, type: value as any };
setValue("attributes", newAttributes);
}}
className={`w-48 ${errorClass}`}
<div
// eslint-disable-next-line react/no-array-index-key
key={`attr-${attr.type}-${attr.include}-${index}`}
className="flex items-start gap-2"
>
{SUBJECT_ATTRIBUTE_TYPE_OPTIONS.map((type) => (
<SelectItem key={type} value={type}>
{ATTRIBUTE_TYPE_LABELS[type]}
</SelectItem>
))}
</Select>
<Select
value={attr.include}
onValueChange={(value) => {
const newAttributes = [...watchedAttributes];
newAttributes[index] = { ...attr, include: value as any };
setValue("attributes", newAttributes);
}}
className={`w-32 ${errorClass}`}
>
{INCLUDE_TYPE_OPTIONS.map((type) => (
<SelectItem key={type} value={type}>
{INCLUDE_TYPE_LABELS[type]}
</SelectItem>
))}
</Select>
<Input
placeholder="Pattern/Value (required - use * for wildcards)"
value={attr.value?.[0] || ""}
onChange={(e) => {
const newAttributes = [...watchedAttributes];
newAttributes[index] = {
...attr,
value: e.target.value.trim() ? [e.target.value.trim()] : []
};
setValue("attributes", newAttributes);
}}
className={`flex-1 ${errorClass} ${
attr.value && attr.value.length > 0 && attr.value[0] === ""
? "border-red-500 focus:border-red-500"
: ""
}`}
required
/>
</div>
</Tooltip>
) : (
<>
<Select
value={attr.type}
onValueChange={(value) => {
@@ -492,9 +549,9 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
}}
className="w-32"
>
{INCLUDE_TYPE_OPTIONS.map((type) => (
{SUBJECT_ATTRIBUTE_INCLUDE_OPTIONS.map((type) => (
<SelectItem key={type} value={type}>
{INCLUDE_TYPE_LABELS[type]}
{SUBJECT_ATTRIBUTE_LABELS[type]}
</SelectItem>
))}
</Select>
@@ -517,8 +574,6 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
}`}
required
/>
</>
)}
{watchedAttributes.length > 1 && (
<IconButton
@@ -557,13 +612,17 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
<div className="space-y-2">
{watchedSans.length === 0 ? (
<div className="text-bunker-300 py-8 text-center">
<div className="py-8 text-center text-bunker-300">
No subject alternative names configured yet. Click &quot;Add SAN&quot; to
get started.
</div>
) : (
watchedSans.map((san, index) => (
<div key={`san-${san.type}-${san.include}-${index}`} className="flex items-start gap-2">
<div
// eslint-disable-next-line react/no-array-index-key
key={`san-${san.type}-${san.include}-${index}`}
className="flex items-start gap-2"
>
<Select
value={san.type}
onValueChange={(value) => {
@@ -589,9 +648,9 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
}}
className="w-32"
>
{INCLUDE_TYPE_OPTIONS.map((type) => (
{SAN_INCLUDE_OPTIONS.map((type) => (
<SelectItem key={type} value={type}>
{INCLUDE_TYPE_LABELS[type]}
{SAN_INCLUDE_LABELS[type]}
</SelectItem>
))}
</Select>
@@ -643,10 +702,104 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
</AccordionContent>
</AccordionItem>
<AccordionItem value="constraints" className="mt-4">
<AccordionTrigger>Constraints</AccordionTrigger>
<AccordionItem value="algorithms" className="mt-4">
<AccordionTrigger>Algorithms</AccordionTrigger>
<AccordionContent>
<div className="space-y-6">
<div>
<h4 className="mb-3 text-sm font-medium text-mineshaft-200">
Signature Algorithms
</h4>
<Controller
control={control}
name="signatureAlgorithm.allowedAlgorithms"
render={({ field, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<div className="grid grid-cols-2 gap-2">
{SIGNATURE_ALGORITHMS.map((alg) => {
const isSelected = field.value?.includes(alg);
return (
<div key={alg} className="flex items-center space-x-3">
<Checkbox
id={`sig-alg-${alg}`}
isChecked={isSelected}
onCheckedChange={(checked) => {
const current = field.value || [];
let newValue;
if (checked && !isSelected) {
newValue = [...current, alg];
} else if (!checked && isSelected) {
newValue = current.filter((a) => a !== alg);
} else {
return;
}
field.onChange(newValue);
}}
/>
<label
htmlFor={`sig-alg-${alg}`}
className="cursor-pointer text-sm font-medium text-mineshaft-200"
>
{alg}
</label>
</div>
);
})}
</div>
</FormControl>
)}
/>
</div>
<div>
<h4 className="mb-3 text-sm font-medium text-mineshaft-200">Key Algorithms</h4>
<Controller
control={control}
name="keyAlgorithm.allowedKeyTypes"
render={({ field, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<div className="grid grid-cols-2 gap-2">
{KEY_ALGORITHMS.map((alg) => {
const isSelected = field.value?.includes(alg);
return (
<div key={alg} className="flex items-center space-x-3">
<Checkbox
id={`key-alg-${alg}`}
isChecked={isSelected}
onCheckedChange={(checked) => {
const current = field.value || [];
let newValue;
if (checked && !isSelected) {
newValue = [...current, alg];
} else if (!checked && isSelected) {
newValue = current.filter((a) => a !== alg);
} else {
return;
}
field.onChange(newValue);
}}
/>
<label
htmlFor={`key-alg-${alg}`}
className="cursor-pointer text-sm font-medium text-mineshaft-200"
>
{alg}
</label>
</div>
);
})}
</div>
</FormControl>
)}
/>
</div>
</div>
</AccordionContent>
</AccordionItem>
<AccordionItem value="validity" className="mt-4">
<AccordionTrigger>Certificate Validity</AccordionTrigger>
<AccordionContent>
<div className="space-y-4">
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<Controller
@@ -692,174 +845,6 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
/>
</div>
</div>
<div className="space-y-3">
<div className="space-y-4">
<Controller
control={control}
name="signatureAlgorithm.allowedAlgorithms"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Allowed Signature Algorithms"
isError={Boolean(error)}
errorText={error?.message}
>
<div className="grid grid-cols-2 gap-2 pl-2">
{SIGNATURE_ALGORITHMS.map((alg) => {
const isSelected = field.value?.includes(alg);
return (
<div key={alg} className="flex items-center space-x-3">
<Checkbox
id={`sig-alg-${alg}`}
isChecked={isSelected}
onCheckedChange={(checked) => {
const current = field.value || [];
let newValue;
if (checked && !isSelected) {
newValue = [...current, alg];
} else if (!checked && isSelected) {
if (current.length > 1) {
newValue = current.filter((a) => a !== alg);
} else {
return;
}
} else {
return;
}
field.onChange(newValue);
const currentDefault = watch(
"signatureAlgorithm.defaultAlgorithm"
);
if (currentDefault && !newValue.includes(currentDefault)) {
setValue(
"signatureAlgorithm.defaultAlgorithm",
newValue[0]
);
}
}}
/>
<label
htmlFor={`sig-alg-${alg}`}
className="text-mineshaft-200 cursor-pointer text-sm font-medium"
>
{alg}
</label>
</div>
);
})}
</div>
</FormControl>
)}
/>
<Controller
control={control}
name="signatureAlgorithm.defaultAlgorithm"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Default Signature Algorithm"
isError={Boolean(error)}
errorText={error?.message}
>
<Select
value={field.value || ""}
onValueChange={field.onChange}
className="w-full"
position="popper"
>
{(watch("signatureAlgorithm.allowedAlgorithms") || []).map(
(alg: string) => (
<SelectItem key={alg} value={alg}>
{alg}
</SelectItem>
)
)}
</Select>
</FormControl>
)}
/>
<Controller
control={control}
name="keyAlgorithm.allowedKeyTypes"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Allowed Key Algorithms"
isError={Boolean(error)}
errorText={error?.message}
>
<div className="grid grid-cols-2 gap-2 pl-2">
{KEY_ALGORITHMS.map((alg) => {
const isSelected = field.value?.includes(alg);
return (
<div key={alg} className="flex items-center space-x-3">
<Checkbox
id={`key-alg-${alg}`}
isChecked={isSelected}
onCheckedChange={(checked) => {
const current = field.value || [];
let newValue;
if (checked && !isSelected) {
newValue = [...current, alg];
} else if (!checked && isSelected) {
if (current.length > 1) {
newValue = current.filter((a) => a !== alg);
} else {
return;
}
} else {
return;
}
field.onChange(newValue);
const currentDefault = watch("keyAlgorithm.defaultKeyType");
if (currentDefault && !newValue.includes(currentDefault)) {
setValue("keyAlgorithm.defaultKeyType", newValue[0]);
}
}}
/>
<label
htmlFor={`key-alg-${alg}`}
className="text-mineshaft-200 cursor-pointer text-sm font-medium"
>
{alg}
</label>
</div>
);
})}
</div>
</FormControl>
)}
/>
<Controller
control={control}
name="keyAlgorithm.defaultKeyType"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Default Key Algorithm"
isError={Boolean(error)}
errorText={error?.message}
>
<Select
value={field.value || ""}
onValueChange={field.onChange}
className="w-full"
position="popper"
>
{(watch("keyAlgorithm.allowedKeyTypes") || []).map((alg: string) => (
<SelectItem key={alg} value={alg}>
{alg}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
</div>
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
@@ -870,9 +855,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
colorSchema="primary"
isLoading={isEdit ? updateTemplate.isPending : createTemplate.isPending}
isDisabled={
!formState.isValid ||
!attributeValidation.isValid ||
(isEdit ? updateTemplate.isPending : createTemplate.isPending)
!formState.isValid || (isEdit ? updateTemplate.isPending : createTemplate.isPending)
}
>
{isEdit ? "Save Changes" : "Create"}
@@ -75,14 +75,17 @@ export const TemplateList = ({ onEditTemplate, onDeleteTemplate }: Props) => {
</Td>
</Tr>
)}
{!isLoading && templates && templates.length > 0 && templates.map((template) => (
{!isLoading &&
templates &&
templates.length > 0 &&
templates.map((template) => (
<Tr
key={template.id}
className="h-10 transition-colors duration-100 hover:bg-mineshaft-700"
>
<Td>
<div className="flex items-center gap-2">
<div className="font-medium">{template.slug}</div>
<div className="font-medium">{template.name}</div>
{template.description && (
<Tooltip content={template.description}>
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
@@ -26,10 +26,16 @@ export enum CertExtendedKeyUsageType {
TIME_STAMPING = "time_stamping"
}
export enum CertIncludeType {
MANDATORY = "mandatory",
OPTIONAL = "optional",
PROHIBIT = "prohibit"
export enum CertAttributeRule {
ALLOW = "allow",
DENY = "deny",
REQUIRE = "require"
}
export enum CertSanEffect {
ALLOW = "allow",
DENY = "deny",
REQUIRE = "require"
}
export enum CertDurationUnit {
@@ -39,7 +45,9 @@ export enum CertDurationUnit {
}
export enum CertSubjectAttributeType {
COMMON_NAME = "common_name"
COMMON_NAME = "common_name",
ORGANIZATION = "organization",
COUNTRY = "country"
}
export const formatSANType = (type: CertSubjectAlternativeNameType): string => {
@@ -104,61 +112,117 @@ export const formatExtendedKeyUsage = (usage: CertExtendedKeyUsageType): string
export const formatSubjectAttributeType = (type: CertSubjectAttributeType): string => {
switch (type) {
case CertSubjectAttributeType.COMMON_NAME:
return "Common Name";
return "Common Name (CN)";
case CertSubjectAttributeType.ORGANIZATION:
return "Organization";
case CertSubjectAttributeType.COUNTRY:
return "Country";
default:
return type;
}
};
export const formatIncludeType = (include: CertIncludeType): string => {
switch (include) {
case CertIncludeType.MANDATORY:
return "Mandatory";
case CertIncludeType.OPTIONAL:
return "Optional";
case CertIncludeType.PROHIBIT:
return "Prohibit";
export const formatAttributeRule = (rule: CertAttributeRule): string => {
switch (rule) {
case CertAttributeRule.ALLOW:
return "Allow";
case CertAttributeRule.DENY:
return "Deny";
case CertAttributeRule.REQUIRE:
return "Require";
default:
return include;
return rule;
}
};
export const mapLegacySANTypeToStandard = (type: string): CertSubjectAlternativeNameType => {
switch (type) {
case "dns":
case "dns_name":
return CertSubjectAlternativeNameType.DNS_NAME;
case "ip":
case "ip_address":
return CertSubjectAlternativeNameType.IP_ADDRESS;
case "email":
return CertSubjectAlternativeNameType.EMAIL;
case "uri":
case "url":
return CertSubjectAlternativeNameType.URI;
export const formatSanEffect = (effect: CertSanEffect): string => {
switch (effect) {
case CertSanEffect.ALLOW:
return "Allow";
case CertSanEffect.DENY:
return "Deny";
case CertSanEffect.REQUIRE:
return "Require";
default:
throw new Error(`Unknown SAN type: ${type}`);
return effect;
}
};
export const mapSANTypeToLegacy = (type: CertSubjectAlternativeNameType): string => {
switch (type) {
case CertSubjectAlternativeNameType.DNS_NAME:
return "dns";
case CertSubjectAlternativeNameType.IP_ADDRESS:
return "ip";
case CertSubjectAlternativeNameType.EMAIL:
return "email";
case CertSubjectAlternativeNameType.URI:
return "uri";
default:
return type;
}
};
export const SAN_TYPE_OPTIONS = Object.values(CertSubjectAlternativeNameType);
export const KEY_USAGE_OPTIONS = Object.values(CertKeyUsageType);
export const EXTENDED_KEY_USAGE_OPTIONS = Object.values(CertExtendedKeyUsageType);
export const INCLUDE_TYPE_OPTIONS = Object.values(CertIncludeType);
export const DURATION_UNIT_OPTIONS = Object.values(CertDurationUnit);
export const SUBJECT_ATTRIBUTE_TYPE_OPTIONS = Object.values(CertSubjectAttributeType);
export const ATTRIBUTE_RULE_OPTIONS = Object.values(CertAttributeRule);
export const SAN_EFFECT_OPTIONS = Object.values(CertSanEffect);
export const SUBJECT_ATTRIBUTE_INCLUDE_OPTIONS = ["optional", "prohibit"] as const;
export const SAN_INCLUDE_OPTIONS = ["mandatory", "optional", "prohibit"] as const;
export const USAGE_STATES = {
REQUIRED: "required",
OPTIONAL: "optional"
} as const;
export type UsageState = typeof USAGE_STATES[keyof typeof USAGE_STATES] | undefined;
export const TEMPLATE_SIGNATURE_ALGORITHMS = [
"SHA256-RSA",
"SHA384-RSA",
"SHA512-RSA",
"SHA256-ECDSA",
"SHA384-ECDSA",
"SHA512-ECDSA"
] as const;
export const TEMPLATE_KEY_ALGORITHMS = [
"RSA-2048",
"RSA-3072",
"RSA-4096",
"ECDSA-P256",
"ECDSA-P384"
] as const;
// API format algorithm constants
export const API_SIGNATURE_ALGORITHMS = [
"RSA-SHA256",
"RSA-SHA384",
"RSA-SHA512",
"ECDSA-SHA256",
"ECDSA-SHA384",
"ECDSA-SHA512"
] as const;
export const API_KEY_ALGORITHMS = [
"RSA_2048",
"RSA_3072",
"RSA_4096",
"EC_prime256v1",
"EC_secp384r1"
] as const;
// Mapping functions between template and API formats
export const mapTemplateSignatureAlgorithmToApi = (templateFormat: string): string => {
const mapping: Record<string, string> = {
"SHA256-RSA": "RSA-SHA256",
"SHA384-RSA": "RSA-SHA384",
"SHA512-RSA": "RSA-SHA512",
"SHA256-ECDSA": "ECDSA-SHA256",
"SHA384-ECDSA": "ECDSA-SHA384",
"SHA512-ECDSA": "ECDSA-SHA512"
};
return mapping[templateFormat] || templateFormat;
};
export const mapTemplateKeyAlgorithmToApi = (templateFormat: string): string => {
const mapping: Record<string, string> = {
"RSA-2048": "RSA_2048",
"RSA-3072": "RSA_3072",
"RSA-4096": "RSA_4096",
"ECDSA-P256": "EC_prime256v1",
"ECDSA-P384": "EC_secp384r1"
};
return mapping[templateFormat] || templateFormat;
};
@@ -1,152 +1,159 @@
import { Checkbox } from "@app/components/v2";
import React from "react";
import { Select, SelectItem } from "@app/components/v2";
import {
CertExtendedKeyUsageType,
CertKeyUsageType,
EXTENDED_KEY_USAGE_OPTIONS,
formatExtendedKeyUsage,
formatKeyUsage,
EXTENDED_KEY_USAGE_OPTIONS,
KEY_USAGE_OPTIONS
} from "./certificate-constants";
type UsageState = "mandatory" | "optional" | undefined;
type KeyUsagePolicy = "allow" | "require" | "deny" | "none";
type ThreeStateCheckboxProps = {
value: UsageState;
onChange: (newValue: UsageState) => void;
label: string;
id: string;
};
const ThreeStateCheckbox = ({ value, onChange, label, id }: ThreeStateCheckboxProps) => {
const handleClick = () => {
if (value === undefined) {
onChange("optional");
} else if (value === "optional") {
onChange("mandatory");
} else {
onChange(undefined);
}
interface KeyUsagesSectionProps {
watchedKeyUsages: {
requiredUsages: CertKeyUsageType[];
optionalUsages: CertKeyUsageType[];
};
const getCheckboxState = () => {
if (value) return true;
return false;
watchedExtendedKeyUsages: {
requiredUsages: CertExtendedKeyUsageType[];
optionalUsages: CertExtendedKeyUsageType[];
};
onKeyUsagesChange: (usages: {
requiredUsages: CertKeyUsageType[];
optionalUsages: CertKeyUsageType[];
}) => void;
onExtendedKeyUsagesChange: (usages: {
requiredUsages: CertExtendedKeyUsageType[];
optionalUsages: CertExtendedKeyUsageType[];
}) => void;
}
const getIndeterminateState = () => {
return value === "optional";
};
const getStateLabel = () => {
if (value === "mandatory") return " (Mandatory)";
if (value === "optional") return " (Optional)";
return "";
};
return (
<div className="flex items-center space-x-3">
<Checkbox
id={id}
isChecked={getCheckboxState()}
isIndeterminate={getIndeterminateState()}
onCheckedChange={handleClick}
/>
<label
htmlFor={id}
className="text-mineshaft-200 cursor-pointer text-sm font-medium"
>
{label}
{value && (
<span className="text-mineshaft-400 text-xs ml-1">
{getStateLabel()}
</span>
)}
</label>
</div>
);
};
type KeyUsagesSectionProps = {
watchedKeyUsages?: { requiredUsages?: string[]; optionalUsages?: string[] };
watchedExtendedKeyUsages?: { requiredUsages?: string[]; optionalUsages?: string[] };
onKeyUsagesChange: (usages: { requiredUsages: string[]; optionalUsages: string[] }) => void;
onExtendedKeyUsagesChange: (usages: { requiredUsages: string[]; optionalUsages: string[] }) => void;
};
export const KeyUsagesSection = ({
watchedKeyUsages = { requiredUsages: [], optionalUsages: [] },
watchedExtendedKeyUsages = { requiredUsages: [], optionalUsages: [] },
export const KeyUsagesSection: React.FC<KeyUsagesSectionProps> = ({
watchedKeyUsages,
watchedExtendedKeyUsages,
onKeyUsagesChange,
onExtendedKeyUsagesChange
}: KeyUsagesSectionProps) => {
const getUsageState = (usage: string, data: { requiredUsages?: string[]; optionalUsages?: string[] }): UsageState => {
if (data.requiredUsages?.includes(usage)) return "mandatory";
if (data.optionalUsages?.includes(usage)) return "optional";
return undefined;
}) => {
const getKeyUsagePolicy = (usage: CertKeyUsageType): KeyUsagePolicy => {
if (watchedKeyUsages.requiredUsages.includes(usage)) return "require";
if (watchedKeyUsages.optionalUsages.includes(usage)) return "allow";
return "deny";
};
const handleKeyUsageChange = (usage: CertKeyUsageType, newState: UsageState) => {
const currentRequired = watchedKeyUsages.requiredUsages || [];
const currentOptional = watchedKeyUsages.optionalUsages || [];
const getExtendedKeyUsagePolicy = (usage: CertExtendedKeyUsageType): KeyUsagePolicy => {
if (watchedExtendedKeyUsages.requiredUsages.includes(usage)) return "require";
if (watchedExtendedKeyUsages.optionalUsages.includes(usage)) return "allow";
return "deny";
};
let newRequired = currentRequired.filter(u => u !== usage);
let newOptional = currentOptional.filter(u => u !== usage);
const handleKeyUsagePolicyChange = (usage: CertKeyUsageType, policy: KeyUsagePolicy) => {
const newRequired = watchedKeyUsages.requiredUsages.filter((u) => u !== usage);
const newOptional = watchedKeyUsages.optionalUsages.filter((u) => u !== usage);
if (newState === "mandatory") {
newRequired = [...newRequired, usage];
} else if (newState === "optional") {
newOptional = [...newOptional, usage];
if (policy === "require") {
newRequired.push(usage);
} else if (policy === "allow") {
newOptional.push(usage);
}
onKeyUsagesChange({ requiredUsages: newRequired, optionalUsages: newOptional });
onKeyUsagesChange({
requiredUsages: newRequired,
optionalUsages: newOptional
});
};
const handleExtendedKeyUsageChange = (usage: CertExtendedKeyUsageType, newState: UsageState) => {
const currentRequired = watchedExtendedKeyUsages.requiredUsages || [];
const currentOptional = watchedExtendedKeyUsages.optionalUsages || [];
const handleExtendedKeyUsagePolicyChange = (
usage: CertExtendedKeyUsageType,
policy: KeyUsagePolicy
) => {
const newRequired = watchedExtendedKeyUsages.requiredUsages.filter((u) => u !== usage);
const newOptional = watchedExtendedKeyUsages.optionalUsages.filter((u) => u !== usage);
let newRequired = currentRequired.filter(u => u !== usage);
let newOptional = currentOptional.filter(u => u !== usage);
if (newState === "mandatory") {
newRequired = [...newRequired, usage];
} else if (newState === "optional") {
newOptional = [...newOptional, usage];
if (policy === "require") {
newRequired.push(usage);
} else if (policy === "allow") {
newOptional.push(usage);
}
onExtendedKeyUsagesChange({ requiredUsages: newRequired, optionalUsages: newOptional });
onExtendedKeyUsagesChange({
requiredUsages: newRequired,
optionalUsages: newOptional
});
};
const keyUsagePolicyOptions = [
{ value: "deny", label: "Deny" },
{ value: "allow", label: "Allow" },
{ value: "require", label: "Require" }
];
const extendedKeyUsagePolicyOptions = [
{ value: "deny", label: "Deny" },
{ value: "allow", label: "Allow" },
{ value: "require", label: "Require" }
];
return (
<div className="space-y-6">
<div className="space-y-3">
<h3 className="text-mineshaft-200 text-sm font-medium">Key Usages</h3>
<div className="grid grid-cols-2 gap-2 pl-2">
{KEY_USAGE_OPTIONS.map((usage) => (
<ThreeStateCheckbox
key={usage}
id={`key-usage-${usage}`}
label={formatKeyUsage(usage)}
value={getUsageState(usage, watchedKeyUsages)}
onChange={(newState) => handleKeyUsageChange(usage, newState)}
/>
<div className="space-y-8">
{/* Key Usages */}
<div className="space-y-4">
<h4 className="text-sm font-medium text-bunker-200">Key Usages</h4>
<div className="grid grid-cols-2 gap-4">
{KEY_USAGE_OPTIONS.map((usage) => {
const policy = getKeyUsagePolicy(usage);
return (
<div key={usage} className="flex items-center justify-between">
<span className="text-sm text-bunker-200">{formatKeyUsage(usage)}</span>
<Select
value={policy}
onValueChange={(value) =>
handleKeyUsagePolicyChange(usage, value as KeyUsagePolicy)
}
className="w-32"
>
{keyUsagePolicyOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</Select>
</div>
);
})}
</div>
</div>
<div className="space-y-3">
<h3 className="text-mineshaft-200 text-sm font-medium">Extended Key Usages</h3>
<div className="grid grid-cols-2 gap-2 pl-2">
{EXTENDED_KEY_USAGE_OPTIONS.map((usage) => (
<ThreeStateCheckbox
key={usage}
id={`ext-key-usage-${usage}`}
label={formatExtendedKeyUsage(usage)}
value={getUsageState(usage, watchedExtendedKeyUsages)}
onChange={(newState) => handleExtendedKeyUsageChange(usage, newState)}
/>
{/* Extended Key Usages */}
<div className="space-y-4">
<h4 className="text-sm font-medium text-bunker-200">Extended Key Usages</h4>
<div className="grid grid-cols-2 gap-4">
{EXTENDED_KEY_USAGE_OPTIONS.map((usage) => {
const policy = getExtendedKeyUsagePolicy(usage);
return (
<div key={usage} className="flex items-center justify-between">
<span className="text-sm text-bunker-200">{formatExtendedKeyUsage(usage)}</span>
<Select
value={policy}
onValueChange={(value) =>
handleExtendedKeyUsagePolicyChange(usage, value as KeyUsagePolicy)
}
className="w-32"
>
{extendedKeyUsagePolicyOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</Select>
</div>
);
})}
</div>
</div>
</div>
@@ -3,93 +3,121 @@ import { z } from "zod";
import {
CertDurationUnit,
CertExtendedKeyUsageType,
CertIncludeType,
CertKeyUsageType,
CertSubjectAlternativeNameType,
CertSubjectAttributeType
CertSubjectAttributeType,
SUBJECT_ATTRIBUTE_INCLUDE_OPTIONS,
SAN_INCLUDE_OPTIONS
} from "./certificate-constants";
export const attributeSchema = z.object({
export const uiAttributeSchema = z.object({
type: z.nativeEnum(CertSubjectAttributeType),
include: z.nativeEnum(CertIncludeType),
value: z.array(z.string().min(1, "Value cannot be empty")).optional()
include: z.enum(SUBJECT_ATTRIBUTE_INCLUDE_OPTIONS),
value: z.array(z.string().min(1, "Value cannot be empty"))
});
export const sanSchema = z.object({
export const uiSanSchema = z.object({
type: z.nativeEnum(CertSubjectAlternativeNameType),
include: z.nativeEnum(CertIncludeType),
value: z.array(z.string().min(1, "Value cannot be empty")).optional()
include: z.enum(SAN_INCLUDE_OPTIONS),
value: z.array(z.string().min(1, "Value cannot be empty"))
});
export const uiKeyUsagesSchema = z.object({
requiredUsages: z.array(z.nativeEnum(CertKeyUsageType)),
optionalUsages: z.array(z.nativeEnum(CertKeyUsageType))
});
export const uiExtendedKeyUsagesSchema = z.object({
requiredUsages: z.array(z.nativeEnum(CertExtendedKeyUsageType)),
optionalUsages: z.array(z.nativeEnum(CertExtendedKeyUsageType))
});
export const uiValiditySchema = z.object({
maxDuration: z.object({
value: z.number().min(1, "Duration must be at least 1"),
unit: z.nativeEnum(CertDurationUnit)
})
});
export const uiSignatureAlgorithmSchema = z.object({
allowedAlgorithms: z.array(z.string()).optional(),
defaultAlgorithm: z.string().optional()
});
export const uiKeyAlgorithmSchema = z.object({
allowedKeyTypes: z.array(z.string()).optional(),
defaultKeyType: z.string().optional()
});
export const templateSchema = z.object({
slug: z.string().trim().min(1, "Template name is required"),
description: z.string().optional(),
attributes: z.array(attributeSchema).optional().refine((attributes) => {
if (!attributes) return true;
attributes: z.array(uiAttributeSchema).optional(),
subjectAlternativeNames: z.array(uiSanSchema).optional(),
keyUsages: uiKeyUsagesSchema.optional(),
extendedKeyUsages: uiExtendedKeyUsagesSchema.optional(),
validity: uiValiditySchema.optional(),
signatureAlgorithm: uiSignatureAlgorithmSchema.optional(),
keyAlgorithm: uiKeyAlgorithmSchema.optional()
});
const attributesByType = attributes.reduce((acc, attr) => {
if (!acc[attr.type]) acc[attr.type] = [];
acc[attr.type].push(attr);
return acc;
}, {} as Record<string, typeof attributes>);
export type TemplateFormData = z.infer<typeof templateSchema>;
for (const [, attrs] of Object.entries(attributesByType)) {
const mandatoryAttrs = attrs.filter(attr => attr.include === 'mandatory');
export const apiSubjectSchema = z
.object({
type: z.nativeEnum(CertSubjectAttributeType),
allowed: z.array(z.string().min(1, "Value cannot be empty")).optional(),
required: z.array(z.string().min(1, "Value cannot be empty")).optional(),
denied: z.array(z.string().min(1, "Value cannot be empty")).optional()
})
.refine((data) => data.allowed || data.required || data.denied, {
message: "At least one allowed, required, or denied value must be provided"
});
if (mandatoryAttrs.length > 1) {
return false;
}
export const apiSanSchema = z
.object({
type: z.nativeEnum(CertSubjectAlternativeNameType),
allowed: z.array(z.string().min(1, "Value cannot be empty")).optional(),
required: z.array(z.string().min(1, "Value cannot be empty")).optional(),
denied: z.array(z.string().min(1, "Value cannot be empty")).optional()
})
.refine((data) => data.allowed || data.required || data.denied, {
message: "At least one allowed, required, or denied value must be provided"
});
if (mandatoryAttrs.length === 1 && attrs.length > 1) {
return false;
}
}
return true;
}, {
message: "Attribute validation failed: when a mandatory value exists, no other values are allowed for that attribute type"
}),
export const apiTemplateSchema = z.object({
name: z.string().trim().min(1, "Template name is required"),
description: z.string().optional(),
subject: z.array(apiSubjectSchema).optional(),
sans: z.array(apiSanSchema).optional(),
keyUsages: z
.object({
requiredUsages: z.array(z.nativeEnum(CertKeyUsageType)).optional(),
optionalUsages: z.array(z.nativeEnum(CertKeyUsageType)).optional()
allowed: z.array(z.nativeEnum(CertKeyUsageType)).optional(),
required: z.array(z.nativeEnum(CertKeyUsageType)).optional(),
denied: z.array(z.nativeEnum(CertKeyUsageType)).optional()
})
.optional(),
extendedKeyUsages: z
.object({
requiredUsages: z.array(z.nativeEnum(CertExtendedKeyUsageType)).optional(),
optionalUsages: z.array(z.nativeEnum(CertExtendedKeyUsageType)).optional()
allowed: z.array(z.nativeEnum(CertExtendedKeyUsageType)).optional(),
required: z.array(z.nativeEnum(CertExtendedKeyUsageType)).optional(),
denied: z.array(z.nativeEnum(CertExtendedKeyUsageType)).optional()
})
.optional(),
algorithms: z
.object({
signature: z.array(z.string()).optional(),
keyAlgorithm: z.array(z.string()).optional()
})
.optional(),
subjectAlternativeNames: z.array(sanSchema).optional(),
validity: z
.object({
maxDuration: z
.object({
value: z.number().min(1, "Duration must be at least 1"),
unit: z.nativeEnum(CertDurationUnit)
})
.optional(),
minDuration: z
.object({
value: z.number().min(1, "Duration must be at least 1"),
unit: z.nativeEnum(CertDurationUnit)
})
max: z
.string()
.regex(/^\d+[dhmy]$/, "Must be in format like '365d', '12m', '1y', or '24h'")
.optional()
})
.optional(),
signatureAlgorithm: z
.object({
allowedAlgorithms: z.array(z.string()).optional(),
defaultAlgorithm: z.string().optional()
})
.optional(),
keyAlgorithm: z
.object({
allowedKeyTypes: z.array(z.string()).optional(),
defaultKeyType: z.string().optional()
})
.optional()
});
export type TemplateFormData = z.infer<typeof templateSchema>;
export type ApiTemplateFormData = z.infer<typeof apiTemplateSchema>;
@@ -2,7 +2,9 @@ import {
CertExtendedKeyUsageType,
CertKeyUsageType,
formatExtendedKeyUsage,
formatKeyUsage
formatKeyUsage,
USAGE_STATES,
UsageState
} from "./certificate-constants";
export const formatUsageName = (usage: string): string => {
@@ -14,6 +16,7 @@ export const formatUsageName = (usage: string): string => {
return formatExtendedKeyUsage(usage as CertExtendedKeyUsageType);
}
} catch {
// Handle any errors in type checking
}
return usage.replace(/_/g, " ");
};
@@ -22,15 +25,15 @@ export const getUsageState = (
usage: CertKeyUsageType | CertExtendedKeyUsageType,
requiredUsages: (CertKeyUsageType | CertExtendedKeyUsageType)[],
optionalUsages: (CertKeyUsageType | CertExtendedKeyUsageType)[]
): "required" | "optional" | undefined => {
if (requiredUsages.includes(usage)) return "required";
if (optionalUsages.includes(usage)) return "optional";
): UsageState => {
if (requiredUsages.includes(usage)) return USAGE_STATES.REQUIRED;
if (optionalUsages.includes(usage)) return USAGE_STATES.OPTIONAL;
return undefined;
};
export const toggleUsageState = (
usage: CertKeyUsageType | CertExtendedKeyUsageType,
newState: "required" | "optional" | undefined,
newState: UsageState,
currentRequiredUsages: (CertKeyUsageType | CertExtendedKeyUsageType)[],
currentOptionalUsages: (CertKeyUsageType | CertExtendedKeyUsageType)[],
toggleRequired: (usage: CertKeyUsageType | CertExtendedKeyUsageType) => void,
@@ -39,10 +42,10 @@ export const toggleUsageState = (
const isRequired = currentRequiredUsages.includes(usage);
const isOptional = currentOptionalUsages.includes(usage);
if (newState === "required") {
if (newState === USAGE_STATES.REQUIRED) {
if (isOptional) toggleOptional(usage);
if (!isRequired) toggleRequired(usage);
} else if (newState === "optional") {
} else if (newState === USAGE_STATES.OPTIONAL) {
if (isRequired) toggleRequired(usage);
if (!isOptional) toggleOptional(usage);
} else {