From 1c749c84f201ea9dce99844cb357ce1394e09f18 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Tue, 10 Sep 2024 21:42:41 +0800 Subject: [PATCH] misc: key usages setup --- .../20240910070128_add-pki-key-usages.ts | 40 +++++++ .../src/db/schemas/certificate-templates.ts | 4 +- .../routes/v1/certificate-template-router.ts | 11 +- .../certificate-template-dal.ts | 4 +- .../certificate-template-schema.ts | 3 +- .../certificate-template-service.ts | 12 +- .../certificate-template-types.ts | 3 + .../services/certificate/certificate-types.ts | 12 ++ .../hooks/api/certificateTemplates/types.ts | 5 + frontend/src/hooks/api/certificates/types.ts | 12 ++ .../components/CertificateTemplateModal.tsx | 113 ++++++++++++++++-- 11 files changed, 199 insertions(+), 20 deletions(-) create mode 100644 backend/src/db/migrations/20240910070128_add-pki-key-usages.ts diff --git a/backend/src/db/migrations/20240910070128_add-pki-key-usages.ts b/backend/src/db/migrations/20240910070128_add-pki-key-usages.ts new file mode 100644 index 000000000..3e720d27f --- /dev/null +++ b/backend/src/db/migrations/20240910070128_add-pki-key-usages.ts @@ -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 { + 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 { + 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"); + } + }); +} diff --git a/backend/src/db/schemas/certificate-templates.ts b/backend/src/db/schemas/certificate-templates.ts index 6e1989195..c332d7cf7 100644 --- a/backend/src/db/schemas/certificate-templates.ts +++ b/backend/src/db/schemas/certificate-templates.ts @@ -16,7 +16,9 @@ export const CertificateTemplatesSchema = z.object({ subjectAlternativeName: z.string(), ttl: z.string(), 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; diff --git a/backend/src/server/routes/v1/certificate-template-router.ts b/backend/src/server/routes/v1/certificate-template-router.ts index c9d2410fd..785e48674 100644 --- a/backend/src/server/routes/v1/certificate-template-router.ts +++ b/backend/src/server/routes/v1/certificate-template-router.ts @@ -7,6 +7,7 @@ import { CERTIFICATE_TEMPLATES } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; 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 { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators"; @@ -74,7 +75,12 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid ttl: z .string() .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: { 200: sanitizedCertificateTemplate @@ -130,7 +136,8 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid .string() .refine((val) => ms(val) > 0, "TTL must be a positive number") .optional() - .describe(CERTIFICATE_TEMPLATES.UPDATE.ttl) + .describe(CERTIFICATE_TEMPLATES.UPDATE.ttl), + keyUsages: z.nativeEnum(CertKeyUsage).array().optional() }), params: z.object({ certificateTemplateId: z.string().describe(CERTIFICATE_TEMPLATES.UPDATE.certificateTemplateId) diff --git a/backend/src/services/certificate-template/certificate-template-dal.ts b/backend/src/services/certificate-template/certificate-template-dal.ts index c500833d1..c4833bcc1 100644 --- a/backend/src/services/certificate-template/certificate-template-dal.ts +++ b/backend/src/services/certificate-template/certificate-template-dal.ts @@ -50,7 +50,9 @@ export const certificateTemplateDALFactory = (db: TDbClient) => { ) .first(); - return certTemplate; + if (certTemplate) { + return { ...certTemplate, keyUsages: certTemplate.keyUsages || [] }; + } } catch (error) { throw new DatabaseError({ error, name: "Get certificate template by ID" }); } diff --git a/backend/src/services/certificate-template/certificate-template-schema.ts b/backend/src/services/certificate-template/certificate-template-schema.ts index 2ce787050..be00f4132 100644 --- a/backend/src/services/certificate-template/certificate-template-schema.ts +++ b/backend/src/services/certificate-template/certificate-template-schema.ts @@ -13,6 +13,7 @@ export const sanitizedCertificateTemplate = CertificateTemplatesSchema.pick({ }).merge( z.object({ projectId: z.string(), - caName: z.string() + caName: z.string(), + keyUsages: z.string().array() }) ); diff --git a/backend/src/services/certificate-template/certificate-template-service.ts b/backend/src/services/certificate-template/certificate-template-service.ts index 2a8134093..015995677 100644 --- a/backend/src/services/certificate-template/certificate-template-service.ts +++ b/backend/src/services/certificate-template/certificate-template-service.ts @@ -57,7 +57,8 @@ export const certificateTemplateServiceFactory = ({ actorId, actorAuthMethod, actor, - actorOrgId + actorOrgId, + keyUsages }: TCreateCertTemplateDTO) => { const ca = await certificateAuthorityDAL.findById(caId); if (!ca) { @@ -86,7 +87,8 @@ export const certificateTemplateServiceFactory = ({ name, commonName, subjectAlternativeName, - ttl + ttl, + keyUsages }, tx ); @@ -113,7 +115,8 @@ export const certificateTemplateServiceFactory = ({ actorId, actorAuthMethod, actor, - actorOrgId + actorOrgId, + keyUsages }: TUpdateCertTemplateDTO) => { const certTemplate = await certificateTemplateDAL.getById(id); if (!certTemplate) { @@ -153,7 +156,8 @@ export const certificateTemplateServiceFactory = ({ commonName, subjectAlternativeName, name, - ttl + ttl, + keyUsages }, tx ); diff --git a/backend/src/services/certificate-template/certificate-template-types.ts b/backend/src/services/certificate-template/certificate-template-types.ts index 74281e7b8..3a0050a86 100644 --- a/backend/src/services/certificate-template/certificate-template-types.ts +++ b/backend/src/services/certificate-template/certificate-template-types.ts @@ -1,4 +1,5 @@ import { TProjectPermission } from "@app/lib/types"; +import { CertKeyUsage } from "@app/services/certificate/certificate-types"; export type TCreateCertTemplateDTO = { caId: string; @@ -7,6 +8,7 @@ export type TCreateCertTemplateDTO = { commonName: string; subjectAlternativeName: string; ttl: string; + keyUsages: CertKeyUsage[]; } & Omit; export type TUpdateCertTemplateDTO = { @@ -17,6 +19,7 @@ export type TUpdateCertTemplateDTO = { commonName?: string; subjectAlternativeName?: string; ttl?: string; + keyUsages?: CertKeyUsage[]; } & Omit; export type TGetCertTemplateDTO = { diff --git a/backend/src/services/certificate/certificate-types.ts b/backend/src/services/certificate/certificate-types.ts index 93f72afe3..4b8b49cd1 100644 --- a/backend/src/services/certificate/certificate-types.ts +++ b/backend/src/services/certificate/certificate-types.ts @@ -12,6 +12,18 @@ export enum CertKeyAlgorithm { 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 { UNSPECIFIED = "UNSPECIFIED", KEY_COMPROMISE = "KEY_COMPROMISE", diff --git a/frontend/src/hooks/api/certificateTemplates/types.ts b/frontend/src/hooks/api/certificateTemplates/types.ts index c7c69c3a2..2bd07846e 100644 --- a/frontend/src/hooks/api/certificateTemplates/types.ts +++ b/frontend/src/hooks/api/certificateTemplates/types.ts @@ -1,3 +1,5 @@ +import { CertKeyUsage } from "../certificates/types"; + export type TCertificateTemplate = { id: string; caId: string; @@ -8,6 +10,7 @@ export type TCertificateTemplate = { commonName: string; subjectAlternativeName: string; ttl: string; + keyUsages: CertKeyUsage[]; }; export type TCreateCertificateTemplateDTO = { @@ -18,6 +21,7 @@ export type TCreateCertificateTemplateDTO = { subjectAlternativeName: string; ttl: string; projectId: string; + keyUsages: CertKeyUsage[]; }; export type TUpdateCertificateTemplateDTO = { @@ -29,6 +33,7 @@ export type TUpdateCertificateTemplateDTO = { subjectAlternativeName?: string; ttl?: string; projectId: string; + keyUsages?: CertKeyUsage[]; }; export type TDeleteCertificateTemplateDTO = { diff --git a/frontend/src/hooks/api/certificates/types.ts b/frontend/src/hooks/api/certificates/types.ts index 8057998aa..87f973887 100644 --- a/frontend/src/hooks/api/certificates/types.ts +++ b/frontend/src/hooks/api/certificates/types.ts @@ -23,3 +23,15 @@ export type TRevokeCertDTO = { serialNumber: 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" +} diff --git a/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateTemplateModal.tsx b/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateTemplateModal.tsx index 1910a5f7b..f7a58dc70 100644 --- a/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateTemplateModal.tsx +++ b/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateTemplateModal.tsx @@ -7,7 +7,12 @@ import { z } from "zod"; import { createNotification } from "@app/components/notifications"; import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, Button, + Checkbox, FormControl, FormLabel, Input, @@ -25,8 +30,10 @@ import { useGetCertTemplate, useListWorkspaceCas, useListWorkspacePkiCollections, - useUpdateCertTemplate} from "@app/hooks/api"; + useUpdateCertTemplate +} from "@app/hooks/api"; import { caTypeToNameMap } from "@app/hooks/api/ca/constants"; +import { CertKeyUsage } from "@app/hooks/api/certificates/types"; import { UsePopUpState } from "@app/hooks/usePopUp"; const validateTemplateRegexField = z @@ -45,7 +52,18 @@ const schema = z.object({ name: z.string().min(1), commonName: 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; @@ -59,11 +77,23 @@ type Props = { ) => 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) => { const { currentWorkspace } = useWorkspace(); - + const { data: ca } = useGetCaById(caId); - + const { data: certTemplate } = useGetCertTemplate( (popUp?.certificateTemplate?.data as { id: string })?.id || "" ); @@ -86,7 +116,13 @@ export const CertificateTemplateModal = ({ popUp, handlePopUpToggle, caId }: Pro reset, formState: { isSubmitting } } = useForm({ - resolver: zodResolver(schema) + resolver: zodResolver(schema), + defaultValues: { + keyUsages: { + [CertKeyUsage.DIGITAL_SIGNATURE]: true, + [CertKeyUsage.KEY_ENCIPHERMENT]: true + } + } }); useEffect(() => { @@ -97,14 +133,19 @@ export const CertificateTemplateModal = ({ popUp, handlePopUpToggle, caId }: Pro commonName: certTemplate.commonName, subjectAlternativeName: certTemplate.subjectAlternativeName, collectionId: certTemplate.pkiCollectionId ?? undefined, - ttl: certTemplate.ttl + ttl: certTemplate.ttl, + keyUsages: Object.fromEntries(certTemplate.keyUsages.map((name) => [name, true])) }); } else { reset({ caId, name: "", commonName: "", - ttl: "" + ttl: "", + keyUsages: { + [CertKeyUsage.DIGITAL_SIGNATURE]: true, + [CertKeyUsage.KEY_ENCIPHERMENT]: true + } }); } }, [certTemplate, ca]); @@ -114,7 +155,8 @@ export const CertificateTemplateModal = ({ popUp, handlePopUpToggle, caId }: Pro name, commonName, subjectAlternativeName, - ttl + ttl, + keyUsages }: FormData) => { if (!currentWorkspace?.id) { return; @@ -130,7 +172,10 @@ export const CertificateTemplateModal = ({ popUp, handlePopUpToggle, caId }: Pro name, commonName, subjectAlternativeName, - ttl + ttl, + keyUsages: Object.entries(keyUsages) + .filter(([, value]) => value) + .map(([key]) => key as CertKeyUsage) }); createNotification({ @@ -145,7 +190,10 @@ export const CertificateTemplateModal = ({ popUp, handlePopUpToggle, caId }: Pro name, commonName, subjectAlternativeName, - ttl + ttl, + keyUsages: Object.entries(keyUsages) + .filter(([, value]) => value) + .map(([key]) => key as CertKeyUsage) }); createNotification({ @@ -332,7 +380,50 @@ export const CertificateTemplateModal = ({ popUp, handlePopUpToggle, caId }: Pro )} /> -
+ + + +
Key Usages
+
+ + { + return ( + +
+ {KEY_USAGES_OPTIONS.map(({ label, value: optionValue }) => { + return ( + { + onChange({ + ...value, + [optionValue]: state + }); + }} + > + {label} + + ); + })} +
+
+ ); + }} + /> +
+
+
+