diff --git a/backend/src/server/routes/v1/certificate-authority-router.ts b/backend/src/server/routes/v1/certificate-authority-router.ts index ea779be86..188eb28f3 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, CertKeyUsage } from "@app/services/certificate/certificate-types"; +import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types"; import { CaRenewalType, CaStatus, CaType } from "@app/services/certificate-authority/certificate-authority-types"; import { validateAltNamesField, @@ -574,7 +574,8 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { .describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.ttl), notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.notBefore), notAfter: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.notAfter), - keyUsages: z.nativeEnum(CertKeyUsage).array().optional() + keyUsages: z.nativeEnum(CertKeyUsage).array().optional(), + extendedKeyUsages: z.nativeEnum(CertExtendedKeyUsage).array().optional() }) .refine( (data) => { @@ -655,7 +656,8 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { .describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.ttl), notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.notBefore), notAfter: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.notAfter), - keyUsages: z.nativeEnum(CertKeyUsage).array().optional() + keyUsages: z.nativeEnum(CertKeyUsage).array().optional(), + extendedKeyUsages: z.nativeEnum(CertExtendedKeyUsage).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 dcc3ddf06..b13a790d5 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 { CertKeyUsage, CrlReason } from "@app/services/certificate/certificate-types"; +import { CertExtendedKeyUsage, CertKeyUsage, CrlReason } from "@app/services/certificate/certificate-types"; import { validateAltNamesField, validateCaDateField @@ -87,7 +87,8 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { .describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.ttl), notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.notBefore), notAfter: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.notAfter), - keyUsages: z.nativeEnum(CertKeyUsage).array().optional() + keyUsages: z.nativeEnum(CertKeyUsage).array().optional(), + extendedKeyUsages: z.nativeEnum(CertExtendedKeyUsage).array().optional() }) .refine( (data) => { @@ -179,7 +180,8 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { .describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.ttl), notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.notBefore), notAfter: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.notAfter), - keyUsages: z.nativeEnum(CertKeyUsage).array().optional() + keyUsages: z.nativeEnum(CertKeyUsage).array().optional(), + extendedKeyUsages: z.nativeEnum(CertExtendedKeyUsage).array().optional() }) .refine( (data) => { diff --git a/backend/src/server/routes/v1/certificate-template-router.ts b/backend/src/server/routes/v1/certificate-template-router.ts index 785e48674..c87639be1 100644 --- a/backend/src/server/routes/v1/certificate-template-router.ts +++ b/backend/src/server/routes/v1/certificate-template-router.ts @@ -7,7 +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 { CertExtendedKeyUsage, 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"; @@ -80,7 +80,8 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid .nativeEnum(CertKeyUsage) .array() .optional() - .default([CertKeyUsage.DIGITAL_SIGNATURE, CertKeyUsage.KEY_ENCIPHERMENT]) + .default([CertKeyUsage.DIGITAL_SIGNATURE, CertKeyUsage.KEY_ENCIPHERMENT]), + extendedKeyUsages: z.nativeEnum(CertExtendedKeyUsage).array().optional().default([]) }), response: { 200: sanitizedCertificateTemplate @@ -137,7 +138,8 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid .refine((val) => ms(val) > 0, "TTL must be a positive number") .optional() .describe(CERTIFICATE_TEMPLATES.UPDATE.ttl), - keyUsages: z.nativeEnum(CertKeyUsage).array().optional() + keyUsages: z.nativeEnum(CertKeyUsage).array().optional(), + extendedKeyUsages: z.nativeEnum(CertExtendedKeyUsage).array().optional() }), params: z.object({ certificateTemplateId: z.string().describe(CERTIFICATE_TEMPLATES.UPDATE.certificateTemplateId) diff --git a/backend/src/services/certificate-authority/certificate-authority-service.ts b/backend/src/services/certificate-authority/certificate-authority-service.ts index 53255c0a2..f2a58922e 100644 --- a/backend/src/services/certificate-authority/certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/certificate-authority-service.ts @@ -19,7 +19,13 @@ 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, CertKeyUsage, CertStatus } from "../certificate/certificate-types"; +import { + CertExtendedKeyUsage, + CertExtendedKeyUsageOIDToName, + 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"; @@ -1053,7 +1059,8 @@ export const certificateAuthorityServiceFactory = ({ actorAuthMethod, actor, actorOrgId, - keyUsages + keyUsages, + extendedKeyUsages }: TIssueCertFromCaDTO) => { let ca: TCertificateAuthorities | undefined; let certificateTemplate: TCertificateTemplates | undefined; @@ -1170,7 +1177,14 @@ export const certificateAuthorityServiceFactory = ({ const appCfg = getConfig(); const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/pki/crl/${caCrl.id}`; + const extensions: x509.Extension[] = [ + new x509.BasicConstraintsExtension(false), + new x509.CRLDistributionPointsExtension([distributionPointUrl]), + await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false), + await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey) + ]; + // handle key usages let selectedKeyUsages: CertKeyUsage[] = keyUsages ?? []; if (keyUsages === undefined && !certificateTemplate) { selectedKeyUsages = [CertKeyUsage.DIGITAL_SIGNATURE, CertKeyUsage.KEY_ENCIPHERMENT]; @@ -1184,24 +1198,42 @@ export const certificateAuthorityServiceFactory = ({ const validKeyUsages = certificateTemplate.keyUsages || []; if (keyUsages.some((keyUsage) => !validKeyUsages.includes(keyUsage))) { throw new BadRequestError({ - message: "Invalid key usage value for certificate" + message: "Invalid key usage value based on template policy" }); } selectedKeyUsages = keyUsages; } - const extensions: x509.Extension[] = [ - 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)); } + // handle extended key usages + let selectedExtendedKeyUsages: CertExtendedKeyUsage[] = extendedKeyUsages ?? []; + if (extendedKeyUsages === undefined && certificateTemplate) { + selectedExtendedKeyUsages = (certificateTemplate.extendedKeyUsages ?? []) as CertExtendedKeyUsage[]; + } + + if (extendedKeyUsages?.length && certificateTemplate) { + const validExtendedKeyUsages = certificateTemplate.extendedKeyUsages || []; + if (extendedKeyUsages.some((eku) => !validExtendedKeyUsages.includes(eku))) { + throw new BadRequestError({ + message: "Invalid extended key usage value based on template policy" + }); + } + selectedExtendedKeyUsages = extendedKeyUsages; + } + + if (selectedExtendedKeyUsages.length) { + extensions.push( + new x509.ExtendedKeyUsageExtension( + selectedExtendedKeyUsages.map((eku) => x509.ExtendedKeyUsage[eku]), + true + ) + ); + } + let altNamesArray: { type: "email" | "dns"; value: string; @@ -1284,7 +1316,8 @@ export const certificateAuthorityServiceFactory = ({ serialNumber, notBefore: notBeforeDate, notAfter: notAfterDate, - keyUsages: selectedKeyUsages + keyUsages: selectedKeyUsages, + extendedKeyUsages: selectedExtendedKeyUsages }, tx ); @@ -1347,7 +1380,8 @@ export const certificateAuthorityServiceFactory = ({ ttl, notBefore, notAfter, - keyUsages + keyUsages, + extendedKeyUsages } = dto; let collectionId = pkiCollectionId; @@ -1472,6 +1506,7 @@ export const certificateAuthorityServiceFactory = ({ await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey) ]; + // handle key usages const csrKeyUsageExtension = csrObj.getExtension("2.5.29.15") as x509.KeyUsagesExtension; let csrKeyUsages: CertKeyUsage[] = []; if (csrKeyUsageExtension) { @@ -1494,7 +1529,7 @@ export const certificateAuthorityServiceFactory = ({ const validKeyUsages = certificateTemplate.keyUsages || []; if (csrKeyUsages.some((keyUsage) => !validKeyUsages.includes(keyUsage))) { throw new BadRequestError({ - message: "Invalid key usage value for certificate" + message: "Invalid key usage value based on template policy" }); } selectedKeyUsages = csrKeyUsages; @@ -1507,7 +1542,7 @@ export const certificateAuthorityServiceFactory = ({ const validKeyUsages = certificateTemplate.keyUsages || []; if (keyUsages.some((keyUsage) => !validKeyUsages.includes(keyUsage))) { throw new BadRequestError({ - message: "Invalid key usage value for certificate" + message: "Invalid key usage value based on template policy" }); } selectedKeyUsages = keyUsages; @@ -1518,6 +1553,53 @@ export const certificateAuthorityServiceFactory = ({ extensions.push(new x509.KeyUsagesExtension(keyUsagesBitValue, true)); } + // handle extended key usages + const csrExtendedKeyUsageExtension = csrObj.getExtension("2.5.29.37") as x509.ExtendedKeyUsageExtension; + let csrExtendedKeyUsages: CertExtendedKeyUsage[] = []; + if (csrExtendedKeyUsageExtension) { + csrExtendedKeyUsages = csrExtendedKeyUsageExtension.usages.map( + (ekuOid) => CertExtendedKeyUsageOIDToName[ekuOid as string] + ); + } + + let selectedExtendedKeyUsages: CertExtendedKeyUsage[] = extendedKeyUsages ?? []; + if (extendedKeyUsages === undefined && !certificateTemplate && csrExtendedKeyUsageExtension) { + selectedExtendedKeyUsages = csrExtendedKeyUsages; + } + + if (extendedKeyUsages === undefined && certificateTemplate) { + if (csrExtendedKeyUsageExtension) { + const validExtendedKeyUsages = certificateTemplate.extendedKeyUsages || []; + if (csrExtendedKeyUsages.some((eku) => !validExtendedKeyUsages.includes(eku))) { + throw new BadRequestError({ + message: "Invalid extended key usage value based on template policy" + }); + } + selectedExtendedKeyUsages = csrExtendedKeyUsages; + } else { + selectedExtendedKeyUsages = (certificateTemplate.extendedKeyUsages ?? []) as CertExtendedKeyUsage[]; + } + } + + if (extendedKeyUsages?.length && certificateTemplate) { + const validExtendedKeyUsages = certificateTemplate.extendedKeyUsages || []; + if (extendedKeyUsages.some((keyUsage) => !validExtendedKeyUsages.includes(keyUsage))) { + throw new BadRequestError({ + message: "Invalid extended key usage value based on template policy" + }); + } + selectedExtendedKeyUsages = extendedKeyUsages; + } + + if (selectedExtendedKeyUsages.length) { + extensions.push( + new x509.ExtendedKeyUsageExtension( + selectedExtendedKeyUsages.map((eku) => x509.ExtendedKeyUsage[eku]), + true + ) + ); + } + let altNamesFromCsr: string = ""; let altNamesArray: { type: "email" | "dns"; @@ -1614,7 +1696,8 @@ export const certificateAuthorityServiceFactory = ({ serialNumber, notBefore: notBeforeDate, notAfter: notAfterDate, - keyUsages: selectedKeyUsages + keyUsages: selectedKeyUsages, + extendedKeyUsages: selectedExtendedKeyUsages }, tx ); diff --git a/backend/src/services/certificate-authority/certificate-authority-types.ts b/backend/src/services/certificate-authority/certificate-authority-types.ts index f2a584576..e2f523348 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, CertKeyUsage } from "../certificate/certificate-types"; +import { CertExtendedKeyUsage, 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"; @@ -98,6 +98,7 @@ export type TIssueCertFromCaDTO = { notBefore?: string; notAfter?: string; keyUsages?: CertKeyUsage[]; + extendedKeyUsages?: CertExtendedKeyUsage[]; } & Omit; export type TSignCertFromCaDTO = @@ -114,6 +115,7 @@ export type TSignCertFromCaDTO = notBefore?: string; notAfter?: string; keyUsages?: CertKeyUsage[]; + extendedKeyUsages?: CertExtendedKeyUsage[]; } | ({ isInternal: false; @@ -128,6 +130,7 @@ export type TSignCertFromCaDTO = notBefore?: string; notAfter?: string; keyUsages?: CertKeyUsage[]; + extendedKeyUsages?: CertExtendedKeyUsage[]; } & Omit); export type TGetCaCertificateTemplatesDTO = { diff --git a/backend/src/services/certificate-template/certificate-template-schema.ts b/backend/src/services/certificate-template/certificate-template-schema.ts index a6c464252..7a87daddf 100644 --- a/backend/src/services/certificate-template/certificate-template-schema.ts +++ b/backend/src/services/certificate-template/certificate-template-schema.ts @@ -10,7 +10,8 @@ export const sanitizedCertificateTemplate = CertificateTemplatesSchema.pick({ subjectAlternativeName: true, pkiCollectionId: true, ttl: true, - keyUsages: true + keyUsages: true, + extendedKeyUsages: true }).merge( z.object({ projectId: z.string(), diff --git a/backend/src/services/certificate-template/certificate-template-service.ts b/backend/src/services/certificate-template/certificate-template-service.ts index 015995677..3e7f80e85 100644 --- a/backend/src/services/certificate-template/certificate-template-service.ts +++ b/backend/src/services/certificate-template/certificate-template-service.ts @@ -58,7 +58,8 @@ export const certificateTemplateServiceFactory = ({ actorAuthMethod, actor, actorOrgId, - keyUsages + keyUsages, + extendedKeyUsages }: TCreateCertTemplateDTO) => { const ca = await certificateAuthorityDAL.findById(caId); if (!ca) { @@ -88,7 +89,8 @@ export const certificateTemplateServiceFactory = ({ commonName, subjectAlternativeName, ttl, - keyUsages + keyUsages, + extendedKeyUsages }, tx ); @@ -116,7 +118,8 @@ export const certificateTemplateServiceFactory = ({ actorAuthMethod, actor, actorOrgId, - keyUsages + keyUsages, + extendedKeyUsages }: TUpdateCertTemplateDTO) => { const certTemplate = await certificateTemplateDAL.getById(id); if (!certTemplate) { @@ -157,7 +160,8 @@ export const certificateTemplateServiceFactory = ({ subjectAlternativeName, name, ttl, - keyUsages + keyUsages, + extendedKeyUsages }, tx ); diff --git a/backend/src/services/certificate-template/certificate-template-types.ts b/backend/src/services/certificate-template/certificate-template-types.ts index 3a0050a86..6d6488f2c 100644 --- a/backend/src/services/certificate-template/certificate-template-types.ts +++ b/backend/src/services/certificate-template/certificate-template-types.ts @@ -1,5 +1,5 @@ import { TProjectPermission } from "@app/lib/types"; -import { CertKeyUsage } from "@app/services/certificate/certificate-types"; +import { CertExtendedKeyUsage, CertKeyUsage } from "@app/services/certificate/certificate-types"; export type TCreateCertTemplateDTO = { caId: string; @@ -9,6 +9,7 @@ export type TCreateCertTemplateDTO = { subjectAlternativeName: string; ttl: string; keyUsages: CertKeyUsage[]; + extendedKeyUsages: CertExtendedKeyUsage[]; } & Omit; export type TUpdateCertTemplateDTO = { @@ -20,6 +21,7 @@ export type TUpdateCertTemplateDTO = { subjectAlternativeName?: string; ttl?: string; keyUsages?: CertKeyUsage[]; + extendedKeyUsages?: CertExtendedKeyUsage[]; } & Omit; export type TGetCertTemplateDTO = { diff --git a/backend/src/services/certificate/certificate-types.ts b/backend/src/services/certificate/certificate-types.ts index 4b8b49cd1..ef63f142d 100644 --- a/backend/src/services/certificate/certificate-types.ts +++ b/backend/src/services/certificate/certificate-types.ts @@ -1,3 +1,5 @@ +import * as x509 from "@peculiar/x509"; + import { TProjectPermission } from "@app/lib/types"; export enum CertStatus { @@ -24,6 +26,24 @@ export enum CertKeyUsage { DECIPHER_ONLY = "decipherOnly" } +export enum CertExtendedKeyUsage { + CLIENT_AUTH = "clientAuth", + SERVER_AUTH = "serverAuth", + CODE_SIGNING = "codeSigning", + EMAIL_PROTECTION = "emailProtection", + TIMESTAMPING = "timeStamping", + OCSP_SIGNING = "ocspSigning" +} + +export const CertExtendedKeyUsageOIDToName: Record = { + [x509.ExtendedKeyUsage.clientAuth]: CertExtendedKeyUsage.CLIENT_AUTH, + [x509.ExtendedKeyUsage.serverAuth]: CertExtendedKeyUsage.SERVER_AUTH, + [x509.ExtendedKeyUsage.codeSigning]: CertExtendedKeyUsage.CODE_SIGNING, + [x509.ExtendedKeyUsage.emailProtection]: CertExtendedKeyUsage.EMAIL_PROTECTION, + [x509.ExtendedKeyUsage.ocspSigning]: CertExtendedKeyUsage.OCSP_SIGNING, + [x509.ExtendedKeyUsage.timeStamping]: CertExtendedKeyUsage.TIMESTAMPING +}; + export enum CrlReason { UNSPECIFIED = "UNSPECIFIED", KEY_COMPROMISE = "KEY_COMPROMISE", diff --git a/frontend/src/hooks/api/ca/types.ts b/frontend/src/hooks/api/ca/types.ts index af9ffef81..e36e34f36 100644 --- a/frontend/src/hooks/api/ca/types.ts +++ b/frontend/src/hooks/api/ca/types.ts @@ -1,5 +1,5 @@ import { CertKeyAlgorithm } from "../certificates/enums"; -import { CertKeyUsage } from "../certificates/types"; +import { CertExtendedKeyUsage, CertKeyUsage } from "../certificates/types"; import { CaRenewalType, CaStatus, CaType } from "./enums"; export type TCertificateAuthority = { @@ -93,6 +93,7 @@ export type TCreateCertificateDTO = { notBefore?: string; notAfter?: string; keyUsages: CertKeyUsage[]; + extendedKeyUsages: CertExtendedKeyUsage[]; }; export type TCreateCertificateResponse = { diff --git a/frontend/src/hooks/api/certificateTemplates/types.ts b/frontend/src/hooks/api/certificateTemplates/types.ts index 2bd07846e..048694641 100644 --- a/frontend/src/hooks/api/certificateTemplates/types.ts +++ b/frontend/src/hooks/api/certificateTemplates/types.ts @@ -1,4 +1,4 @@ -import { CertKeyUsage } from "../certificates/types"; +import { CertExtendedKeyUsage, CertKeyUsage } from "../certificates/types"; export type TCertificateTemplate = { id: string; @@ -11,6 +11,7 @@ export type TCertificateTemplate = { subjectAlternativeName: string; ttl: string; keyUsages: CertKeyUsage[]; + extendedKeyUsages: CertExtendedKeyUsage[]; }; export type TCreateCertificateTemplateDTO = { @@ -22,6 +23,7 @@ export type TCreateCertificateTemplateDTO = { ttl: string; projectId: string; keyUsages: CertKeyUsage[]; + extendedKeyUsages: CertExtendedKeyUsage[]; }; export type TUpdateCertificateTemplateDTO = { @@ -34,6 +36,7 @@ export type TUpdateCertificateTemplateDTO = { ttl?: string; projectId: string; keyUsages?: CertKeyUsage[]; + extendedKeyUsages?: CertExtendedKeyUsage[]; }; export type TDeleteCertificateTemplateDTO = { diff --git a/frontend/src/hooks/api/certificates/types.ts b/frontend/src/hooks/api/certificates/types.ts index 61186b6fa..be341f675 100644 --- a/frontend/src/hooks/api/certificates/types.ts +++ b/frontend/src/hooks/api/certificates/types.ts @@ -12,6 +12,7 @@ export type TCertificate = { notBefore: string; notAfter: string; keyUsages: CertKeyUsage[]; + extendedKeyUsages: CertExtendedKeyUsage[]; }; export type TDeleteCertDTO = { @@ -48,3 +49,21 @@ export const KEY_USAGES_OPTIONS = [ { value: CertKeyUsage.ENCIPHER_ONLY, label: "Encipher Only" }, { value: CertKeyUsage.DECIPHER_ONLY, label: "Decipher Only" } ] as const; + +export enum CertExtendedKeyUsage { + CLIENT_AUTH = "clientAuth", + SERVER_AUTH = "serverAuth", + CODE_SIGNING = "codeSigning", + EMAIL_PROTECTION = "emailProtection", + TIMESTAMPING = "timeStamping", + OCSP_SIGNING = "ocspSigning" +} + +export const EXTENDED_KEY_USAGES_OPTIONS = [ + { value: CertExtendedKeyUsage.CLIENT_AUTH, label: "Client Auth" }, + { value: CertExtendedKeyUsage.SERVER_AUTH, label: "Server Auth" }, + { value: CertExtendedKeyUsage.EMAIL_PROTECTION, label: "Email Protection" }, + { value: CertExtendedKeyUsage.OCSP_SIGNING, label: "OCSP Signing" }, + { value: CertExtendedKeyUsage.CODE_SIGNING, label: "Code Signing" }, + { value: CertExtendedKeyUsage.TIMESTAMPING, label: "Timestamping" } +] 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 52cf4e0e2..10f8f7ea4 100644 --- a/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateModal.tsx +++ b/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateModal.tsx @@ -33,7 +33,12 @@ 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 { + CertExtendedKeyUsage, + CertKeyUsage, + EXTENDED_KEY_USAGES_OPTIONS, + KEY_USAGES_OPTIONS +} from "@app/hooks/api/certificates/types"; import { UsePopUpState } from "@app/hooks/usePopUp"; import { CertificateContent } from "./CertificateContent"; @@ -56,6 +61,14 @@ const schema = z.object({ [CertKeyUsage.CRL_SIGN]: z.boolean().optional(), [CertKeyUsage.ENCIPHER_ONLY]: z.boolean().optional(), [CertKeyUsage.DECIPHER_ONLY]: z.boolean().optional() + }), + extendedKeyUsages: z.object({ + [CertExtendedKeyUsage.CLIENT_AUTH]: z.boolean().optional(), + [CertExtendedKeyUsage.CODE_SIGNING]: 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() }) }); @@ -110,7 +123,8 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => { keyUsages: { [CertKeyUsage.DIGITAL_SIGNATURE]: true, [CertKeyUsage.KEY_ENCIPHERMENT]: true - } + }, + extendedKeyUsages: {} } }); @@ -131,7 +145,10 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => { altNames: cert.altNames, certificateTemplateId: cert.certificateTemplateId ?? CERT_TEMPLATE_NONE_VALUE, ttl: "", - keyUsages: Object.fromEntries(cert.keyUsages.map((name) => [name, true])) + keyUsages: Object.fromEntries((cert.keyUsages || []).map((name) => [name, true])), + extendedKeyUsages: Object.fromEntries( + (cert.extendedKeyUsages || []).map((name) => [name, true]) + ) }); } else { reset({ @@ -144,7 +161,8 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => { keyUsages: { [CertKeyUsage.DIGITAL_SIGNATURE]: true, [CertKeyUsage.KEY_ENCIPHERMENT]: true - } + }, + extendedKeyUsages: {} }); } }, [cert]); @@ -156,6 +174,10 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => { "keyUsages", Object.fromEntries(selectedCertTemplate.keyUsages.map((name) => [name, true])) ); + setValue( + "extendedKeyUsages", + Object.fromEntries(selectedCertTemplate.extendedKeyUsages.map((name) => [name, true])) + ); } }, [selectedCertTemplate, cert]); @@ -166,7 +188,8 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => { commonName, altNames, ttl, - keyUsages + keyUsages, + extendedKeyUsages }: FormData) => { try { if (!currentWorkspace?.slug) return; @@ -182,7 +205,10 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => { ttl, keyUsages: Object.entries(keyUsages) .filter(([, value]) => value) - .map(([key]) => key as CertKeyUsage) + .map(([key]) => key as CertKeyUsage), + extendedKeyUsages: Object.entries(extendedKeyUsages) + .filter(([, value]) => value) + .map(([key]) => key as CertExtendedKeyUsage) }); reset(); @@ -440,6 +466,41 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => { ); }} /> + { + return ( + +
+ {EXTENDED_KEY_USAGES_OPTIONS.map(({ label, value: optionValue }) => { + return ( + { + onChange({ + ...value, + [optionValue]: state + }); + }} + > + {label} + + ); + })} +
+
+ ); + }} + /> 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 62bb9b817..3ead118bf 100644 --- a/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateTemplateModal.tsx +++ b/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateTemplateModal.tsx @@ -33,7 +33,12 @@ import { useUpdateCertTemplate } from "@app/hooks/api"; import { caTypeToNameMap } from "@app/hooks/api/ca/constants"; -import { CertKeyUsage, KEY_USAGES_OPTIONS } from "@app/hooks/api/certificates/types"; +import { + CertExtendedKeyUsage, + CertKeyUsage, + EXTENDED_KEY_USAGES_OPTIONS, + KEY_USAGES_OPTIONS +} from "@app/hooks/api/certificates/types"; import { UsePopUpState } from "@app/hooks/usePopUp"; const validateTemplateRegexField = z @@ -63,6 +68,14 @@ const schema = z.object({ [CertKeyUsage.CRL_SIGN]: z.boolean().optional(), [CertKeyUsage.ENCIPHER_ONLY]: z.boolean().optional(), [CertKeyUsage.DECIPHER_ONLY]: z.boolean().optional() + }), + extendedKeyUsages: z.object({ + [CertExtendedKeyUsage.CLIENT_AUTH]: z.boolean().optional(), + [CertExtendedKeyUsage.CODE_SIGNING]: 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() }) }); @@ -122,7 +135,10 @@ export const CertificateTemplateModal = ({ popUp, handlePopUpToggle, caId }: Pro subjectAlternativeName: certTemplate.subjectAlternativeName, collectionId: certTemplate.pkiCollectionId ?? undefined, ttl: certTemplate.ttl, - keyUsages: Object.fromEntries(certTemplate.keyUsages.map((name) => [name, true])) + keyUsages: Object.fromEntries(certTemplate.keyUsages.map((name) => [name, true]) ?? []), + extendedKeyUsages: Object.fromEntries( + certTemplate.extendedKeyUsages.map((name) => [name, true]) ?? [] + ) }); } else { reset({ @@ -133,7 +149,8 @@ export const CertificateTemplateModal = ({ popUp, handlePopUpToggle, caId }: Pro keyUsages: { [CertKeyUsage.DIGITAL_SIGNATURE]: true, [CertKeyUsage.KEY_ENCIPHERMENT]: true - } + }, + extendedKeyUsages: {} }); } }, [certTemplate, ca]); @@ -144,7 +161,8 @@ export const CertificateTemplateModal = ({ popUp, handlePopUpToggle, caId }: Pro commonName, subjectAlternativeName, ttl, - keyUsages + keyUsages, + extendedKeyUsages }: FormData) => { if (!currentWorkspace?.id) { return; @@ -163,7 +181,10 @@ export const CertificateTemplateModal = ({ popUp, handlePopUpToggle, caId }: Pro ttl, keyUsages: Object.entries(keyUsages) .filter(([, value]) => value) - .map(([key]) => key as CertKeyUsage) + .map(([key]) => key as CertKeyUsage), + extendedKeyUsages: Object.entries(extendedKeyUsages) + .filter(([, value]) => value) + .map(([key]) => key as CertExtendedKeyUsage) }); createNotification({ @@ -181,7 +202,10 @@ export const CertificateTemplateModal = ({ popUp, handlePopUpToggle, caId }: Pro ttl, keyUsages: Object.entries(keyUsages) .filter(([, value]) => value) - .map(([key]) => key as CertKeyUsage) + .map(([key]) => key as CertKeyUsage), + extendedKeyUsages: Object.entries(extendedKeyUsages) + .filter(([, value]) => value) + .map(([key]) => key as CertExtendedKeyUsage) }); createNotification({ @@ -408,6 +432,40 @@ export const CertificateTemplateModal = ({ popUp, handlePopUpToggle, caId }: Pro ); }} /> + { + return ( + +
+ {EXTENDED_KEY_USAGES_OPTIONS.map(({ label, value: optionValue }) => { + return ( + { + onChange({ + ...value, + [optionValue]: state + }); + }} + > + {label} + + ); + })} +
+
+ ); + }} + />