PKI revamp, last changes and improvements on API and UI

This commit is contained in:
Carlos Monastyrski
2025-10-16 12:29:12 -03:00
parent 42800fdfe5
commit 5684127ce0
28 changed files with 2482 additions and 2257 deletions
@@ -165,7 +165,7 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
if (isEdit) {
const updateData: any = {
profileId: profile.id,
name: data.slug,
slug: data.slug,
description: data.description
};
@@ -240,7 +240,7 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="your-profile-name" isDisabled={Boolean(isEdit)} />
<Input {...field} placeholder="your-profile-name" />
</FormControl>
)}
/>
@@ -319,7 +319,7 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
>
{certificateTemplates.map((template) => (
<SelectItem key={template.id} value={template.id}>
{template.slug}
{template.name}
</SelectItem>
))}
</Select>
@@ -376,17 +376,17 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
name="estConfig.disableBootstrapCaValidation"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<div className="border-mineshaft-600 bg-mineshaft-900 flex items-center gap-3 rounded-md border p-4">
<div className="flex items-center gap-3 rounded-md border border-mineshaft-600 bg-mineshaft-900 p-4">
<Checkbox
id="disableBootstrapCaValidation"
isChecked={value}
onCheckedChange={onChange}
/>
<div className="space-y-1">
<span className="text-mineshaft-100 text-sm font-medium">
<span className="text-sm font-medium text-mineshaft-100">
Disable Bootstrap CA Validation
</span>
<p className="text-bunker-300 text-xs">
<p className="text-xs text-bunker-300">
Skip CA certificate validation during EST bootstrap phase
</p>
</div>
@@ -433,7 +433,7 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
rows={6}
className="w-full font-mono text-xs"
/>
<p className="text-bunker-400 text-xs">
<p className="text-xs text-bunker-400">
Paste the complete CA certificate chain in PEM format
</p>
</div>
@@ -1,7 +1,17 @@
/* eslint-disable no-nested-ternary */
import { faCheck, faCircleInfo, faCopy, faEdit, faEllipsis, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
import { useCallback } from "react";
import {
faCheck,
faCircleInfo,
faCopy,
faEdit,
faEllipsis,
faPlus,
faTrash
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import {
Badge,
DropdownMenu,
@@ -18,12 +28,10 @@ import {
ProjectPermissionCertificateProfileActions,
ProjectPermissionSub
} from "@app/context/ProjectPermissionContext/types";
import { usePopUp, useToggle } from "@app/hooks";
import { useGetCaById } from "@app/hooks/api/ca/queries";
import { TCertificateProfile } from "@app/hooks/api/certificateProfiles";
import { useGetCertificateTemplateV2ById } from "@app/hooks/api/certificateTemplates/queries";
import { usePopUp, useToggle } from "@app/hooks";
import { createNotification } from "@app/components/notifications";
import { useCallback } from "react";
import { CertificateIssuanceModal } from "@app/pages/cert-manager/CertificatesPage/components/CertificateIssuanceModal";
interface Props {
@@ -37,9 +45,7 @@ export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) =
const { data: caData } = useGetCaById(profile.caId);
const { popUp, handlePopUpToggle } = usePopUp([
"certificateIssuance"
] as const);
const { popUp, handlePopUpToggle } = usePopUp(["certificateIssuance"] as const);
const [isIdCopied, setIsIdCopied] = useToggle(false);
@@ -100,15 +106,15 @@ export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) =
)}
</div>
</Td>
<Td className="text-center">{getEnrollmentTypeBadge(profile.enrollmentType)}</Td>
<Td className="text-center">
<Td className="text-start">{getEnrollmentTypeBadge(profile.enrollmentType)}</Td>
<Td className="text-start">
<span className="text-sm text-mineshaft-300">
{caData?.friendlyName || caData?.commonName || profile.caId}
</span>
</Td>
<Td>
<span className="text-sm text-mineshaft-300">
{templateData?.slug || profile.certificateTemplateId}
{templateData?.name || profile.certificateTemplateId}
</span>
</Td>
<Td>
@@ -176,19 +182,17 @@ export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) =
Edit Profile
</DropdownMenuItem>
)}
{
canIssueCertificate && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
handlePopUpToggle("certificateIssuance");
}}
icon={<FontAwesomeIcon icon={faPlus} />}
>
Issue Certificate
</DropdownMenuItem>
)
}
{canIssueCertificate && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
handlePopUpToggle("certificateIssuance");
}}
icon={<FontAwesomeIcon icon={faPlus} />}
>
Issue Certificate
</DropdownMenuItem>
)}
{canDeleteProfile && (
<DropdownMenuItem
onClick={(e) => {
@@ -202,7 +206,11 @@ export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) =
)}
</DropdownMenuContent>
</DropdownMenu>
<CertificateIssuanceModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} profileId={profile.id}/>
<CertificateIssuanceModal
popUp={popUp}
handlePopUpToggle={handlePopUpToggle}
profileId={profile.id}
/>
</Td>
</Tr>
);
@@ -54,7 +54,7 @@ export const CertificateTemplatesV2Tab = () => {
setIsDeleteModalOpen(false);
setSelectedTemplate(null);
createNotification({
text: `Certificate template "${selectedTemplate.slug}" deleted successfully`,
text: `Certificate template "${selectedTemplate.name}" deleted successfully`,
type: "success"
});
} catch (error: any) {
@@ -103,12 +103,12 @@ export const CertificateTemplatesV2Tab = () => {
<DeleteActionModal
isOpen={isDeleteModalOpen}
title={`Delete Certificate Template ${selectedTemplate.slug}?`}
title={`Delete Certificate Template ${selectedTemplate.name}?`}
onChange={(isOpen) => {
setIsDeleteModalOpen(isOpen);
if (!isOpen) setSelectedTemplate(null);
}}
deleteKey={selectedTemplate.slug}
deleteKey={selectedTemplate.name}
onDeleteApproved={handleDeleteConfirm}
/>
</>
@@ -75,61 +75,64 @@ export const TemplateList = ({ onEditTemplate, onDeleteTemplate }: Props) => {
</Td>
</Tr>
)}
{!isLoading && templates && templates.length > 0 && templates.map((template) => (
<Tr
key={template.id}
className="h-10 transition-colors duration-100 hover:bg-mineshaft-700"
>
<Td>
<div className="flex items-center gap-2">
<div className="font-medium">{template.slug}</div>
{template.description && (
<Tooltip content={template.description}>
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
</Tooltip>
)}
</div>
</Td>
<Td>
<span className="text-sm text-bunker-300">{formatDate(template.createdAt)}</span>
</Td>
<Td className="text-right">
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
<Tooltip content="More options">
<FontAwesomeIcon size="lg" icon={faEllipsis} />
{!isLoading &&
templates &&
templates.length > 0 &&
templates.map((template) => (
<Tr
key={template.id}
className="h-10 transition-colors duration-100 hover:bg-mineshaft-700"
>
<Td>
<div className="flex items-center gap-2">
<div className="font-medium">{template.name}</div>
{template.description && (
<Tooltip content={template.description}>
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
</Tooltip>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
{canEditTemplate && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onEditTemplate(template);
}}
icon={<FontAwesomeIcon icon={faEdit} />}
>
Edit Template
</DropdownMenuItem>
)}
{canDeleteTemplate && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onDeleteTemplate(template);
}}
icon={<FontAwesomeIcon icon={faTrash} />}
>
Delete Template
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
</Td>
</Tr>
))}
</div>
</Td>
<Td>
<span className="text-sm text-bunker-300">{formatDate(template.createdAt)}</span>
</Td>
<Td className="text-right">
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
<Tooltip content="More options">
<FontAwesomeIcon size="lg" icon={faEllipsis} />
</Tooltip>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
{canEditTemplate && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onEditTemplate(template);
}}
icon={<FontAwesomeIcon icon={faEdit} />}
>
Edit Template
</DropdownMenuItem>
)}
{canDeleteTemplate && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onDeleteTemplate(template);
}}
icon={<FontAwesomeIcon icon={faTrash} />}
>
Delete Template
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
</Td>
</Tr>
))}
</TBody>
</Table>
</TableContainer>
@@ -26,10 +26,16 @@ export enum CertExtendedKeyUsageType {
TIME_STAMPING = "time_stamping"
}
export enum CertIncludeType {
MANDATORY = "mandatory",
OPTIONAL = "optional",
PROHIBIT = "prohibit"
export enum CertAttributeRule {
ALLOW = "allow",
DENY = "deny",
REQUIRE = "require"
}
export enum CertSanEffect {
ALLOW = "allow",
DENY = "deny",
REQUIRE = "require"
}
export enum CertDurationUnit {
@@ -39,7 +45,9 @@ export enum CertDurationUnit {
}
export enum CertSubjectAttributeType {
COMMON_NAME = "common_name"
COMMON_NAME = "common_name",
ORGANIZATION = "organization",
COUNTRY = "country"
}
export const formatSANType = (type: CertSubjectAlternativeNameType): string => {
@@ -104,61 +112,117 @@ export const formatExtendedKeyUsage = (usage: CertExtendedKeyUsageType): string
export const formatSubjectAttributeType = (type: CertSubjectAttributeType): string => {
switch (type) {
case CertSubjectAttributeType.COMMON_NAME:
return "Common Name";
return "Common Name (CN)";
case CertSubjectAttributeType.ORGANIZATION:
return "Organization";
case CertSubjectAttributeType.COUNTRY:
return "Country";
default:
return type;
}
};
export const formatIncludeType = (include: CertIncludeType): string => {
switch (include) {
case CertIncludeType.MANDATORY:
return "Mandatory";
case CertIncludeType.OPTIONAL:
return "Optional";
case CertIncludeType.PROHIBIT:
return "Prohibit";
export const formatAttributeRule = (rule: CertAttributeRule): string => {
switch (rule) {
case CertAttributeRule.ALLOW:
return "Allow";
case CertAttributeRule.DENY:
return "Deny";
case CertAttributeRule.REQUIRE:
return "Require";
default:
return include;
return rule;
}
};
export const mapLegacySANTypeToStandard = (type: string): CertSubjectAlternativeNameType => {
switch (type) {
case "dns":
case "dns_name":
return CertSubjectAlternativeNameType.DNS_NAME;
case "ip":
case "ip_address":
return CertSubjectAlternativeNameType.IP_ADDRESS;
case "email":
return CertSubjectAlternativeNameType.EMAIL;
case "uri":
case "url":
return CertSubjectAlternativeNameType.URI;
export const formatSanEffect = (effect: CertSanEffect): string => {
switch (effect) {
case CertSanEffect.ALLOW:
return "Allow";
case CertSanEffect.DENY:
return "Deny";
case CertSanEffect.REQUIRE:
return "Require";
default:
throw new Error(`Unknown SAN type: ${type}`);
return effect;
}
};
export const mapSANTypeToLegacy = (type: CertSubjectAlternativeNameType): string => {
switch (type) {
case CertSubjectAlternativeNameType.DNS_NAME:
return "dns";
case CertSubjectAlternativeNameType.IP_ADDRESS:
return "ip";
case CertSubjectAlternativeNameType.EMAIL:
return "email";
case CertSubjectAlternativeNameType.URI:
return "uri";
default:
return type;
}
};
export const SAN_TYPE_OPTIONS = Object.values(CertSubjectAlternativeNameType);
export const KEY_USAGE_OPTIONS = Object.values(CertKeyUsageType);
export const EXTENDED_KEY_USAGE_OPTIONS = Object.values(CertExtendedKeyUsageType);
export const INCLUDE_TYPE_OPTIONS = Object.values(CertIncludeType);
export const DURATION_UNIT_OPTIONS = Object.values(CertDurationUnit);
export const SUBJECT_ATTRIBUTE_TYPE_OPTIONS = Object.values(CertSubjectAttributeType);
export const SUBJECT_ATTRIBUTE_TYPE_OPTIONS = Object.values(CertSubjectAttributeType);
export const ATTRIBUTE_RULE_OPTIONS = Object.values(CertAttributeRule);
export const SAN_EFFECT_OPTIONS = Object.values(CertSanEffect);
export const SUBJECT_ATTRIBUTE_INCLUDE_OPTIONS = ["optional", "prohibit"] as const;
export const SAN_INCLUDE_OPTIONS = ["mandatory", "optional", "prohibit"] as const;
export const USAGE_STATES = {
REQUIRED: "required",
OPTIONAL: "optional"
} as const;
export type UsageState = typeof USAGE_STATES[keyof typeof USAGE_STATES] | undefined;
export const TEMPLATE_SIGNATURE_ALGORITHMS = [
"SHA256-RSA",
"SHA384-RSA",
"SHA512-RSA",
"SHA256-ECDSA",
"SHA384-ECDSA",
"SHA512-ECDSA"
] as const;
export const TEMPLATE_KEY_ALGORITHMS = [
"RSA-2048",
"RSA-3072",
"RSA-4096",
"ECDSA-P256",
"ECDSA-P384"
] as const;
// API format algorithm constants
export const API_SIGNATURE_ALGORITHMS = [
"RSA-SHA256",
"RSA-SHA384",
"RSA-SHA512",
"ECDSA-SHA256",
"ECDSA-SHA384",
"ECDSA-SHA512"
] as const;
export const API_KEY_ALGORITHMS = [
"RSA_2048",
"RSA_3072",
"RSA_4096",
"EC_prime256v1",
"EC_secp384r1"
] as const;
// Mapping functions between template and API formats
export const mapTemplateSignatureAlgorithmToApi = (templateFormat: string): string => {
const mapping: Record<string, string> = {
"SHA256-RSA": "RSA-SHA256",
"SHA384-RSA": "RSA-SHA384",
"SHA512-RSA": "RSA-SHA512",
"SHA256-ECDSA": "ECDSA-SHA256",
"SHA384-ECDSA": "ECDSA-SHA384",
"SHA512-ECDSA": "ECDSA-SHA512"
};
return mapping[templateFormat] || templateFormat;
};
export const mapTemplateKeyAlgorithmToApi = (templateFormat: string): string => {
const mapping: Record<string, string> = {
"RSA-2048": "RSA_2048",
"RSA-3072": "RSA_3072",
"RSA-4096": "RSA_4096",
"ECDSA-P256": "EC_prime256v1",
"ECDSA-P384": "EC_secp384r1"
};
return mapping[templateFormat] || templateFormat;
};
@@ -1,152 +1,159 @@
import { Checkbox } from "@app/components/v2";
import React from "react";
import { Select, SelectItem } from "@app/components/v2";
import {
CertExtendedKeyUsageType,
CertKeyUsageType,
EXTENDED_KEY_USAGE_OPTIONS,
formatExtendedKeyUsage,
formatKeyUsage,
EXTENDED_KEY_USAGE_OPTIONS,
KEY_USAGE_OPTIONS
} from "./certificate-constants";
type UsageState = "mandatory" | "optional" | undefined;
type KeyUsagePolicy = "allow" | "require" | "deny" | "none";
type ThreeStateCheckboxProps = {
value: UsageState;
onChange: (newValue: UsageState) => void;
label: string;
id: string;
};
const ThreeStateCheckbox = ({ value, onChange, label, id }: ThreeStateCheckboxProps) => {
const handleClick = () => {
if (value === undefined) {
onChange("optional");
} else if (value === "optional") {
onChange("mandatory");
} else {
onChange(undefined);
}
interface KeyUsagesSectionProps {
watchedKeyUsages: {
requiredUsages: CertKeyUsageType[];
optionalUsages: CertKeyUsageType[];
};
const getCheckboxState = () => {
if (value) return true;
return false;
watchedExtendedKeyUsages: {
requiredUsages: CertExtendedKeyUsageType[];
optionalUsages: CertExtendedKeyUsageType[];
};
onKeyUsagesChange: (usages: {
requiredUsages: CertKeyUsageType[];
optionalUsages: CertKeyUsageType[];
}) => void;
onExtendedKeyUsagesChange: (usages: {
requiredUsages: CertExtendedKeyUsageType[];
optionalUsages: CertExtendedKeyUsageType[];
}) => void;
}
const getIndeterminateState = () => {
return value === "optional";
};
const getStateLabel = () => {
if (value === "mandatory") return " (Mandatory)";
if (value === "optional") return " (Optional)";
return "";
};
return (
<div className="flex items-center space-x-3">
<Checkbox
id={id}
isChecked={getCheckboxState()}
isIndeterminate={getIndeterminateState()}
onCheckedChange={handleClick}
/>
<label
htmlFor={id}
className="text-mineshaft-200 cursor-pointer text-sm font-medium"
>
{label}
{value && (
<span className="text-mineshaft-400 text-xs ml-1">
{getStateLabel()}
</span>
)}
</label>
</div>
);
};
type KeyUsagesSectionProps = {
watchedKeyUsages?: { requiredUsages?: string[]; optionalUsages?: string[] };
watchedExtendedKeyUsages?: { requiredUsages?: string[]; optionalUsages?: string[] };
onKeyUsagesChange: (usages: { requiredUsages: string[]; optionalUsages: string[] }) => void;
onExtendedKeyUsagesChange: (usages: { requiredUsages: string[]; optionalUsages: string[] }) => void;
};
export const KeyUsagesSection = ({
watchedKeyUsages = { requiredUsages: [], optionalUsages: [] },
watchedExtendedKeyUsages = { requiredUsages: [], optionalUsages: [] },
export const KeyUsagesSection: React.FC<KeyUsagesSectionProps> = ({
watchedKeyUsages,
watchedExtendedKeyUsages,
onKeyUsagesChange,
onExtendedKeyUsagesChange
}: KeyUsagesSectionProps) => {
const getUsageState = (usage: string, data: { requiredUsages?: string[]; optionalUsages?: string[] }): UsageState => {
if (data.requiredUsages?.includes(usage)) return "mandatory";
if (data.optionalUsages?.includes(usage)) return "optional";
return undefined;
}) => {
const getKeyUsagePolicy = (usage: CertKeyUsageType): KeyUsagePolicy => {
if (watchedKeyUsages.requiredUsages.includes(usage)) return "require";
if (watchedKeyUsages.optionalUsages.includes(usage)) return "allow";
return "deny";
};
const handleKeyUsageChange = (usage: CertKeyUsageType, newState: UsageState) => {
const currentRequired = watchedKeyUsages.requiredUsages || [];
const currentOptional = watchedKeyUsages.optionalUsages || [];
const getExtendedKeyUsagePolicy = (usage: CertExtendedKeyUsageType): KeyUsagePolicy => {
if (watchedExtendedKeyUsages.requiredUsages.includes(usage)) return "require";
if (watchedExtendedKeyUsages.optionalUsages.includes(usage)) return "allow";
return "deny";
};
let newRequired = currentRequired.filter(u => u !== usage);
let newOptional = currentOptional.filter(u => u !== usage);
const handleKeyUsagePolicyChange = (usage: CertKeyUsageType, policy: KeyUsagePolicy) => {
const newRequired = watchedKeyUsages.requiredUsages.filter((u) => u !== usage);
const newOptional = watchedKeyUsages.optionalUsages.filter((u) => u !== usage);
if (newState === "mandatory") {
newRequired = [...newRequired, usage];
} else if (newState === "optional") {
newOptional = [...newOptional, usage];
if (policy === "require") {
newRequired.push(usage);
} else if (policy === "allow") {
newOptional.push(usage);
}
onKeyUsagesChange({ requiredUsages: newRequired, optionalUsages: newOptional });
onKeyUsagesChange({
requiredUsages: newRequired,
optionalUsages: newOptional
});
};
const handleExtendedKeyUsageChange = (usage: CertExtendedKeyUsageType, newState: UsageState) => {
const currentRequired = watchedExtendedKeyUsages.requiredUsages || [];
const currentOptional = watchedExtendedKeyUsages.optionalUsages || [];
const handleExtendedKeyUsagePolicyChange = (
usage: CertExtendedKeyUsageType,
policy: KeyUsagePolicy
) => {
const newRequired = watchedExtendedKeyUsages.requiredUsages.filter((u) => u !== usage);
const newOptional = watchedExtendedKeyUsages.optionalUsages.filter((u) => u !== usage);
let newRequired = currentRequired.filter(u => u !== usage);
let newOptional = currentOptional.filter(u => u !== usage);
if (newState === "mandatory") {
newRequired = [...newRequired, usage];
} else if (newState === "optional") {
newOptional = [...newOptional, usage];
if (policy === "require") {
newRequired.push(usage);
} else if (policy === "allow") {
newOptional.push(usage);
}
onExtendedKeyUsagesChange({ requiredUsages: newRequired, optionalUsages: newOptional });
onExtendedKeyUsagesChange({
requiredUsages: newRequired,
optionalUsages: newOptional
});
};
const keyUsagePolicyOptions = [
{ value: "deny", label: "Deny" },
{ value: "allow", label: "Allow" },
{ value: "require", label: "Require" }
];
const extendedKeyUsagePolicyOptions = [
{ value: "deny", label: "Deny" },
{ value: "allow", label: "Allow" },
{ value: "require", label: "Require" }
];
return (
<div className="space-y-6">
<div className="space-y-3">
<h3 className="text-mineshaft-200 text-sm font-medium">Key Usages</h3>
<div className="grid grid-cols-2 gap-2 pl-2">
{KEY_USAGE_OPTIONS.map((usage) => (
<ThreeStateCheckbox
key={usage}
id={`key-usage-${usage}`}
label={formatKeyUsage(usage)}
value={getUsageState(usage, watchedKeyUsages)}
onChange={(newState) => handleKeyUsageChange(usage, newState)}
/>
))}
<div className="space-y-8">
{/* Key Usages */}
<div className="space-y-4">
<h4 className="text-sm font-medium text-bunker-200">Key Usages</h4>
<div className="grid grid-cols-2 gap-4">
{KEY_USAGE_OPTIONS.map((usage) => {
const policy = getKeyUsagePolicy(usage);
return (
<div key={usage} className="flex items-center justify-between">
<span className="text-sm text-bunker-200">{formatKeyUsage(usage)}</span>
<Select
value={policy}
onValueChange={(value) =>
handleKeyUsagePolicyChange(usage, value as KeyUsagePolicy)
}
className="w-32"
>
{keyUsagePolicyOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</Select>
</div>
);
})}
</div>
</div>
<div className="space-y-3">
<h3 className="text-mineshaft-200 text-sm font-medium">Extended Key Usages</h3>
<div className="grid grid-cols-2 gap-2 pl-2">
{EXTENDED_KEY_USAGE_OPTIONS.map((usage) => (
<ThreeStateCheckbox
key={usage}
id={`ext-key-usage-${usage}`}
label={formatExtendedKeyUsage(usage)}
value={getUsageState(usage, watchedExtendedKeyUsages)}
onChange={(newState) => handleExtendedKeyUsageChange(usage, newState)}
/>
))}
{/* Extended Key Usages */}
<div className="space-y-4">
<h4 className="text-sm font-medium text-bunker-200">Extended Key Usages</h4>
<div className="grid grid-cols-2 gap-4">
{EXTENDED_KEY_USAGE_OPTIONS.map((usage) => {
const policy = getExtendedKeyUsagePolicy(usage);
return (
<div key={usage} className="flex items-center justify-between">
<span className="text-sm text-bunker-200">{formatExtendedKeyUsage(usage)}</span>
<Select
value={policy}
onValueChange={(value) =>
handleExtendedKeyUsagePolicyChange(usage, value as KeyUsagePolicy)
}
className="w-32"
>
{extendedKeyUsagePolicyOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</Select>
</div>
);
})}
</div>
</div>
</div>
@@ -3,93 +3,121 @@ import { z } from "zod";
import {
CertDurationUnit,
CertExtendedKeyUsageType,
CertIncludeType,
CertKeyUsageType,
CertSubjectAlternativeNameType,
CertSubjectAttributeType
CertSubjectAttributeType,
SUBJECT_ATTRIBUTE_INCLUDE_OPTIONS,
SAN_INCLUDE_OPTIONS
} from "./certificate-constants";
export const attributeSchema = z.object({
export const uiAttributeSchema = z.object({
type: z.nativeEnum(CertSubjectAttributeType),
include: z.nativeEnum(CertIncludeType),
value: z.array(z.string().min(1, "Value cannot be empty")).optional()
include: z.enum(SUBJECT_ATTRIBUTE_INCLUDE_OPTIONS),
value: z.array(z.string().min(1, "Value cannot be empty"))
});
export const sanSchema = z.object({
export const uiSanSchema = z.object({
type: z.nativeEnum(CertSubjectAlternativeNameType),
include: z.nativeEnum(CertIncludeType),
value: z.array(z.string().min(1, "Value cannot be empty")).optional()
include: z.enum(SAN_INCLUDE_OPTIONS),
value: z.array(z.string().min(1, "Value cannot be empty"))
});
export const uiKeyUsagesSchema = z.object({
requiredUsages: z.array(z.nativeEnum(CertKeyUsageType)),
optionalUsages: z.array(z.nativeEnum(CertKeyUsageType))
});
export const uiExtendedKeyUsagesSchema = z.object({
requiredUsages: z.array(z.nativeEnum(CertExtendedKeyUsageType)),
optionalUsages: z.array(z.nativeEnum(CertExtendedKeyUsageType))
});
export const uiValiditySchema = z.object({
maxDuration: z.object({
value: z.number().min(1, "Duration must be at least 1"),
unit: z.nativeEnum(CertDurationUnit)
})
});
export const uiSignatureAlgorithmSchema = z.object({
allowedAlgorithms: z.array(z.string()).optional(),
defaultAlgorithm: z.string().optional()
});
export const uiKeyAlgorithmSchema = z.object({
allowedKeyTypes: z.array(z.string()).optional(),
defaultKeyType: z.string().optional()
});
export const templateSchema = z.object({
slug: z.string().trim().min(1, "Template name is required"),
description: z.string().optional(),
attributes: z.array(attributeSchema).optional().refine((attributes) => {
if (!attributes) return true;
attributes: z.array(uiAttributeSchema).optional(),
subjectAlternativeNames: z.array(uiSanSchema).optional(),
keyUsages: uiKeyUsagesSchema.optional(),
extendedKeyUsages: uiExtendedKeyUsagesSchema.optional(),
validity: uiValiditySchema.optional(),
signatureAlgorithm: uiSignatureAlgorithmSchema.optional(),
keyAlgorithm: uiKeyAlgorithmSchema.optional()
});
const attributesByType = attributes.reduce((acc, attr) => {
if (!acc[attr.type]) acc[attr.type] = [];
acc[attr.type].push(attr);
return acc;
}, {} as Record<string, typeof attributes>);
export type TemplateFormData = z.infer<typeof templateSchema>;
for (const [, attrs] of Object.entries(attributesByType)) {
const mandatoryAttrs = attrs.filter(attr => attr.include === 'mandatory');
export const apiSubjectSchema = z
.object({
type: z.nativeEnum(CertSubjectAttributeType),
allowed: z.array(z.string().min(1, "Value cannot be empty")).optional(),
required: z.array(z.string().min(1, "Value cannot be empty")).optional(),
denied: z.array(z.string().min(1, "Value cannot be empty")).optional()
})
.refine((data) => data.allowed || data.required || data.denied, {
message: "At least one allowed, required, or denied value must be provided"
});
if (mandatoryAttrs.length > 1) {
return false;
}
export const apiSanSchema = z
.object({
type: z.nativeEnum(CertSubjectAlternativeNameType),
allowed: z.array(z.string().min(1, "Value cannot be empty")).optional(),
required: z.array(z.string().min(1, "Value cannot be empty")).optional(),
denied: z.array(z.string().min(1, "Value cannot be empty")).optional()
})
.refine((data) => data.allowed || data.required || data.denied, {
message: "At least one allowed, required, or denied value must be provided"
});
if (mandatoryAttrs.length === 1 && attrs.length > 1) {
return false;
}
}
return true;
}, {
message: "Attribute validation failed: when a mandatory value exists, no other values are allowed for that attribute type"
}),
export const apiTemplateSchema = z.object({
name: z.string().trim().min(1, "Template name is required"),
description: z.string().optional(),
subject: z.array(apiSubjectSchema).optional(),
sans: z.array(apiSanSchema).optional(),
keyUsages: z
.object({
requiredUsages: z.array(z.nativeEnum(CertKeyUsageType)).optional(),
optionalUsages: z.array(z.nativeEnum(CertKeyUsageType)).optional()
allowed: z.array(z.nativeEnum(CertKeyUsageType)).optional(),
required: z.array(z.nativeEnum(CertKeyUsageType)).optional(),
denied: z.array(z.nativeEnum(CertKeyUsageType)).optional()
})
.optional(),
extendedKeyUsages: z
.object({
requiredUsages: z.array(z.nativeEnum(CertExtendedKeyUsageType)).optional(),
optionalUsages: z.array(z.nativeEnum(CertExtendedKeyUsageType)).optional()
allowed: z.array(z.nativeEnum(CertExtendedKeyUsageType)).optional(),
required: z.array(z.nativeEnum(CertExtendedKeyUsageType)).optional(),
denied: z.array(z.nativeEnum(CertExtendedKeyUsageType)).optional()
})
.optional(),
algorithms: z
.object({
signature: z.array(z.string()).optional(),
keyAlgorithm: z.array(z.string()).optional()
})
.optional(),
subjectAlternativeNames: z.array(sanSchema).optional(),
validity: z
.object({
maxDuration: z
.object({
value: z.number().min(1, "Duration must be at least 1"),
unit: z.nativeEnum(CertDurationUnit)
})
.optional(),
minDuration: z
.object({
value: z.number().min(1, "Duration must be at least 1"),
unit: z.nativeEnum(CertDurationUnit)
})
max: z
.string()
.regex(/^\d+[dhmy]$/, "Must be in format like '365d', '12m', '1y', or '24h'")
.optional()
})
.optional(),
signatureAlgorithm: z
.object({
allowedAlgorithms: z.array(z.string()).optional(),
defaultAlgorithm: z.string().optional()
})
.optional(),
keyAlgorithm: z
.object({
allowedKeyTypes: z.array(z.string()).optional(),
defaultKeyType: z.string().optional()
})
.optional()
});
export type TemplateFormData = z.infer<typeof templateSchema>;
export type ApiTemplateFormData = z.infer<typeof apiTemplateSchema>;
@@ -2,7 +2,9 @@ import {
CertExtendedKeyUsageType,
CertKeyUsageType,
formatExtendedKeyUsage,
formatKeyUsage
formatKeyUsage,
USAGE_STATES,
UsageState
} from "./certificate-constants";
export const formatUsageName = (usage: string): string => {
@@ -14,6 +16,7 @@ export const formatUsageName = (usage: string): string => {
return formatExtendedKeyUsage(usage as CertExtendedKeyUsageType);
}
} catch {
// Handle any errors in type checking
}
return usage.replace(/_/g, " ");
};
@@ -22,15 +25,15 @@ export const getUsageState = (
usage: CertKeyUsageType | CertExtendedKeyUsageType,
requiredUsages: (CertKeyUsageType | CertExtendedKeyUsageType)[],
optionalUsages: (CertKeyUsageType | CertExtendedKeyUsageType)[]
): "required" | "optional" | undefined => {
if (requiredUsages.includes(usage)) return "required";
if (optionalUsages.includes(usage)) return "optional";
): UsageState => {
if (requiredUsages.includes(usage)) return USAGE_STATES.REQUIRED;
if (optionalUsages.includes(usage)) return USAGE_STATES.OPTIONAL;
return undefined;
};
export const toggleUsageState = (
usage: CertKeyUsageType | CertExtendedKeyUsageType,
newState: "required" | "optional" | undefined,
newState: UsageState,
currentRequiredUsages: (CertKeyUsageType | CertExtendedKeyUsageType)[],
currentOptionalUsages: (CertKeyUsageType | CertExtendedKeyUsageType)[],
toggleRequired: (usage: CertKeyUsageType | CertExtendedKeyUsageType) => void,
@@ -39,10 +42,10 @@ export const toggleUsageState = (
const isRequired = currentRequiredUsages.includes(usage);
const isOptional = currentOptionalUsages.includes(usage);
if (newState === "required") {
if (newState === USAGE_STATES.REQUIRED) {
if (isOptional) toggleOptional(usage);
if (!isRequired) toggleRequired(usage);
} else if (newState === "optional") {
} else if (newState === USAGE_STATES.OPTIONAL) {
if (isRequired) toggleRequired(usage);
if (!isOptional) toggleOptional(usage);
} else {