From 4e6b289e1bbdc446fdb766f3666930148b4a559a Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Wed, 11 Sep 2024 01:57:16 +0800 Subject: [PATCH] misc: integrated custom key usages for issue-cert endpoint --- .../20240910070128_add-pki-key-usages.ts | 45 +++++++++ backend/src/db/schemas/certificates.ts | 4 +- .../routes/v1/certificate-authority-router.ts | 5 +- .../server/routes/v1/certificate-router.ts | 5 +- .../certificate-authority-service.ts | 33 ++++++- .../certificate-authority-types.ts | 3 +- .../certificate-template-dal.ts | 4 +- .../certificate-template-schema.ts | 6 +- frontend/src/hooks/api/ca/types.ts | 2 + frontend/src/hooks/api/certificates/types.ts | 13 +++ .../components/CertificateModal.tsx | 94 +++++++++++++++++-- .../components/CertificateTemplateModal.tsx | 14 +-- 12 files changed, 192 insertions(+), 36 deletions(-) diff --git a/backend/src/db/migrations/20240910070128_add-pki-key-usages.ts b/backend/src/db/migrations/20240910070128_add-pki-key-usages.ts index 3e720d27f..93bfa59db 100644 --- a/backend/src/db/migrations/20240910070128_add-pki-key-usages.ts +++ b/backend/src/db/migrations/20240910070128_add-pki-key-usages.ts @@ -5,6 +5,7 @@ import { CertKeyUsage } from "@app/services/certificate/certificate-types"; import { TableName } from "../schemas"; export async function up(knex: Knex): Promise { + // Certificate template const hasKeyUsagesCol = await knex.schema.hasColumn(TableName.CertificateTemplate, "keyUsages"); const hasExtendedKeyUsagesCol = await knex.schema.hasColumn(TableName.CertificateTemplate, "extendedKeyUsages"); @@ -23,9 +24,41 @@ export async function up(knex: Knex): Promise { keyUsages: [CertKeyUsage.DIGITAL_SIGNATURE, CertKeyUsage.KEY_ENCIPHERMENT] }); } + + if (!hasExtendedKeyUsagesCol) { + await knex(TableName.CertificateTemplate).update({ + extendedKeyUsages: [] + }); + } + + // Certificate + const doesCertTableHaveKeyUsages = await knex.schema.hasColumn(TableName.Certificate, "keyUsages"); + const doesCertTableHaveExtendedKeyUsages = await knex.schema.hasColumn(TableName.Certificate, "extendedKeyUsages"); + await knex.schema.alterTable(TableName.Certificate, (tb) => { + if (!doesCertTableHaveKeyUsages) { + tb.specificType("keyUsages", "text[]"); + } + + if (!doesCertTableHaveExtendedKeyUsages) { + tb.specificType("extendedKeyUsages", "text[]"); + } + }); + + if (!doesCertTableHaveKeyUsages) { + await knex(TableName.Certificate).update({ + keyUsages: [CertKeyUsage.DIGITAL_SIGNATURE, CertKeyUsage.KEY_ENCIPHERMENT] + }); + } + + if (!doesCertTableHaveExtendedKeyUsages) { + await knex(TableName.Certificate).update({ + extendedKeyUsages: [] + }); + } } export async function down(knex: Knex): Promise { + // Certificate Template const hasKeyUsagesCol = await knex.schema.hasColumn(TableName.CertificateTemplate, "keyUsages"); const hasExtendedKeyUsagesCol = await knex.schema.hasColumn(TableName.CertificateTemplate, "extendedKeyUsages"); @@ -37,4 +70,16 @@ export async function down(knex: Knex): Promise { t.dropColumn("extendedKeyUsages"); } }); + + // Certificate + const doesCertTableHaveKeyUsages = await knex.schema.hasColumn(TableName.Certificate, "keyUsages"); + const doesCertTableHaveExtendedKeyUsages = await knex.schema.hasColumn(TableName.Certificate, "extendedKeyUsages"); + await knex.schema.alterTable(TableName.Certificate, (t) => { + if (doesCertTableHaveKeyUsages) { + t.dropColumn("keyUsages"); + } + if (doesCertTableHaveExtendedKeyUsages) { + t.dropColumn("extendedKeyUsages"); + } + }); } diff --git a/backend/src/db/schemas/certificates.ts b/backend/src/db/schemas/certificates.ts index 7ef56e505..bde35002f 100644 --- a/backend/src/db/schemas/certificates.ts +++ b/backend/src/db/schemas/certificates.ts @@ -22,7 +22,9 @@ export const CertificatesSchema = z.object({ revocationReason: z.number().nullable().optional(), altNames: z.string().default("").nullable().optional(), caCertId: z.string().uuid(), - certificateTemplateId: z.string().uuid().nullable().optional() + certificateTemplateId: z.string().uuid().nullable().optional(), + keyUsages: z.string().array().nullable().optional(), + extendedKeyUsages: z.string().array().nullable().optional() }); export type TCertificates = z.infer; diff --git a/backend/src/server/routes/v1/certificate-authority-router.ts b/backend/src/server/routes/v1/certificate-authority-router.ts index 77ee70e57..f85b37eee 100644 --- a/backend/src/server/routes/v1/certificate-authority-router.ts +++ b/backend/src/server/routes/v1/certificate-authority-router.ts @@ -7,7 +7,7 @@ import { CERTIFICATE_AUTHORITIES } 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 { CertKeyAlgorithm } from "@app/services/certificate/certificate-types"; +import { CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types"; import { CaRenewalType, CaStatus, CaType } from "@app/services/certificate-authority/certificate-authority-types"; import { validateAltNamesField, @@ -573,7 +573,8 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { .refine((val) => ms(val) > 0, "TTL must be a positive number") .describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.ttl), notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.notBefore), - notAfter: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.notAfter) + notAfter: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.notAfter), + keyUsages: z.nativeEnum(CertKeyUsage).array().optional() }) .refine( (data) => { diff --git a/backend/src/server/routes/v1/certificate-router.ts b/backend/src/server/routes/v1/certificate-router.ts index 91ae85982..ba8b6150e 100644 --- a/backend/src/server/routes/v1/certificate-router.ts +++ b/backend/src/server/routes/v1/certificate-router.ts @@ -7,7 +7,7 @@ import { CERTIFICATE_AUTHORITIES, CERTIFICATES } 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 { CrlReason } from "@app/services/certificate/certificate-types"; +import { CertKeyUsage, CrlReason } from "@app/services/certificate/certificate-types"; import { validateAltNamesField, validateCaDateField @@ -86,7 +86,8 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { .refine((val) => ms(val) > 0, "TTL must be a positive number") .describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.ttl), notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.notBefore), - notAfter: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.notAfter) + notAfter: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.notAfter), + keyUsages: z.nativeEnum(CertKeyUsage).array().optional() }) .refine( (data) => { diff --git a/backend/src/services/certificate-authority/certificate-authority-service.ts b/backend/src/services/certificate-authority/certificate-authority-service.ts index 1c2a5a689..2a6354d35 100644 --- a/backend/src/services/certificate-authority/certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/certificate-authority-service.ts @@ -19,7 +19,7 @@ import { TProjectDALFactory } from "@app/services/project/project-dal"; import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns"; import { TCertificateAuthorityCrlDALFactory } from "../../ee/services/certificate-authority-crl/certificate-authority-crl-dal"; -import { CertKeyAlgorithm, CertStatus } from "../certificate/certificate-types"; +import { CertKeyAlgorithm, CertKeyUsage, CertStatus } from "../certificate/certificate-types"; import { TCertificateTemplateDALFactory } from "../certificate-template/certificate-template-dal"; import { validateCertificateDetailsAgainstTemplate } from "../certificate-template/certificate-template-fns"; import { TCertificateAuthorityCertDALFactory } from "./certificate-authority-cert-dal"; @@ -1052,7 +1052,8 @@ export const certificateAuthorityServiceFactory = ({ actorId, actorAuthMethod, actor, - actorOrgId + actorOrgId, + keyUsages }: TIssueCertFromCaDTO) => { let ca: TCertificateAuthorities | undefined; let certificateTemplate: TCertificateTemplates | undefined; @@ -1170,14 +1171,37 @@ export const certificateAuthorityServiceFactory = ({ const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/pki/crl/${caCrl.id}`; + let selectedKeyUsages: CertKeyUsage[] = keyUsages ?? []; + if (keyUsages === undefined && !certificateTemplate) { + selectedKeyUsages = [CertKeyUsage.DIGITAL_SIGNATURE, CertKeyUsage.KEY_ENCIPHERMENT]; + } + + if (keyUsages === undefined && certificateTemplate) { + selectedKeyUsages = (certificateTemplate.keyUsages ?? []) as CertKeyUsage[]; + } + + if (keyUsages?.length && certificateTemplate) { + const validKeyUsages = certificateTemplate.keyUsages || []; + if (keyUsages.some((keyUsage) => !validKeyUsages.includes(keyUsage))) { + throw new BadRequestError({ + message: "Invalid key usage value for certificate" + }); + } + selectedKeyUsages = keyUsages; + } + const extensions: x509.Extension[] = [ - new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment, true), new x509.BasicConstraintsExtension(false), new x509.CRLDistributionPointsExtension([distributionPointUrl]), await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false), await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey) ]; + const keyUsagesBitValue = selectedKeyUsages.reduce((accum, keyUsage) => accum | x509.KeyUsageFlags[keyUsage], 0); + if (keyUsagesBitValue) { + extensions.push(new x509.KeyUsagesExtension(keyUsagesBitValue, true)); + } + let altNamesArray: { type: "email" | "dns"; value: string; @@ -1259,7 +1283,8 @@ export const certificateAuthorityServiceFactory = ({ altNames, serialNumber, notBefore: notBeforeDate, - notAfter: notAfterDate + notAfter: notAfterDate, + keyUsages: selectedKeyUsages }, tx ); diff --git a/backend/src/services/certificate-authority/certificate-authority-types.ts b/backend/src/services/certificate-authority/certificate-authority-types.ts index 5876c0057..124c6aea2 100644 --- a/backend/src/services/certificate-authority/certificate-authority-types.ts +++ b/backend/src/services/certificate-authority/certificate-authority-types.ts @@ -4,7 +4,7 @@ import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TCertificateAuthorityCrlDALFactory } from "../../ee/services/certificate-authority-crl/certificate-authority-crl-dal"; -import { CertKeyAlgorithm } from "../certificate/certificate-types"; +import { CertKeyAlgorithm, CertKeyUsage } from "../certificate/certificate-types"; import { TCertificateAuthorityCertDALFactory } from "./certificate-authority-cert-dal"; import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal"; import { TCertificateAuthoritySecretDALFactory } from "./certificate-authority-secret-dal"; @@ -97,6 +97,7 @@ export type TIssueCertFromCaDTO = { ttl: string; notBefore?: string; notAfter?: string; + keyUsages?: CertKeyUsage[]; } & Omit; export type TSignCertFromCaDTO = diff --git a/backend/src/services/certificate-template/certificate-template-dal.ts b/backend/src/services/certificate-template/certificate-template-dal.ts index c4833bcc1..c500833d1 100644 --- a/backend/src/services/certificate-template/certificate-template-dal.ts +++ b/backend/src/services/certificate-template/certificate-template-dal.ts @@ -50,9 +50,7 @@ export const certificateTemplateDALFactory = (db: TDbClient) => { ) .first(); - if (certTemplate) { - return { ...certTemplate, keyUsages: certTemplate.keyUsages || [] }; - } + return certTemplate; } 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 be00f4132..a6c464252 100644 --- a/backend/src/services/certificate-template/certificate-template-schema.ts +++ b/backend/src/services/certificate-template/certificate-template-schema.ts @@ -9,11 +9,11 @@ export const sanitizedCertificateTemplate = CertificateTemplatesSchema.pick({ commonName: true, subjectAlternativeName: true, pkiCollectionId: true, - ttl: true + ttl: true, + keyUsages: true }).merge( z.object({ projectId: z.string(), - caName: z.string(), - keyUsages: z.string().array() + caName: z.string() }) ); diff --git a/frontend/src/hooks/api/ca/types.ts b/frontend/src/hooks/api/ca/types.ts index 52e363fa7..af9ffef81 100644 --- a/frontend/src/hooks/api/ca/types.ts +++ b/frontend/src/hooks/api/ca/types.ts @@ -1,4 +1,5 @@ import { CertKeyAlgorithm } from "../certificates/enums"; +import { CertKeyUsage } from "../certificates/types"; import { CaRenewalType, CaStatus, CaType } from "./enums"; export type TCertificateAuthority = { @@ -91,6 +92,7 @@ export type TCreateCertificateDTO = { ttl: string; // string compatible with ms notBefore?: string; notAfter?: string; + keyUsages: CertKeyUsage[]; }; export type TCreateCertificateResponse = { diff --git a/frontend/src/hooks/api/certificates/types.ts b/frontend/src/hooks/api/certificates/types.ts index 87f973887..61186b6fa 100644 --- a/frontend/src/hooks/api/certificates/types.ts +++ b/frontend/src/hooks/api/certificates/types.ts @@ -11,6 +11,7 @@ export type TCertificate = { serialNumber: string; notBefore: string; notAfter: string; + keyUsages: CertKeyUsage[]; }; export type TDeleteCertDTO = { @@ -35,3 +36,15 @@ export enum CertKeyUsage { ENCIPHER_ONLY = "encipherOnly", DECIPHER_ONLY = "decipherOnly" } + +export 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: "Certificate Sign" }, + { value: CertKeyUsage.CRL_SIGN, label: "CRL Sign" }, + { value: CertKeyUsage.ENCIPHER_ONLY, label: "Encipher Only" }, + { value: CertKeyUsage.DECIPHER_ONLY, label: "Decipher Only" } +] as const; diff --git a/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateModal.tsx b/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateModal.tsx index a5520c3c3..52cf4e0e2 100644 --- a/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateModal.tsx +++ b/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateModal.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, @@ -28,6 +33,7 @@ import { useListWorkspacePkiCollections } from "@app/hooks/api"; import { caTypeToNameMap } from "@app/hooks/api/ca/constants"; +import { CertKeyUsage, KEY_USAGES_OPTIONS } from "@app/hooks/api/certificates/types"; import { UsePopUpState } from "@app/hooks/usePopUp"; import { CertificateContent } from "./CertificateContent"; @@ -39,7 +45,18 @@ const schema = z.object({ friendlyName: z.string(), commonName: z.string().trim().min(1), altNames: z.string(), - ttl: z.string().trim() + ttl: z.string().trim(), + 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; @@ -88,7 +105,13 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => { setValue, watch } = useForm({ - resolver: zodResolver(schema) + resolver: zodResolver(schema), + defaultValues: { + keyUsages: { + [CertKeyUsage.DIGITAL_SIGNATURE]: true, + [CertKeyUsage.KEY_ENCIPHERMENT]: true + } + } }); const selectedCertTemplateId = watch("certificateTemplateId"); @@ -107,7 +130,8 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => { commonName: cert.commonName, altNames: cert.altNames, certificateTemplateId: cert.certificateTemplateId ?? CERT_TEMPLATE_NONE_VALUE, - ttl: "" + ttl: "", + keyUsages: Object.fromEntries(cert.keyUsages.map((name) => [name, true])) }); } else { reset({ @@ -116,7 +140,11 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => { commonName: "", altNames: "", ttl: "", - certificateTemplateId: CERT_TEMPLATE_NONE_VALUE + certificateTemplateId: CERT_TEMPLATE_NONE_VALUE, + keyUsages: { + [CertKeyUsage.DIGITAL_SIGNATURE]: true, + [CertKeyUsage.KEY_ENCIPHERMENT]: true + } }); } }, [cert]); @@ -124,6 +152,10 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => { useEffect(() => { if (!cert && selectedCertTemplate) { setValue("ttl", selectedCertTemplate.ttl); + setValue( + "keyUsages", + Object.fromEntries(selectedCertTemplate.keyUsages.map((name) => [name, true])) + ); } }, [selectedCertTemplate, cert]); @@ -133,7 +165,8 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => { collectionId, commonName, altNames, - ttl + ttl, + keyUsages }: FormData) => { try { if (!currentWorkspace?.slug) return; @@ -146,7 +179,10 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => { friendlyName, commonName, altNames, - ttl + ttl, + keyUsages: Object.entries(keyUsages) + .filter(([, value]) => value) + .map(([key]) => key as CertKeyUsage) }); reset(); @@ -363,8 +399,52 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => { )} /> + + + +
Key Usages
+
+ + { + return ( + +
+ {KEY_USAGES_OPTIONS.map(({ label, value: optionValue }) => { + return ( + { + onChange({ + ...value, + [optionValue]: state + }); + }} + > + {label} + + ); + })} +
+
+ ); + }} + /> +
+
+
{!cert && ( -
+