mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 21:27:10 +00:00
PKI revamp EST improvements and fixes
This commit is contained in:
@@ -34,7 +34,7 @@ export async function up(knex: Knex): Promise<void> {
|
|||||||
|
|
||||||
t.boolean("disableBootstrapCaValidation").defaultTo(false);
|
t.boolean("disableBootstrapCaValidation").defaultTo(false);
|
||||||
t.text("hashedPassphrase").notNullable();
|
t.text("hashedPassphrase").notNullable();
|
||||||
t.binary("encryptedCaChain").notNullable();
|
t.binary("encryptedCaChain");
|
||||||
|
|
||||||
t.timestamps(true, true, true);
|
t.timestamps(true, true, true);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export const PkiEstEnrollmentConfigsSchema = z.object({
|
|||||||
id: z.string().uuid(),
|
id: z.string().uuid(),
|
||||||
disableBootstrapCaValidation: z.boolean().default(false).nullable().optional(),
|
disableBootstrapCaValidation: z.boolean().default(false).nullable().optional(),
|
||||||
hashedPassphrase: z.string(),
|
hashedPassphrase: z.string(),
|
||||||
encryptedCaChain: zodBuffer,
|
encryptedCaChain: zodBuffer.nullable().optional(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date()
|
updatedAt: z.date()
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,41 +4,22 @@ import { getConfig } from "@app/lib/config/env";
|
|||||||
import { crypto } from "@app/lib/crypto/cryptography";
|
import { crypto } from "@app/lib/crypto/cryptography";
|
||||||
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
|
|
||||||
|
|
||||||
export const registerCertificateEstRouter = async (server: FastifyZodProvider) => {
|
export const registerCertificateEstRouter = async (server: FastifyZodProvider) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
|
|
||||||
const getIdentifierType = async ({
|
const getIdentifierType = async (identifier: string): Promise<"template" | "profile" | null> => {
|
||||||
identifier,
|
|
||||||
actor,
|
|
||||||
actorId,
|
|
||||||
actorAuthMethod,
|
|
||||||
actorOrgId
|
|
||||||
}: {
|
|
||||||
identifier: string;
|
|
||||||
actor: ActorType;
|
|
||||||
actorId: string;
|
|
||||||
actorAuthMethod: ActorAuthMethod;
|
|
||||||
actorOrgId: string;
|
|
||||||
}): Promise<"template" | "profile" | null> => {
|
|
||||||
try {
|
try {
|
||||||
|
// Try to find as profile first using internal access
|
||||||
await server.services.certificateProfile.getEstConfigurationByProfile({
|
await server.services.certificateProfile.getEstConfigurationByProfile({
|
||||||
actor,
|
profileId: identifier,
|
||||||
actorId,
|
isInternal: true
|
||||||
actorAuthMethod,
|
|
||||||
actorOrgId,
|
|
||||||
profileId: identifier
|
|
||||||
});
|
});
|
||||||
return "profile";
|
return "profile";
|
||||||
} catch (profileError) {
|
} catch (profileError) {
|
||||||
try {
|
try {
|
||||||
await server.services.certificateTemplate.getEstConfiguration({
|
await server.services.certificateTemplate.getEstConfiguration({
|
||||||
isInternal: false,
|
isInternal: true,
|
||||||
actor,
|
|
||||||
actorId,
|
|
||||||
actorAuthMethod,
|
|
||||||
actorOrgId,
|
|
||||||
certificateTemplateId: identifier
|
certificateTemplateId: identifier
|
||||||
});
|
});
|
||||||
return "template";
|
return "template";
|
||||||
@@ -109,13 +90,7 @@ export const registerCertificateEstRouter = async (server: FastifyZodProvider) =
|
|||||||
|
|
||||||
const identifier = urlFragments.slice(-2)[0];
|
const identifier = urlFragments.slice(-2)[0];
|
||||||
|
|
||||||
const identifierType = await getIdentifierType({
|
const identifierType = await getIdentifierType(identifier);
|
||||||
identifier,
|
|
||||||
actor: req.permission.type,
|
|
||||||
actorId: req.permission.id,
|
|
||||||
actorAuthMethod: req.permission.authMethod,
|
|
||||||
actorOrgId: req.permission.orgId
|
|
||||||
});
|
|
||||||
if (!identifierType) {
|
if (!identifierType) {
|
||||||
res.raw.statusCode = 404;
|
res.raw.statusCode = 404;
|
||||||
res.raw.setHeader("Content-Type", "text/plain");
|
res.raw.setHeader("Content-Type", "text/plain");
|
||||||
@@ -127,11 +102,8 @@ export const registerCertificateEstRouter = async (server: FastifyZodProvider) =
|
|||||||
let estConfig;
|
let estConfig;
|
||||||
if (identifierType === "profile") {
|
if (identifierType === "profile") {
|
||||||
estConfig = await server.services.certificateProfile.getEstConfigurationByProfile({
|
estConfig = await server.services.certificateProfile.getEstConfigurationByProfile({
|
||||||
actor: req.permission.type,
|
profileId: identifier,
|
||||||
actorId: req.permission.id,
|
isInternal: true
|
||||||
actorAuthMethod: req.permission.authMethod,
|
|
||||||
actorOrgId: req.permission.orgId,
|
|
||||||
profileId: identifier
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
estConfig = await server.services.certificateTemplate.getEstConfiguration({
|
estConfig = await server.services.certificateTemplate.getEstConfiguration({
|
||||||
@@ -188,13 +160,7 @@ export const registerCertificateEstRouter = async (server: FastifyZodProvider) =
|
|||||||
void res.header("Content-Transfer-Encoding", "base64");
|
void res.header("Content-Transfer-Encoding", "base64");
|
||||||
|
|
||||||
const { identifier } = req.params;
|
const { identifier } = req.params;
|
||||||
const identifierType = await getIdentifierType({
|
const identifierType = await getIdentifierType(identifier);
|
||||||
identifier,
|
|
||||||
actor: req.permission.type,
|
|
||||||
actorId: req.permission.id,
|
|
||||||
actorOrgId: req.permission.orgId,
|
|
||||||
actorAuthMethod: req.permission.authMethod
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!identifierType) {
|
if (!identifierType) {
|
||||||
throw new BadRequestError({ message: "Certificate template or profile not found" });
|
throw new BadRequestError({ message: "Certificate template or profile not found" });
|
||||||
@@ -235,13 +201,7 @@ export const registerCertificateEstRouter = async (server: FastifyZodProvider) =
|
|||||||
void res.header("Content-Transfer-Encoding", "base64");
|
void res.header("Content-Transfer-Encoding", "base64");
|
||||||
|
|
||||||
const { identifier } = req.params;
|
const { identifier } = req.params;
|
||||||
const identifierType = await getIdentifierType({
|
const identifierType = await getIdentifierType(identifier);
|
||||||
identifier,
|
|
||||||
actor: req.permission.type,
|
|
||||||
actorId: req.permission.id,
|
|
||||||
actorOrgId: req.permission.orgId,
|
|
||||||
actorAuthMethod: req.permission.authMethod
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!identifierType) {
|
if (!identifierType) {
|
||||||
throw new BadRequestError({ message: "Certificate template or profile not found" });
|
throw new BadRequestError({ message: "Certificate template or profile not found" });
|
||||||
@@ -281,13 +241,7 @@ export const registerCertificateEstRouter = async (server: FastifyZodProvider) =
|
|||||||
void res.header("Content-Transfer-Encoding", "base64");
|
void res.header("Content-Transfer-Encoding", "base64");
|
||||||
|
|
||||||
const { identifier } = req.params;
|
const { identifier } = req.params;
|
||||||
const identifierType = await getIdentifierType({
|
const identifierType = await getIdentifierType(identifier);
|
||||||
identifier,
|
|
||||||
actor: req.permission.type,
|
|
||||||
actorId: req.permission.id,
|
|
||||||
actorOrgId: req.permission.orgId,
|
|
||||||
actorAuthMethod: req.permission.authMethod
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!identifierType) {
|
if (!identifierType) {
|
||||||
throw new BadRequestError({ message: "Certificate template or profile not found" });
|
throw new BadRequestError({ message: "Certificate template or profile not found" });
|
||||||
|
|||||||
@@ -2126,6 +2126,7 @@ export const registerRoutes = async (
|
|||||||
const certificateEstV3Service = certificateEstV3ServiceFactory({
|
const certificateEstV3Service = certificateEstV3ServiceFactory({
|
||||||
internalCertificateAuthorityService,
|
internalCertificateAuthorityService,
|
||||||
certificateTemplateDAL,
|
certificateTemplateDAL,
|
||||||
|
certificateTemplateV2Service,
|
||||||
certificateAuthorityDAL,
|
certificateAuthorityDAL,
|
||||||
certificateAuthorityCertDAL,
|
certificateAuthorityCertDAL,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
|||||||
.object({
|
.object({
|
||||||
disableBootstrapCaValidation: z.boolean().default(false),
|
disableBootstrapCaValidation: z.boolean().default(false),
|
||||||
passphraseInput: z.string().min(1),
|
passphraseInput: z.string().min(1),
|
||||||
encryptedCaChain: z.string()
|
encryptedCaChain: z.string().optional()
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
apiConfig: z
|
apiConfig: z
|
||||||
|
|||||||
@@ -1,30 +1,55 @@
|
|||||||
import RE2 from "re2";
|
import * as x509 from "@peculiar/x509";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { ApiDocsTags } from "@app/lib/api-docs";
|
import { ApiDocsTags } from "@app/lib/api-docs";
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { ms } from "@app/lib/ms";
|
import { ms } from "@app/lib/ms";
|
||||||
import { writeLimit } from "@app/server/config/rateLimiter";
|
import { writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
import {
|
import {
|
||||||
ACMESANType,
|
ACMESANType,
|
||||||
|
CertExtendedKeyUsageOIDToName,
|
||||||
CertificateOrderStatus,
|
CertificateOrderStatus,
|
||||||
CertKeyAlgorithm,
|
CertKeyAlgorithm,
|
||||||
CertSignatureAlgorithm
|
CertKeyUsage,
|
||||||
|
CertSignatureAlgorithm,
|
||||||
|
mapLegacyAltNameType,
|
||||||
|
TAltNameMapping
|
||||||
} from "@app/services/certificate/certificate-types";
|
} from "@app/services/certificate/certificate-types";
|
||||||
|
import { parseDistinguishedName } from "@app/services/certificate-authority/certificate-authority-fns";
|
||||||
import {
|
import {
|
||||||
validateAltNamesField,
|
|
||||||
validateAndMapAltNameType,
|
validateAndMapAltNameType,
|
||||||
validateCaDateField
|
validateCaDateField
|
||||||
} from "@app/services/certificate-authority/certificate-authority-validators";
|
} from "@app/services/certificate-authority/certificate-authority-validators";
|
||||||
import {
|
import {
|
||||||
CertExtendedKeyUsageType,
|
CertExtendedKeyUsageType,
|
||||||
CertKeyUsageType,
|
CertKeyUsageType,
|
||||||
CertSubjectAlternativeNameType
|
CertSubjectAlternativeNameType,
|
||||||
|
mapLegacyExtendedKeyUsageToStandard,
|
||||||
|
mapLegacyKeyUsageToStandard
|
||||||
} from "@app/services/certificate-common/certificate-constants";
|
} from "@app/services/certificate-common/certificate-constants";
|
||||||
import { mapEnumsForValidation } from "@app/services/certificate-common/certificate-utils";
|
import { mapEnumsForValidation } from "@app/services/certificate-common/certificate-utils";
|
||||||
import { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators";
|
import { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators";
|
||||||
|
import { TCertificateRequest } from "@app/services/certificate-template-v2/certificate-template-v2-types";
|
||||||
|
|
||||||
|
interface CertificateRequestForService {
|
||||||
|
commonName?: string;
|
||||||
|
keyUsages?: CertKeyUsageType[];
|
||||||
|
extendedKeyUsages?: CertExtendedKeyUsageType[];
|
||||||
|
altNames?: Array<{
|
||||||
|
type: CertSubjectAlternativeNameType;
|
||||||
|
value: string;
|
||||||
|
}>;
|
||||||
|
validity: {
|
||||||
|
ttl: string;
|
||||||
|
};
|
||||||
|
notBefore?: Date;
|
||||||
|
notAfter?: Date;
|
||||||
|
signatureAlgorithm?: string;
|
||||||
|
keyAlgorithm?: string;
|
||||||
|
}
|
||||||
|
|
||||||
const validateTtlAndDateFields = (data: { notBefore?: string; notAfter?: string; ttl?: string }) => {
|
const validateTtlAndDateFields = (data: { notBefore?: string; notAfter?: string; ttl?: string }) => {
|
||||||
const hasDateFields = data.notBefore || data.notAfter;
|
const hasDateFields = data.notBefore || data.notAfter;
|
||||||
@@ -41,6 +66,67 @@ const validateDateOrder = (data: { notBefore?: string; notAfter?: string }) => {
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const extractCertificateRequestFromCSR = (csr: string): TCertificateRequest => {
|
||||||
|
let csrPem = csr;
|
||||||
|
if (!csr.includes("-----BEGIN CERTIFICATE REQUEST-----")) {
|
||||||
|
try {
|
||||||
|
csrPem = Buffer.from(csr, "base64").toString("utf8");
|
||||||
|
} catch (error) {
|
||||||
|
throw new BadRequestError({ message: "Invalid base64 CSR encoding" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const csrObj = new x509.Pkcs10CertificateRequest(csrPem);
|
||||||
|
const subject = parseDistinguishedName(csrObj.subject);
|
||||||
|
|
||||||
|
const certificateRequest: TCertificateRequest = {
|
||||||
|
commonName: subject.commonName,
|
||||||
|
organization: subject.organization,
|
||||||
|
organizationUnit: subject.ou,
|
||||||
|
locality: subject.locality,
|
||||||
|
state: subject.province,
|
||||||
|
country: subject.country
|
||||||
|
};
|
||||||
|
|
||||||
|
const csrKeyUsageExtension = csrObj.getExtension("2.5.29.15") as x509.KeyUsagesExtension;
|
||||||
|
if (csrKeyUsageExtension) {
|
||||||
|
const csrKeyUsages = Object.values(CertKeyUsage).filter(
|
||||||
|
// eslint-disable-next-line no-bitwise
|
||||||
|
(keyUsage) => (x509.KeyUsageFlags[keyUsage] & csrKeyUsageExtension.usages) !== 0
|
||||||
|
);
|
||||||
|
certificateRequest.keyUsages = csrKeyUsages.map(mapLegacyKeyUsageToStandard);
|
||||||
|
}
|
||||||
|
|
||||||
|
const csrExtendedKeyUsageExtension = csrObj.getExtension("2.5.29.37") as x509.ExtendedKeyUsageExtension;
|
||||||
|
if (csrExtendedKeyUsageExtension) {
|
||||||
|
const csrExtendedKeyUsages = csrExtendedKeyUsageExtension.usages
|
||||||
|
.map((ekuOid) => CertExtendedKeyUsageOIDToName[ekuOid as string])
|
||||||
|
.filter((eku) => eku !== undefined);
|
||||||
|
certificateRequest.extendedKeyUsages = csrExtendedKeyUsages.map(mapLegacyExtendedKeyUsageToStandard);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sanExtension = csrObj.extensions.find((ext) => ext.type === "2.5.29.17");
|
||||||
|
if (sanExtension) {
|
||||||
|
const sanNames = new x509.GeneralNames(sanExtension.value);
|
||||||
|
const altNamesArray: TAltNameMapping[] = sanNames.items
|
||||||
|
.filter((value) => value.type === "email" || value.type === "dns" || value.type === "url" || value.type === "ip")
|
||||||
|
.map((name): TAltNameMapping => {
|
||||||
|
const altNameType = validateAndMapAltNameType(name.value);
|
||||||
|
if (!altNameType) {
|
||||||
|
throw new BadRequestError({ message: `Invalid altName from CSR: ${name.value}` });
|
||||||
|
}
|
||||||
|
return altNameType;
|
||||||
|
});
|
||||||
|
|
||||||
|
certificateRequest.subjectAlternativeNames = altNamesArray.map((altName) => ({
|
||||||
|
type: mapLegacyAltNameType(altName.type),
|
||||||
|
value: altName.value
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return certificateRequest;
|
||||||
|
};
|
||||||
|
|
||||||
export const registerCertificatesRouter = async (server: FastifyZodProvider) => {
|
export const registerCertificatesRouter = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
server.route({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -54,6 +140,7 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
|||||||
body: z
|
body: z
|
||||||
.object({
|
.object({
|
||||||
profileId: z.string().uuid(),
|
profileId: z.string().uuid(),
|
||||||
|
csr: z.string().trim().optional(),
|
||||||
commonName: validateTemplateRegexField.optional(),
|
commonName: validateTemplateRegexField.optional(),
|
||||||
ttl: z
|
ttl: z
|
||||||
.string()
|
.string()
|
||||||
@@ -64,7 +151,14 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
|||||||
extendedKeyUsages: z.nativeEnum(CertExtendedKeyUsageType).array().optional(),
|
extendedKeyUsages: z.nativeEnum(CertExtendedKeyUsageType).array().optional(),
|
||||||
notBefore: validateCaDateField.optional(),
|
notBefore: validateCaDateField.optional(),
|
||||||
notAfter: validateCaDateField.optional(),
|
notAfter: validateCaDateField.optional(),
|
||||||
subjectAltNames: validateAltNamesField.optional(),
|
altNames: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
type: z.nativeEnum(CertSubjectAlternativeNameType),
|
||||||
|
value: z.string().min(1, "SAN value cannot be empty")
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
signatureAlgorithm: z.nativeEnum(CertSignatureAlgorithm).optional(),
|
signatureAlgorithm: z.nativeEnum(CertSignatureAlgorithm).optional(),
|
||||||
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm).optional()
|
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm).optional()
|
||||||
})
|
})
|
||||||
@@ -88,44 +182,45 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
|||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const rawCertificateRequest = {
|
let certificateRequestForService: CertificateRequestForService;
|
||||||
commonName: req.body.commonName,
|
|
||||||
keyUsages: req.body.keyUsages,
|
|
||||||
extendedKeyUsages: req.body.extendedKeyUsages,
|
|
||||||
altNames: req.body.subjectAltNames
|
|
||||||
? (() => {
|
|
||||||
const splitRegex = new RE2("[,;]+");
|
|
||||||
return req.body.subjectAltNames
|
|
||||||
.split(splitRegex)
|
|
||||||
.map((name) => name.trim())
|
|
||||||
.filter((name) => name.length > 0)
|
|
||||||
.map((name) => {
|
|
||||||
const mappedType = validateAndMapAltNameType(name);
|
|
||||||
if (!mappedType) return null;
|
|
||||||
const typeMapping = {
|
|
||||||
dns: "dns_name",
|
|
||||||
ip: "ip_address",
|
|
||||||
email: "email",
|
|
||||||
url: "uri"
|
|
||||||
} as const;
|
|
||||||
return {
|
|
||||||
type: typeMapping[mappedType.type] as CertSubjectAlternativeNameType,
|
|
||||||
value: mappedType.value
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.filter((item): item is NonNullable<typeof item> => item !== null);
|
|
||||||
})()
|
|
||||||
: undefined,
|
|
||||||
validity: {
|
|
||||||
ttl: req.body.ttl
|
|
||||||
},
|
|
||||||
notBefore: req.body.notBefore ? new Date(req.body.notBefore) : undefined,
|
|
||||||
notAfter: req.body.notAfter ? new Date(req.body.notAfter) : undefined,
|
|
||||||
signatureAlgorithm: req.body.signatureAlgorithm,
|
|
||||||
keyAlgorithm: req.body.keyAlgorithm
|
|
||||||
};
|
|
||||||
|
|
||||||
const mappedCertificateRequest = mapEnumsForValidation(rawCertificateRequest);
|
if (req.body.csr) {
|
||||||
|
try {
|
||||||
|
const csrData = extractCertificateRequestFromCSR(req.body.csr);
|
||||||
|
|
||||||
|
certificateRequestForService = {
|
||||||
|
commonName: csrData.commonName,
|
||||||
|
keyUsages: csrData.keyUsages,
|
||||||
|
extendedKeyUsages: csrData.extendedKeyUsages,
|
||||||
|
altNames: csrData.subjectAlternativeNames,
|
||||||
|
validity: {
|
||||||
|
ttl: req.body.ttl
|
||||||
|
},
|
||||||
|
notBefore: req.body.notBefore ? new Date(req.body.notBefore) : undefined,
|
||||||
|
notAfter: req.body.notAfter ? new Date(req.body.notAfter) : undefined,
|
||||||
|
signatureAlgorithm: req.body.signatureAlgorithm,
|
||||||
|
keyAlgorithm: req.body.keyAlgorithm
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
throw new BadRequestError({ message: `Invalid CSR: ${(error as Error).message}` });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
certificateRequestForService = {
|
||||||
|
commonName: req.body.commonName,
|
||||||
|
keyUsages: req.body.keyUsages,
|
||||||
|
extendedKeyUsages: req.body.extendedKeyUsages,
|
||||||
|
altNames: req.body.altNames,
|
||||||
|
validity: {
|
||||||
|
ttl: req.body.ttl
|
||||||
|
},
|
||||||
|
notBefore: req.body.notBefore ? new Date(req.body.notBefore) : undefined,
|
||||||
|
notAfter: req.body.notAfter ? new Date(req.body.notAfter) : undefined,
|
||||||
|
signatureAlgorithm: req.body.signatureAlgorithm,
|
||||||
|
keyAlgorithm: req.body.keyAlgorithm
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const mappedCertificateRequest = mapEnumsForValidation(certificateRequestForService);
|
||||||
|
|
||||||
const data = await server.services.certificateV3.issueCertificateFromProfile({
|
const data = await server.services.certificateV3.issueCertificateFromProfile({
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
@@ -173,7 +268,9 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
|||||||
.min(1, "TTL cannot be empty")
|
.min(1, "TTL cannot be empty")
|
||||||
.refine((val) => ms(val) > 0, "TTL must be a positive number"),
|
.refine((val) => ms(val) > 0, "TTL must be a positive number"),
|
||||||
notBefore: validateCaDateField.optional(),
|
notBefore: validateCaDateField.optional(),
|
||||||
notAfter: validateCaDateField.optional()
|
notAfter: validateCaDateField.optional(),
|
||||||
|
signatureAlgorithm: z.nativeEnum(CertSignatureAlgorithm).optional(),
|
||||||
|
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm).optional()
|
||||||
})
|
})
|
||||||
.refine(validateTtlAndDateFields, {
|
.refine(validateTtlAndDateFields, {
|
||||||
message:
|
message:
|
||||||
@@ -205,7 +302,9 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
|||||||
ttl: req.body.ttl
|
ttl: req.body.ttl
|
||||||
},
|
},
|
||||||
notBefore: req.body.notBefore ? new Date(req.body.notBefore) : undefined,
|
notBefore: req.body.notBefore ? new Date(req.body.notBefore) : undefined,
|
||||||
notAfter: req.body.notAfter ? new Date(req.body.notAfter) : undefined
|
notAfter: req.body.notAfter ? new Date(req.body.notAfter) : undefined,
|
||||||
|
signatureAlgorithm: req.body.signatureAlgorithm,
|
||||||
|
keyAlgorithm: req.body.keyAlgorithm
|
||||||
});
|
});
|
||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
|
|||||||
@@ -0,0 +1,734 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
/* eslint-disable no-bitwise */
|
||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
|
||||||
|
import { EnrollmentType } from "../certificate-profile/certificate-profile-types";
|
||||||
|
import { certificateEstV3ServiceFactory, TCertificateEstV3ServiceFactory } from "./certificate-est-v3-service";
|
||||||
|
|
||||||
|
// Mock the x509 module
|
||||||
|
vi.mock("@peculiar/x509", () => ({
|
||||||
|
Pkcs10CertificateRequest: vi.fn(),
|
||||||
|
GeneralNames: vi.fn(),
|
||||||
|
KeyUsagesExtension: vi.fn(),
|
||||||
|
ExtendedKeyUsageExtension: vi.fn(),
|
||||||
|
X509Certificate: vi.fn(),
|
||||||
|
KeyUsageFlags: {
|
||||||
|
digitalSignature: 1,
|
||||||
|
nonRepudiation: 2,
|
||||||
|
keyEncipherment: 4,
|
||||||
|
dataEncipherment: 8,
|
||||||
|
keyAgreement: 16,
|
||||||
|
keyCertSign: 32,
|
||||||
|
cRLSign: 64,
|
||||||
|
encipherOnly: 128,
|
||||||
|
decipherOnly: 256
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock other dependencies
|
||||||
|
vi.mock("@app/services/certificate-authority/certificate-authority-fns", () => ({
|
||||||
|
parseDistinguishedName: vi.fn((subject: string) => {
|
||||||
|
const parts = subject.split(",");
|
||||||
|
const result: any = {};
|
||||||
|
parts.forEach((part) => {
|
||||||
|
const [key, value] = part.split("=");
|
||||||
|
switch (key.trim()) {
|
||||||
|
case "CN":
|
||||||
|
result.commonName = value;
|
||||||
|
break;
|
||||||
|
case "O":
|
||||||
|
result.organization = value;
|
||||||
|
break;
|
||||||
|
case "OU":
|
||||||
|
result.ou = value;
|
||||||
|
break;
|
||||||
|
case "L":
|
||||||
|
result.locality = value;
|
||||||
|
break;
|
||||||
|
case "ST":
|
||||||
|
result.province = value;
|
||||||
|
break;
|
||||||
|
case "C":
|
||||||
|
result.country = value;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@app/services/certificate-authority/certificate-authority-validators", () => ({
|
||||||
|
validateAndMapAltNameType: vi.fn((value: string) => {
|
||||||
|
if (value.includes(".") && !value.match(/^\d+\.\d+\.\d+\.\d+$/)) {
|
||||||
|
return { type: "dns", value };
|
||||||
|
}
|
||||||
|
if (value.match(/^\d+\.\d+\.\d+\.\d+$/)) {
|
||||||
|
return { type: "ip", value };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@app/services/certificate-common/certificate-constants", () => ({
|
||||||
|
mapLegacyKeyUsageToStandard: vi.fn((usage: string) => {
|
||||||
|
const mapping: Record<string, string> = {
|
||||||
|
digitalSignature: "digital_signature",
|
||||||
|
keyEncipherment: "key_encipherment",
|
||||||
|
keyCertSign: "key_cert_sign"
|
||||||
|
};
|
||||||
|
return mapping[usage] || usage;
|
||||||
|
}),
|
||||||
|
mapLegacyExtendedKeyUsageToStandard: vi.fn((usage: string) => {
|
||||||
|
const mapping: Record<string, string> = {
|
||||||
|
clientAuth: "client_auth",
|
||||||
|
serverAuth: "server_auth",
|
||||||
|
codeSigning: "code_signing"
|
||||||
|
};
|
||||||
|
return mapping[usage] || usage;
|
||||||
|
}),
|
||||||
|
CertKeyUsageType: {
|
||||||
|
DIGITAL_SIGNATURE: "digital_signature",
|
||||||
|
KEY_ENCIPHERMENT: "key_encipherment",
|
||||||
|
KEY_CERT_SIGN: "key_cert_sign"
|
||||||
|
},
|
||||||
|
CertExtendedKeyUsageType: {
|
||||||
|
CLIENT_AUTH: "client_auth",
|
||||||
|
SERVER_AUTH: "server_auth",
|
||||||
|
CODE_SIGNING: "code_signing"
|
||||||
|
},
|
||||||
|
CertSubjectAlternativeNameType: {
|
||||||
|
DNS_NAME: "dns_name",
|
||||||
|
IP_ADDRESS: "ip_address",
|
||||||
|
RFC822_NAME: "rfc822_name",
|
||||||
|
UNIFORM_RESOURCE_IDENTIFIER: "uniform_resource_identifier"
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@app/services/certificate/certificate-types", () => ({
|
||||||
|
mapLegacyAltNameType: vi.fn((type: string) => {
|
||||||
|
const mapping: Record<string, string> = {
|
||||||
|
dns: "dns_name",
|
||||||
|
ip: "ip_address",
|
||||||
|
email: "rfc822_name",
|
||||||
|
url: "uniform_resource_identifier"
|
||||||
|
};
|
||||||
|
return mapping[type] || type;
|
||||||
|
}),
|
||||||
|
CertExtendedKeyUsageOIDToName: {
|
||||||
|
"1.3.6.1.5.5.7.3.1": "serverAuth",
|
||||||
|
"1.3.6.1.5.5.7.3.2": "clientAuth",
|
||||||
|
"1.3.6.1.5.5.7.3.3": "codeSigning"
|
||||||
|
},
|
||||||
|
CertKeyUsage: {
|
||||||
|
DIGITAL_SIGNATURE: "digitalSignature",
|
||||||
|
KEY_ENCIPHERMENT: "keyEncipherment",
|
||||||
|
KEY_CERT_SIGN: "keyCertSign",
|
||||||
|
NON_REPUDIATION: "nonRepudiation",
|
||||||
|
DATA_ENCIPHERMENT: "dataEncipherment",
|
||||||
|
KEY_AGREEMENT: "keyAgreement",
|
||||||
|
CRL_SIGN: "cRLSign",
|
||||||
|
ENCIPHER_ONLY: "encipherOnly",
|
||||||
|
DECIPHER_ONLY: "decipherOnly"
|
||||||
|
},
|
||||||
|
CertExtendedKeyUsage: {
|
||||||
|
CLIENT_AUTH: "clientAuth",
|
||||||
|
SERVER_AUTH: "serverAuth",
|
||||||
|
CODE_SIGNING: "codeSigning"
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@app/services/certificate-common/certificate-utils", () => ({
|
||||||
|
mapEnumsForValidation: vi.fn((req: any) => req)
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../../ee/services/certificate-est/certificate-est-fns", () => ({
|
||||||
|
convertRawCertsToPkcs7: vi.fn(() => "mocked-pkcs7-response")
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("CertificateEstV3Service Security Fix", () => {
|
||||||
|
let service: TCertificateEstV3ServiceFactory;
|
||||||
|
|
||||||
|
const mockInternalCertificateAuthorityService = {
|
||||||
|
signCertFromCa: vi.fn()
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockCertificateTemplateDAL = {
|
||||||
|
findById: vi.fn()
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockCertificateTemplateV2Service = {
|
||||||
|
validateCertificateRequest: vi.fn()
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockCertificateAuthorityDAL = {
|
||||||
|
findById: vi.fn(),
|
||||||
|
findByIdWithAssociatedCa: vi.fn()
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockCertificateAuthorityCertDAL = {
|
||||||
|
find: vi.fn(),
|
||||||
|
findById: vi.fn()
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockProjectDAL = {
|
||||||
|
findOne: vi.fn(),
|
||||||
|
updateById: vi.fn(),
|
||||||
|
transaction: vi.fn()
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockKmsService = {
|
||||||
|
decryptWithKmsKey: vi.fn(),
|
||||||
|
generateKmsKey: vi.fn()
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockLicenseService = {
|
||||||
|
getPlan: vi.fn()
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockCertificateProfileDAL = {
|
||||||
|
findByIdWithConfigs: vi.fn()
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockEstEnrollmentConfigDAL = {
|
||||||
|
findById: vi.fn()
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockProfile = {
|
||||||
|
id: "profile-123",
|
||||||
|
projectId: "project-123",
|
||||||
|
caId: "ca-123",
|
||||||
|
certificateTemplateId: "template-v2-123",
|
||||||
|
enrollmentType: EnrollmentType.EST,
|
||||||
|
estConfigId: "est-config-123"
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockEstConfig = {
|
||||||
|
id: "est-config-123",
|
||||||
|
disableBootstrapCaValidation: true
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockProject = {
|
||||||
|
id: "project-123",
|
||||||
|
orgId: "org-123"
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockPlan = {
|
||||||
|
pkiEst: true
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const { Pkcs10CertificateRequest, GeneralNames } = await import("@peculiar/x509");
|
||||||
|
|
||||||
|
service = certificateEstV3ServiceFactory({
|
||||||
|
internalCertificateAuthorityService: mockInternalCertificateAuthorityService,
|
||||||
|
certificateTemplateDAL: mockCertificateTemplateDAL,
|
||||||
|
certificateTemplateV2Service: mockCertificateTemplateV2Service,
|
||||||
|
certificateAuthorityDAL: mockCertificateAuthorityDAL,
|
||||||
|
certificateAuthorityCertDAL: mockCertificateAuthorityCertDAL,
|
||||||
|
projectDAL: mockProjectDAL,
|
||||||
|
kmsService: mockKmsService,
|
||||||
|
licenseService: mockLicenseService,
|
||||||
|
certificateProfileDAL: mockCertificateProfileDAL,
|
||||||
|
estEnrollmentConfigDAL: mockEstEnrollmentConfigDAL
|
||||||
|
});
|
||||||
|
|
||||||
|
mockCertificateProfileDAL.findByIdWithConfigs.mockResolvedValue(mockProfile);
|
||||||
|
mockEstEnrollmentConfigDAL.findById.mockResolvedValue(mockEstConfig);
|
||||||
|
mockProjectDAL.findOne.mockResolvedValue(mockProject);
|
||||||
|
mockLicenseService.getPlan.mockResolvedValue(mockPlan);
|
||||||
|
|
||||||
|
// Set up the default CSR parsing behavior
|
||||||
|
(Pkcs10CertificateRequest as any).mockImplementation((csr: string) => {
|
||||||
|
const parsed = JSON.parse(csr);
|
||||||
|
const mockExtensions: any[] = [];
|
||||||
|
|
||||||
|
if (parsed.sans && parsed.sans.length > 0) {
|
||||||
|
mockExtensions.push({ type: "2.5.29.17", value: "mock-san-value" });
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
subject: parsed.subject,
|
||||||
|
extensions: mockExtensions,
|
||||||
|
getExtension: vi.fn((oid: string) => {
|
||||||
|
if (oid === "2.5.29.15" && parsed.keyUsages && parsed.keyUsages.length > 0) {
|
||||||
|
// Calculate usages as bitwise OR of key usage flags
|
||||||
|
let usages = 0;
|
||||||
|
parsed.keyUsages.forEach((usage: string) => {
|
||||||
|
switch (usage) {
|
||||||
|
case "digital_signature":
|
||||||
|
usages |= 1; // KeyUsageFlags.digitalSignature
|
||||||
|
break;
|
||||||
|
case "key_encipherment":
|
||||||
|
usages |= 4; // KeyUsageFlags.keyEncipherment
|
||||||
|
break;
|
||||||
|
case "key_cert_sign":
|
||||||
|
usages |= 32; // KeyUsageFlags.keyCertSign
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return { usages };
|
||||||
|
}
|
||||||
|
if (oid === "2.5.29.37" && parsed.extendedKeyUsages && parsed.extendedKeyUsages.length > 0) {
|
||||||
|
const ekuOids = parsed.extendedKeyUsages.map((eku: string) => {
|
||||||
|
switch (eku) {
|
||||||
|
case "client_auth":
|
||||||
|
return "1.3.6.1.5.5.7.3.2";
|
||||||
|
case "server_auth":
|
||||||
|
return "1.3.6.1.5.5.7.3.1";
|
||||||
|
case "code_signing":
|
||||||
|
return "1.3.6.1.5.5.7.3.3";
|
||||||
|
default:
|
||||||
|
return "1.3.6.1.5.5.7.3.1";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return { usages: ekuOids };
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
})
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
(GeneralNames as any).mockImplementation(() => ({
|
||||||
|
items: [
|
||||||
|
{ type: "dns", value: "test.example.com" },
|
||||||
|
{ type: "ip", value: "192.168.1.1" }
|
||||||
|
]
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
const createMockCSR = (
|
||||||
|
options: {
|
||||||
|
subject?: string;
|
||||||
|
keyUsages?: string[];
|
||||||
|
extendedKeyUsages?: string[];
|
||||||
|
sans?: Array<{ type: string; value: string }>;
|
||||||
|
} = {}
|
||||||
|
) => {
|
||||||
|
const {
|
||||||
|
subject = "CN=test.example.com,O=Test Org,C=US",
|
||||||
|
keyUsages = [],
|
||||||
|
extendedKeyUsages = [],
|
||||||
|
sans = []
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
return JSON.stringify({
|
||||||
|
subject,
|
||||||
|
keyUsages,
|
||||||
|
extendedKeyUsages,
|
||||||
|
sans
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("CSR Extraction and Template Validation", () => {
|
||||||
|
it("should extract subject attributes from CSR", async () => {
|
||||||
|
const csr = createMockCSR({
|
||||||
|
subject: "CN=test.example.com,O=Test Organization,OU=IT Department,L=San Francisco,ST=California,C=US"
|
||||||
|
});
|
||||||
|
|
||||||
|
mockCertificateTemplateV2Service.validateCertificateRequest.mockResolvedValue({
|
||||||
|
isValid: true,
|
||||||
|
errors: [],
|
||||||
|
warnings: []
|
||||||
|
});
|
||||||
|
|
||||||
|
mockInternalCertificateAuthorityService.signCertFromCa.mockResolvedValue({
|
||||||
|
certificate: { rawData: new ArrayBuffer(0) }
|
||||||
|
});
|
||||||
|
|
||||||
|
await service.simpleEnrollByProfile({
|
||||||
|
csr,
|
||||||
|
profileId: "profile-123",
|
||||||
|
sslClientCert: ""
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockCertificateTemplateV2Service.validateCertificateRequest).toHaveBeenCalledWith(
|
||||||
|
"template-v2-123",
|
||||||
|
expect.objectContaining({
|
||||||
|
commonName: "test.example.com",
|
||||||
|
organization: "Test Organization",
|
||||||
|
organizationUnit: "IT Department",
|
||||||
|
locality: "San Francisco",
|
||||||
|
state: "California",
|
||||||
|
country: "US"
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should extract key usages from CSR", async () => {
|
||||||
|
const csr = createMockCSR({
|
||||||
|
keyUsages: ["digital_signature", "key_encipherment"]
|
||||||
|
});
|
||||||
|
|
||||||
|
mockCertificateTemplateV2Service.validateCertificateRequest.mockResolvedValue({
|
||||||
|
isValid: true,
|
||||||
|
errors: [],
|
||||||
|
warnings: []
|
||||||
|
});
|
||||||
|
|
||||||
|
mockInternalCertificateAuthorityService.signCertFromCa.mockResolvedValue({
|
||||||
|
certificate: { rawData: new ArrayBuffer(0) }
|
||||||
|
});
|
||||||
|
|
||||||
|
await service.simpleEnrollByProfile({
|
||||||
|
csr,
|
||||||
|
profileId: "profile-123",
|
||||||
|
sslClientCert: ""
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockCertificateTemplateV2Service.validateCertificateRequest).toHaveBeenCalledWith(
|
||||||
|
"template-v2-123",
|
||||||
|
expect.objectContaining({
|
||||||
|
keyUsages: expect.arrayContaining(["digital_signature", "key_encipherment"])
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should extract extended key usages from CSR", async () => {
|
||||||
|
const csr = createMockCSR({
|
||||||
|
extendedKeyUsages: ["client_auth", "server_auth"]
|
||||||
|
});
|
||||||
|
|
||||||
|
mockCertificateTemplateV2Service.validateCertificateRequest.mockResolvedValue({
|
||||||
|
isValid: true,
|
||||||
|
errors: [],
|
||||||
|
warnings: []
|
||||||
|
});
|
||||||
|
|
||||||
|
mockInternalCertificateAuthorityService.signCertFromCa.mockResolvedValue({
|
||||||
|
certificate: { rawData: new ArrayBuffer(0) }
|
||||||
|
});
|
||||||
|
|
||||||
|
await service.simpleEnrollByProfile({
|
||||||
|
csr,
|
||||||
|
profileId: "profile-123",
|
||||||
|
sslClientCert: ""
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockCertificateTemplateV2Service.validateCertificateRequest).toHaveBeenCalledWith(
|
||||||
|
"template-v2-123",
|
||||||
|
expect.objectContaining({
|
||||||
|
extendedKeyUsages: expect.arrayContaining(["client_auth", "server_auth"])
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should extract Subject Alternative Names from CSR", async () => {
|
||||||
|
const { GeneralNames } = await import("@peculiar/x509");
|
||||||
|
|
||||||
|
const csr = createMockCSR({
|
||||||
|
sans: [
|
||||||
|
{ type: "dns", value: "test.example.com" },
|
||||||
|
{ type: "ip", value: "192.168.1.1" }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
(GeneralNames as any).mockImplementation(() => ({
|
||||||
|
items: [
|
||||||
|
{ type: "dns", value: "test.example.com" },
|
||||||
|
{ type: "ip", value: "192.168.1.1" }
|
||||||
|
]
|
||||||
|
}));
|
||||||
|
|
||||||
|
mockCertificateTemplateV2Service.validateCertificateRequest.mockResolvedValue({
|
||||||
|
isValid: true,
|
||||||
|
errors: [],
|
||||||
|
warnings: []
|
||||||
|
});
|
||||||
|
|
||||||
|
mockInternalCertificateAuthorityService.signCertFromCa.mockResolvedValue({
|
||||||
|
certificate: { rawData: new ArrayBuffer(0) }
|
||||||
|
});
|
||||||
|
|
||||||
|
await service.simpleEnrollByProfile({
|
||||||
|
csr,
|
||||||
|
profileId: "profile-123",
|
||||||
|
sslClientCert: ""
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockCertificateTemplateV2Service.validateCertificateRequest).toHaveBeenCalledWith(
|
||||||
|
"template-v2-123",
|
||||||
|
expect.objectContaining({
|
||||||
|
subjectAlternativeNames: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
type: "dns_name",
|
||||||
|
value: "test.example.com"
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
type: "ip_address",
|
||||||
|
value: "192.168.1.1"
|
||||||
|
})
|
||||||
|
])
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Template Validation Enforcement", () => {
|
||||||
|
const basicCSR = createMockCSR();
|
||||||
|
|
||||||
|
it("should enforce template validation and reject invalid requests", async () => {
|
||||||
|
mockCertificateTemplateV2Service.validateCertificateRequest.mockResolvedValue({
|
||||||
|
isValid: false,
|
||||||
|
errors: ["Common name 'test.example.com' is not allowed", "Key usage 'digital_signature' is denied"],
|
||||||
|
warnings: []
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.simpleEnrollByProfile({
|
||||||
|
csr: basicCSR,
|
||||||
|
profileId: "profile-123",
|
||||||
|
sslClientCert: ""
|
||||||
|
})
|
||||||
|
).rejects.toThrow(BadRequestError);
|
||||||
|
|
||||||
|
expect(mockInternalCertificateAuthorityService.signCertFromCa).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should allow valid requests that pass template validation", async () => {
|
||||||
|
mockCertificateTemplateV2Service.validateCertificateRequest.mockResolvedValue({
|
||||||
|
isValid: true,
|
||||||
|
errors: [],
|
||||||
|
warnings: []
|
||||||
|
});
|
||||||
|
|
||||||
|
mockInternalCertificateAuthorityService.signCertFromCa.mockResolvedValue({
|
||||||
|
certificate: { rawData: new ArrayBuffer(0) }
|
||||||
|
});
|
||||||
|
|
||||||
|
await service.simpleEnrollByProfile({
|
||||||
|
csr: basicCSR,
|
||||||
|
profileId: "profile-123",
|
||||||
|
sslClientCert: ""
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockCertificateTemplateV2Service.validateCertificateRequest).toHaveBeenCalledWith(
|
||||||
|
"template-v2-123",
|
||||||
|
expect.any(Object)
|
||||||
|
);
|
||||||
|
expect(mockInternalCertificateAuthorityService.signCertFromCa).toHaveBeenCalledWith({
|
||||||
|
isInternal: true,
|
||||||
|
caId: "ca-123",
|
||||||
|
csr: basicCSR
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should validate template for both simpleEnrollByProfile and simpleReenrollByProfile", async () => {
|
||||||
|
mockCertificateTemplateV2Service.validateCertificateRequest.mockResolvedValue({
|
||||||
|
isValid: false,
|
||||||
|
errors: ["SAN value 'evil.com' is denied"],
|
||||||
|
warnings: []
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.simpleEnrollByProfile({
|
||||||
|
csr: basicCSR,
|
||||||
|
profileId: "profile-123",
|
||||||
|
sslClientCert: ""
|
||||||
|
})
|
||||||
|
).rejects.toThrow(BadRequestError);
|
||||||
|
|
||||||
|
expect(mockCertificateTemplateV2Service.validateCertificateRequest).toHaveBeenCalled();
|
||||||
|
expect(mockInternalCertificateAuthorityService.signCertFromCa).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Policy Bypass Prevention", () => {
|
||||||
|
const maliciousCSR = createMockCSR({
|
||||||
|
subject: "CN=evil.com,O=Evil Corp,C=XX",
|
||||||
|
keyUsages: ["key_cert_sign"],
|
||||||
|
sans: [
|
||||||
|
{ type: "dns", value: "*.example.com" },
|
||||||
|
{ type: "ip", value: "127.0.0.1" }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should block attempts to bypass subject attribute policies", async () => {
|
||||||
|
mockCertificateTemplateV2Service.validateCertificateRequest.mockResolvedValue({
|
||||||
|
isValid: false,
|
||||||
|
errors: ["Organization 'Evil Corp' is denied", "Country 'XX' is not allowed"],
|
||||||
|
warnings: []
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.simpleEnrollByProfile({
|
||||||
|
csr: maliciousCSR,
|
||||||
|
profileId: "profile-123",
|
||||||
|
sslClientCert: ""
|
||||||
|
})
|
||||||
|
).rejects.toThrow(BadRequestError);
|
||||||
|
|
||||||
|
expect(mockCertificateTemplateV2Service.validateCertificateRequest).toHaveBeenCalledWith(
|
||||||
|
"template-v2-123",
|
||||||
|
expect.objectContaining({
|
||||||
|
commonName: "evil.com",
|
||||||
|
organization: "Evil Corp",
|
||||||
|
country: "XX"
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should block attempts to bypass key usage policies", async () => {
|
||||||
|
mockCertificateTemplateV2Service.validateCertificateRequest.mockResolvedValue({
|
||||||
|
isValid: false,
|
||||||
|
errors: ["Key usage 'key_cert_sign' is denied - certificate authority privileges not allowed"],
|
||||||
|
warnings: []
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.simpleEnrollByProfile({
|
||||||
|
csr: maliciousCSR,
|
||||||
|
profileId: "profile-123",
|
||||||
|
sslClientCert: ""
|
||||||
|
})
|
||||||
|
).rejects.toThrow(BadRequestError);
|
||||||
|
|
||||||
|
expect(mockCertificateTemplateV2Service.validateCertificateRequest).toHaveBeenCalledWith(
|
||||||
|
"template-v2-123",
|
||||||
|
expect.objectContaining({
|
||||||
|
keyUsages: expect.arrayContaining(["key_cert_sign"])
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should block attempts to bypass SAN policies", async () => {
|
||||||
|
const { GeneralNames } = await import("@peculiar/x509");
|
||||||
|
|
||||||
|
(GeneralNames as any).mockImplementation(() => ({
|
||||||
|
items: [
|
||||||
|
{ type: "dns", value: "*.example.com" },
|
||||||
|
{ type: "ip", value: "127.0.0.1" }
|
||||||
|
]
|
||||||
|
}));
|
||||||
|
|
||||||
|
mockCertificateTemplateV2Service.validateCertificateRequest.mockResolvedValue({
|
||||||
|
isValid: false,
|
||||||
|
errors: ["SAN value '*.example.com' matches denied wildcard pattern", "SAN value '127.0.0.1' is denied"],
|
||||||
|
warnings: []
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.simpleEnrollByProfile({
|
||||||
|
csr: maliciousCSR,
|
||||||
|
profileId: "profile-123",
|
||||||
|
sslClientCert: ""
|
||||||
|
})
|
||||||
|
).rejects.toThrow(BadRequestError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Error Handling", () => {
|
||||||
|
const basicCSR = createMockCSR();
|
||||||
|
|
||||||
|
it("should handle profile not found", async () => {
|
||||||
|
mockCertificateProfileDAL.findByIdWithConfigs.mockResolvedValue(null);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.simpleEnrollByProfile({
|
||||||
|
csr: basicCSR,
|
||||||
|
profileId: "nonexistent",
|
||||||
|
sslClientCert: ""
|
||||||
|
})
|
||||||
|
).rejects.toThrow(NotFoundError);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle non-EST enrollment type", async () => {
|
||||||
|
mockCertificateProfileDAL.findByIdWithConfigs.mockResolvedValue({
|
||||||
|
...mockProfile,
|
||||||
|
enrollmentType: EnrollmentType.API
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.simpleEnrollByProfile({
|
||||||
|
csr: basicCSR,
|
||||||
|
profileId: "profile-123",
|
||||||
|
sslClientCert: ""
|
||||||
|
})
|
||||||
|
).rejects.toThrow(BadRequestError);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle template validation service errors", async () => {
|
||||||
|
mockCertificateTemplateV2Service.validateCertificateRequest.mockRejectedValue(
|
||||||
|
new Error("Template validation service unavailable")
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.simpleEnrollByProfile({
|
||||||
|
csr: basicCSR,
|
||||||
|
profileId: "profile-123",
|
||||||
|
sslClientCert: ""
|
||||||
|
})
|
||||||
|
).rejects.toThrow("Template validation service unavailable");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Integration with existing flow", () => {
|
||||||
|
const basicCSR = createMockCSR();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockCertificateTemplateV2Service.validateCertificateRequest.mockResolvedValue({
|
||||||
|
isValid: true,
|
||||||
|
errors: [],
|
||||||
|
warnings: []
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call internal CA service with correct parameters after validation", async () => {
|
||||||
|
mockInternalCertificateAuthorityService.signCertFromCa.mockResolvedValue({
|
||||||
|
certificate: { rawData: new ArrayBuffer(0) }
|
||||||
|
});
|
||||||
|
|
||||||
|
await service.simpleEnrollByProfile({
|
||||||
|
csr: basicCSR,
|
||||||
|
profileId: "profile-123",
|
||||||
|
sslClientCert: ""
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockInternalCertificateAuthorityService.signCertFromCa).toHaveBeenCalledWith({
|
||||||
|
isInternal: true,
|
||||||
|
caId: "ca-123",
|
||||||
|
csr: basicCSR
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should use profile's CA ID instead of template ID to avoid v1/v2 mismatch", async () => {
|
||||||
|
mockInternalCertificateAuthorityService.signCertFromCa.mockResolvedValue({
|
||||||
|
certificate: { rawData: new ArrayBuffer(0) }
|
||||||
|
});
|
||||||
|
|
||||||
|
await service.simpleEnrollByProfile({
|
||||||
|
csr: basicCSR,
|
||||||
|
profileId: "profile-123",
|
||||||
|
sslClientCert: ""
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify it uses caId from profile, not certificateTemplateId
|
||||||
|
expect(mockInternalCertificateAuthorityService.signCertFromCa).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
caId: "ca-123"
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify it does NOT pass certificateTemplateId to avoid v1/v2 confusion
|
||||||
|
expect(mockInternalCertificateAuthorityService.signCertFromCa).toHaveBeenCalledWith(
|
||||||
|
expect.not.objectContaining({
|
||||||
|
certificateTemplateId: expect.anything()
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -3,13 +3,31 @@ import * as x509 from "@peculiar/x509";
|
|||||||
import { extractX509CertFromChain } from "@app/lib/certificates/extract-certificate";
|
import { extractX509CertFromChain } from "@app/lib/certificates/extract-certificate";
|
||||||
import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||||
import { isCertChainValid } from "@app/services/certificate/certificate-fns";
|
import { isCertChainValid } from "@app/services/certificate/certificate-fns";
|
||||||
|
import {
|
||||||
|
CertExtendedKeyUsageOIDToName,
|
||||||
|
CertKeyUsage,
|
||||||
|
mapLegacyAltNameType,
|
||||||
|
TAltNameMapping
|
||||||
|
} from "@app/services/certificate/certificate-types";
|
||||||
import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
|
import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
|
||||||
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
|
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
|
||||||
import { getCaCertChain, getCaCertChains } from "@app/services/certificate-authority/certificate-authority-fns";
|
import {
|
||||||
|
getCaCertChain,
|
||||||
|
getCaCertChains,
|
||||||
|
parseDistinguishedName
|
||||||
|
} from "@app/services/certificate-authority/certificate-authority-fns";
|
||||||
|
import { validateAndMapAltNameType } from "@app/services/certificate-authority/certificate-authority-validators";
|
||||||
import { TInternalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service";
|
import { TInternalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service";
|
||||||
|
import {
|
||||||
|
mapLegacyExtendedKeyUsageToStandard,
|
||||||
|
mapLegacyKeyUsageToStandard
|
||||||
|
} from "@app/services/certificate-common/certificate-constants";
|
||||||
|
import { mapEnumsForValidation } from "@app/services/certificate-common/certificate-utils";
|
||||||
import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal";
|
import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal";
|
||||||
import { EnrollmentType } from "@app/services/certificate-profile/certificate-profile-types";
|
import { EnrollmentType } from "@app/services/certificate-profile/certificate-profile-types";
|
||||||
import { TCertificateTemplateDALFactory } from "@app/services/certificate-template/certificate-template-dal";
|
import { TCertificateTemplateDALFactory } from "@app/services/certificate-template/certificate-template-dal";
|
||||||
|
import { TCertificateTemplateV2ServiceFactory } from "@app/services/certificate-template-v2/certificate-template-v2-service";
|
||||||
|
import { TCertificateRequest } from "@app/services/certificate-template-v2/certificate-template-v2-types";
|
||||||
import { TEstEnrollmentConfigDALFactory } from "@app/services/enrollment-config/est-enrollment-config-dal";
|
import { TEstEnrollmentConfigDALFactory } from "@app/services/enrollment-config/est-enrollment-config-dal";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
@@ -21,6 +39,7 @@ import { TLicenseServiceFactory } from "../../ee/services/license/license-servic
|
|||||||
type TCertificateEstV3ServiceFactoryDep = {
|
type TCertificateEstV3ServiceFactoryDep = {
|
||||||
internalCertificateAuthorityService: Pick<TInternalCertificateAuthorityServiceFactory, "signCertFromCa">;
|
internalCertificateAuthorityService: Pick<TInternalCertificateAuthorityServiceFactory, "signCertFromCa">;
|
||||||
certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "findById">;
|
certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "findById">;
|
||||||
|
certificateTemplateV2Service: Pick<TCertificateTemplateV2ServiceFactory, "validateCertificateRequest">;
|
||||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById" | "findByIdWithAssociatedCa">;
|
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById" | "findByIdWithAssociatedCa">;
|
||||||
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "find" | "findById">;
|
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "find" | "findById">;
|
||||||
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
|
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
|
||||||
@@ -35,6 +54,7 @@ export type TCertificateEstV3ServiceFactory = ReturnType<typeof certificateEstV3
|
|||||||
export const certificateEstV3ServiceFactory = ({
|
export const certificateEstV3ServiceFactory = ({
|
||||||
internalCertificateAuthorityService,
|
internalCertificateAuthorityService,
|
||||||
certificateTemplateDAL,
|
certificateTemplateDAL,
|
||||||
|
certificateTemplateV2Service,
|
||||||
certificateAuthorityCertDAL,
|
certificateAuthorityCertDAL,
|
||||||
certificateAuthorityDAL,
|
certificateAuthorityDAL,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
@@ -43,6 +63,59 @@ export const certificateEstV3ServiceFactory = ({
|
|||||||
certificateProfileDAL,
|
certificateProfileDAL,
|
||||||
estEnrollmentConfigDAL
|
estEnrollmentConfigDAL
|
||||||
}: TCertificateEstV3ServiceFactoryDep) => {
|
}: TCertificateEstV3ServiceFactoryDep) => {
|
||||||
|
const extractCertificateRequestFromCSR = (csr: string): TCertificateRequest => {
|
||||||
|
const csrObj = new x509.Pkcs10CertificateRequest(csr);
|
||||||
|
const subject = parseDistinguishedName(csrObj.subject);
|
||||||
|
|
||||||
|
const certificateRequest: TCertificateRequest = {
|
||||||
|
commonName: subject.commonName,
|
||||||
|
organization: subject.organization,
|
||||||
|
organizationUnit: subject.ou,
|
||||||
|
locality: subject.locality,
|
||||||
|
state: subject.province,
|
||||||
|
country: subject.country
|
||||||
|
};
|
||||||
|
|
||||||
|
const csrKeyUsageExtension = csrObj.getExtension("2.5.29.15") as x509.KeyUsagesExtension;
|
||||||
|
if (csrKeyUsageExtension) {
|
||||||
|
const csrKeyUsages = Object.values(CertKeyUsage).filter(
|
||||||
|
// eslint-disable-next-line no-bitwise
|
||||||
|
(keyUsage) => (x509.KeyUsageFlags[keyUsage] & csrKeyUsageExtension.usages) !== 0
|
||||||
|
);
|
||||||
|
certificateRequest.keyUsages = csrKeyUsages.map(mapLegacyKeyUsageToStandard);
|
||||||
|
}
|
||||||
|
|
||||||
|
const csrExtendedKeyUsageExtension = csrObj.getExtension("2.5.29.37") as x509.ExtendedKeyUsageExtension;
|
||||||
|
if (csrExtendedKeyUsageExtension) {
|
||||||
|
const csrExtendedKeyUsages = csrExtendedKeyUsageExtension.usages.map(
|
||||||
|
(ekuOid) => CertExtendedKeyUsageOIDToName[ekuOid as string]
|
||||||
|
);
|
||||||
|
certificateRequest.extendedKeyUsages = csrExtendedKeyUsages.map(mapLegacyExtendedKeyUsageToStandard);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sanExtension = csrObj.extensions.find((ext) => ext.type === "2.5.29.17");
|
||||||
|
if (sanExtension) {
|
||||||
|
const sanNames = new x509.GeneralNames(sanExtension.value);
|
||||||
|
const altNamesArray: TAltNameMapping[] = sanNames.items
|
||||||
|
.filter(
|
||||||
|
(value) => value.type === "email" || value.type === "dns" || value.type === "url" || value.type === "ip"
|
||||||
|
)
|
||||||
|
.map((name): TAltNameMapping => {
|
||||||
|
const altNameType = validateAndMapAltNameType(name.value);
|
||||||
|
if (!altNameType) {
|
||||||
|
throw new BadRequestError({ message: `Invalid altName from CSR: ${name.value}` });
|
||||||
|
}
|
||||||
|
return altNameType;
|
||||||
|
});
|
||||||
|
|
||||||
|
certificateRequest.subjectAlternativeNames = altNamesArray.map((altName) => ({
|
||||||
|
type: mapLegacyAltNameType(altName.type),
|
||||||
|
value: altName.value
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return certificateRequest;
|
||||||
|
};
|
||||||
const simpleEnrollByProfile = async ({
|
const simpleEnrollByProfile = async ({
|
||||||
csr,
|
csr,
|
||||||
profileId,
|
profileId,
|
||||||
@@ -122,9 +195,22 @@ export const certificateEstV3ServiceFactory = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const certificateRequest = extractCertificateRequestFromCSR(csr);
|
||||||
|
const mappedCertificateRequest = mapEnumsForValidation(certificateRequest);
|
||||||
|
const validationResult = await certificateTemplateV2Service.validateCertificateRequest(
|
||||||
|
profile.certificateTemplateId,
|
||||||
|
mappedCertificateRequest
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!validationResult.isValid) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Certificate request validation failed: ${validationResult.errors.join(", ")}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const { certificate } = await internalCertificateAuthorityService.signCertFromCa({
|
const { certificate } = await internalCertificateAuthorityService.signCertFromCa({
|
||||||
isInternal: true,
|
isInternal: true,
|
||||||
certificateTemplateId: profile.certificateTemplateId,
|
caId: profile.caId,
|
||||||
csr
|
csr
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -235,9 +321,22 @@ export const certificateEstV3ServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const certificateRequest = extractCertificateRequestFromCSR(csr);
|
||||||
|
const mappedCertificateRequest = mapEnumsForValidation(certificateRequest);
|
||||||
|
const validationResult = await certificateTemplateV2Service.validateCertificateRequest(
|
||||||
|
profile.certificateTemplateId,
|
||||||
|
mappedCertificateRequest
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!validationResult.isValid) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Certificate request validation failed: ${validationResult.errors.join(", ")}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const { certificate } = await internalCertificateAuthorityService.signCertFromCa({
|
const { certificate } = await internalCertificateAuthorityService.signCertFromCa({
|
||||||
isInternal: true,
|
isInternal: true,
|
||||||
certificateTemplateId: profile.certificateTemplateId,
|
caId: profile.caId,
|
||||||
csr
|
csr
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -142,17 +142,19 @@ export const certificateProfileServiceFactory = ({
|
|||||||
// Hash the passphrase
|
// Hash the passphrase
|
||||||
const hashedPassphrase = await crypto.hashing().createHash(data.estConfig.passphraseInput, appCfg.SALT_ROUNDS);
|
const hashedPassphrase = await crypto.hashing().createHash(data.estConfig.passphraseInput, appCfg.SALT_ROUNDS);
|
||||||
|
|
||||||
let encryptedCaChainBuffer: Buffer;
|
let encryptedCaChainBuffer: Buffer | null = null;
|
||||||
try {
|
if (!data.estConfig.disableBootstrapCaValidation) {
|
||||||
if (!data.estConfig.encryptedCaChain || typeof data.estConfig.encryptedCaChain !== "string") {
|
try {
|
||||||
throw new BadRequestError({ message: "Invalid or missing CA chain data" });
|
if (!data.estConfig.encryptedCaChain || typeof data.estConfig.encryptedCaChain !== "string") {
|
||||||
|
throw new BadRequestError({ message: "Invalid or missing CA chain data" });
|
||||||
|
}
|
||||||
|
encryptedCaChainBuffer = Buffer.from(data.estConfig.encryptedCaChain, "base64");
|
||||||
|
if (encryptedCaChainBuffer.toString("base64") !== data.estConfig.encryptedCaChain) {
|
||||||
|
throw new BadRequestError({ message: "Invalid Base64 encoding in CA chain data" });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw new BadRequestError({ message: "Failed to decode CA chain data: Invalid Base64 format" });
|
||||||
}
|
}
|
||||||
encryptedCaChainBuffer = Buffer.from(data.estConfig.encryptedCaChain, "base64");
|
|
||||||
if (encryptedCaChainBuffer.toString("base64") !== data.estConfig.encryptedCaChain) {
|
|
||||||
throw new BadRequestError({ message: "Invalid Base64 encoding in CA chain data" });
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
throw new BadRequestError({ message: "Failed to decode CA chain data: Invalid Base64 format" });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const estConfig = await estEnrollmentConfigDAL.create(
|
const estConfig = await estEnrollmentConfigDAL.create(
|
||||||
@@ -614,37 +616,49 @@ export const certificateProfileServiceFactory = ({
|
|||||||
return metrics;
|
return metrics;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getEstConfigurationByProfile = async ({
|
const getEstConfigurationByProfile = async (
|
||||||
actor,
|
params:
|
||||||
actorId,
|
| {
|
||||||
actorAuthMethod,
|
profileId: string;
|
||||||
actorOrgId,
|
isInternal: true;
|
||||||
profileId
|
}
|
||||||
}: {
|
| {
|
||||||
actor: ActorType;
|
actor: ActorType;
|
||||||
actorId: string;
|
actorId: string;
|
||||||
actorAuthMethod: ActorAuthMethod;
|
actorAuthMethod: ActorAuthMethod;
|
||||||
actorOrgId: string | undefined;
|
actorOrgId: string | undefined;
|
||||||
profileId: string;
|
profileId: string;
|
||||||
}) => {
|
isInternal?: false;
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
const { profileId, isInternal = false } = params;
|
||||||
const profile = await certificateProfileDAL.findByIdWithConfigs(profileId);
|
const profile = await certificateProfileDAL.findByIdWithConfigs(profileId);
|
||||||
if (!profile) {
|
if (!profile) {
|
||||||
throw new NotFoundError({ message: "Certificate profile not found" });
|
throw new NotFoundError({ message: "Certificate profile not found" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { permission } = await permissionService.getProjectPermission({
|
if (!isInternal) {
|
||||||
actor,
|
const { actor, actorId, actorAuthMethod, actorOrgId } = params as {
|
||||||
actorId,
|
actor: ActorType;
|
||||||
projectId: profile.projectId,
|
actorId: string;
|
||||||
actorAuthMethod,
|
actorAuthMethod: ActorAuthMethod;
|
||||||
actorOrgId,
|
actorOrgId: string | undefined;
|
||||||
actionProjectType: ActionProjectType.CertificateManager
|
};
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
ProjectPermissionCertificateProfileActions.Read,
|
actor,
|
||||||
ProjectPermissionSub.CertificateProfiles
|
actorId,
|
||||||
);
|
projectId: profile.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
actionProjectType: ActionProjectType.CertificateManager
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionCertificateProfileActions.Read,
|
||||||
|
ProjectPermissionSub.CertificateProfiles
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (profile.enrollmentType !== EnrollmentType.EST) {
|
if (profile.enrollmentType !== EnrollmentType.EST) {
|
||||||
throw new ForbiddenRequestError({
|
throw new ForbiddenRequestError({
|
||||||
|
|||||||
@@ -27,12 +27,34 @@ type TCertificateTemplateV2ServiceFactoryDep = {
|
|||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TCertificateTemplateV2ServiceFactory = ReturnType<typeof certificateTemplateV2ServiceFactory>;
|
|
||||||
|
|
||||||
export const certificateTemplateV2ServiceFactory = ({
|
export const certificateTemplateV2ServiceFactory = ({
|
||||||
certificateTemplateV2DAL,
|
certificateTemplateV2DAL,
|
||||||
permissionService
|
permissionService
|
||||||
}: TCertificateTemplateV2ServiceFactoryDep) => {
|
}: TCertificateTemplateV2ServiceFactoryDep) => {
|
||||||
|
const consolidateAttributeArray = <
|
||||||
|
T extends { type: string; allowed?: string[]; required?: string[]; denied?: string[] }
|
||||||
|
>(
|
||||||
|
attributes: T[]
|
||||||
|
): T[] => {
|
||||||
|
const consolidated = new Map<string, T>();
|
||||||
|
|
||||||
|
attributes.forEach((attr) => {
|
||||||
|
const existing = consolidated.get(attr.type);
|
||||||
|
if (existing) {
|
||||||
|
consolidated.set(attr.type, {
|
||||||
|
...attr,
|
||||||
|
allowed: [...new Set([...(existing.allowed || []), ...(attr.allowed || [])])],
|
||||||
|
required: [...new Set([...(existing.required || []), ...(attr.required || [])])],
|
||||||
|
denied: [...new Set([...(existing.denied || []), ...(attr.denied || [])])]
|
||||||
|
} as T);
|
||||||
|
} else {
|
||||||
|
consolidated.set(attr.type, attr);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Array.from(consolidated.values());
|
||||||
|
};
|
||||||
|
|
||||||
const parseTTL = (ttl: string): number => {
|
const parseTTL = (ttl: string): number => {
|
||||||
const regex = new RE2("^(\\d+)([dmyh])$");
|
const regex = new RE2("^(\\d+)([dmyh])$");
|
||||||
const match = regex.exec(ttl);
|
const match = regex.exec(ttl);
|
||||||
@@ -298,44 +320,14 @@ export const certificateTemplateV2ServiceFactory = ({
|
|||||||
if (request.country) requestAttributes.set(CertSubjectAttributeType.COUNTRY, request.country);
|
if (request.country) requestAttributes.set(CertSubjectAttributeType.COUNTRY, request.country);
|
||||||
|
|
||||||
if (subjectPolicies && subjectPolicies.length > 0) {
|
if (subjectPolicies && subjectPolicies.length > 0) {
|
||||||
// Validate each template subject attribute policy
|
|
||||||
for (const attrPolicy of subjectPolicies) {
|
for (const attrPolicy of subjectPolicies) {
|
||||||
const requestValue = requestAttributes.get(attrPolicy.type);
|
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) {
|
if (attrPolicy.required && attrPolicy.required.length > 0) {
|
||||||
const requestValue = requestAttributes.get(attrPolicy.type);
|
|
||||||
if (!requestValue) {
|
if (!requestValue) {
|
||||||
errors.push(`Missing required ${attrPolicy.type} attribute`);
|
errors.push(`Missing required ${attrPolicy.type} attribute`);
|
||||||
} else {
|
} else {
|
||||||
// Validate that the request value matches at least one required pattern
|
// Validate that the request value matches the required pattern
|
||||||
const hasMatchingRequired = attrPolicy.required.some((requiredValue) => {
|
const hasMatchingRequired = attrPolicy.required.some((requiredValue) => {
|
||||||
const validation = validateValueAgainstConstraints(requestValue, [requiredValue], attrPolicy.type);
|
const validation = validateValueAgainstConstraints(requestValue, [requiredValue], attrPolicy.type);
|
||||||
return validation.isValid;
|
return validation.isValid;
|
||||||
@@ -347,6 +339,38 @@ export const certificateTemplateV2ServiceFactory = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (requestValue) {
|
||||||
|
let isValueDenied = false;
|
||||||
|
if (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`);
|
||||||
|
isValueDenied = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isValueDenied && attrPolicy.allowed && attrPolicy.allowed.length > 0) {
|
||||||
|
let satisfiesRequired = false;
|
||||||
|
if (attrPolicy.required && attrPolicy.required.length > 0) {
|
||||||
|
satisfiesRequired = attrPolicy.required.some((requiredValue) => {
|
||||||
|
const validation = validateValueAgainstConstraints(requestValue, [requiredValue], attrPolicy.type);
|
||||||
|
return validation.isValid;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!satisfiesRequired) {
|
||||||
|
const allowedValidation = validateValueAgainstConstraints(
|
||||||
|
requestValue,
|
||||||
|
attrPolicy.allowed,
|
||||||
|
attrPolicy.type
|
||||||
|
);
|
||||||
|
if (!allowedValidation.isValid && allowedValidation.error) {
|
||||||
|
errors.push(allowedValidation.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if any request attributes are not covered by template policies
|
// Check if any request attributes are not covered by template policies
|
||||||
@@ -409,9 +433,19 @@ export const certificateTemplateV2ServiceFactory = ({
|
|||||||
// Check ALLOWED values - if present, all SANs must match at least one allowed pattern
|
// Check ALLOWED values - if present, all SANs must match at least one allowed pattern
|
||||||
if (sanPolicy.allowed && sanPolicy.allowed.length > 0 && requestSans.length > 0) {
|
if (sanPolicy.allowed && sanPolicy.allowed.length > 0 && requestSans.length > 0) {
|
||||||
for (const sanValue of requestSans) {
|
for (const sanValue of requestSans) {
|
||||||
const validation = validateValueAgainstConstraints(sanValue, sanPolicy.allowed, `${sanPolicy.type} SAN`);
|
let satisfiesRequired = false;
|
||||||
if (!validation.isValid && validation.error) {
|
if (sanPolicy.required && sanPolicy.required.length > 0) {
|
||||||
errors.push(validation.error);
|
satisfiesRequired = sanPolicy.required.some((requiredValue) => {
|
||||||
|
const validation = validateValueAgainstConstraints(sanValue, [requiredValue], `${sanPolicy.type} SAN`);
|
||||||
|
return validation.isValid;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!satisfiesRequired) {
|
||||||
|
const validation = validateValueAgainstConstraints(sanValue, sanPolicy.allowed, `${sanPolicy.type} SAN`);
|
||||||
|
if (!validation.isValid && validation.error) {
|
||||||
|
errors.push(validation.error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -615,20 +649,26 @@ export const certificateTemplateV2ServiceFactory = ({
|
|||||||
throw new Error("Template data is required");
|
throw new Error("Template data is required");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.subject) {
|
const consolidatedData = {
|
||||||
validateSubjectAttributePolicy(data.subject);
|
...data,
|
||||||
|
subject: data.subject ? consolidateAttributeArray(data.subject) : undefined,
|
||||||
|
sans: data.sans ? consolidateAttributeArray(data.sans) : undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
if (consolidatedData.subject) {
|
||||||
|
validateSubjectAttributePolicy(consolidatedData.subject);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.sans) {
|
if (consolidatedData.sans) {
|
||||||
validateSanPolicy(data.sans);
|
validateSanPolicy(consolidatedData.sans);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.keyUsages) {
|
if (consolidatedData.keyUsages) {
|
||||||
validateKeyUsagePolicy(data.keyUsages);
|
validateKeyUsagePolicy(consolidatedData.keyUsages);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.extendedKeyUsages) {
|
if (consolidatedData.extendedKeyUsages) {
|
||||||
validateExtendedKeyUsagePolicy(data.extendedKeyUsages);
|
validateExtendedKeyUsagePolicy(consolidatedData.extendedKeyUsages);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate slug from name and ensure it's unique within project
|
// Generate slug from name and ensure it's unique within project
|
||||||
@@ -640,7 +680,7 @@ export const certificateTemplateV2ServiceFactory = ({
|
|||||||
const uniqueSlug = await ensureUniqueSlug(projectId, slug);
|
const uniqueSlug = await ensureUniqueSlug(projectId, slug);
|
||||||
|
|
||||||
const template = await certificateTemplateV2DAL.create({
|
const template = await certificateTemplateV2DAL.create({
|
||||||
...data,
|
...consolidatedData,
|
||||||
name: uniqueSlug,
|
name: uniqueSlug,
|
||||||
projectId
|
projectId
|
||||||
});
|
});
|
||||||
@@ -682,23 +722,29 @@ export const certificateTemplateV2ServiceFactory = ({
|
|||||||
ProjectPermissionSub.CertificateTemplates
|
ProjectPermissionSub.CertificateTemplates
|
||||||
);
|
);
|
||||||
|
|
||||||
if (data.subject) {
|
const consolidatedData = {
|
||||||
validateSubjectAttributePolicy(data.subject);
|
...data,
|
||||||
|
subject: data.subject ? consolidateAttributeArray(data.subject) : undefined,
|
||||||
|
sans: data.sans ? consolidateAttributeArray(data.sans) : undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
if (consolidatedData.subject) {
|
||||||
|
validateSubjectAttributePolicy(consolidatedData.subject);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.sans) {
|
if (consolidatedData.sans) {
|
||||||
validateSanPolicy(data.sans);
|
validateSanPolicy(consolidatedData.sans);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.keyUsages) {
|
if (consolidatedData.keyUsages) {
|
||||||
validateKeyUsagePolicy(data.keyUsages);
|
validateKeyUsagePolicy(consolidatedData.keyUsages);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.extendedKeyUsages) {
|
if (consolidatedData.extendedKeyUsages) {
|
||||||
validateExtendedKeyUsagePolicy(data.extendedKeyUsages);
|
validateExtendedKeyUsagePolicy(consolidatedData.extendedKeyUsages);
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateData = { ...data };
|
const updateData = { ...consolidatedData };
|
||||||
if (data.name && typeof data.name === "string") {
|
if (data.name && typeof data.name === "string") {
|
||||||
const newSlug = generateTemplateSlug(data.name);
|
const newSlug = generateTemplateSlug(data.name);
|
||||||
if (newSlug !== existingTemplate.name) {
|
if (newSlug !== existingTemplate.name) {
|
||||||
@@ -871,7 +917,9 @@ export const certificateTemplateV2ServiceFactory = ({
|
|||||||
const isInUse = await certificateTemplateV2DAL.isTemplateInUse(templateId);
|
const isInUse = await certificateTemplateV2DAL.isTemplateInUse(templateId);
|
||||||
if (isInUse) {
|
if (isInUse) {
|
||||||
const profilesUsingTemplate = await certificateTemplateV2DAL.getProfilesUsingTemplate(templateId);
|
const profilesUsingTemplate = await certificateTemplateV2DAL.getProfilesUsingTemplate(templateId);
|
||||||
const profileNames = profilesUsingTemplate.map((profile) => profile.slug || profile.id).join(", ");
|
const profileNames = profilesUsingTemplate
|
||||||
|
.map((profile: { slug?: string; id: string }) => profile.slug || profile.id)
|
||||||
|
.join(", ");
|
||||||
|
|
||||||
throw new ForbiddenRequestError({
|
throw new ForbiddenRequestError({
|
||||||
message:
|
message:
|
||||||
@@ -910,3 +958,5 @@ export const certificateTemplateV2ServiceFactory = ({
|
|||||||
validateCertificateRequest
|
validateCertificateRequest
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TCertificateTemplateV2ServiceFactory = ReturnType<typeof certificateTemplateV2ServiceFactory>;
|
||||||
|
|||||||
@@ -192,6 +192,26 @@ export const certificateV3ServiceFactory = ({
|
|||||||
...certificateRequest,
|
...certificateRequest,
|
||||||
subjectAlternativeNames: certificateRequest.altNames
|
subjectAlternativeNames: certificateRequest.altNames
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let template;
|
||||||
|
try {
|
||||||
|
template = await certificateTemplateV2Service.getTemplateV2ById({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
templateId: profile.certificateTemplateId
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Certificate profile is using a legacy template (${profile.certificateTemplateId}) that doesn't support security validation policies. Please migrate to a template v2 for proper security enforcement.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!template) {
|
||||||
|
throw new NotFoundError({ message: "Certificate template not found for this profile" });
|
||||||
|
}
|
||||||
|
|
||||||
const validationResult = await certificateTemplateV2Service.validateCertificateRequest(
|
const validationResult = await certificateTemplateV2Service.validateCertificateRequest(
|
||||||
profile.certificateTemplateId,
|
profile.certificateTemplateId,
|
||||||
mappedCertificateRequest
|
mappedCertificateRequest
|
||||||
@@ -214,18 +234,6 @@ export const certificateV3ServiceFactory = ({
|
|||||||
throw new BadRequestError({ message: "Authentication method is required for certificate issuance" });
|
throw new BadRequestError({ message: "Authentication method is required for certificate issuance" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const template = await certificateTemplateV2Service.getTemplateV2ById({
|
|
||||||
actor,
|
|
||||||
actorId,
|
|
||||||
actorAuthMethod,
|
|
||||||
actorOrgId,
|
|
||||||
templateId: profile.certificateTemplateId
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!template) {
|
|
||||||
throw new NotFoundError({ message: "Certificate template not found for this profile" });
|
|
||||||
}
|
|
||||||
|
|
||||||
validateAlgorithmCompatibility(ca, template);
|
validateAlgorithmCompatibility(ca, template);
|
||||||
|
|
||||||
const effectiveSignatureAlgorithm = certificateRequest.signatureAlgorithm as CertSignatureAlgorithm | undefined;
|
const effectiveSignatureAlgorithm = certificateRequest.signatureAlgorithm as CertSignatureAlgorithm | undefined;
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export type TApiEnrollmentConfigUpdate = TPkiApiEnrollmentConfigsUpdate;
|
|||||||
export interface TEstConfigData {
|
export interface TEstConfigData {
|
||||||
disableBootstrapCaValidation: boolean;
|
disableBootstrapCaValidation: boolean;
|
||||||
passphraseInput: string;
|
passphraseInput: string;
|
||||||
encryptedCaChain: string;
|
encryptedCaChain?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TApiConfigData {
|
export interface TApiConfigData {
|
||||||
|
|||||||
@@ -291,6 +291,16 @@ Supports conditions and permission inversion
|
|||||||
| `create` | Issue new certificates |
|
| `create` | Issue new certificates |
|
||||||
| `delete` | Revoke or remove certificates |
|
| `delete` | Revoke or remove certificates |
|
||||||
|
|
||||||
|
#### Subject: `certificate-profiles`
|
||||||
|
|
||||||
|
| Action | Description |
|
||||||
|
| -------- | -------------------------------- |
|
||||||
|
| `read` | View certificate profiles |
|
||||||
|
| `create` | Create new certificate profiles |
|
||||||
|
| `edit` | Modify profile configurations |
|
||||||
|
| `delete` | Remove certificate profiles |
|
||||||
|
| `issue-cert` | Issue new certificates |
|
||||||
|
|
||||||
#### Subject: `certificate-templates`
|
#### Subject: `certificate-templates`
|
||||||
|
|
||||||
| Action | Description |
|
| Action | Description |
|
||||||
|
|||||||
@@ -48,8 +48,8 @@ export type TCreateCertificateProfileDTO = {
|
|||||||
enrollmentType: "api" | "est";
|
enrollmentType: "api" | "est";
|
||||||
estConfig?: {
|
estConfig?: {
|
||||||
disableBootstrapCaValidation?: boolean;
|
disableBootstrapCaValidation?: boolean;
|
||||||
passphrase: string;
|
passphraseInput: string;
|
||||||
caChain: string;
|
caChain?: string;
|
||||||
};
|
};
|
||||||
apiConfig?: {
|
apiConfig?: {
|
||||||
autoRenew?: boolean;
|
autoRenew?: boolean;
|
||||||
|
|||||||
@@ -7,17 +7,17 @@ import { projectKeys } from "../projects";
|
|||||||
import { certTemplateKeys } from "./queries";
|
import { certTemplateKeys } from "./queries";
|
||||||
import {
|
import {
|
||||||
TCertificateTemplate,
|
TCertificateTemplate,
|
||||||
TCertificateTemplateV2New,
|
TCertificateTemplateV2WithPolicies,
|
||||||
TCreateCertificateTemplateDTO,
|
TCreateCertificateTemplateDTO,
|
||||||
TCreateCertificateTemplateV2DTO,
|
TCreateCertificateTemplateV2DTO,
|
||||||
TCreateCertificateTemplateV2NewDTO,
|
TCreateCertificateTemplateV2WithPoliciesDTO,
|
||||||
TCreateEstConfigDTO,
|
TCreateEstConfigDTO,
|
||||||
TDeleteCertificateTemplateDTO,
|
TDeleteCertificateTemplateDTO,
|
||||||
TDeleteCertificateTemplateV2DTO,
|
TDeleteCertificateTemplateV2DTO,
|
||||||
TDeleteCertificateTemplateV2NewDTO,
|
TDeleteCertificateTemplateV2WithPoliciesDTO,
|
||||||
TUpdateCertificateTemplateDTO,
|
TUpdateCertificateTemplateDTO,
|
||||||
TUpdateCertificateTemplateV2DTO,
|
TUpdateCertificateTemplateV2DTO,
|
||||||
TUpdateCertificateTemplateV2NewDTO,
|
TUpdateCertificateTemplateV2WithPoliciesDTO,
|
||||||
TUpdateEstConfigDTO
|
TUpdateEstConfigDTO
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
@@ -168,12 +168,16 @@ export const useUpdateEstConfig = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useCreateCertificateTemplateV2New = () => {
|
export const useCreateCertificateTemplateV2WithPolicies = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation<TCertificateTemplateV2New, object, TCreateCertificateTemplateV2NewDTO>({
|
return useMutation<
|
||||||
|
TCertificateTemplateV2WithPolicies,
|
||||||
|
object,
|
||||||
|
TCreateCertificateTemplateV2WithPoliciesDTO
|
||||||
|
>({
|
||||||
mutationFn: async (data) => {
|
mutationFn: async (data) => {
|
||||||
const { data: response } = await apiRequest.post<{
|
const { data: response } = await apiRequest.post<{
|
||||||
certificateTemplate: TCertificateTemplateV2New;
|
certificateTemplate: TCertificateTemplateV2WithPolicies;
|
||||||
}>("/api/v2/certificate-templates", data);
|
}>("/api/v2/certificate-templates", data);
|
||||||
return response.certificateTemplate;
|
return response.certificateTemplate;
|
||||||
},
|
},
|
||||||
@@ -185,12 +189,16 @@ export const useCreateCertificateTemplateV2New = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useUpdateCertificateTemplateV2New = () => {
|
export const useUpdateCertificateTemplateV2WithPolicies = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation<TCertificateTemplateV2New, object, TUpdateCertificateTemplateV2NewDTO>({
|
return useMutation<
|
||||||
|
TCertificateTemplateV2WithPolicies,
|
||||||
|
object,
|
||||||
|
TUpdateCertificateTemplateV2WithPoliciesDTO
|
||||||
|
>({
|
||||||
mutationFn: async ({ templateId, ...data }) => {
|
mutationFn: async ({ templateId, ...data }) => {
|
||||||
const { data: response } = await apiRequest.patch<{
|
const { data: response } = await apiRequest.patch<{
|
||||||
certificateTemplate: TCertificateTemplateV2New;
|
certificateTemplate: TCertificateTemplateV2WithPolicies;
|
||||||
}>(`/api/v2/certificate-templates/${templateId}`, data);
|
}>(`/api/v2/certificate-templates/${templateId}`, data);
|
||||||
return response.certificateTemplate;
|
return response.certificateTemplate;
|
||||||
},
|
},
|
||||||
@@ -205,12 +213,16 @@ export const useUpdateCertificateTemplateV2New = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useDeleteCertificateTemplateV2New = () => {
|
export const useDeleteCertificateTemplateV2WithPolicies = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation<TCertificateTemplateV2New, object, TDeleteCertificateTemplateV2NewDTO>({
|
return useMutation<
|
||||||
|
TCertificateTemplateV2WithPolicies,
|
||||||
|
object,
|
||||||
|
TDeleteCertificateTemplateV2WithPoliciesDTO
|
||||||
|
>({
|
||||||
mutationFn: async ({ templateId }) => {
|
mutationFn: async ({ templateId }) => {
|
||||||
const { data: response } = await apiRequest.delete<{
|
const { data: response } = await apiRequest.delete<{
|
||||||
certificateTemplate: TCertificateTemplateV2New;
|
certificateTemplate: TCertificateTemplateV2WithPolicies;
|
||||||
}>(`/api/v2/certificate-templates/${templateId}`);
|
}>(`/api/v2/certificate-templates/${templateId}`);
|
||||||
return response.certificateTemplate;
|
return response.certificateTemplate;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { apiRequest } from "@app/config/request";
|
|||||||
import {
|
import {
|
||||||
TCertificateTemplate,
|
TCertificateTemplate,
|
||||||
TCertificateTemplateV2,
|
TCertificateTemplateV2,
|
||||||
TCertificateTemplateV2New,
|
TCertificateTemplateV2WithPolicies,
|
||||||
TEstConfig,
|
TEstConfig,
|
||||||
TGetCertificateTemplateV2ByIdDTO,
|
TGetCertificateTemplateV2ByIdDTO,
|
||||||
TListCertificateTemplatesDTO,
|
TListCertificateTemplatesDTO,
|
||||||
@@ -90,7 +90,7 @@ export const useListCertificateTemplatesV2 = ({
|
|||||||
queryKey: certTemplateKeys.listTemplatesV2({ projectId, limit, offset }),
|
queryKey: certTemplateKeys.listTemplatesV2({ projectId, limit, offset }),
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const { data } = await apiRequest.get<{
|
const { data } = await apiRequest.get<{
|
||||||
certificateTemplates: TCertificateTemplateV2New[];
|
certificateTemplates: TCertificateTemplateV2WithPolicies[];
|
||||||
totalCount: number;
|
totalCount: number;
|
||||||
}>("/api/v2/certificate-templates", {
|
}>("/api/v2/certificate-templates", {
|
||||||
params: {
|
params: {
|
||||||
@@ -112,7 +112,7 @@ export const useGetCertificateTemplateV2ById = ({
|
|||||||
queryKey: certTemplateKeys.getTemplateV2ById(templateId),
|
queryKey: certTemplateKeys.getTemplateV2ById(templateId),
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const { data } = await apiRequest.get<{
|
const { data } = await apiRequest.get<{
|
||||||
certificateTemplate: TCertificateTemplateV2New;
|
certificateTemplate: TCertificateTemplateV2WithPolicies;
|
||||||
}>(`/api/v2/certificate-templates/${templateId}`);
|
}>(`/api/v2/certificate-templates/${templateId}`);
|
||||||
return data.certificateTemplate;
|
return data.certificateTemplate;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ export type TCertificateTemplateV2Policy = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TCertificateTemplateV2New = {
|
export type TCertificateTemplateV2WithPolicies = {
|
||||||
id: string;
|
id: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -171,7 +171,7 @@ export type TCertificateTemplateV2New = {
|
|||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TCreateCertificateTemplateV2NewDTO = {
|
export type TCreateCertificateTemplateV2WithPoliciesDTO = {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
@@ -183,7 +183,7 @@ export type TCreateCertificateTemplateV2NewDTO = {
|
|||||||
validity?: TCertificateTemplateV2Policy["validity"];
|
validity?: TCertificateTemplateV2Policy["validity"];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TUpdateCertificateTemplateV2NewDTO = {
|
export type TUpdateCertificateTemplateV2WithPoliciesDTO = {
|
||||||
templateId: string;
|
templateId: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
@@ -195,7 +195,7 @@ export type TUpdateCertificateTemplateV2NewDTO = {
|
|||||||
validity?: TCertificateTemplateV2Policy["validity"];
|
validity?: TCertificateTemplateV2Policy["validity"];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TDeleteCertificateTemplateV2NewDTO = {
|
export type TDeleteCertificateTemplateV2WithPoliciesDTO = {
|
||||||
templateId: string;
|
templateId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import { Control, Controller } from "react-hook-form";
|
||||||
|
|
||||||
|
import { FormControl, Select, SelectItem } from "@app/components/v2";
|
||||||
|
|
||||||
|
type AlgorithmOption = {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type AlgorithmSelectorsProps = {
|
||||||
|
control: Control<any>;
|
||||||
|
availableSignatureAlgorithms: AlgorithmOption[];
|
||||||
|
availableKeyAlgorithms: AlgorithmOption[];
|
||||||
|
signatureError?: string;
|
||||||
|
keyError?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AlgorithmSelectors = ({
|
||||||
|
control,
|
||||||
|
availableSignatureAlgorithms,
|
||||||
|
availableKeyAlgorithms,
|
||||||
|
signatureError,
|
||||||
|
keyError
|
||||||
|
}: AlgorithmSelectorsProps) => {
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="signatureAlgorithm"
|
||||||
|
render={({ field: { onChange, ...field } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Signature Algorithm"
|
||||||
|
errorText={signatureError}
|
||||||
|
isError={Boolean(signatureError)}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
defaultValue=""
|
||||||
|
{...field}
|
||||||
|
onValueChange={(e) => onChange(e)}
|
||||||
|
className="w-full"
|
||||||
|
placeholder={
|
||||||
|
availableSignatureAlgorithms.length > 0
|
||||||
|
? "Select signature algorithm"
|
||||||
|
: "No algorithms available"
|
||||||
|
}
|
||||||
|
position="popper"
|
||||||
|
>
|
||||||
|
{availableSignatureAlgorithms.map((algorithm) => (
|
||||||
|
<SelectItem key={algorithm.value} value={algorithm.value}>
|
||||||
|
{algorithm.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="keyAlgorithm"
|
||||||
|
render={({ field: { onChange, ...field } }) => (
|
||||||
|
<FormControl label="Key Algorithm" errorText={keyError} isError={Boolean(keyError)}>
|
||||||
|
<Select
|
||||||
|
defaultValue=""
|
||||||
|
{...field}
|
||||||
|
onValueChange={(e) => onChange(e)}
|
||||||
|
className="w-full"
|
||||||
|
placeholder={
|
||||||
|
availableKeyAlgorithms.length > 0
|
||||||
|
? "Select key algorithm"
|
||||||
|
: "No algorithms available"
|
||||||
|
}
|
||||||
|
position="popper"
|
||||||
|
>
|
||||||
|
{availableKeyAlgorithms.map((algorithm) => (
|
||||||
|
<SelectItem key={algorithm.value} value={algorithm.value}>
|
||||||
|
{algorithm.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+140
-550
@@ -1,6 +1,6 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { faPlus, faQuestionCircle, faTrash } from "@fortawesome/free-solid-svg-icons";
|
import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -8,14 +8,9 @@ import { z } from "zod";
|
|||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import {
|
import {
|
||||||
Accordion,
|
Accordion,
|
||||||
AccordionContent,
|
|
||||||
AccordionItem,
|
|
||||||
AccordionTrigger,
|
|
||||||
Button,
|
Button,
|
||||||
Checkbox,
|
|
||||||
FormControl,
|
FormControl,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
IconButton,
|
|
||||||
Input,
|
Input,
|
||||||
Modal,
|
Modal,
|
||||||
ModalContent,
|
ModalContent,
|
||||||
@@ -24,21 +19,24 @@ import {
|
|||||||
Tooltip
|
Tooltip
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useProject } from "@app/context";
|
import { useProject } from "@app/context";
|
||||||
import { useCreateCertificateV3, useGetCert } from "@app/hooks/api";
|
import { useGetCert } from "@app/hooks/api";
|
||||||
|
import { useCreateCertificateV3 } from "@app/hooks/api/ca";
|
||||||
import { useListCertificateProfiles } from "@app/hooks/api/certificateProfiles";
|
import { useListCertificateProfiles } from "@app/hooks/api/certificateProfiles";
|
||||||
import {
|
|
||||||
EXTENDED_KEY_USAGES_OPTIONS,
|
|
||||||
KEY_USAGES_OPTIONS
|
|
||||||
} from "@app/hooks/api/certificates/constants";
|
|
||||||
import { CertExtendedKeyUsage, CertKeyUsage } from "@app/hooks/api/certificates/enums";
|
import { CertExtendedKeyUsage, CertKeyUsage } from "@app/hooks/api/certificates/enums";
|
||||||
import { useGetCertificateTemplateV2ById } from "@app/hooks/api/certificateTemplates/queries";
|
import { useGetCertificateTemplateV2ById } from "@app/hooks/api/certificateTemplates/queries";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
import {
|
|
||||||
mapTemplateKeyAlgorithmToApi,
|
|
||||||
mapTemplateSignatureAlgorithmToApi
|
|
||||||
} from "@app/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/shared/certificate-constants";
|
|
||||||
|
|
||||||
|
import { AlgorithmSelectors } from "./AlgorithmSelectors";
|
||||||
import { CertificateContent } from "./CertificateContent";
|
import { CertificateContent } from "./CertificateContent";
|
||||||
|
import {
|
||||||
|
filterUsages,
|
||||||
|
formatSubjectAltNames,
|
||||||
|
FrontendSanType,
|
||||||
|
getAttributeValue
|
||||||
|
} from "./certificateUtils";
|
||||||
|
import { KeyUsageSection } from "./KeyUsageSection";
|
||||||
|
import { SubjectAltNamesField } from "./SubjectAltNamesField";
|
||||||
|
import { useCertificateTemplate } from "./useCertificateTemplate";
|
||||||
|
|
||||||
const createSchema = (shouldShowSubjectSection: boolean) => {
|
const createSchema = (shouldShowSubjectSection: boolean) => {
|
||||||
return z.object({
|
return z.object({
|
||||||
@@ -63,7 +61,7 @@ const createSchema = (shouldShowSubjectSection: boolean) => {
|
|||||||
subjectAltNames: z
|
subjectAltNames: z
|
||||||
.array(
|
.array(
|
||||||
z.object({
|
z.object({
|
||||||
type: z.enum(["dns", "ip", "email", "uri"]),
|
type: z.nativeEnum(FrontendSanType),
|
||||||
value: z.string().min(1, "Value is required")
|
value: z.string().min(1, "Value is required")
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
@@ -71,25 +69,29 @@ const createSchema = (shouldShowSubjectSection: boolean) => {
|
|||||||
ttl: z.string().trim().min(1, "TTL is required"),
|
ttl: z.string().trim().min(1, "TTL is required"),
|
||||||
signatureAlgorithm: z.string().min(1, "Signature algorithm is required"),
|
signatureAlgorithm: z.string().min(1, "Signature algorithm is required"),
|
||||||
keyAlgorithm: z.string().min(1, "Key algorithm is required"),
|
keyAlgorithm: z.string().min(1, "Key algorithm is required"),
|
||||||
keyUsages: z.object({
|
keyUsages: z
|
||||||
[CertKeyUsage.DIGITAL_SIGNATURE]: z.boolean().optional(),
|
.object({
|
||||||
[CertKeyUsage.KEY_ENCIPHERMENT]: z.boolean().optional(),
|
[CertKeyUsage.DIGITAL_SIGNATURE]: z.boolean().optional(),
|
||||||
[CertKeyUsage.NON_REPUDIATION]: z.boolean().optional(),
|
[CertKeyUsage.KEY_ENCIPHERMENT]: z.boolean().optional(),
|
||||||
[CertKeyUsage.DATA_ENCIPHERMENT]: z.boolean().optional(),
|
[CertKeyUsage.NON_REPUDIATION]: z.boolean().optional(),
|
||||||
[CertKeyUsage.KEY_AGREEMENT]: z.boolean().optional(),
|
[CertKeyUsage.DATA_ENCIPHERMENT]: z.boolean().optional(),
|
||||||
[CertKeyUsage.KEY_CERT_SIGN]: z.boolean().optional(),
|
[CertKeyUsage.KEY_AGREEMENT]: z.boolean().optional(),
|
||||||
[CertKeyUsage.CRL_SIGN]: z.boolean().optional(),
|
[CertKeyUsage.KEY_CERT_SIGN]: z.boolean().optional(),
|
||||||
[CertKeyUsage.ENCIPHER_ONLY]: z.boolean().optional(),
|
[CertKeyUsage.CRL_SIGN]: z.boolean().optional(),
|
||||||
[CertKeyUsage.DECIPHER_ONLY]: z.boolean().optional()
|
[CertKeyUsage.ENCIPHER_ONLY]: z.boolean().optional(),
|
||||||
}),
|
[CertKeyUsage.DECIPHER_ONLY]: z.boolean().optional()
|
||||||
extendedKeyUsages: z.object({
|
})
|
||||||
[CertExtendedKeyUsage.CLIENT_AUTH]: z.boolean().optional(),
|
.default({}),
|
||||||
[CertExtendedKeyUsage.CODE_SIGNING]: z.boolean().optional(),
|
extendedKeyUsages: z
|
||||||
[CertExtendedKeyUsage.EMAIL_PROTECTION]: z.boolean().optional(),
|
.object({
|
||||||
[CertExtendedKeyUsage.OCSP_SIGNING]: z.boolean().optional(),
|
[CertExtendedKeyUsage.CLIENT_AUTH]: z.boolean().optional(),
|
||||||
[CertExtendedKeyUsage.SERVER_AUTH]: z.boolean().optional(),
|
[CertExtendedKeyUsage.CODE_SIGNING]: z.boolean().optional(),
|
||||||
[CertExtendedKeyUsage.TIMESTAMPING]: z.boolean().optional()
|
[CertExtendedKeyUsage.EMAIL_PROTECTION]: z.boolean().optional(),
|
||||||
})
|
[CertExtendedKeyUsage.OCSP_SIGNING]: z.boolean().optional(),
|
||||||
|
[CertExtendedKeyUsage.SERVER_AUTH]: z.boolean().optional(),
|
||||||
|
[CertExtendedKeyUsage.TIMESTAMPING]: z.boolean().optional()
|
||||||
|
})
|
||||||
|
.default({})
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -113,15 +115,6 @@ type TCertificateDetails = {
|
|||||||
|
|
||||||
export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }: Props) => {
|
export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }: Props) => {
|
||||||
const [certificateDetails, setCertificateDetails] = useState<TCertificateDetails | null>(null);
|
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 [allowedSanTypes, setAllowedSanTypes] = useState<string[]>(["dns", "ip", "email", "uri"]);
|
|
||||||
const [shouldShowSanSection, setShouldShowSanSection] = useState<boolean>(true);
|
|
||||||
const [shouldShowSubjectSection, setShouldShowSubjectSection] = useState<boolean>(true);
|
|
||||||
const { currentProject } = useProject();
|
const { currentProject } = useProject();
|
||||||
|
|
||||||
const inputSerialNumber =
|
const inputSerialNumber =
|
||||||
@@ -137,44 +130,11 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
|||||||
|
|
||||||
const { mutateAsync: createCertificate } = useCreateCertificateV3();
|
const { mutateAsync: createCertificate } = useCreateCertificateV3();
|
||||||
|
|
||||||
const {
|
const selectedProfileId = useMemo(() => {
|
||||||
control,
|
const form = document.querySelector('select[name="profileId"]') as HTMLSelectElement;
|
||||||
handleSubmit,
|
return form?.value || profileId || "";
|
||||||
reset,
|
}, [profileId]);
|
||||||
watch,
|
|
||||||
setValue,
|
|
||||||
formState: { isSubmitting }
|
|
||||||
} = useForm<FormData>({
|
|
||||||
resolver: zodResolver(createSchema(shouldShowSubjectSection)),
|
|
||||||
defaultValues: {
|
|
||||||
profileId: profileId || "",
|
|
||||||
subjectAttributes: shouldShowSubjectSection
|
|
||||||
? [{ type: "common_name", value: "" }]
|
|
||||||
: undefined,
|
|
||||||
subjectAltNames: [],
|
|
||||||
ttl: "30d",
|
|
||||||
signatureAlgorithm: "",
|
|
||||||
keyAlgorithm: "",
|
|
||||||
keyUsages: {},
|
|
||||||
extendedKeyUsages: {}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const resetAllState = useCallback(() => {
|
|
||||||
setCertificateDetails(null);
|
|
||||||
setAllowedKeyUsages([]);
|
|
||||||
setAllowedExtendedKeyUsages([]);
|
|
||||||
setRequiredKeyUsages([]);
|
|
||||||
setRequiredExtendedKeyUsages([]);
|
|
||||||
setAllowedSignatureAlgorithms([]);
|
|
||||||
setAllowedKeyAlgorithms([]);
|
|
||||||
setAllowedSanTypes(["dns", "ip", "email", "uri"]);
|
|
||||||
setShouldShowSanSection(true);
|
|
||||||
setShouldShowSubjectSection(true);
|
|
||||||
reset();
|
|
||||||
}, [reset]);
|
|
||||||
|
|
||||||
const selectedProfileId = watch("profileId");
|
|
||||||
const selectedProfile = useMemo(
|
const selectedProfile = useMemo(
|
||||||
() => profilesData?.certificateProfiles?.find((p) => p.id === selectedProfileId),
|
() => profilesData?.certificateProfiles?.find((p) => p.id === selectedProfileId),
|
||||||
[profilesData?.certificateProfiles, selectedProfileId]
|
[profilesData?.certificateProfiles, selectedProfileId]
|
||||||
@@ -184,135 +144,54 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
|||||||
templateId: selectedProfile?.certificateTemplateId || ""
|
templateId: selectedProfile?.certificateTemplateId || ""
|
||||||
});
|
});
|
||||||
|
|
||||||
const filteredKeyUsages = useMemo(() => {
|
const {
|
||||||
return KEY_USAGES_OPTIONS.filter(({ value }) => allowedKeyUsages.includes(value));
|
control,
|
||||||
}, [allowedKeyUsages]);
|
handleSubmit,
|
||||||
|
reset,
|
||||||
const filteredExtendedKeyUsages = useMemo(() => {
|
watch,
|
||||||
return EXTENDED_KEY_USAGES_OPTIONS.filter(({ value }) =>
|
setValue,
|
||||||
allowedExtendedKeyUsages.includes(value)
|
formState,
|
||||||
);
|
formState: { isSubmitting }
|
||||||
}, [allowedExtendedKeyUsages]);
|
} = useForm<FormData>({
|
||||||
|
resolver: zodResolver(createSchema((templateData?.subject?.length || 0) > 0)),
|
||||||
const mapBackendSanTypeToFrontend = (backendType: string): string => {
|
defaultValues: {
|
||||||
switch (backendType) {
|
profileId: profileId || "",
|
||||||
case "dns_name":
|
subjectAttributes: [],
|
||||||
return "dns";
|
subjectAltNames: [],
|
||||||
case "ip_address":
|
ttl: "30d",
|
||||||
return "ip";
|
signatureAlgorithm: "",
|
||||||
case "email":
|
keyAlgorithm: "",
|
||||||
return "email";
|
keyUsages: {},
|
||||||
case "uri":
|
extendedKeyUsages: {}
|
||||||
return "uri";
|
|
||||||
default:
|
|
||||||
return backendType;
|
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
const availableSignatureAlgorithms = useMemo(() => {
|
const actualSelectedProfileId = watch("profileId");
|
||||||
return allowedSignatureAlgorithms.map((templateAlgorithm) => {
|
const actualSelectedProfile = useMemo(
|
||||||
const apiAlgorithm = mapTemplateSignatureAlgorithmToApi(templateAlgorithm);
|
() => profilesData?.certificateProfiles?.find((p) => p.id === actualSelectedProfileId),
|
||||||
return {
|
[profilesData?.certificateProfiles, actualSelectedProfileId]
|
||||||
value: apiAlgorithm,
|
);
|
||||||
label: apiAlgorithm
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}, [allowedSignatureAlgorithms]);
|
|
||||||
|
|
||||||
const availableKeyAlgorithms = useMemo(() => {
|
const {
|
||||||
return allowedKeyAlgorithms.map((templateAlgorithm) => {
|
constraints,
|
||||||
const apiAlgorithm = mapTemplateKeyAlgorithmToApi(templateAlgorithm);
|
filteredKeyUsages,
|
||||||
return {
|
filteredExtendedKeyUsages,
|
||||||
value: apiAlgorithm,
|
availableSignatureAlgorithms,
|
||||||
label: apiAlgorithm
|
availableKeyAlgorithms,
|
||||||
};
|
resetConstraints
|
||||||
});
|
} = useCertificateTemplate(
|
||||||
}, [allowedKeyAlgorithms]);
|
templateData,
|
||||||
|
actualSelectedProfile,
|
||||||
|
popUp?.certificateIssuance?.isOpen || false,
|
||||||
|
setValue,
|
||||||
|
watch
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
const resetAllState = useCallback(() => {
|
||||||
if (templateData && selectedProfile && popUp?.certificateIssuance?.isOpen) {
|
setCertificateDetails(null);
|
||||||
if (templateData.algorithms?.signature && templateData.algorithms.signature.length > 0) {
|
resetConstraints();
|
||||||
setAllowedSignatureAlgorithms(templateData.algorithms.signature);
|
reset();
|
||||||
} else {
|
}, [reset, resetConstraints]);
|
||||||
setAllowedSignatureAlgorithms([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
templateData.algorithms?.keyAlgorithm &&
|
|
||||||
templateData.algorithms.keyAlgorithm.length > 0
|
|
||||||
) {
|
|
||||||
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 || []);
|
|
||||||
|
|
||||||
if (templateData.sans && templateData.sans.length > 0) {
|
|
||||||
const sanTypes: string[] = [];
|
|
||||||
templateData.sans.forEach((sanPolicy) => {
|
|
||||||
const frontendType = mapBackendSanTypeToFrontend(sanPolicy.type);
|
|
||||||
if (!sanTypes.includes(frontendType)) {
|
|
||||||
sanTypes.push(frontendType);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
setAllowedSanTypes(sanTypes);
|
|
||||||
setShouldShowSanSection(true);
|
|
||||||
} else {
|
|
||||||
setAllowedSanTypes([]);
|
|
||||||
setShouldShowSanSection(false);
|
|
||||||
setValue("subjectAltNames", []);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (templateData.subject && templateData.subject.length > 0) {
|
|
||||||
setShouldShowSubjectSection(true);
|
|
||||||
const currentSubjectAttrs = watch("subjectAttributes");
|
|
||||||
if (!currentSubjectAttrs || currentSubjectAttrs.length === 0) {
|
|
||||||
setValue("subjectAttributes", [{ type: "common_name", value: "" }]);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setShouldShowSubjectSection(false);
|
|
||||||
setValue("subjectAttributes", undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialKeyUsages: Record<string, boolean> = {};
|
|
||||||
const initialExtendedKeyUsages: Record<string, boolean> = {};
|
|
||||||
|
|
||||||
(templateData.keyUsages?.required || []).forEach((usage: string) => {
|
|
||||||
initialKeyUsages[usage] = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
(templateData.extendedKeyUsages?.required || []).forEach((usage: string) => {
|
|
||||||
initialExtendedKeyUsages[usage] = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
setValue("keyUsages", initialKeyUsages);
|
|
||||||
setValue("extendedKeyUsages", initialExtendedKeyUsages);
|
|
||||||
}
|
|
||||||
}, [templateData, selectedProfile, setValue, watch, popUp?.certificateIssuance?.isOpen]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (cert) {
|
if (cert) {
|
||||||
@@ -327,14 +206,16 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
|||||||
subjectAltNames: cert.subjectAltNames
|
subjectAltNames: cert.subjectAltNames
|
||||||
? cert.subjectAltNames.split(",").map((name) => {
|
? cert.subjectAltNames.split(",").map((name) => {
|
||||||
const trimmed = name.trim();
|
const trimmed = name.trim();
|
||||||
if (trimmed.includes("@")) return { type: "email" as const, value: trimmed };
|
if (trimmed.includes("@")) return { type: FrontendSanType.EMAIL, value: trimmed };
|
||||||
if (trimmed.match(/^\d+\.\d+\.\d+\.\d+$/))
|
if (trimmed.match(/^\d+\.\d+\.\d+\.\d+$/))
|
||||||
return { type: "ip" as const, value: trimmed };
|
return { type: FrontendSanType.IP, value: trimmed };
|
||||||
if (trimmed.startsWith("http")) return { type: "uri" as const, value: trimmed };
|
if (trimmed.startsWith("http")) return { type: FrontendSanType.URI, value: trimmed };
|
||||||
return { type: "dns" as const, value: trimmed };
|
return { type: FrontendSanType.DNS, value: trimmed };
|
||||||
})
|
})
|
||||||
: [],
|
: [],
|
||||||
ttl: "",
|
ttl: "",
|
||||||
|
signatureAlgorithm: "",
|
||||||
|
keyAlgorithm: "",
|
||||||
keyUsages: Object.fromEntries((cert.keyUsages || []).map((name) => [name, true])),
|
keyUsages: Object.fromEntries((cert.keyUsages || []).map((name) => [name, true])),
|
||||||
extendedKeyUsages: Object.fromEntries(
|
extendedKeyUsages: Object.fromEntries(
|
||||||
(cert.extendedKeyUsages || []).map((name) => [name, true])
|
(cert.extendedKeyUsages || []).map((name) => [name, true])
|
||||||
@@ -349,27 +230,6 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
|||||||
}
|
}
|
||||||
}, [popUp?.certificateIssuance?.isOpen, profileId, cert, setValue]);
|
}, [popUp?.certificateIssuance?.isOpen, profileId, cert, setValue]);
|
||||||
|
|
||||||
const getAttributeValue = useCallback(
|
|
||||||
(subjectAttributes: FormData["subjectAttributes"], type: string) => {
|
|
||||||
const foundAttr = subjectAttributes?.find((attr) => attr.type === type);
|
|
||||||
return foundAttr?.value || "";
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const formatSubjectAltNames = useCallback((subjectAltNames: FormData["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(
|
const onFormSubmit = useCallback(
|
||||||
async ({
|
async ({
|
||||||
profileId: formProfileId,
|
profileId: formProfileId,
|
||||||
@@ -399,7 +259,11 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
|||||||
}
|
}
|
||||||
|
|
||||||
let commonName = "";
|
let commonName = "";
|
||||||
if (shouldShowSubjectSection && subjectAttributes && subjectAttributes.length > 0) {
|
if (
|
||||||
|
constraints.shouldShowSubjectSection &&
|
||||||
|
subjectAttributes &&
|
||||||
|
subjectAttributes.length > 0
|
||||||
|
) {
|
||||||
commonName = getAttributeValue(subjectAttributes, "common_name");
|
commonName = getAttributeValue(subjectAttributes, "common_name");
|
||||||
if (!commonName.trim()) {
|
if (!commonName.trim()) {
|
||||||
createNotification({
|
createNotification({
|
||||||
@@ -420,13 +284,13 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
|||||||
extendedKeyUsages: filterUsages(extendedKeyUsages) as CertExtendedKeyUsage[]
|
extendedKeyUsages: filterUsages(extendedKeyUsages) as CertExtendedKeyUsage[]
|
||||||
};
|
};
|
||||||
|
|
||||||
if (shouldShowSubjectSection && commonName) {
|
if (constraints.shouldShowSubjectSection && commonName) {
|
||||||
certificateRequest.commonName = commonName;
|
certificateRequest.commonName = commonName;
|
||||||
}
|
}
|
||||||
if (shouldShowSanSection && subjectAltNames && subjectAltNames.length > 0) {
|
if (constraints.shouldShowSanSection && subjectAltNames && subjectAltNames.length > 0) {
|
||||||
const formattedSans = formatSubjectAltNames(subjectAltNames);
|
const formattedSans = formatSubjectAltNames(subjectAltNames);
|
||||||
if (formattedSans) {
|
if (formattedSans && formattedSans.length > 0) {
|
||||||
certificateRequest.subjectAltNames = formattedSans;
|
certificateRequest.altNames = formattedSans;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -459,11 +323,8 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
|||||||
[
|
[
|
||||||
currentProject?.slug,
|
currentProject?.slug,
|
||||||
createCertificate,
|
createCertificate,
|
||||||
shouldShowSubjectSection,
|
constraints.shouldShowSubjectSection,
|
||||||
shouldShowSanSection,
|
constraints.shouldShowSanSection
|
||||||
getAttributeValue,
|
|
||||||
formatSubjectAltNames,
|
|
||||||
filterUsages
|
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -479,21 +340,6 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
|||||||
return "Issue a new certificate using a certificate profile";
|
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 (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={popUp?.certificateIssuance?.isOpen}
|
isOpen={popUp?.certificateIssuance?.isOpen}
|
||||||
@@ -576,165 +422,36 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(selectedProfile || profileId) && (
|
{(actualSelectedProfile || profileId) && (
|
||||||
<>
|
<>
|
||||||
{shouldShowSubjectSection && (
|
{constraints.shouldShowSubjectSection && (
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="subjectAttributes"
|
name="subjectAttributes"
|
||||||
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
||||||
<FormControl
|
<FormControl
|
||||||
label="Subject Attributes"
|
label="Common Name"
|
||||||
isRequired
|
isRequired
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
isError={Boolean(error)}
|
isError={Boolean(error)}
|
||||||
>
|
>
|
||||||
<div className="space-y-2">
|
<Input
|
||||||
{(value || []).map((attr, index) => (
|
value={value?.[0]?.value || ""}
|
||||||
// eslint-disable-next-line react/no-array-index-key
|
onChange={(e) => {
|
||||||
<div key={`subject-attr-${index}`} className="flex items-center gap-2">
|
onChange([{ type: "common_name", value: e.target.value }]);
|
||||||
<Select
|
}}
|
||||||
value={attr.type}
|
placeholder="example.com"
|
||||||
onValueChange={(newType) => {
|
/>
|
||||||
const newValue = [...(value || [])];
|
|
||||||
newValue[index] = {
|
|
||||||
...attr,
|
|
||||||
type: newType as typeof attr.type
|
|
||||||
};
|
|
||||||
onChange(newValue);
|
|
||||||
}}
|
|
||||||
className="w-48"
|
|
||||||
>
|
|
||||||
<SelectItem value="common_name">Common Name</SelectItem>
|
|
||||||
</Select>
|
|
||||||
<Input
|
|
||||||
value={attr.value}
|
|
||||||
onChange={(e) => {
|
|
||||||
const newValue = [...(value || [])];
|
|
||||||
newValue[index] = { ...attr, value: e.target.value };
|
|
||||||
onChange(newValue);
|
|
||||||
}}
|
|
||||||
placeholder="example.com"
|
|
||||||
className="flex-1"
|
|
||||||
/>
|
|
||||||
{(value || []).length > 1 && (
|
|
||||||
<IconButton
|
|
||||||
ariaLabel="Remove Subject Attribute"
|
|
||||||
variant="plain"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => {
|
|
||||||
const newValue = (value || []).filter((_, i) => i !== index);
|
|
||||||
onChange(newValue);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faTrash} />
|
|
||||||
</IconButton>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline_bg"
|
|
||||||
size="xs"
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
|
||||||
onClick={() => {
|
|
||||||
onChange([...(value || []), { type: "common_name", value: "" }]);
|
|
||||||
}}
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
Add Subject Attribute
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{shouldShowSanSection && (
|
{constraints.shouldShowSanSection && (
|
||||||
<Controller
|
<SubjectAltNamesField
|
||||||
control={control}
|
control={control}
|
||||||
name="subjectAltNames"
|
allowedSanTypes={constraints.allowedSanTypes}
|
||||||
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
error={formState.errors.subjectAltNames?.message}
|
||||||
<FormControl
|
|
||||||
label="Subject Alternative Names (SANs)"
|
|
||||||
errorText={error?.message}
|
|
||||||
isError={Boolean(error)}
|
|
||||||
>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{value.map((san, index) => (
|
|
||||||
<div
|
|
||||||
// eslint-disable-next-line react/no-array-index-key
|
|
||||||
key={`subject-alt-name-${index}`}
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
value={san.type}
|
|
||||||
onValueChange={(newType) => {
|
|
||||||
const newValue = [...value];
|
|
||||||
newValue[index] = {
|
|
||||||
...san,
|
|
||||||
type: newType as "dns" | "ip" | "email" | "uri"
|
|
||||||
};
|
|
||||||
onChange(newValue);
|
|
||||||
}}
|
|
||||||
className="w-24"
|
|
||||||
>
|
|
||||||
{allowedSanTypes.includes("dns") && (
|
|
||||||
<SelectItem value="dns">DNS</SelectItem>
|
|
||||||
)}
|
|
||||||
{allowedSanTypes.includes("ip") && (
|
|
||||||
<SelectItem value="ip">IP</SelectItem>
|
|
||||||
)}
|
|
||||||
{allowedSanTypes.includes("email") && (
|
|
||||||
<SelectItem value="email">Email</SelectItem>
|
|
||||||
)}
|
|
||||||
{allowedSanTypes.includes("uri") && (
|
|
||||||
<SelectItem value="uri">URI</SelectItem>
|
|
||||||
)}
|
|
||||||
</Select>
|
|
||||||
<Input
|
|
||||||
value={san.value}
|
|
||||||
onChange={(e) => {
|
|
||||||
const newValue = [...value];
|
|
||||||
newValue[index] = { ...san, value: e.target.value };
|
|
||||||
onChange(newValue);
|
|
||||||
}}
|
|
||||||
placeholder={getSanPlaceholder(san.type)}
|
|
||||||
className="flex-1"
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
ariaLabel="Remove SAN"
|
|
||||||
variant="plain"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => {
|
|
||||||
const newValue = value.filter((_, i) => i !== index);
|
|
||||||
onChange(newValue);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faTrash} />
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline_bg"
|
|
||||||
size="xs"
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
|
||||||
onClick={() => {
|
|
||||||
const defaultType =
|
|
||||||
allowedSanTypes.length > 0 ? allowedSanTypes[0] : "dns";
|
|
||||||
onChange([
|
|
||||||
...value,
|
|
||||||
{ type: defaultType as "dns" | "ip" | "email" | "uri", value: "" }
|
|
||||||
]);
|
|
||||||
}}
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
Add SAN
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -753,158 +470,31 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<AlgorithmSelectors
|
||||||
<div>
|
control={control}
|
||||||
<Controller
|
availableSignatureAlgorithms={availableSignatureAlgorithms}
|
||||||
control={control}
|
availableKeyAlgorithms={availableKeyAlgorithms}
|
||||||
name="signatureAlgorithm"
|
signatureError={formState.errors.signatureAlgorithm?.message}
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
keyError={formState.errors.keyAlgorithm?.message}
|
||||||
<FormControl
|
/>
|
||||||
label="Signature Algorithm"
|
|
||||||
errorText={error?.message}
|
|
||||||
isError={Boolean(error)}
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
defaultValue=""
|
|
||||||
{...field}
|
|
||||||
onValueChange={(e) => onChange(e)}
|
|
||||||
className="w-full"
|
|
||||||
placeholder={
|
|
||||||
availableSignatureAlgorithms.length > 0
|
|
||||||
? "Select signature algorithm"
|
|
||||||
: "No algorithms available"
|
|
||||||
}
|
|
||||||
position="popper"
|
|
||||||
>
|
|
||||||
{availableSignatureAlgorithms.map((algorithm) => (
|
|
||||||
<SelectItem key={algorithm.value} value={algorithm.value}>
|
|
||||||
{algorithm.label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="keyAlgorithm"
|
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Key Algorithm"
|
|
||||||
errorText={error?.message}
|
|
||||||
isError={Boolean(error)}
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
defaultValue=""
|
|
||||||
{...field}
|
|
||||||
onValueChange={(e) => onChange(e)}
|
|
||||||
className="w-full"
|
|
||||||
placeholder={
|
|
||||||
availableKeyAlgorithms.length > 0
|
|
||||||
? "Select key algorithm"
|
|
||||||
: "No algorithms available"
|
|
||||||
}
|
|
||||||
position="popper"
|
|
||||||
>
|
|
||||||
{availableKeyAlgorithms.map((algorithm) => (
|
|
||||||
<SelectItem key={algorithm.value} value={algorithm.value}>
|
|
||||||
{algorithm.label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Accordion type="single" collapsible className="w-full">
|
<Accordion type="single" collapsible className="w-full">
|
||||||
{filteredKeyUsages.length > 0 && (
|
<KeyUsageSection
|
||||||
<AccordionItem value="key-usages">
|
control={control}
|
||||||
<AccordionTrigger>Key Usages</AccordionTrigger>
|
title="Key Usages"
|
||||||
<AccordionContent>
|
accordionValue="key-usages"
|
||||||
<div className="grid grid-cols-2 gap-2 pl-2">
|
namePrefix="keyUsages"
|
||||||
{filteredKeyUsages.map(({ label, value }) => {
|
options={filteredKeyUsages}
|
||||||
const isRequired = requiredKeyUsages.includes(value);
|
requiredUsages={constraints.requiredKeyUsages}
|
||||||
return (
|
/>
|
||||||
<Controller
|
<KeyUsageSection
|
||||||
key={label}
|
control={control}
|
||||||
control={control}
|
title="Extended Key Usages"
|
||||||
name={`keyUsages.${value}` as any}
|
accordionValue="extended-key-usages"
|
||||||
render={({ field }) => (
|
namePrefix="extendedKeyUsages"
|
||||||
<div className="flex items-center space-x-3">
|
options={filteredExtendedKeyUsages}
|
||||||
<Checkbox
|
requiredUsages={constraints.requiredExtendedKeyUsages}
|
||||||
id={`key-usage-${value}`}
|
/>
|
||||||
isChecked={field.value || false}
|
|
||||||
onCheckedChange={(checked) => {
|
|
||||||
if (!isRequired) {
|
|
||||||
field.onChange(checked);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
isDisabled={isRequired}
|
|
||||||
/>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<FormLabel
|
|
||||||
id={`key-usage-${value}`}
|
|
||||||
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>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{filteredExtendedKeyUsages.length > 0 && (
|
|
||||||
<AccordionItem value="extended-key-usages">
|
|
||||||
<AccordionTrigger>Extended Key Usages</AccordionTrigger>
|
|
||||||
<AccordionContent>
|
|
||||||
<div className="grid grid-cols-2 gap-2 pl-2">
|
|
||||||
{filteredExtendedKeyUsages.map(({ label, value }) => {
|
|
||||||
const isRequired = requiredExtendedKeyUsages.includes(value);
|
|
||||||
return (
|
|
||||||
<Controller
|
|
||||||
key={label}
|
|
||||||
control={control}
|
|
||||||
name={`extendedKeyUsages.${value}` as any}
|
|
||||||
render={({ field }) => (
|
|
||||||
<div className="flex items-center space-x-3">
|
|
||||||
<Checkbox
|
|
||||||
id={`ext-key-usage-${value}`}
|
|
||||||
isChecked={field.value || false}
|
|
||||||
onCheckedChange={(checked) => {
|
|
||||||
if (!isRequired) {
|
|
||||||
field.onChange(checked);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
isDisabled={isRequired}
|
|
||||||
/>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<FormLabel
|
|
||||||
id={`ext-key-usage-${value}`}
|
|
||||||
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>
|
|
||||||
)}
|
|
||||||
</Accordion>
|
</Accordion>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -915,7 +505,7 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
|||||||
size="sm"
|
size="sm"
|
||||||
type="submit"
|
type="submit"
|
||||||
isLoading={isSubmitting}
|
isLoading={isSubmitting}
|
||||||
isDisabled={isSubmitting || (!selectedProfile && !profileId)}
|
isDisabled={isSubmitting || (!actualSelectedProfile && !profileId)}
|
||||||
>
|
>
|
||||||
{cert ? "Update" : "Issue Certificate"}
|
{cert ? "Update" : "Issue Certificate"}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import { Control, Controller } from "react-hook-form";
|
||||||
|
|
||||||
|
import {
|
||||||
|
AccordionContent,
|
||||||
|
AccordionItem,
|
||||||
|
AccordionTrigger,
|
||||||
|
Checkbox,
|
||||||
|
FormLabel
|
||||||
|
} from "@app/components/v2";
|
||||||
|
|
||||||
|
type KeyUsageOption = {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type KeyUsageSectionProps = {
|
||||||
|
control: Control<any>;
|
||||||
|
title: string;
|
||||||
|
accordionValue: string;
|
||||||
|
namePrefix: "keyUsages" | "extendedKeyUsages";
|
||||||
|
options: KeyUsageOption[];
|
||||||
|
requiredUsages: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const KeyUsageSection = ({
|
||||||
|
control,
|
||||||
|
title,
|
||||||
|
accordionValue,
|
||||||
|
namePrefix,
|
||||||
|
options,
|
||||||
|
requiredUsages
|
||||||
|
}: KeyUsageSectionProps) => {
|
||||||
|
if (options.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AccordionItem value={accordionValue}>
|
||||||
|
<AccordionTrigger>{title}</AccordionTrigger>
|
||||||
|
<AccordionContent>
|
||||||
|
<div className="grid grid-cols-2 gap-2 pl-2">
|
||||||
|
{options.map(({ label, value }) => {
|
||||||
|
const isRequired = requiredUsages.includes(value);
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
key={label}
|
||||||
|
control={control}
|
||||||
|
name={`${namePrefix}.${value}` as any}
|
||||||
|
render={({ field }) => (
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<Checkbox
|
||||||
|
id={`${namePrefix}-${value}`}
|
||||||
|
isChecked={field.value || false}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
if (!isRequired) {
|
||||||
|
field.onChange(checked);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
isDisabled={isRequired}
|
||||||
|
/>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<FormLabel
|
||||||
|
id={`${namePrefix}-${value}`}
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import { Control, Controller } from "react-hook-form";
|
||||||
|
import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { Button, FormControl, IconButton, Input, Select, SelectItem } from "@app/components/v2";
|
||||||
|
|
||||||
|
import {
|
||||||
|
FrontendSanType,
|
||||||
|
getSanPlaceholder,
|
||||||
|
getSanTypeLabels,
|
||||||
|
SubjectAltName
|
||||||
|
} from "./certificateUtils";
|
||||||
|
|
||||||
|
type SubjectAltNamesFieldProps = {
|
||||||
|
control: Control<any>;
|
||||||
|
allowedSanTypes: FrontendSanType[];
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SubjectAltNamesField = ({
|
||||||
|
control,
|
||||||
|
allowedSanTypes,
|
||||||
|
error
|
||||||
|
}: SubjectAltNamesFieldProps) => {
|
||||||
|
const sanTypeLabels = getSanTypeLabels();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="subjectAltNames"
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Subject Alternative Names (SANs)"
|
||||||
|
errorText={error}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{value.map((san: SubjectAltName, index: number) => (
|
||||||
|
// 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) => {
|
||||||
|
const newValue = [...value];
|
||||||
|
newValue[index] = {
|
||||||
|
...san,
|
||||||
|
type: newType as FrontendSanType
|
||||||
|
};
|
||||||
|
onChange(newValue);
|
||||||
|
}}
|
||||||
|
className="w-24"
|
||||||
|
>
|
||||||
|
{allowedSanTypes.map((sanType) => (
|
||||||
|
<SelectItem key={sanType} value={sanType}>
|
||||||
|
{sanTypeLabels[sanType]}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
<Input
|
||||||
|
value={san.value}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newValue = [...value];
|
||||||
|
newValue[index] = { ...san, value: e.target.value };
|
||||||
|
onChange(newValue);
|
||||||
|
}}
|
||||||
|
placeholder={getSanPlaceholder(san.type)}
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="Remove SAN"
|
||||||
|
variant="plain"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
const newValue = value.filter((_: any, i: number) => i !== index);
|
||||||
|
onChange(newValue);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTrash} />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline_bg"
|
||||||
|
size="xs"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
onClick={() => {
|
||||||
|
const defaultType =
|
||||||
|
allowedSanTypes.length > 0 ? allowedSanTypes[0] : FrontendSanType.DNS;
|
||||||
|
onChange([...value, { type: defaultType, value: "" }]);
|
||||||
|
}}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
Add SAN
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import { CertSubjectAlternativeNameType } from "@app/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/shared/certificate-constants";
|
||||||
|
|
||||||
|
export enum FrontendSanType {
|
||||||
|
DNS = "dns",
|
||||||
|
IP = "ip",
|
||||||
|
EMAIL = "email",
|
||||||
|
URI = "uri"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const mapBackendSanTypeToFrontend = (backendType: string): FrontendSanType => {
|
||||||
|
switch (backendType) {
|
||||||
|
case CertSubjectAlternativeNameType.DNS_NAME:
|
||||||
|
return FrontendSanType.DNS;
|
||||||
|
case CertSubjectAlternativeNameType.IP_ADDRESS:
|
||||||
|
return FrontendSanType.IP;
|
||||||
|
case CertSubjectAlternativeNameType.EMAIL:
|
||||||
|
return FrontendSanType.EMAIL;
|
||||||
|
case CertSubjectAlternativeNameType.URI:
|
||||||
|
return FrontendSanType.URI;
|
||||||
|
default:
|
||||||
|
return backendType as FrontendSanType;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mapFrontendSanTypeToBackend = (frontendType: FrontendSanType): string => {
|
||||||
|
switch (frontendType) {
|
||||||
|
case FrontendSanType.DNS:
|
||||||
|
return CertSubjectAlternativeNameType.DNS_NAME;
|
||||||
|
case FrontendSanType.IP:
|
||||||
|
return CertSubjectAlternativeNameType.IP_ADDRESS;
|
||||||
|
case FrontendSanType.EMAIL:
|
||||||
|
return CertSubjectAlternativeNameType.EMAIL;
|
||||||
|
case FrontendSanType.URI:
|
||||||
|
return CertSubjectAlternativeNameType.URI;
|
||||||
|
default:
|
||||||
|
return frontendType;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getSanPlaceholder = (sanType: FrontendSanType): string => {
|
||||||
|
switch (sanType) {
|
||||||
|
case FrontendSanType.DNS:
|
||||||
|
return "example.com or *.example.com";
|
||||||
|
case FrontendSanType.IP:
|
||||||
|
return "192.168.1.1";
|
||||||
|
case FrontendSanType.EMAIL:
|
||||||
|
return "[email protected]";
|
||||||
|
case FrontendSanType.URI:
|
||||||
|
return "https://example.com";
|
||||||
|
default:
|
||||||
|
return "Enter value";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getSanTypeLabels = () => ({
|
||||||
|
[FrontendSanType.DNS]: "DNS",
|
||||||
|
[FrontendSanType.IP]: "IP",
|
||||||
|
[FrontendSanType.EMAIL]: "Email",
|
||||||
|
[FrontendSanType.URI]: "URI"
|
||||||
|
});
|
||||||
|
|
||||||
|
export type SubjectAltName = {
|
||||||
|
type: FrontendSanType;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatSubjectAltNames = (subjectAltNames: SubjectAltName[]) => {
|
||||||
|
return subjectAltNames
|
||||||
|
.filter((san) => san.value.trim())
|
||||||
|
.map((san) => ({
|
||||||
|
type: mapFrontendSanTypeToBackend(san.type),
|
||||||
|
value: san.value.trim()
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const filterUsages = <T extends Record<string, boolean>>(usages: T): string[] => {
|
||||||
|
return Object.entries(usages)
|
||||||
|
.filter(([, value]) => value)
|
||||||
|
.map(([key]) => key);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAttributeValue = (
|
||||||
|
subjectAttributes: Array<{ type: string; value: string }> | undefined,
|
||||||
|
type: string
|
||||||
|
): string => {
|
||||||
|
const foundAttr = subjectAttributes?.find((attr) => attr.type === type);
|
||||||
|
return foundAttr?.value || "";
|
||||||
|
};
|
||||||
+181
@@ -0,0 +1,181 @@
|
|||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import { UseFormSetValue, UseFormWatch } from "react-hook-form";
|
||||||
|
|
||||||
|
import {
|
||||||
|
EXTENDED_KEY_USAGES_OPTIONS,
|
||||||
|
KEY_USAGES_OPTIONS
|
||||||
|
} from "@app/hooks/api/certificates/constants";
|
||||||
|
import {
|
||||||
|
mapTemplateKeyAlgorithmToApi,
|
||||||
|
mapTemplateSignatureAlgorithmToApi
|
||||||
|
} from "@app/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/shared/certificate-constants";
|
||||||
|
|
||||||
|
import { FrontendSanType, mapBackendSanTypeToFrontend } from "./certificateUtils";
|
||||||
|
|
||||||
|
export type TemplateConstraints = {
|
||||||
|
allowedKeyUsages: string[];
|
||||||
|
allowedExtendedKeyUsages: string[];
|
||||||
|
requiredKeyUsages: string[];
|
||||||
|
requiredExtendedKeyUsages: string[];
|
||||||
|
allowedSignatureAlgorithms: string[];
|
||||||
|
allowedKeyAlgorithms: string[];
|
||||||
|
allowedSanTypes: FrontendSanType[];
|
||||||
|
shouldShowSanSection: boolean;
|
||||||
|
shouldShowSubjectSection: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCertificateTemplate = (
|
||||||
|
templateData: any,
|
||||||
|
selectedProfile: any,
|
||||||
|
isModalOpen: boolean,
|
||||||
|
setValue: UseFormSetValue<any>,
|
||||||
|
watch: UseFormWatch<any>
|
||||||
|
) => {
|
||||||
|
const [constraints, setConstraints] = useState<TemplateConstraints>({
|
||||||
|
allowedKeyUsages: [],
|
||||||
|
allowedExtendedKeyUsages: [],
|
||||||
|
requiredKeyUsages: [],
|
||||||
|
requiredExtendedKeyUsages: [],
|
||||||
|
allowedSignatureAlgorithms: [],
|
||||||
|
allowedKeyAlgorithms: [],
|
||||||
|
allowedSanTypes: [
|
||||||
|
FrontendSanType.DNS,
|
||||||
|
FrontendSanType.IP,
|
||||||
|
FrontendSanType.EMAIL,
|
||||||
|
FrontendSanType.URI
|
||||||
|
],
|
||||||
|
shouldShowSanSection: true,
|
||||||
|
shouldShowSubjectSection: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const filteredKeyUsages = useMemo(() => {
|
||||||
|
return KEY_USAGES_OPTIONS.filter(({ value }) => constraints.allowedKeyUsages.includes(value));
|
||||||
|
}, [constraints.allowedKeyUsages]);
|
||||||
|
|
||||||
|
const filteredExtendedKeyUsages = useMemo(() => {
|
||||||
|
return EXTENDED_KEY_USAGES_OPTIONS.filter(({ value }) =>
|
||||||
|
constraints.allowedExtendedKeyUsages.includes(value)
|
||||||
|
);
|
||||||
|
}, [constraints.allowedExtendedKeyUsages]);
|
||||||
|
|
||||||
|
const availableSignatureAlgorithms = useMemo(() => {
|
||||||
|
return constraints.allowedSignatureAlgorithms.map((templateAlgorithm) => {
|
||||||
|
const apiAlgorithm = mapTemplateSignatureAlgorithmToApi(templateAlgorithm);
|
||||||
|
return {
|
||||||
|
value: apiAlgorithm,
|
||||||
|
label: apiAlgorithm
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [constraints.allowedSignatureAlgorithms]);
|
||||||
|
|
||||||
|
const availableKeyAlgorithms = useMemo(() => {
|
||||||
|
return constraints.allowedKeyAlgorithms.map((templateAlgorithm) => {
|
||||||
|
const apiAlgorithm = mapTemplateKeyAlgorithmToApi(templateAlgorithm);
|
||||||
|
return {
|
||||||
|
value: apiAlgorithm,
|
||||||
|
label: apiAlgorithm
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [constraints.allowedKeyAlgorithms]);
|
||||||
|
|
||||||
|
const resetConstraints = () => {
|
||||||
|
setConstraints({
|
||||||
|
allowedKeyUsages: [],
|
||||||
|
allowedExtendedKeyUsages: [],
|
||||||
|
requiredKeyUsages: [],
|
||||||
|
requiredExtendedKeyUsages: [],
|
||||||
|
allowedSignatureAlgorithms: [],
|
||||||
|
allowedKeyAlgorithms: [],
|
||||||
|
allowedSanTypes: [
|
||||||
|
FrontendSanType.DNS,
|
||||||
|
FrontendSanType.IP,
|
||||||
|
FrontendSanType.EMAIL,
|
||||||
|
FrontendSanType.URI
|
||||||
|
],
|
||||||
|
shouldShowSanSection: true,
|
||||||
|
shouldShowSubjectSection: true
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (templateData && selectedProfile && isModalOpen) {
|
||||||
|
const newConstraints: TemplateConstraints = {
|
||||||
|
allowedSignatureAlgorithms: templateData.algorithms?.signature || [],
|
||||||
|
allowedKeyAlgorithms: templateData.algorithms?.keyAlgorithm || [],
|
||||||
|
allowedKeyUsages: [
|
||||||
|
...(templateData.keyUsages?.required || []),
|
||||||
|
...(templateData.keyUsages?.allowed || [])
|
||||||
|
],
|
||||||
|
allowedExtendedKeyUsages: [
|
||||||
|
...(templateData.extendedKeyUsages?.required || []),
|
||||||
|
...(templateData.extendedKeyUsages?.allowed || [])
|
||||||
|
],
|
||||||
|
requiredKeyUsages: templateData.keyUsages?.required || [],
|
||||||
|
requiredExtendedKeyUsages: templateData.extendedKeyUsages?.required || [],
|
||||||
|
allowedSanTypes: [],
|
||||||
|
shouldShowSanSection: true,
|
||||||
|
shouldShowSubjectSection: true
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set TTL if available
|
||||||
|
if (templateData.validity?.max) {
|
||||||
|
setValue("ttl", templateData.validity.max);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle SAN types
|
||||||
|
if (templateData.sans && templateData.sans.length > 0) {
|
||||||
|
const sanTypes: FrontendSanType[] = [];
|
||||||
|
templateData.sans.forEach((sanPolicy: any) => {
|
||||||
|
const frontendType = mapBackendSanTypeToFrontend(sanPolicy.type);
|
||||||
|
if (!sanTypes.includes(frontendType)) {
|
||||||
|
sanTypes.push(frontendType);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
newConstraints.allowedSanTypes = sanTypes;
|
||||||
|
newConstraints.shouldShowSanSection = true;
|
||||||
|
} else {
|
||||||
|
newConstraints.allowedSanTypes = [];
|
||||||
|
newConstraints.shouldShowSanSection = false;
|
||||||
|
setValue("subjectAltNames", []);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle subject section
|
||||||
|
if (templateData.subject && templateData.subject.length > 0) {
|
||||||
|
newConstraints.shouldShowSubjectSection = true;
|
||||||
|
const currentSubjectAttrs = watch("subjectAttributes");
|
||||||
|
if (!currentSubjectAttrs || currentSubjectAttrs.length === 0) {
|
||||||
|
setValue("subjectAttributes", [{ type: "common_name", value: "" }]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newConstraints.shouldShowSubjectSection = false;
|
||||||
|
setValue("subjectAttributes", undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
setConstraints(newConstraints);
|
||||||
|
|
||||||
|
// Set initial required usages
|
||||||
|
const initialKeyUsages: Record<string, boolean> = {};
|
||||||
|
const initialExtendedKeyUsages: Record<string, boolean> = {};
|
||||||
|
|
||||||
|
(templateData.keyUsages?.required || []).forEach((usage: string) => {
|
||||||
|
initialKeyUsages[usage] = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
(templateData.extendedKeyUsages?.required || []).forEach((usage: string) => {
|
||||||
|
initialExtendedKeyUsages[usage] = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
setValue("keyUsages", initialKeyUsages);
|
||||||
|
setValue("extendedKeyUsages", initialExtendedKeyUsages);
|
||||||
|
}
|
||||||
|
}, [templateData, selectedProfile, setValue, watch, isModalOpen]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
constraints,
|
||||||
|
filteredKeyUsages,
|
||||||
|
filteredExtendedKeyUsages,
|
||||||
|
availableSignatureAlgorithms,
|
||||||
|
availableKeyAlgorithms,
|
||||||
|
resetConstraints
|
||||||
|
};
|
||||||
|
};
|
||||||
+25
-19
@@ -47,7 +47,7 @@ const createSchema = z
|
|||||||
estConfig: z
|
estConfig: z
|
||||||
.object({
|
.object({
|
||||||
disableBootstrapCaValidation: z.boolean().optional(),
|
disableBootstrapCaValidation: z.boolean().optional(),
|
||||||
passphrase: z.string().min(1, "EST passphrase is required"),
|
passphraseInput: z.string().min(1, "EST passphrase is required"),
|
||||||
caChain: z.string().min(1, "EST CA chain is required").optional()
|
caChain: z.string().min(1, "EST CA chain is required").optional()
|
||||||
})
|
})
|
||||||
.refine(
|
.refine(
|
||||||
@@ -58,7 +58,8 @@ const createSchema = z
|
|||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
message: "EST CA chain is required"
|
message: "EST CA chain is required when bootstrap CA validation is enabled",
|
||||||
|
path: ["caChain"]
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.optional(),
|
.optional(),
|
||||||
@@ -106,7 +107,7 @@ const editSchema = z
|
|||||||
estConfig: z
|
estConfig: z
|
||||||
.object({
|
.object({
|
||||||
disableBootstrapCaValidation: z.boolean().optional(),
|
disableBootstrapCaValidation: z.boolean().optional(),
|
||||||
passphrase: z.string().optional(),
|
passphraseInput: z.string().optional(),
|
||||||
caChain: z.string().optional()
|
caChain: z.string().optional()
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
@@ -168,15 +169,22 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
|
|||||||
enrollmentType: profile.enrollmentType,
|
enrollmentType: profile.enrollmentType,
|
||||||
certificateAuthorityId: profile.caId,
|
certificateAuthorityId: profile.caId,
|
||||||
certificateTemplateId: profile.certificateTemplateId,
|
certificateTemplateId: profile.certificateTemplateId,
|
||||||
estConfig: {
|
estConfig:
|
||||||
disableBootstrapCaValidation: profile.estConfig?.disableBootstrapCaValidation || false,
|
profile.enrollmentType === "est"
|
||||||
passphrase: "",
|
? {
|
||||||
caChain: undefined
|
disableBootstrapCaValidation:
|
||||||
},
|
profile.estConfig?.disableBootstrapCaValidation || false,
|
||||||
apiConfig: {
|
passphraseInput: "",
|
||||||
autoRenew: profile.apiConfig?.autoRenew || false,
|
caChain: profile.estConfig?.encryptedCaChain || ""
|
||||||
autoRenewDays: profile.apiConfig?.autoRenewDays || 30
|
}
|
||||||
}
|
: undefined,
|
||||||
|
apiConfig:
|
||||||
|
profile.enrollmentType === "api"
|
||||||
|
? {
|
||||||
|
autoRenew: profile.apiConfig?.autoRenew || false,
|
||||||
|
autoRenewDays: profile.apiConfig?.autoRenewDays || 30
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
slug: "",
|
slug: "",
|
||||||
@@ -229,8 +237,8 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
|
|||||||
|
|
||||||
if (data.enrollmentType === "est" && data.estConfig) {
|
if (data.enrollmentType === "est" && data.estConfig) {
|
||||||
createData.estConfig = {
|
createData.estConfig = {
|
||||||
passphrase: data.estConfig.passphrase,
|
passphraseInput: data.estConfig.passphraseInput,
|
||||||
caChain: data.estConfig.caChain || "",
|
caChain: data.estConfig.caChain || undefined,
|
||||||
disableBootstrapCaValidation: data.estConfig.disableBootstrapCaValidation
|
disableBootstrapCaValidation: data.estConfig.disableBootstrapCaValidation
|
||||||
};
|
};
|
||||||
} else if (data.enrollmentType === "api" && data.apiConfig) {
|
} else if (data.enrollmentType === "api" && data.apiConfig) {
|
||||||
@@ -346,8 +354,7 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
|
|||||||
if (watchedEnrollmentType === "est") {
|
if (watchedEnrollmentType === "est") {
|
||||||
setValue("estConfig", {
|
setValue("estConfig", {
|
||||||
disableBootstrapCaValidation: false,
|
disableBootstrapCaValidation: false,
|
||||||
passphrase: "",
|
passphraseInput: ""
|
||||||
caChain: ""
|
|
||||||
});
|
});
|
||||||
setValue("apiConfig", undefined);
|
setValue("apiConfig", undefined);
|
||||||
} else {
|
} else {
|
||||||
@@ -391,8 +398,7 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
|
|||||||
setValue("apiConfig", undefined);
|
setValue("apiConfig", undefined);
|
||||||
setValue("estConfig", {
|
setValue("estConfig", {
|
||||||
disableBootstrapCaValidation: false,
|
disableBootstrapCaValidation: false,
|
||||||
passphrase: "",
|
passphraseInput: ""
|
||||||
caChain: ""
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setValue("estConfig", undefined);
|
setValue("estConfig", undefined);
|
||||||
@@ -444,7 +450,7 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
|
|||||||
|
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="estConfig.passphrase"
|
name="estConfig.passphraseInput"
|
||||||
render={({ field, fieldState: { error } }) => (
|
render={({ field, fieldState: { error } }) => (
|
||||||
<FormControl
|
<FormControl
|
||||||
label="EST Passphrase"
|
label="EST Passphrase"
|
||||||
|
|||||||
+1
-1
@@ -201,7 +201,7 @@ export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) =
|
|||||||
Edit Profile
|
Edit Profile
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
{canIssueCertificate && (
|
{canIssueCertificate && profile.enrollmentType === "api" && (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|||||||
+7
-6
@@ -9,8 +9,8 @@ import {
|
|||||||
ProjectPermissionPkiTemplateActions,
|
ProjectPermissionPkiTemplateActions,
|
||||||
ProjectPermissionSub
|
ProjectPermissionSub
|
||||||
} from "@app/context/ProjectPermissionContext/types";
|
} from "@app/context/ProjectPermissionContext/types";
|
||||||
import { useDeleteCertificateTemplateV2New } from "@app/hooks/api/certificateTemplates/mutations";
|
import { useDeleteCertificateTemplateV2WithPolicies } from "@app/hooks/api/certificateTemplates/mutations";
|
||||||
import { TCertificateTemplateV2New } from "@app/hooks/api/certificateTemplates/types";
|
import { TCertificateTemplateV2WithPolicies } from "@app/hooks/api/certificateTemplates/types";
|
||||||
|
|
||||||
import { CreateTemplateModal } from "./CreateTemplateModal";
|
import { CreateTemplateModal } from "./CreateTemplateModal";
|
||||||
import { TemplateList } from "./TemplateList";
|
import { TemplateList } from "./TemplateList";
|
||||||
@@ -21,9 +21,10 @@ export const CertificateTemplatesV2Tab = () => {
|
|||||||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
const [selectedTemplate, setSelectedTemplate] = useState<TCertificateTemplateV2New | null>(null);
|
const [selectedTemplate, setSelectedTemplate] =
|
||||||
|
useState<TCertificateTemplateV2WithPolicies | null>(null);
|
||||||
|
|
||||||
const deleteTemplateV2 = useDeleteCertificateTemplateV2New();
|
const deleteTemplateV2 = useDeleteCertificateTemplateV2WithPolicies();
|
||||||
|
|
||||||
const canCreateTemplate = permission.can(
|
const canCreateTemplate = permission.can(
|
||||||
ProjectPermissionPkiTemplateActions.Create,
|
ProjectPermissionPkiTemplateActions.Create,
|
||||||
@@ -34,12 +35,12 @@ export const CertificateTemplatesV2Tab = () => {
|
|||||||
setIsCreateModalOpen(true);
|
setIsCreateModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEditTemplate = (template: TCertificateTemplateV2New) => {
|
const handleEditTemplate = (template: TCertificateTemplateV2WithPolicies) => {
|
||||||
setSelectedTemplate(template);
|
setSelectedTemplate(template);
|
||||||
setIsEditModalOpen(true);
|
setIsEditModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteTemplate = (template: TCertificateTemplateV2New) => {
|
const handleDeleteTemplate = (template: TCertificateTemplateV2WithPolicies) => {
|
||||||
setSelectedTemplate(template);
|
setSelectedTemplate(template);
|
||||||
setIsDeleteModalOpen(true);
|
setIsDeleteModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|||||||
+59
-22
@@ -24,12 +24,12 @@ import {
|
|||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useProject } from "@app/context";
|
import { useProject } from "@app/context";
|
||||||
import {
|
import {
|
||||||
useCreateCertificateTemplateV2New,
|
useCreateCertificateTemplateV2WithPolicies,
|
||||||
useUpdateCertificateTemplateV2New
|
useUpdateCertificateTemplateV2WithPolicies
|
||||||
} from "@app/hooks/api/certificateTemplates/mutations";
|
} from "@app/hooks/api/certificateTemplates/mutations";
|
||||||
import {
|
import {
|
||||||
TCertificateTemplateV2New,
|
TCertificateTemplateV2Policy,
|
||||||
TCertificateTemplateV2Policy
|
TCertificateTemplateV2WithPolicies
|
||||||
} from "@app/hooks/api/certificateTemplates/types";
|
} from "@app/hooks/api/certificateTemplates/types";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -57,7 +57,7 @@ type ValidityTransform = TCertificateTemplateV2Policy["validity"];
|
|||||||
interface Props {
|
interface Props {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
template?: TCertificateTemplateV2New;
|
template?: TCertificateTemplateV2WithPolicies;
|
||||||
mode?: "create" | "edit";
|
mode?: "create" | "edit";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,12 +106,12 @@ const KEY_ALGORITHMS = [
|
|||||||
|
|
||||||
export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create" }: Props) => {
|
export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create" }: Props) => {
|
||||||
const { currentProject } = useProject();
|
const { currentProject } = useProject();
|
||||||
const createTemplate = useCreateCertificateTemplateV2New();
|
const createTemplate = useCreateCertificateTemplateV2WithPolicies();
|
||||||
const updateTemplate = useUpdateCertificateTemplateV2New();
|
const updateTemplate = useUpdateCertificateTemplateV2WithPolicies();
|
||||||
|
|
||||||
const isEdit = mode === "edit" && template;
|
const isEdit = mode === "edit" && template;
|
||||||
|
|
||||||
const convertApiToUiFormat = (templateData: TCertificateTemplateV2New): FormData => {
|
const convertApiToUiFormat = (templateData: TCertificateTemplateV2WithPolicies): FormData => {
|
||||||
const attributes: FormData["attributes"] = [];
|
const attributes: FormData["attributes"] = [];
|
||||||
if (templateData.subject && Array.isArray(templateData.subject)) {
|
if (templateData.subject && Array.isArray(templateData.subject)) {
|
||||||
templateData.subject.forEach((subj) => {
|
templateData.subject.forEach((subj) => {
|
||||||
@@ -244,12 +244,10 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
|||||||
maxDuration: { value: 365, unit: CertDurationUnit.DAYS }
|
maxDuration: { value: 365, unit: CertDurationUnit.DAYS }
|
||||||
},
|
},
|
||||||
signatureAlgorithm: {
|
signatureAlgorithm: {
|
||||||
allowedAlgorithms: [],
|
allowedAlgorithms: []
|
||||||
defaultAlgorithm: ""
|
|
||||||
},
|
},
|
||||||
keyAlgorithm: {
|
keyAlgorithm: {
|
||||||
allowedKeyTypes: [],
|
allowedKeyTypes: []
|
||||||
defaultKeyType: ""
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -275,8 +273,38 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
|||||||
optionalUsages: []
|
optionalUsages: []
|
||||||
};
|
};
|
||||||
|
|
||||||
const transformToNewApiFormat = (data: FormData) => {
|
const consolidateByType = <
|
||||||
const subject =
|
T extends { type: string; allowed?: string[]; required?: string[]; denied?: string[] }
|
||||||
|
>(
|
||||||
|
items: T[]
|
||||||
|
): T[] => {
|
||||||
|
const consolidated = new Map<string, T>();
|
||||||
|
|
||||||
|
items.forEach((item) => {
|
||||||
|
const existing = consolidated.get(item.type);
|
||||||
|
if (existing) {
|
||||||
|
const mergedItem = {
|
||||||
|
...item,
|
||||||
|
allowed: [...new Set([...(existing.allowed || []), ...(item.allowed || [])])],
|
||||||
|
required: [...new Set([...(existing.required || []), ...(item.required || [])])],
|
||||||
|
denied: [...new Set([...(existing.denied || []), ...(item.denied || [])])]
|
||||||
|
} as T;
|
||||||
|
|
||||||
|
if (mergedItem.allowed?.length === 0) delete mergedItem.allowed;
|
||||||
|
if (mergedItem.required?.length === 0) delete mergedItem.required;
|
||||||
|
if (mergedItem.denied?.length === 0) delete mergedItem.denied;
|
||||||
|
|
||||||
|
consolidated.set(item.type, mergedItem);
|
||||||
|
} else {
|
||||||
|
consolidated.set(item.type, item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Array.from(consolidated.values());
|
||||||
|
};
|
||||||
|
|
||||||
|
const transformToApiFormat = (data: FormData) => {
|
||||||
|
const subjectRaw =
|
||||||
data.attributes?.map((attr) => {
|
data.attributes?.map((attr) => {
|
||||||
const result: AttributeTransform = { type: attr.type };
|
const result: AttributeTransform = { type: attr.type };
|
||||||
|
|
||||||
@@ -289,7 +317,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
|||||||
return result;
|
return result;
|
||||||
}) || [];
|
}) || [];
|
||||||
|
|
||||||
const sans =
|
const sansRaw =
|
||||||
data.subjectAlternativeNames?.map((san) => {
|
data.subjectAlternativeNames?.map((san) => {
|
||||||
const result: SanTransform = { type: san.type };
|
const result: SanTransform = { type: san.type };
|
||||||
|
|
||||||
@@ -304,7 +332,13 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
|||||||
return result;
|
return result;
|
||||||
}) || [];
|
}) || [];
|
||||||
|
|
||||||
const keyUsages: KeyUsagesTransform = {};
|
const subject = consolidateByType(subjectRaw);
|
||||||
|
const sans = consolidateByType(sansRaw);
|
||||||
|
|
||||||
|
const keyUsages: KeyUsagesTransform = {
|
||||||
|
required: [],
|
||||||
|
allowed: []
|
||||||
|
};
|
||||||
if (data.keyUsages?.requiredUsages && data.keyUsages.requiredUsages.length > 0) {
|
if (data.keyUsages?.requiredUsages && data.keyUsages.requiredUsages.length > 0) {
|
||||||
keyUsages.required = data.keyUsages.requiredUsages;
|
keyUsages.required = data.keyUsages.requiredUsages;
|
||||||
}
|
}
|
||||||
@@ -312,7 +346,10 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
|||||||
keyUsages.allowed = data.keyUsages.optionalUsages;
|
keyUsages.allowed = data.keyUsages.optionalUsages;
|
||||||
}
|
}
|
||||||
|
|
||||||
const extendedKeyUsages: ExtendedKeyUsagesTransform = {};
|
const extendedKeyUsages: ExtendedKeyUsagesTransform = {
|
||||||
|
required: [],
|
||||||
|
allowed: []
|
||||||
|
};
|
||||||
if (
|
if (
|
||||||
data.extendedKeyUsages?.requiredUsages &&
|
data.extendedKeyUsages?.requiredUsages &&
|
||||||
data.extendedKeyUsages.requiredUsages.length > 0
|
data.extendedKeyUsages.requiredUsages.length > 0
|
||||||
@@ -364,10 +401,10 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
|||||||
description: data.description,
|
description: data.description,
|
||||||
subject,
|
subject,
|
||||||
sans,
|
sans,
|
||||||
keyUsages: Object.keys(keyUsages).length > 0 ? keyUsages : undefined,
|
keyUsages,
|
||||||
extendedKeyUsages: Object.keys(extendedKeyUsages).length > 0 ? extendedKeyUsages : undefined,
|
extendedKeyUsages,
|
||||||
algorithms: Object.keys(algorithms).length > 0 ? algorithms : undefined,
|
algorithms,
|
||||||
validity: Object.keys(validity).length > 0 ? validity : undefined
|
validity
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -391,7 +428,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const transformedData = transformToNewApiFormat(data);
|
const transformedData = transformToApiFormat(data);
|
||||||
|
|
||||||
if (isEdit) {
|
if (isEdit) {
|
||||||
const updateData = {
|
const updateData = {
|
||||||
|
|||||||
+3
-3
@@ -23,11 +23,11 @@ import {
|
|||||||
ProjectPermissionSub
|
ProjectPermissionSub
|
||||||
} from "@app/context/ProjectPermissionContext/types";
|
} from "@app/context/ProjectPermissionContext/types";
|
||||||
import { useListCertificateTemplatesV2 } from "@app/hooks/api/certificateTemplates/queries";
|
import { useListCertificateTemplatesV2 } from "@app/hooks/api/certificateTemplates/queries";
|
||||||
import { TCertificateTemplateV2New } from "@app/hooks/api/certificateTemplates/types";
|
import { TCertificateTemplateV2WithPolicies } from "@app/hooks/api/certificateTemplates/types";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onEditTemplate: (template: TCertificateTemplateV2New) => void;
|
onEditTemplate: (template: TCertificateTemplateV2WithPolicies) => void;
|
||||||
onDeleteTemplate: (template: TCertificateTemplateV2New) => void;
|
onDeleteTemplate: (template: TCertificateTemplateV2WithPolicies) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TemplateList = ({ onEditTemplate, onDeleteTemplate }: Props) => {
|
export const TemplateList = ({ onEditTemplate, onDeleteTemplate }: Props) => {
|
||||||
|
|||||||
+8
-6
@@ -40,13 +40,15 @@ export const uiValiditySchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const uiSignatureAlgorithmSchema = z.object({
|
export const uiSignatureAlgorithmSchema = z.object({
|
||||||
allowedAlgorithms: z.array(z.string()).optional(),
|
allowedAlgorithms: z
|
||||||
defaultAlgorithm: z.string().optional()
|
.array(z.string().min(1, "Algorithm cannot be empty"))
|
||||||
|
.min(1, "At least one algorithm must be selected")
|
||||||
});
|
});
|
||||||
|
|
||||||
export const uiKeyAlgorithmSchema = z.object({
|
export const uiKeyAlgorithmSchema = z.object({
|
||||||
allowedKeyTypes: z.array(z.string()).optional(),
|
allowedKeyTypes: z
|
||||||
defaultKeyType: z.string().optional()
|
.array(z.string().min(1, "Key type cannot be empty"))
|
||||||
|
.min(1, "At least one key type must be selected")
|
||||||
});
|
});
|
||||||
|
|
||||||
export const templateSchema = z.object({
|
export const templateSchema = z.object({
|
||||||
@@ -69,8 +71,8 @@ export const templateSchema = z.object({
|
|||||||
keyUsages: uiKeyUsagesSchema.optional(),
|
keyUsages: uiKeyUsagesSchema.optional(),
|
||||||
extendedKeyUsages: uiExtendedKeyUsagesSchema.optional(),
|
extendedKeyUsages: uiExtendedKeyUsagesSchema.optional(),
|
||||||
validity: uiValiditySchema.optional(),
|
validity: uiValiditySchema.optional(),
|
||||||
signatureAlgorithm: uiSignatureAlgorithmSchema.optional(),
|
signatureAlgorithm: uiSignatureAlgorithmSchema,
|
||||||
keyAlgorithm: uiKeyAlgorithmSchema.optional()
|
keyAlgorithm: uiKeyAlgorithmSchema
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TemplateFormData = z.infer<typeof templateSchema>;
|
export type TemplateFormData = z.infer<typeof templateSchema>;
|
||||||
|
|||||||
Reference in New Issue
Block a user