From bda74ce13e6e8a4f8619a49ddc24cf61ee2045b7 Mon Sep 17 00:00:00 2001 From: x Date: Wed, 30 Apr 2025 20:20:31 -0400 Subject: [PATCH] logging, finalizing some functions, and other tweaks --- .../ee/services/audit-log/audit-log-types.ts | 11 ++ backend/src/server/routes/index.ts | 4 +- .../server/routes/v1/certificate-router.ts | 59 +++---- .../certificate-authority-service.ts | 4 +- .../services/certificate/certificate-fns.ts | 3 + .../certificate/certificate-service.ts | 150 +++++++++++------- .../services/certificate/certificate-types.ts | 4 +- .../pki-collection/pki-collection-service.ts | 8 +- .../src/services/project/project-service.ts | 6 +- .../secret-v2-bridge/secret-v2-bridge-dal.ts | 2 +- .../src/hooks/api/auditLogs/constants.tsx | 1 + frontend/src/hooks/api/auditLogs/enums.tsx | 1 + frontend/src/hooks/api/auditLogs/types.tsx | 9 ++ frontend/src/hooks/api/certificates/index.tsx | 2 +- frontend/src/hooks/api/certificates/types.ts | 6 +- .../layouts/ProjectLayout/ProjectLayout.tsx | 2 +- .../components/CertificateImportModal.tsx | 81 ++++------ .../components/CertificatesSection.tsx | 2 +- 18 files changed, 190 insertions(+), 165 deletions(-) diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index b6527f3f4..8ee701abc 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -209,6 +209,7 @@ export enum EventType { IMPORT_CA_CERT = "import-certificate-authority-cert", GET_CA_CRLS = "get-certificate-authority-crls", ISSUE_CERT = "issue-cert", + IMPORT_CERT = "import-cert", SIGN_CERT = "sign-cert", GET_CA_CERTIFICATE_TEMPLATES = "get-ca-certificate-templates", GET_CERT = "get-cert", @@ -1666,6 +1667,15 @@ interface IssueCert { }; } +interface ImportCert { + type: EventType.IMPORT_CERT; + metadata: { + certId: string; + cn: string; + serialNumber: string; + }; +} + interface SignCert { type: EventType.SIGN_CERT; metadata: { @@ -2685,6 +2695,7 @@ export type Event = | ImportCaCert | GetCaCrls | IssueCert + | ImportCert | SignCert | GetCaCertificateTemplates | GetCert diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index a71a69c20..d0b885961 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -805,7 +805,9 @@ export const registerRoutes = async ( certificateAuthoritySecretDAL, projectDAL, kmsService, - permissionService + permissionService, + pkiCollectionDAL, + pkiCollectionItemDAL }); const certificateAuthorityQueue = certificateAuthorityQueueFactory({ diff --git a/backend/src/server/routes/v1/certificate-router.ts b/backend/src/server/routes/v1/certificate-router.ts index 1975a4a44..6984aadcb 100644 --- a/backend/src/server/routes/v1/certificate-router.ts +++ b/backend/src/server/routes/v1/certificate-router.ts @@ -192,8 +192,8 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { projectSlug: z.string().trim().min(1).describe(CERTIFICATES.IMPORT.projectSlug), certificatePem: z.string().trim().min(1).describe(CERTIFICATES.IMPORT.certificatePem), - privateKeyPem: z.string().trim().optional().describe(CERTIFICATES.IMPORT.privateKeyPem), - chainPem: z.string().trim().optional().describe(CERTIFICATES.IMPORT.chainPem), + privateKeyPem: z.string().trim().describe(CERTIFICATES.IMPORT.privateKeyPem), + chainPem: z.string().trim().describe(CERTIFICATES.IMPORT.chainPem), friendlyName: z.string().trim().optional().describe(CERTIFICATES.IMPORT.friendlyName), pkiCollectionId: z.string().trim().optional().describe(CERTIFICATES.IMPORT.pkiCollectionId) @@ -201,46 +201,35 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { response: { 200: z.object({ certificate: z.string().trim().describe(CERTIFICATES.IMPORT.certificate), - certificateChain: z.string().trim().optional().describe(CERTIFICATES.IMPORT.certificateChain), - privateKey: z.string().trim().optional().describe(CERTIFICATES.IMPORT.privateKey), + certificateChain: z.string().trim().describe(CERTIFICATES.IMPORT.certificateChain), + privateKey: z.string().trim().describe(CERTIFICATES.IMPORT.privateKey), serialNumber: z.string().trim().describe(CERTIFICATES.IMPORT.serialNumber) }) } }, handler: async (req) => { - const { certificate, certificateChain, privateKey, serialNumber } = await server.services.certificate.importCert({ - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - ...req.body + const { certificate, certificateChain, privateKey, serialNumber, cert } = + await server.services.certificate.importCert({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: cert.projectId, + event: { + type: EventType.IMPORT_CERT, + metadata: { + certId: cert.id, + cn: cert.commonName, + serialNumber + } + } }); - // TODO(andrey): Add logs - // await server.services.auditLog.createAuditLog({ - // ...req.auditLogInfo, - // projectId: ca.projectId, - // event: { - // type: EventType.ISSUE_CERT, - // metadata: { - // caId: ca.id, - // dn: ca.dn, - // serialNumber - // } - // } - // }); - - // await server.services.telemetry.sendPostHogEvents({ - // event: PostHogEventTypes.IssueCert, - // distinctId: getTelemetryDistinctId(req), - // properties: { - // caId: req.body.caId, - // certificateTemplateId: req.body.certificateTemplateId, - // commonName: req.body.commonName, - // ...req.auditLogInfo - // } - // }); - return { certificate, certificateChain, diff --git a/backend/src/services/certificate-authority/certificate-authority-service.ts b/backend/src/services/certificate-authority/certificate-authority-service.ts index 0ab09e7fb..8d1d7f9cd 100644 --- a/backend/src/services/certificate-authority/certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/certificate-authority-service.ts @@ -1389,7 +1389,7 @@ export const certificateAuthorityServiceFactory = ({ notAfter: notAfterDate, keyUsages: selectedKeyUsages, extendedKeyUsages: selectedExtendedKeyUsages, - projectId: ca.projectId + projectId: (ca as TCertificateAuthorities).projectId }, tx ); @@ -1782,7 +1782,7 @@ export const certificateAuthorityServiceFactory = ({ notAfter: notAfterDate, keyUsages: selectedKeyUsages, extendedKeyUsages: selectedExtendedKeyUsages, - projectId: ca.projectId + projectId: (ca as TCertificateAuthorities).projectId }, tx ); diff --git a/backend/src/services/certificate/certificate-fns.ts b/backend/src/services/certificate/certificate-fns.ts index 45ad5963c..ff4e4394a 100644 --- a/backend/src/services/certificate/certificate-fns.ts +++ b/backend/src/services/certificate/certificate-fns.ts @@ -46,3 +46,6 @@ export const constructPemChainFromCerts = (certificates: x509.X509Certificate[]) .map((cert) => cert.toString("pem")) .join("\n") .trim(); + +export const splitPemChain = (pemText: string) => + pemText.match(/-----BEGIN CERTIFICATE-----[^-]+-----END CERTIFICATE-----/g) || []; diff --git a/backend/src/services/certificate/certificate-service.ts b/backend/src/services/certificate/certificate-service.ts index aa5a113f3..9ead0fd6f 100644 --- a/backend/src/services/certificate/certificate-service.ts +++ b/backend/src/services/certificate/certificate-service.ts @@ -19,8 +19,11 @@ import { TProjectDALFactory } from "@app/services/project/project-dal"; import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns"; import { getCaCertChain, rebuildCaCrl } from "../certificate-authority/certificate-authority-fns"; -import { revocationReasonToCrlCode } from "./certificate-fns"; +import { revocationReasonToCrlCode, splitPemChain } from "./certificate-fns"; import { + CertExtendedKeyUsage, + CertExtendedKeyUsageOIDToName, + CertKeyUsage, CertStatus, TDeleteCertDTO, TGetCertBodyDTO, @@ -273,46 +276,54 @@ export const certificateServiceFactory = ({ } // Parse the certificate - const certObj = new x509.X509Certificate(certificatePem); + const leafCert = new x509.X509Certificate(certificatePem); // Verify the certificate chain - if (chainPem) { - const chainCert = new x509.X509Certificate(chainPem); - if (!(await certObj.verify({ publicKey: chainCert.publicKey }))) { - throw new BadRequestError({ message: "Certificate chain verification failed" }); - } + const chainCerts = splitPemChain(chainPem).map((pem) => new x509.X509Certificate(pem)); + if (chainCerts.length === 0) { + throw new BadRequestError({ + message: "Certificate chain must contain at least one issuer certificate" + }); } - // If private key provided, verify it matches the certificate - if (privateKeyPem) { - try { - const message = Buffer.from("certificate-verification-test"); + const chainValidationPromises = chainCerts.map((issuerCert) => + leafCert.verify({ publicKey: issuerCert.publicKey }).catch(() => false) + ); - const privateKey = createPrivateKey(privateKeyPem); - const publicKey = createPublicKey(certificatePem); + const results = await Promise.all(chainValidationPromises); - const signature = sign(null, message, privateKey); - const isValid = verify(null, message, publicKey, signature); + if (!results.some((result) => result === true)) { + throw new BadRequestError({ message: "Certificate chain verification failed" }); + } - if (!isValid) { - throw new BadRequestError({ message: "Private key does not match certificate" }); - } - } catch (err) { - throw new BadRequestError({ message: "Invalid private key format" }); + // Verify private key matches the certificate + try { + const message = Buffer.from("certificate-verification-test"); + + const privateKey = createPrivateKey(privateKeyPem); + const publicKey = createPublicKey(certificatePem); + + const signature = sign(null, message, privateKey); + const isValid = verify(null, message, publicKey, signature); + + if (!isValid) { + throw new BadRequestError({ message: "Private key does not match certificate" }); } + } catch (err) { + throw new BadRequestError({ message: "Invalid private key format" }); } // Get certificate attributes - const commonName = Array.from(certObj.subjectName.getField("CN")?.values() || [])[0] || ""; + const commonName = Array.from(leafCert.subjectName.getField("CN")?.values() || [])[0] || ""; let altNames: undefined | string; - const sanExtension = certObj.extensions.find((ext) => ext.type === "2.5.29.17"); + const sanExtension = leafCert.extensions.find((ext) => ext.type === "2.5.29.17"); if (sanExtension) { const sanNames = new x509.GeneralNames(sanExtension.value); altNames = sanNames.items.map((name) => name.value).join(", "); } - const { serialNumber, notBefore, notAfter } = certObj; + const { serialNumber, notBefore, notAfter } = leafCert; // Encrypt certificate for storage const certificateManagerKeyId = await getProjectKmsCertificateKeyId({ @@ -328,50 +339,83 @@ export const certificateServiceFactory = ({ plainText: Buffer.from(certificatePem) }); - await certificateDAL.transaction(async (tx) => { - const cert = await certificateDAL.create( - { - status: CertStatus.ACTIVE, - friendlyName: friendlyName || commonName, - commonName, - altNames, - serialNumber, - notBefore, - notAfter, - projectId - // TODO(andrey): Add keyUsages and extendedKeyUsages - // keyUsages, - // extendedKeyUsages - }, - tx - ); + // Extract Key Usage + const keyUsagesExt = leafCert.getExtension("2.5.29.15") as x509.KeyUsagesExtension; - await certificateBodyDAL.create( - { - certId: cert.id, - encryptedCertificate - }, - tx + let keyUsages: CertKeyUsage[] = []; + if (keyUsagesExt) { + keyUsages = Object.values(CertKeyUsage).filter( + // eslint-disable-next-line no-bitwise + (keyUsage) => (x509.KeyUsageFlags[keyUsage] & keyUsagesExt.usages) !== 0 ); + } - if (collectionId) { - await pkiCollectionItemDAL.create( + // Extract Extended Key Usage + const extKeyUsageExt = leafCert.getExtension("2.5.29.37") as x509.ExtendedKeyUsageExtension; + let extendedKeyUsages: CertExtendedKeyUsage[] = []; + if (extKeyUsageExt) { + extendedKeyUsages = extKeyUsageExt.usages.map((ekuOid) => CertExtendedKeyUsageOIDToName[ekuOid as string]); + } + + const cert = await certificateDAL.transaction(async (tx) => { + try { + const txCert = await certificateDAL.create( { - pkiCollectionId: collectionId, - certId: cert.id + status: CertStatus.ACTIVE, + friendlyName: friendlyName || commonName, + commonName, + altNames, + serialNumber, + notBefore, + notAfter, + projectId, + keyUsages, + extendedKeyUsages }, tx ); - } - return cert; + await certificateBodyDAL.create( + { + certId: txCert.id, + encryptedCertificate + }, + tx + ); + + if (collectionId) { + await pkiCollectionItemDAL.create( + { + pkiCollectionId: collectionId, + certId: txCert.id + }, + tx + ); + } + + return txCert; + } catch (error: unknown) { + if ( + typeof error === "object" && + error !== null && + "error" in error && + error.error && + typeof error.error === "object" && + "code" in error.error && + error.error.code === "23505" + ) { + throw new BadRequestError({ message: "Certificate serial already exists in your project" }); + } + throw error; + } }); return { certificate: certificatePem, certificateChain: chainPem, privateKey: privateKeyPem, - serialNumber + serialNumber, + cert }; }; diff --git a/backend/src/services/certificate/certificate-types.ts b/backend/src/services/certificate/certificate-types.ts index 228597661..79c2cc538 100644 --- a/backend/src/services/certificate/certificate-types.ts +++ b/backend/src/services/certificate/certificate-types.ts @@ -81,6 +81,6 @@ export type TImportCertDTO = { pkiCollectionId?: string; certificatePem: string; - privateKeyPem?: string; - chainPem?: string; + privateKeyPem: string; + chainPem: string; } & Omit; diff --git a/backend/src/services/pki-collection/pki-collection-service.ts b/backend/src/services/pki-collection/pki-collection-service.ts index bee3ee621..577441bfb 100644 --- a/backend/src/services/pki-collection/pki-collection-service.ts +++ b/backend/src/services/pki-collection/pki-collection-service.ts @@ -269,14 +269,8 @@ export const pkiCollectionServiceFactory = ({ }); if (isCertAdded) throw new BadRequestError({ message: "Certificate already part of the PKI collection" }); - // validate that there exists a certificate in same project as PKI collection - const cas = await certificateAuthorityDAL.find({ projectId: pkiCollection.projectId }); - - // TODO: consider making this more efficient const [certificate] = await certificateDAL.find({ - $in: { - caId: cas.map((ca) => ca.id) - }, + projectId: pkiCollection.projectId, id: itemId }); if (!certificate) throw new NotFoundError({ message: `Certificate with ID '${itemId}' not found` }); diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index 9f2de8a85..5bb8c8b8c 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -929,13 +929,9 @@ export const projectServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates); - const cas = await certificateAuthorityDAL.find({ projectId }); - const certificates = await certificateDAL.find( { - $in: { - caId: cas.map((ca) => ca.id) - }, + projectId, ...(friendlyName && { friendlyName }), ...(commonName && { commonName }) }, diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts index 6ab348520..cd2773172 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts @@ -7,6 +7,7 @@ import { ProjectType, SecretsV2Schema, SecretType, TableName, TSecretsV2, TSecre import { TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig } from "@app/lib/config/env"; import { generateCacheKeyFromData } from "@app/lib/crypto/cache"; +import { applyJitter } from "@app/lib/dates"; import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors"; import { buildFindFilter, @@ -22,7 +23,6 @@ import type { TFindSecretsByFolderIdsFilter, TGetSecretsDTO } from "@app/services/secret-v2-bridge/secret-v2-bridge-types"; -import { applyJitter } from "@app/lib/dates"; export const SecretServiceCacheKeys = { get productKey() { diff --git a/frontend/src/hooks/api/auditLogs/constants.tsx b/frontend/src/hooks/api/auditLogs/constants.tsx index 229f822da..45167b74c 100644 --- a/frontend/src/hooks/api/auditLogs/constants.tsx +++ b/frontend/src/hooks/api/auditLogs/constants.tsx @@ -68,6 +68,7 @@ export const eventToNameMap: { [K in EventType]: string } = { [EventType.IMPORT_CA_CERT]: "Import CA certificate", [EventType.GET_CA_CRL]: "Get CA CRL", [EventType.ISSUE_CERT]: "Issue certificate", + [EventType.IMPORT_CERT]: "Import certificate", [EventType.GET_CERT]: "Get certificate", [EventType.DELETE_CERT]: "Delete certificate", [EventType.REVOKE_CERT]: "Revoke certificate", diff --git a/frontend/src/hooks/api/auditLogs/enums.tsx b/frontend/src/hooks/api/auditLogs/enums.tsx index d465fb820..455885896 100644 --- a/frontend/src/hooks/api/auditLogs/enums.tsx +++ b/frontend/src/hooks/api/auditLogs/enums.tsx @@ -74,6 +74,7 @@ export enum EventType { IMPORT_CA_CERT = "import-certificate-authority-cert", GET_CA_CRL = "get-certificate-authority-crl", ISSUE_CERT = "issue-cert", + IMPORT_CERT = "import-cert", GET_CERT = "get-cert", DELETE_CERT = "delete-cert", REVOKE_CERT = "revoke-cert", diff --git a/frontend/src/hooks/api/auditLogs/types.tsx b/frontend/src/hooks/api/auditLogs/types.tsx index 2524f84f4..1bbfb9aa8 100644 --- a/frontend/src/hooks/api/auditLogs/types.tsx +++ b/frontend/src/hooks/api/auditLogs/types.tsx @@ -582,6 +582,14 @@ interface IssueCert { serialNumber: string; }; } +interface ImportCert { + type: EventType.IMPORT_CERT; + metadata: { + certId: string; + cn: string; + serialNumber: string; + }; +} interface GetCert { type: EventType.GET_CERT; @@ -874,6 +882,7 @@ export type Event = | ImportCaCert | GetCaCrl | IssueCert + | ImportCert | GetCert | DeleteCert | RevokeCert diff --git a/frontend/src/hooks/api/certificates/index.tsx b/frontend/src/hooks/api/certificates/index.tsx index 2a79b647c..ddac04730 100644 --- a/frontend/src/hooks/api/certificates/index.tsx +++ b/frontend/src/hooks/api/certificates/index.tsx @@ -1,2 +1,2 @@ -export { useDeleteCert, useRevokeCert, useImportCertificate } from "./mutations"; +export { useDeleteCert, useImportCertificate, useRevokeCert } from "./mutations"; export { useGetCert, useGetCertBody } from "./queries"; diff --git a/frontend/src/hooks/api/certificates/types.ts b/frontend/src/hooks/api/certificates/types.ts index 263f9d2be..c1dd59eca 100644 --- a/frontend/src/hooks/api/certificates/types.ts +++ b/frontend/src/hooks/api/certificates/types.ts @@ -30,17 +30,15 @@ export type TImportCertificateDTO = { projectSlug: string; certificatePem: string; - privateKeyPem?: string; - chainPem?: string; + privateKeyPem: string; + chainPem: string; pkiCollectionId?: string; friendlyName?: string; }; -// TODO(andrey): Change this export type TImportCertificateResponse = { certificate: string; - issuingCertificate: string; certificateChain: string; privateKey: string; serialNumber: string; diff --git a/frontend/src/layouts/ProjectLayout/ProjectLayout.tsx b/frontend/src/layouts/ProjectLayout/ProjectLayout.tsx index a862c31a3..8533d7007 100644 --- a/frontend/src/layouts/ProjectLayout/ProjectLayout.tsx +++ b/frontend/src/layouts/ProjectLayout/ProjectLayout.tsx @@ -13,9 +13,9 @@ import { TBreadcrumbFormat } from "@app/components/v2"; import { - useProjectPermission, ProjectPermissionActions, ProjectPermissionSub, + useProjectPermission, useSubscription, useWorkspace } from "@app/context"; diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateImportModal.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateImportModal.tsx index 0c921f8fc..6bba7a862 100644 --- a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateImportModal.tsx +++ b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateImportModal.tsx @@ -15,43 +15,18 @@ import { TextArea } from "@app/components/v2"; import { useWorkspace } from "@app/context"; -import { useImportCertificate, useGetCert, useListWorkspacePkiCollections } from "@app/hooks/api"; +import { useGetCert, useImportCertificate, useListWorkspacePkiCollections } from "@app/hooks/api"; import { UsePopUpState } from "@app/hooks/usePopUp"; import { CertificateContent } from "./CertificateContent"; const schema = z.object({ certificatePem: z.string().trim().min(1, "Certificate PEM is required"), - privateKeyPem: z.string().trim().optional(), - chainPem: z.string().trim().optional(), + privateKeyPem: z.string().trim(), + chainPem: z.string().trim(), friendlyName: z.string(), collectionId: z.string().optional() - - // Can be added as override fields in the future | Also edit /frontend/src/hooks/api/ca/types.ts - // commonName: z.string().trim().min(1), - // altNames: z.string(), - - // Can be added as override fields in the future | Also edit /frontend/src/hooks/api/ca/types.ts - // 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; @@ -195,33 +170,14 @@ export const CertificateImportModal = ({ popUp, handlePopUpToggle }: Props) => { name="certificatePem" render={({ field, fieldState: { error } }) => (