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..93bfa59db --- /dev/null +++ b/backend/src/db/migrations/20240910070128_add-pki-key-usages.ts @@ -0,0 +1,85 @@ +import { Knex } from "knex"; + +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"); + + 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] + }); + } + + 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"); + + await knex.schema.alterTable(TableName.CertificateTemplate, (t) => { + if (hasKeyUsagesCol) { + t.dropColumn("keyUsages"); + } + if (hasExtendedKeyUsagesCol) { + 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/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/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/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 3c0a5fe52..ca71e5d41 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -1122,11 +1122,15 @@ export const CERTIFICATE_AUTHORITIES = { issuingCaCertificate: "The certificate of the issuing CA", certificateChain: "The certificate chain of the issued certificate", privateKey: "The private key of the issued certificate", - serialNumber: "The serial number of the issued certificate" + serialNumber: "The serial number of the issued certificate", + keyUsages: "The key usage extension of the certificate", + extendedKeyUsages: "The extended key usage extension of the certificate" }, SIGN_CERT: { caId: "The ID of the CA to issue the certificate from", pkiCollectionId: "The ID of the PKI collection to add the certificate to", + keyUsages: "The key usage extension of the certificate", + extendedKeyUsages: "The extended key usage extension of the certificate", csr: "The pem-encoded CSR to sign with the CA to be used for certificate issuance", friendlyName: "A friendly name for the certificate", commonName: "The common name (CN) for the certificate", @@ -1176,7 +1180,10 @@ export const CERTIFICATE_TEMPLATES = { name: "The name of the template", commonName: "The regular expression string to use for validating common names", subjectAlternativeName: "The regular expression string to use for validating subject alternative names", - ttl: "The max TTL for the template" + ttl: "The max TTL for the template", + keyUsages: "The key usage constraint or default value for when template is used during certificate issuance", + extendedKeyUsages: + "The extended key usage constraint or default value for when template is used during certificate issuance" }, GET: { certificateTemplateId: "The ID of the certificate template to get" @@ -1188,7 +1195,11 @@ export const CERTIFICATE_TEMPLATES = { name: "The updated name of the template", commonName: "The updated regular expression string for validating common names", subjectAlternativeName: "The updated regular expression string for validating subject alternative names", - ttl: "The updated max TTL for the template" + ttl: "The updated max TTL for the template", + keyUsages: + "The updated key usage constraint or default value for when template is used during certificate issuance", + extendedKeyUsages: + "The updated extended key usage constraint or default value for when template is used during certificate issuance" }, DELETE: { certificateTemplateId: "The ID of the certificate template to delete" diff --git a/backend/src/server/routes/v1/certificate-authority-router.ts b/backend/src/server/routes/v1/certificate-authority-router.ts index 77ee70e57..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 } 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, @@ -573,7 +573,9 @@ 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(), + extendedKeyUsages: z.nativeEnum(CertExtendedKeyUsage).array().optional() }) .refine( (data) => { @@ -653,7 +655,9 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { .refine((val) => ms(val) > 0, "TTL must be a positive number") .describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.ttl), notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.notBefore), - notAfter: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.notAfter) + notAfter: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.notAfter), + 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 91ae85982..99d57e802 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 { CertExtendedKeyUsage, CertKeyUsage, CrlReason } from "@app/services/certificate/certificate-types"; import { validateAltNamesField, validateCaDateField @@ -86,7 +86,17 @@ 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() + .describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.keyUsages), + extendedKeyUsages: z + .nativeEnum(CertExtendedKeyUsage) + .array() + .optional() + .describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.extendedKeyUsages) }) .refine( (data) => { @@ -177,7 +187,17 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { .refine((val) => ms(val) > 0, "TTL must be a positive number") .describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.ttl), notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.notBefore), - notAfter: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.notAfter) + notAfter: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.notAfter), + keyUsages: z + .nativeEnum(CertKeyUsage) + .array() + .optional() + .describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.keyUsages), + extendedKeyUsages: z + .nativeEnum(CertExtendedKeyUsage) + .array() + .optional() + .describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.extendedKeyUsages) }) .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 c9d2410fd..54ce571a2 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 { 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"; @@ -74,7 +75,19 @@ 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]) + .describe(CERTIFICATE_TEMPLATES.CREATE.keyUsages), + extendedKeyUsages: z + .nativeEnum(CertExtendedKeyUsage) + .array() + .optional() + .default([]) + .describe(CERTIFICATE_TEMPLATES.CREATE.extendedKeyUsages) }), response: { 200: sanitizedCertificateTemplate @@ -130,7 +143,13 @@ 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().describe(CERTIFICATE_TEMPLATES.UPDATE.keyUsages), + extendedKeyUsages: z + .nativeEnum(CertExtendedKeyUsage) + .array() + .optional() + .describe(CERTIFICATE_TEMPLATES.UPDATE.extendedKeyUsages) }), 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 1c2a5a689..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, 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"; @@ -1052,7 +1058,9 @@ export const certificateAuthorityServiceFactory = ({ actorId, actorAuthMethod, actor, - actorOrgId + actorOrgId, + keyUsages, + extendedKeyUsages }: TIssueCertFromCaDTO) => { let ca: TCertificateAuthorities | undefined; let certificateTemplate: TCertificateTemplates | undefined; @@ -1169,15 +1177,63 @@ export const certificateAuthorityServiceFactory = ({ const appCfg = getConfig(); const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/pki/crl/${caCrl.id}`; - 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) ]; + // handle key usages + 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 based on template policy" + }); + } + selectedKeyUsages = keyUsages; + } + + 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; @@ -1259,7 +1315,9 @@ export const certificateAuthorityServiceFactory = ({ altNames, serialNumber, notBefore: notBeforeDate, - notAfter: notAfterDate + notAfter: notAfterDate, + keyUsages: selectedKeyUsages, + extendedKeyUsages: selectedExtendedKeyUsages }, tx ); @@ -1321,7 +1379,9 @@ export const certificateAuthorityServiceFactory = ({ altNames, ttl, notBefore, - notAfter + notAfter, + keyUsages, + extendedKeyUsages } = dto; let collectionId = pkiCollectionId; @@ -1441,12 +1501,105 @@ export const certificateAuthorityServiceFactory = ({ }); const extensions: x509.Extension[] = [ - new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment, true), new x509.BasicConstraintsExtension(false), await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false), 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) { + csrKeyUsages = Object.values(CertKeyUsage).filter( + (keyUsage) => (x509.KeyUsageFlags[keyUsage] & csrKeyUsageExtension.usages) !== 0 + ); + } + + let selectedKeyUsages: CertKeyUsage[] = keyUsages ?? []; + if (keyUsages === undefined && !certificateTemplate) { + if (csrKeyUsageExtension) { + selectedKeyUsages = csrKeyUsages; + } else { + selectedKeyUsages = [CertKeyUsage.DIGITAL_SIGNATURE, CertKeyUsage.KEY_ENCIPHERMENT]; + } + } + + if (keyUsages === undefined && certificateTemplate) { + if (csrKeyUsageExtension) { + const validKeyUsages = certificateTemplate.keyUsages || []; + if (csrKeyUsages.some((keyUsage) => !validKeyUsages.includes(keyUsage))) { + throw new BadRequestError({ + message: "Invalid key usage value based on template policy" + }); + } + selectedKeyUsages = csrKeyUsages; + } else { + 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 based on template policy" + }); + } + selectedKeyUsages = keyUsages; + } + + const keyUsagesBitValue = selectedKeyUsages.reduce((accum, keyUsage) => accum | x509.KeyUsageFlags[keyUsage], 0); + if (keyUsagesBitValue) { + 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"; @@ -1542,7 +1695,9 @@ export const certificateAuthorityServiceFactory = ({ altNames: altNamesFromCsr || altNames, serialNumber, notBefore: notBeforeDate, - notAfter: notAfterDate + notAfter: notAfterDate, + 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 5876c0057..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 } 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"; @@ -97,6 +97,8 @@ export type TIssueCertFromCaDTO = { ttl: string; notBefore?: string; notAfter?: string; + keyUsages?: CertKeyUsage[]; + extendedKeyUsages?: CertExtendedKeyUsage[]; } & Omit; export type TSignCertFromCaDTO = @@ -112,6 +114,8 @@ export type TSignCertFromCaDTO = ttl?: string; notBefore?: string; notAfter?: string; + keyUsages?: CertKeyUsage[]; + extendedKeyUsages?: CertExtendedKeyUsage[]; } | ({ isInternal: false; @@ -125,6 +129,8 @@ export type TSignCertFromCaDTO = ttl: string; 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 2ce787050..7a87daddf 100644 --- a/backend/src/services/certificate-template/certificate-template-schema.ts +++ b/backend/src/services/certificate-template/certificate-template-schema.ts @@ -9,7 +9,9 @@ export const sanitizedCertificateTemplate = CertificateTemplatesSchema.pick({ commonName: true, subjectAlternativeName: true, pkiCollectionId: true, - ttl: true + ttl: 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 2a8134093..3e7f80e85 100644 --- a/backend/src/services/certificate-template/certificate-template-service.ts +++ b/backend/src/services/certificate-template/certificate-template-service.ts @@ -57,7 +57,9 @@ export const certificateTemplateServiceFactory = ({ actorId, actorAuthMethod, actor, - actorOrgId + actorOrgId, + keyUsages, + extendedKeyUsages }: TCreateCertTemplateDTO) => { const ca = await certificateAuthorityDAL.findById(caId); if (!ca) { @@ -86,7 +88,9 @@ export const certificateTemplateServiceFactory = ({ name, commonName, subjectAlternativeName, - ttl + ttl, + keyUsages, + extendedKeyUsages }, tx ); @@ -113,7 +117,9 @@ export const certificateTemplateServiceFactory = ({ actorId, actorAuthMethod, actor, - actorOrgId + actorOrgId, + keyUsages, + extendedKeyUsages }: TUpdateCertTemplateDTO) => { const certTemplate = await certificateTemplateDAL.getById(id); if (!certTemplate) { @@ -153,7 +159,9 @@ export const certificateTemplateServiceFactory = ({ commonName, subjectAlternativeName, name, - ttl + ttl, + 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 74281e7b8..6d6488f2c 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 { CertExtendedKeyUsage, CertKeyUsage } from "@app/services/certificate/certificate-types"; export type TCreateCertTemplateDTO = { caId: string; @@ -7,6 +8,8 @@ export type TCreateCertTemplateDTO = { commonName: string; subjectAlternativeName: string; ttl: string; + keyUsages: CertKeyUsage[]; + extendedKeyUsages: CertExtendedKeyUsage[]; } & Omit; export type TUpdateCertTemplateDTO = { @@ -17,6 +20,8 @@ export type TUpdateCertTemplateDTO = { commonName?: string; 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 93f72afe3..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 { @@ -12,6 +14,36 @@ 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 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/docs/documentation/platform/pki/certificates.mdx b/docs/documentation/platform/pki/certificates.mdx index a4f1ba02c..4976b5e18 100644 --- a/docs/documentation/platform/pki/certificates.mdx +++ b/docs/documentation/platform/pki/certificates.mdx @@ -60,6 +60,8 @@ In the following steps, we explore how to issue a X.509 certificate under a CA. - Common Name (CN): A regular expression used to validate the common name in certificate requests. - Alternative Names (SANs): A regular expression used to validate subject alternative names in certificate requests. - TTL: The maximum Time-to-Live (TTL) for certificates issued using this template. + - Key Usage: The key usage constraint or default value for certificates issued using this template. + - Extended Key Usage: The extended key usage constraint or default value for certificates issued using this template. To create a certificate, head to your Project > Internal PKI > Certificates and press **Issue** under the Certificates section. @@ -76,13 +78,16 @@ In the following steps, we explore how to issue a X.509 certificate under a CA. - Common Name (CN): The (common) name for the certificate like `service.acme.com`. - Alternative Names (SANs): A comma-delimited list of Subject Alternative Names (SANs) for the certificate; these can be host names or email addresses like `app1.acme.com, app2.acme.com`. - TTL: The lifetime of the certificate in seconds. - + - Key Usage: The key usage extension of the certificate. + - Extended Key Usage: The extended key usage extension of the certificate. + Note that Infisical PKI supports issuing certificates without certificate templates as well. If this is desired, then you can set the **Certificate Template** field to **None** and specify the **Issuing CA** and optional **Certificate Collection** fields; the rest of the fields for the issued certificate remain the same. - + That said, we recommend using certificate templates to enforce policies and attach expiration monitoring on issued certificates. + Once you have created the certificate from step 1, you'll be presented with the certificate details including the **Certificate Body**, **Certificate Chain**, and **Private Key**. @@ -105,7 +110,7 @@ In the following steps, we explore how to issue a X.509 certificate under a CA. With certificate templates, you can specify, for example, that issued certificates must have a common name (CN) adhering to a specific format like .*.acme.com or perhaps that the max TTL cannot be more than 1 year. To create a certificate template, make an API request to the [Create Certificate Template](/api-reference/endpoints/certificate-templates/create) API endpoint, specifying the issuing CA. - + ### Sample request ```bash Request @@ -132,6 +137,7 @@ In the following steps, we explore how to issue a X.509 certificate under a CA. ttl: "...", } ``` + To create a certificate under the certificate template, make an API request to the [Issue Certificate](/api-reference/endpoints/certificates/issue-cert) API endpoint, @@ -164,7 +170,7 @@ In the following steps, we explore how to issue a X.509 certificate under a CA. Note that Infisical PKI supports issuing certificates without certificate templates as well. If this is desired, then you can set the **Certificate Template** field to **None** and specify the **Issuing CA** and optional **Certificate Collection** fields; the rest of the fields for the issued certificate remain the same. - + That said, we recommend using certificate templates to enforce policies and attach expiration monitoring on issued certificates. @@ -197,6 +203,7 @@ In the following steps, we explore how to issue a X.509 certificate under a CA. serialNumber: "..." } ``` + diff --git a/docs/images/platform/pki/certificate/cert-issue-modal.png b/docs/images/platform/pki/certificate/cert-issue-modal.png index f73462c8f..352c4979c 100644 Binary files a/docs/images/platform/pki/certificate/cert-issue-modal.png and b/docs/images/platform/pki/certificate/cert-issue-modal.png differ diff --git a/docs/images/platform/pki/certificate/cert-template-modal.png b/docs/images/platform/pki/certificate/cert-template-modal.png index f3995b6e4..2f6c88166 100644 Binary files a/docs/images/platform/pki/certificate/cert-template-modal.png and b/docs/images/platform/pki/certificate/cert-template-modal.png differ diff --git a/frontend/src/hooks/api/ca/types.ts b/frontend/src/hooks/api/ca/types.ts index 52e363fa7..25e5112e0 100644 --- a/frontend/src/hooks/api/ca/types.ts +++ b/frontend/src/hooks/api/ca/types.ts @@ -1,4 +1,4 @@ -import { CertKeyAlgorithm } from "../certificates/enums"; +import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "../certificates/enums"; import { CaRenewalType, CaStatus, CaType } from "./enums"; export type TCertificateAuthority = { @@ -91,6 +91,8 @@ export type TCreateCertificateDTO = { ttl: string; // string compatible with ms 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 c7c69c3a2..e1ea5ed81 100644 --- a/frontend/src/hooks/api/certificateTemplates/types.ts +++ b/frontend/src/hooks/api/certificateTemplates/types.ts @@ -1,3 +1,5 @@ +import { CertExtendedKeyUsage, CertKeyUsage } from "../certificates/enums"; + export type TCertificateTemplate = { id: string; caId: string; @@ -8,6 +10,8 @@ export type TCertificateTemplate = { commonName: string; subjectAlternativeName: string; ttl: string; + keyUsages: CertKeyUsage[]; + extendedKeyUsages: CertExtendedKeyUsage[]; }; export type TCreateCertificateTemplateDTO = { @@ -18,6 +22,8 @@ export type TCreateCertificateTemplateDTO = { subjectAlternativeName: string; ttl: string; projectId: string; + keyUsages: CertKeyUsage[]; + extendedKeyUsages: CertExtendedKeyUsage[]; }; export type TUpdateCertificateTemplateDTO = { @@ -29,6 +35,8 @@ export type TUpdateCertificateTemplateDTO = { subjectAlternativeName?: string; ttl?: string; projectId: string; + keyUsages?: CertKeyUsage[]; + extendedKeyUsages?: CertExtendedKeyUsage[]; }; export type TDeleteCertificateTemplateDTO = { diff --git a/frontend/src/hooks/api/certificates/constants.tsx b/frontend/src/hooks/api/certificates/constants.tsx index 2f2972bbc..0384ea6cd 100644 --- a/frontend/src/hooks/api/certificates/constants.tsx +++ b/frontend/src/hooks/api/certificates/constants.tsx @@ -1,4 +1,10 @@ -import { CertKeyAlgorithm, CertStatus, CrlReason } from "./enums"; +import { + CertExtendedKeyUsage, + CertKeyAlgorithm, + CertKeyUsage, + CertStatus, + CrlReason +} from "./enums"; export const certStatusToNameMap: { [K in CertStatus]: string } = { [CertStatus.ACTIVE]: "Active", @@ -69,3 +75,24 @@ export const crlReasons = [ }, { label: crlReasonToNameMap[CrlReason.A_A_COMPROMISE], value: CrlReason.A_A_COMPROMISE } ]; + +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; + +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/hooks/api/certificates/enums.tsx b/frontend/src/hooks/api/certificates/enums.tsx index d0da0273a..566da7506 100644 --- a/frontend/src/hooks/api/certificates/enums.tsx +++ b/frontend/src/hooks/api/certificates/enums.tsx @@ -22,3 +22,24 @@ export enum CrlReason { PRIVILEGE_WITHDRAWN = "PRIVILEGE_WITHDRAWN", A_A_COMPROMISE = "A_A_COMPROMISE" } + +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 CertExtendedKeyUsage { + CLIENT_AUTH = "clientAuth", + SERVER_AUTH = "serverAuth", + CODE_SIGNING = "codeSigning", + EMAIL_PROTECTION = "emailProtection", + TIMESTAMPING = "timeStamping", + OCSP_SIGNING = "ocspSigning" +} diff --git a/frontend/src/hooks/api/certificates/types.ts b/frontend/src/hooks/api/certificates/types.ts index 8057998aa..a9bcf5fbc 100644 --- a/frontend/src/hooks/api/certificates/types.ts +++ b/frontend/src/hooks/api/certificates/types.ts @@ -1,4 +1,4 @@ -import { CertStatus } from "./enums"; +import { CertExtendedKeyUsage, CertKeyUsage, CertStatus } from "./enums"; export type TCertificate = { id: string; @@ -11,6 +11,8 @@ export type TCertificate = { serialNumber: string; notBefore: string; notAfter: string; + keyUsages: CertKeyUsage[]; + extendedKeyUsages: CertExtendedKeyUsage[]; }; export type TDeleteCertDTO = { 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..f14ed7aa2 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,11 @@ import { useListWorkspacePkiCollections } from "@app/hooks/api"; import { caTypeToNameMap } from "@app/hooks/api/ca/constants"; +import { + EXTENDED_KEY_USAGES_OPTIONS, + KEY_USAGES_OPTIONS +} from "@app/hooks/api/certificates/constants"; +import { CertExtendedKeyUsage, CertKeyUsage } from "@app/hooks/api/certificates/enums"; import { UsePopUpState } from "@app/hooks/usePopUp"; import { CertificateContent } from "./CertificateContent"; @@ -39,7 +49,26 @@ 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() + }), + 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() + }) }); export type FormData = z.infer; @@ -88,7 +117,14 @@ 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 + }, + extendedKeyUsages: {} + } }); const selectedCertTemplateId = watch("certificateTemplateId"); @@ -107,7 +143,11 @@ 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])), + extendedKeyUsages: Object.fromEntries( + (cert.extendedKeyUsages || []).map((name) => [name, true]) + ) }); } else { reset({ @@ -116,7 +156,12 @@ 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 + }, + extendedKeyUsages: {} }); } }, [cert]); @@ -124,6 +169,14 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => { useEffect(() => { if (!cert && selectedCertTemplate) { setValue("ttl", selectedCertTemplate.ttl); + setValue( + "keyUsages", + Object.fromEntries(selectedCertTemplate.keyUsages.map((name) => [name, true])) + ); + setValue( + "extendedKeyUsages", + Object.fromEntries(selectedCertTemplate.extendedKeyUsages.map((name) => [name, true])) + ); } }, [selectedCertTemplate, cert]); @@ -133,7 +186,9 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => { collectionId, commonName, altNames, - ttl + ttl, + keyUsages, + extendedKeyUsages }: FormData) => { try { if (!currentWorkspace?.slug) return; @@ -146,7 +201,13 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => { friendlyName, commonName, altNames, - ttl + ttl, + keyUsages: Object.entries(keyUsages) + .filter(([, value]) => value) + .map(([key]) => key as CertKeyUsage), + extendedKeyUsages: Object.entries(extendedKeyUsages) + .filter(([, value]) => value) + .map(([key]) => key as CertExtendedKeyUsage) }); reset(); @@ -363,8 +424,87 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => { )} /> + + + +
Key Usage
+
+ + { + return ( + +
+ {KEY_USAGES_OPTIONS.map(({ label, value: optionValue }) => { + return ( + { + onChange({ + ...value, + [optionValue]: state + }); + }} + > + {label} + + ); + })} +
+
+ ); + }} + /> + { + return ( + +
+ {EXTENDED_KEY_USAGES_OPTIONS.map(({ label, value: optionValue }) => { + return ( + { + onChange({ + ...value, + [optionValue]: state + }); + }} + > + {label} + + ); + })} +
+
+ ); + }} + /> +
+
+
{!cert && ( -
+