misc: key usages setup

This commit is contained in:
Sheen Capadngan
2024-09-10 21:42:41 +08:00
parent a8fd83652d
commit 1c749c84f2
11 changed files with 199 additions and 20 deletions
@@ -0,0 +1,40 @@
import { Knex } from "knex";
import { CertKeyUsage } from "@app/services/certificate/certificate-types";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasKeyUsagesCol = await knex.schema.hasColumn(TableName.CertificateTemplate, "keyUsages");
const hasExtendedKeyUsagesCol = await knex.schema.hasColumn(TableName.CertificateTemplate, "extendedKeyUsages");
await knex.schema.alterTable(TableName.CertificateTemplate, (tb) => {
if (!hasKeyUsagesCol) {
tb.specificType("keyUsages", "text[]");
}
if (!hasExtendedKeyUsagesCol) {
tb.specificType("extendedKeyUsages", "text[]");
}
});
if (!hasKeyUsagesCol) {
await knex(TableName.CertificateTemplate).update({
keyUsages: [CertKeyUsage.DIGITAL_SIGNATURE, CertKeyUsage.KEY_ENCIPHERMENT]
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasKeyUsagesCol = await knex.schema.hasColumn(TableName.CertificateTemplate, "keyUsages");
const hasExtendedKeyUsagesCol = await knex.schema.hasColumn(TableName.CertificateTemplate, "extendedKeyUsages");
await knex.schema.alterTable(TableName.CertificateTemplate, (t) => {
if (hasKeyUsagesCol) {
t.dropColumn("keyUsages");
}
if (hasExtendedKeyUsagesCol) {
t.dropColumn("extendedKeyUsages");
}
});
}
@@ -16,7 +16,9 @@ export const CertificateTemplatesSchema = z.object({
subjectAlternativeName: z.string(), subjectAlternativeName: z.string(),
ttl: z.string(), ttl: z.string(),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date() updatedAt: z.date(),
keyUsages: z.string().array().nullable().optional(),
extendedKeyUsages: z.string().array().nullable().optional()
}); });
export type TCertificateTemplates = z.infer<typeof CertificateTemplatesSchema>; export type TCertificateTemplates = z.infer<typeof CertificateTemplatesSchema>;
@@ -7,6 +7,7 @@ import { CERTIFICATE_TEMPLATES } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
import { CertKeyUsage } from "@app/services/certificate/certificate-types";
import { sanitizedCertificateTemplate } from "@app/services/certificate-template/certificate-template-schema"; import { sanitizedCertificateTemplate } from "@app/services/certificate-template/certificate-template-schema";
import { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators"; import { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators";
@@ -74,7 +75,12 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid
ttl: z ttl: z
.string() .string()
.refine((val) => ms(val) > 0, "TTL must be a positive number") .refine((val) => ms(val) > 0, "TTL must be a positive number")
.describe(CERTIFICATE_TEMPLATES.CREATE.ttl) .describe(CERTIFICATE_TEMPLATES.CREATE.ttl),
keyUsages: z
.nativeEnum(CertKeyUsage)
.array()
.optional()
.default([CertKeyUsage.DIGITAL_SIGNATURE, CertKeyUsage.KEY_ENCIPHERMENT])
}), }),
response: { response: {
200: sanitizedCertificateTemplate 200: sanitizedCertificateTemplate
@@ -130,7 +136,8 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid
.string() .string()
.refine((val) => ms(val) > 0, "TTL must be a positive number") .refine((val) => ms(val) > 0, "TTL must be a positive number")
.optional() .optional()
.describe(CERTIFICATE_TEMPLATES.UPDATE.ttl) .describe(CERTIFICATE_TEMPLATES.UPDATE.ttl),
keyUsages: z.nativeEnum(CertKeyUsage).array().optional()
}), }),
params: z.object({ params: z.object({
certificateTemplateId: z.string().describe(CERTIFICATE_TEMPLATES.UPDATE.certificateTemplateId) certificateTemplateId: z.string().describe(CERTIFICATE_TEMPLATES.UPDATE.certificateTemplateId)
@@ -50,7 +50,9 @@ export const certificateTemplateDALFactory = (db: TDbClient) => {
) )
.first(); .first();
return certTemplate; if (certTemplate) {
return { ...certTemplate, keyUsages: certTemplate.keyUsages || [] };
}
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "Get certificate template by ID" }); throw new DatabaseError({ error, name: "Get certificate template by ID" });
} }
@@ -13,6 +13,7 @@ export const sanitizedCertificateTemplate = CertificateTemplatesSchema.pick({
}).merge( }).merge(
z.object({ z.object({
projectId: z.string(), projectId: z.string(),
caName: z.string() caName: z.string(),
keyUsages: z.string().array()
}) })
); );
@@ -57,7 +57,8 @@ export const certificateTemplateServiceFactory = ({
actorId, actorId,
actorAuthMethod, actorAuthMethod,
actor, actor,
actorOrgId actorOrgId,
keyUsages
}: TCreateCertTemplateDTO) => { }: TCreateCertTemplateDTO) => {
const ca = await certificateAuthorityDAL.findById(caId); const ca = await certificateAuthorityDAL.findById(caId);
if (!ca) { if (!ca) {
@@ -86,7 +87,8 @@ export const certificateTemplateServiceFactory = ({
name, name,
commonName, commonName,
subjectAlternativeName, subjectAlternativeName,
ttl ttl,
keyUsages
}, },
tx tx
); );
@@ -113,7 +115,8 @@ export const certificateTemplateServiceFactory = ({
actorId, actorId,
actorAuthMethod, actorAuthMethod,
actor, actor,
actorOrgId actorOrgId,
keyUsages
}: TUpdateCertTemplateDTO) => { }: TUpdateCertTemplateDTO) => {
const certTemplate = await certificateTemplateDAL.getById(id); const certTemplate = await certificateTemplateDAL.getById(id);
if (!certTemplate) { if (!certTemplate) {
@@ -153,7 +156,8 @@ export const certificateTemplateServiceFactory = ({
commonName, commonName,
subjectAlternativeName, subjectAlternativeName,
name, name,
ttl ttl,
keyUsages
}, },
tx tx
); );
@@ -1,4 +1,5 @@
import { TProjectPermission } from "@app/lib/types"; import { TProjectPermission } from "@app/lib/types";
import { CertKeyUsage } from "@app/services/certificate/certificate-types";
export type TCreateCertTemplateDTO = { export type TCreateCertTemplateDTO = {
caId: string; caId: string;
@@ -7,6 +8,7 @@ export type TCreateCertTemplateDTO = {
commonName: string; commonName: string;
subjectAlternativeName: string; subjectAlternativeName: string;
ttl: string; ttl: string;
keyUsages: CertKeyUsage[];
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TUpdateCertTemplateDTO = { export type TUpdateCertTemplateDTO = {
@@ -17,6 +19,7 @@ export type TUpdateCertTemplateDTO = {
commonName?: string; commonName?: string;
subjectAlternativeName?: string; subjectAlternativeName?: string;
ttl?: string; ttl?: string;
keyUsages?: CertKeyUsage[];
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TGetCertTemplateDTO = { export type TGetCertTemplateDTO = {
@@ -12,6 +12,18 @@ export enum CertKeyAlgorithm {
ECDSA_P384 = "EC_secp384r1" ECDSA_P384 = "EC_secp384r1"
} }
export enum CertKeyUsage {
DIGITAL_SIGNATURE = "digitalSignature",
KEY_ENCIPHERMENT = "keyEncipherment",
NON_REPUDIATION = "nonRepudiation",
DATA_ENCIPHERMENT = "dataEncipherment",
KEY_AGREEMENT = "keyAgreement",
KEY_CERT_SIGN = "keyCertSign",
CRL_SIGN = "cRLSign",
ENCIPHER_ONLY = "encipherOnly",
DECIPHER_ONLY = "decipherOnly"
}
export enum CrlReason { export enum CrlReason {
UNSPECIFIED = "UNSPECIFIED", UNSPECIFIED = "UNSPECIFIED",
KEY_COMPROMISE = "KEY_COMPROMISE", KEY_COMPROMISE = "KEY_COMPROMISE",
@@ -1,3 +1,5 @@
import { CertKeyUsage } from "../certificates/types";
export type TCertificateTemplate = { export type TCertificateTemplate = {
id: string; id: string;
caId: string; caId: string;
@@ -8,6 +10,7 @@ export type TCertificateTemplate = {
commonName: string; commonName: string;
subjectAlternativeName: string; subjectAlternativeName: string;
ttl: string; ttl: string;
keyUsages: CertKeyUsage[];
}; };
export type TCreateCertificateTemplateDTO = { export type TCreateCertificateTemplateDTO = {
@@ -18,6 +21,7 @@ export type TCreateCertificateTemplateDTO = {
subjectAlternativeName: string; subjectAlternativeName: string;
ttl: string; ttl: string;
projectId: string; projectId: string;
keyUsages: CertKeyUsage[];
}; };
export type TUpdateCertificateTemplateDTO = { export type TUpdateCertificateTemplateDTO = {
@@ -29,6 +33,7 @@ export type TUpdateCertificateTemplateDTO = {
subjectAlternativeName?: string; subjectAlternativeName?: string;
ttl?: string; ttl?: string;
projectId: string; projectId: string;
keyUsages?: CertKeyUsage[];
}; };
export type TDeleteCertificateTemplateDTO = { export type TDeleteCertificateTemplateDTO = {
@@ -23,3 +23,15 @@ export type TRevokeCertDTO = {
serialNumber: string; serialNumber: string;
revocationReason: string; revocationReason: string;
}; };
export enum CertKeyUsage {
DIGITAL_SIGNATURE = "digitalSignature",
KEY_ENCIPHERMENT = "keyEncipherment",
NON_REPUDIATION = "nonRepudiation",
DATA_ENCIPHERMENT = "dataEncipherment",
KEY_AGREEMENT = "keyAgreement",
KEY_CERT_SIGN = "keyCertSign",
CRL_SIGN = "cRLSign",
ENCIPHER_ONLY = "encipherOnly",
DECIPHER_ONLY = "decipherOnly"
}
@@ -7,7 +7,12 @@ import { z } from "zod";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
Button, Button,
Checkbox,
FormControl, FormControl,
FormLabel, FormLabel,
Input, Input,
@@ -25,8 +30,10 @@ import {
useGetCertTemplate, useGetCertTemplate,
useListWorkspaceCas, useListWorkspaceCas,
useListWorkspacePkiCollections, useListWorkspacePkiCollections,
useUpdateCertTemplate} from "@app/hooks/api"; useUpdateCertTemplate
} from "@app/hooks/api";
import { caTypeToNameMap } from "@app/hooks/api/ca/constants"; import { caTypeToNameMap } from "@app/hooks/api/ca/constants";
import { CertKeyUsage } from "@app/hooks/api/certificates/types";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
const validateTemplateRegexField = z const validateTemplateRegexField = z
@@ -45,7 +52,18 @@ const schema = z.object({
name: z.string().min(1), name: z.string().min(1),
commonName: validateTemplateRegexField, commonName: validateTemplateRegexField,
subjectAlternativeName: validateTemplateRegexField, subjectAlternativeName: validateTemplateRegexField,
ttl: z.string().trim().min(1) ttl: z.string().trim().min(1),
keyUsages: z.object({
[CertKeyUsage.DIGITAL_SIGNATURE]: z.boolean().optional(),
[CertKeyUsage.KEY_ENCIPHERMENT]: z.boolean().optional(),
[CertKeyUsage.NON_REPUDIATION]: z.boolean().optional(),
[CertKeyUsage.DATA_ENCIPHERMENT]: z.boolean().optional(),
[CertKeyUsage.KEY_AGREEMENT]: z.boolean().optional(),
[CertKeyUsage.KEY_CERT_SIGN]: z.boolean().optional(),
[CertKeyUsage.CRL_SIGN]: z.boolean().optional(),
[CertKeyUsage.ENCIPHER_ONLY]: z.boolean().optional(),
[CertKeyUsage.DECIPHER_ONLY]: z.boolean().optional()
})
}); });
export type FormData = z.infer<typeof schema>; export type FormData = z.infer<typeof schema>;
@@ -59,6 +77,18 @@ type Props = {
) => void; ) => void;
}; };
const KEY_USAGES_OPTIONS = [
{ value: CertKeyUsage.DIGITAL_SIGNATURE, label: "Digital Signature" },
{ value: CertKeyUsage.KEY_ENCIPHERMENT, label: "Key Encipherment" },
{ value: CertKeyUsage.NON_REPUDIATION, label: "Non Repudiation" },
{ value: CertKeyUsage.DATA_ENCIPHERMENT, label: "Data Encipherment" },
{ value: CertKeyUsage.KEY_AGREEMENT, label: "Key Agreement" },
{ value: CertKeyUsage.KEY_CERT_SIGN, label: "Key Certification Sign" },
{ value: CertKeyUsage.CRL_SIGN, label: "CRL Sign" },
{ value: CertKeyUsage.ENCIPHER_ONLY, label: "Encipher Only" },
{ value: CertKeyUsage.DECIPHER_ONLY, label: "Decipher Only" }
] as const;
export const CertificateTemplateModal = ({ popUp, handlePopUpToggle, caId }: Props) => { export const CertificateTemplateModal = ({ popUp, handlePopUpToggle, caId }: Props) => {
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
@@ -86,7 +116,13 @@ export const CertificateTemplateModal = ({ popUp, handlePopUpToggle, caId }: Pro
reset, reset,
formState: { isSubmitting } formState: { isSubmitting }
} = useForm<FormData>({ } = useForm<FormData>({
resolver: zodResolver(schema) resolver: zodResolver(schema),
defaultValues: {
keyUsages: {
[CertKeyUsage.DIGITAL_SIGNATURE]: true,
[CertKeyUsage.KEY_ENCIPHERMENT]: true
}
}
}); });
useEffect(() => { useEffect(() => {
@@ -97,14 +133,19 @@ export const CertificateTemplateModal = ({ popUp, handlePopUpToggle, caId }: Pro
commonName: certTemplate.commonName, commonName: certTemplate.commonName,
subjectAlternativeName: certTemplate.subjectAlternativeName, subjectAlternativeName: certTemplate.subjectAlternativeName,
collectionId: certTemplate.pkiCollectionId ?? undefined, collectionId: certTemplate.pkiCollectionId ?? undefined,
ttl: certTemplate.ttl ttl: certTemplate.ttl,
keyUsages: Object.fromEntries(certTemplate.keyUsages.map((name) => [name, true]))
}); });
} else { } else {
reset({ reset({
caId, caId,
name: "", name: "",
commonName: "", commonName: "",
ttl: "" ttl: "",
keyUsages: {
[CertKeyUsage.DIGITAL_SIGNATURE]: true,
[CertKeyUsage.KEY_ENCIPHERMENT]: true
}
}); });
} }
}, [certTemplate, ca]); }, [certTemplate, ca]);
@@ -114,7 +155,8 @@ export const CertificateTemplateModal = ({ popUp, handlePopUpToggle, caId }: Pro
name, name,
commonName, commonName,
subjectAlternativeName, subjectAlternativeName,
ttl ttl,
keyUsages
}: FormData) => { }: FormData) => {
if (!currentWorkspace?.id) { if (!currentWorkspace?.id) {
return; return;
@@ -130,7 +172,10 @@ export const CertificateTemplateModal = ({ popUp, handlePopUpToggle, caId }: Pro
name, name,
commonName, commonName,
subjectAlternativeName, subjectAlternativeName,
ttl ttl,
keyUsages: Object.entries(keyUsages)
.filter(([, value]) => value)
.map(([key]) => key as CertKeyUsage)
}); });
createNotification({ createNotification({
@@ -145,7 +190,10 @@ export const CertificateTemplateModal = ({ popUp, handlePopUpToggle, caId }: Pro
name, name,
commonName, commonName,
subjectAlternativeName, subjectAlternativeName,
ttl ttl,
keyUsages: Object.entries(keyUsages)
.filter(([, value]) => value)
.map(([key]) => key as CertKeyUsage)
}); });
createNotification({ createNotification({
@@ -332,7 +380,50 @@ export const CertificateTemplateModal = ({ popUp, handlePopUpToggle, caId }: Pro
</FormControl> </FormControl>
)} )}
/> />
<div className="flex items-center"> <Accordion type="single" collapsible className="w-full">
<AccordionItem value="key-usages" className="data-[state=open]:border-none">
<AccordionTrigger className="h-fit flex-none pl-1 text-sm">
<div className="order-1 ml-3">Key Usages</div>
</AccordionTrigger>
<AccordionContent>
<Controller
control={control}
name="keyUsages"
render={({ field: { onChange, value }, fieldState: { error } }) => {
return (
<FormControl
label="Key Usages"
errorText={error?.message}
isError={Boolean(error)}
>
<div className="mt-2 mb-7 grid grid-cols-2 gap-2">
{KEY_USAGES_OPTIONS.map(({ label, value: optionValue }) => {
return (
<Checkbox
id={optionValue}
key={optionValue}
className="data-[state=checked]:bg-primary"
isChecked={value[optionValue]}
onCheckedChange={(state) => {
onChange({
...value,
[optionValue]: state
});
}}
>
{label}
</Checkbox>
);
})}
</div>
</FormControl>
);
}}
/>
</AccordionContent>
</AccordionItem>
</Accordion>
<div className="mt-4 flex items-center">
<Button <Button
className="mr-4" className="mr-4"
size="sm" size="sm"