logging, finalizing some functions, and other tweaks

This commit is contained in:
x
2025-04-30 20:20:31 -04:00
parent 6a973be6f3
commit bda74ce13e
18 changed files with 190 additions and 165 deletions
@@ -209,6 +209,7 @@ export enum EventType {
IMPORT_CA_CERT = "import-certificate-authority-cert", IMPORT_CA_CERT = "import-certificate-authority-cert",
GET_CA_CRLS = "get-certificate-authority-crls", GET_CA_CRLS = "get-certificate-authority-crls",
ISSUE_CERT = "issue-cert", ISSUE_CERT = "issue-cert",
IMPORT_CERT = "import-cert",
SIGN_CERT = "sign-cert", SIGN_CERT = "sign-cert",
GET_CA_CERTIFICATE_TEMPLATES = "get-ca-certificate-templates", GET_CA_CERTIFICATE_TEMPLATES = "get-ca-certificate-templates",
GET_CERT = "get-cert", 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 { interface SignCert {
type: EventType.SIGN_CERT; type: EventType.SIGN_CERT;
metadata: { metadata: {
@@ -2685,6 +2695,7 @@ export type Event =
| ImportCaCert | ImportCaCert
| GetCaCrls | GetCaCrls
| IssueCert | IssueCert
| ImportCert
| SignCert | SignCert
| GetCaCertificateTemplates | GetCaCertificateTemplates
| GetCert | GetCert
+3 -1
View File
@@ -805,7 +805,9 @@ export const registerRoutes = async (
certificateAuthoritySecretDAL, certificateAuthoritySecretDAL,
projectDAL, projectDAL,
kmsService, kmsService,
permissionService permissionService,
pkiCollectionDAL,
pkiCollectionItemDAL
}); });
const certificateAuthorityQueue = certificateAuthorityQueueFactory({ const certificateAuthorityQueue = certificateAuthorityQueueFactory({
@@ -192,8 +192,8 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
projectSlug: z.string().trim().min(1).describe(CERTIFICATES.IMPORT.projectSlug), projectSlug: z.string().trim().min(1).describe(CERTIFICATES.IMPORT.projectSlug),
certificatePem: z.string().trim().min(1).describe(CERTIFICATES.IMPORT.certificatePem), certificatePem: z.string().trim().min(1).describe(CERTIFICATES.IMPORT.certificatePem),
privateKeyPem: z.string().trim().optional().describe(CERTIFICATES.IMPORT.privateKeyPem), privateKeyPem: z.string().trim().describe(CERTIFICATES.IMPORT.privateKeyPem),
chainPem: z.string().trim().optional().describe(CERTIFICATES.IMPORT.chainPem), chainPem: z.string().trim().describe(CERTIFICATES.IMPORT.chainPem),
friendlyName: z.string().trim().optional().describe(CERTIFICATES.IMPORT.friendlyName), friendlyName: z.string().trim().optional().describe(CERTIFICATES.IMPORT.friendlyName),
pkiCollectionId: z.string().trim().optional().describe(CERTIFICATES.IMPORT.pkiCollectionId) pkiCollectionId: z.string().trim().optional().describe(CERTIFICATES.IMPORT.pkiCollectionId)
@@ -201,14 +201,15 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
response: { response: {
200: z.object({ 200: z.object({
certificate: z.string().trim().describe(CERTIFICATES.IMPORT.certificate), certificate: z.string().trim().describe(CERTIFICATES.IMPORT.certificate),
certificateChain: z.string().trim().optional().describe(CERTIFICATES.IMPORT.certificateChain), certificateChain: z.string().trim().describe(CERTIFICATES.IMPORT.certificateChain),
privateKey: z.string().trim().optional().describe(CERTIFICATES.IMPORT.privateKey), privateKey: z.string().trim().describe(CERTIFICATES.IMPORT.privateKey),
serialNumber: z.string().trim().describe(CERTIFICATES.IMPORT.serialNumber) serialNumber: z.string().trim().describe(CERTIFICATES.IMPORT.serialNumber)
}) })
} }
}, },
handler: async (req) => { handler: async (req) => {
const { certificate, certificateChain, privateKey, serialNumber } = await server.services.certificate.importCert({ const { certificate, certificateChain, privateKey, serialNumber, cert } =
await server.services.certificate.importCert({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
@@ -216,30 +217,18 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
...req.body ...req.body
}); });
// TODO(andrey): Add logs await server.services.auditLog.createAuditLog({
// await server.services.auditLog.createAuditLog({ ...req.auditLogInfo,
// ...req.auditLogInfo, projectId: cert.projectId,
// projectId: ca.projectId, event: {
// event: { type: EventType.IMPORT_CERT,
// type: EventType.ISSUE_CERT, metadata: {
// metadata: { certId: cert.id,
// caId: ca.id, cn: cert.commonName,
// dn: ca.dn, serialNumber
// 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 { return {
certificate, certificate,
@@ -1389,7 +1389,7 @@ export const certificateAuthorityServiceFactory = ({
notAfter: notAfterDate, notAfter: notAfterDate,
keyUsages: selectedKeyUsages, keyUsages: selectedKeyUsages,
extendedKeyUsages: selectedExtendedKeyUsages, extendedKeyUsages: selectedExtendedKeyUsages,
projectId: ca.projectId projectId: (ca as TCertificateAuthorities).projectId
}, },
tx tx
); );
@@ -1782,7 +1782,7 @@ export const certificateAuthorityServiceFactory = ({
notAfter: notAfterDate, notAfter: notAfterDate,
keyUsages: selectedKeyUsages, keyUsages: selectedKeyUsages,
extendedKeyUsages: selectedExtendedKeyUsages, extendedKeyUsages: selectedExtendedKeyUsages,
projectId: ca.projectId projectId: (ca as TCertificateAuthorities).projectId
}, },
tx tx
); );
@@ -46,3 +46,6 @@ export const constructPemChainFromCerts = (certificates: x509.X509Certificate[])
.map((cert) => cert.toString("pem")) .map((cert) => cert.toString("pem"))
.join("\n") .join("\n")
.trim(); .trim();
export const splitPemChain = (pemText: string) =>
pemText.match(/-----BEGIN CERTIFICATE-----[^-]+-----END CERTIFICATE-----/g) || [];
@@ -19,8 +19,11 @@ import { TProjectDALFactory } from "@app/services/project/project-dal";
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns"; import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
import { getCaCertChain, rebuildCaCrl } from "../certificate-authority/certificate-authority-fns"; import { getCaCertChain, rebuildCaCrl } from "../certificate-authority/certificate-authority-fns";
import { revocationReasonToCrlCode } from "./certificate-fns"; import { revocationReasonToCrlCode, splitPemChain } from "./certificate-fns";
import { import {
CertExtendedKeyUsage,
CertExtendedKeyUsageOIDToName,
CertKeyUsage,
CertStatus, CertStatus,
TDeleteCertDTO, TDeleteCertDTO,
TGetCertBodyDTO, TGetCertBodyDTO,
@@ -273,18 +276,27 @@ export const certificateServiceFactory = ({
} }
// Parse the certificate // Parse the certificate
const certObj = new x509.X509Certificate(certificatePem); const leafCert = new x509.X509Certificate(certificatePem);
// Verify the certificate chain // Verify the certificate chain
if (chainPem) { const chainCerts = splitPemChain(chainPem).map((pem) => new x509.X509Certificate(pem));
const chainCert = new x509.X509Certificate(chainPem); if (chainCerts.length === 0) {
if (!(await certObj.verify({ publicKey: chainCert.publicKey }))) { throw new BadRequestError({
throw new BadRequestError({ message: "Certificate chain verification failed" }); message: "Certificate chain must contain at least one issuer certificate"
} });
} }
// If private key provided, verify it matches the certificate const chainValidationPromises = chainCerts.map((issuerCert) =>
if (privateKeyPem) { leafCert.verify({ publicKey: issuerCert.publicKey }).catch(() => false)
);
const results = await Promise.all(chainValidationPromises);
if (!results.some((result) => result === true)) {
throw new BadRequestError({ message: "Certificate chain verification failed" });
}
// Verify private key matches the certificate
try { try {
const message = Buffer.from("certificate-verification-test"); const message = Buffer.from("certificate-verification-test");
@@ -300,19 +312,18 @@ export const certificateServiceFactory = ({
} catch (err) { } catch (err) {
throw new BadRequestError({ message: "Invalid private key format" }); throw new BadRequestError({ message: "Invalid private key format" });
} }
}
// Get certificate attributes // 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; 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) { if (sanExtension) {
const sanNames = new x509.GeneralNames(sanExtension.value); const sanNames = new x509.GeneralNames(sanExtension.value);
altNames = sanNames.items.map((name) => name.value).join(", "); altNames = sanNames.items.map((name) => name.value).join(", ");
} }
const { serialNumber, notBefore, notAfter } = certObj; const { serialNumber, notBefore, notAfter } = leafCert;
// Encrypt certificate for storage // Encrypt certificate for storage
const certificateManagerKeyId = await getProjectKmsCertificateKeyId({ const certificateManagerKeyId = await getProjectKmsCertificateKeyId({
@@ -328,8 +339,27 @@ export const certificateServiceFactory = ({
plainText: Buffer.from(certificatePem) plainText: Buffer.from(certificatePem)
}); });
await certificateDAL.transaction(async (tx) => { // Extract Key Usage
const cert = await certificateDAL.create( const keyUsagesExt = leafCert.getExtension("2.5.29.15") as x509.KeyUsagesExtension;
let keyUsages: CertKeyUsage[] = [];
if (keyUsagesExt) {
keyUsages = Object.values(CertKeyUsage).filter(
// eslint-disable-next-line no-bitwise
(keyUsage) => (x509.KeyUsageFlags[keyUsage] & keyUsagesExt.usages) !== 0
);
}
// 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(
{ {
status: CertStatus.ACTIVE, status: CertStatus.ACTIVE,
friendlyName: friendlyName || commonName, friendlyName: friendlyName || commonName,
@@ -338,17 +368,16 @@ export const certificateServiceFactory = ({
serialNumber, serialNumber,
notBefore, notBefore,
notAfter, notAfter,
projectId projectId,
// TODO(andrey): Add keyUsages and extendedKeyUsages keyUsages,
// keyUsages, extendedKeyUsages
// extendedKeyUsages
}, },
tx tx
); );
await certificateBodyDAL.create( await certificateBodyDAL.create(
{ {
certId: cert.id, certId: txCert.id,
encryptedCertificate encryptedCertificate
}, },
tx tx
@@ -358,20 +387,35 @@ export const certificateServiceFactory = ({
await pkiCollectionItemDAL.create( await pkiCollectionItemDAL.create(
{ {
pkiCollectionId: collectionId, pkiCollectionId: collectionId,
certId: cert.id certId: txCert.id
}, },
tx tx
); );
} }
return cert; 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 { return {
certificate: certificatePem, certificate: certificatePem,
certificateChain: chainPem, certificateChain: chainPem,
privateKey: privateKeyPem, privateKey: privateKeyPem,
serialNumber serialNumber,
cert
}; };
}; };
@@ -81,6 +81,6 @@ export type TImportCertDTO = {
pkiCollectionId?: string; pkiCollectionId?: string;
certificatePem: string; certificatePem: string;
privateKeyPem?: string; privateKeyPem: string;
chainPem?: string; chainPem: string;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
@@ -269,14 +269,8 @@ export const pkiCollectionServiceFactory = ({
}); });
if (isCertAdded) throw new BadRequestError({ message: "Certificate already part of the PKI collection" }); 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({ const [certificate] = await certificateDAL.find({
$in: { projectId: pkiCollection.projectId,
caId: cas.map((ca) => ca.id)
},
id: itemId id: itemId
}); });
if (!certificate) throw new NotFoundError({ message: `Certificate with ID '${itemId}' not found` }); if (!certificate) throw new NotFoundError({ message: `Certificate with ID '${itemId}' not found` });
@@ -929,13 +929,9 @@ export const projectServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
const cas = await certificateAuthorityDAL.find({ projectId });
const certificates = await certificateDAL.find( const certificates = await certificateDAL.find(
{ {
$in: { projectId,
caId: cas.map((ca) => ca.id)
},
...(friendlyName && { friendlyName }), ...(friendlyName && { friendlyName }),
...(commonName && { commonName }) ...(commonName && { commonName })
}, },
@@ -7,6 +7,7 @@ import { ProjectType, SecretsV2Schema, SecretType, TableName, TSecretsV2, TSecre
import { TKeyStoreFactory } from "@app/keystore/keystore"; import { TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { generateCacheKeyFromData } from "@app/lib/crypto/cache"; import { generateCacheKeyFromData } from "@app/lib/crypto/cache";
import { applyJitter } from "@app/lib/dates";
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
import { import {
buildFindFilter, buildFindFilter,
@@ -22,7 +23,6 @@ import type {
TFindSecretsByFolderIdsFilter, TFindSecretsByFolderIdsFilter,
TGetSecretsDTO TGetSecretsDTO
} from "@app/services/secret-v2-bridge/secret-v2-bridge-types"; } from "@app/services/secret-v2-bridge/secret-v2-bridge-types";
import { applyJitter } from "@app/lib/dates";
export const SecretServiceCacheKeys = { export const SecretServiceCacheKeys = {
get productKey() { get productKey() {
@@ -68,6 +68,7 @@ export const eventToNameMap: { [K in EventType]: string } = {
[EventType.IMPORT_CA_CERT]: "Import CA certificate", [EventType.IMPORT_CA_CERT]: "Import CA certificate",
[EventType.GET_CA_CRL]: "Get CA CRL", [EventType.GET_CA_CRL]: "Get CA CRL",
[EventType.ISSUE_CERT]: "Issue certificate", [EventType.ISSUE_CERT]: "Issue certificate",
[EventType.IMPORT_CERT]: "Import certificate",
[EventType.GET_CERT]: "Get certificate", [EventType.GET_CERT]: "Get certificate",
[EventType.DELETE_CERT]: "Delete certificate", [EventType.DELETE_CERT]: "Delete certificate",
[EventType.REVOKE_CERT]: "Revoke certificate", [EventType.REVOKE_CERT]: "Revoke certificate",
@@ -74,6 +74,7 @@ export enum EventType {
IMPORT_CA_CERT = "import-certificate-authority-cert", IMPORT_CA_CERT = "import-certificate-authority-cert",
GET_CA_CRL = "get-certificate-authority-crl", GET_CA_CRL = "get-certificate-authority-crl",
ISSUE_CERT = "issue-cert", ISSUE_CERT = "issue-cert",
IMPORT_CERT = "import-cert",
GET_CERT = "get-cert", GET_CERT = "get-cert",
DELETE_CERT = "delete-cert", DELETE_CERT = "delete-cert",
REVOKE_CERT = "revoke-cert", REVOKE_CERT = "revoke-cert",
@@ -582,6 +582,14 @@ interface IssueCert {
serialNumber: string; serialNumber: string;
}; };
} }
interface ImportCert {
type: EventType.IMPORT_CERT;
metadata: {
certId: string;
cn: string;
serialNumber: string;
};
}
interface GetCert { interface GetCert {
type: EventType.GET_CERT; type: EventType.GET_CERT;
@@ -874,6 +882,7 @@ export type Event =
| ImportCaCert | ImportCaCert
| GetCaCrl | GetCaCrl
| IssueCert | IssueCert
| ImportCert
| GetCert | GetCert
| DeleteCert | DeleteCert
| RevokeCert | RevokeCert
@@ -1,2 +1,2 @@
export { useDeleteCert, useRevokeCert, useImportCertificate } from "./mutations"; export { useDeleteCert, useImportCertificate, useRevokeCert } from "./mutations";
export { useGetCert, useGetCertBody } from "./queries"; export { useGetCert, useGetCertBody } from "./queries";
+2 -4
View File
@@ -30,17 +30,15 @@ export type TImportCertificateDTO = {
projectSlug: string; projectSlug: string;
certificatePem: string; certificatePem: string;
privateKeyPem?: string; privateKeyPem: string;
chainPem?: string; chainPem: string;
pkiCollectionId?: string; pkiCollectionId?: string;
friendlyName?: string; friendlyName?: string;
}; };
// TODO(andrey): Change this
export type TImportCertificateResponse = { export type TImportCertificateResponse = {
certificate: string; certificate: string;
issuingCertificate: string;
certificateChain: string; certificateChain: string;
privateKey: string; privateKey: string;
serialNumber: string; serialNumber: string;
@@ -13,9 +13,9 @@ import {
TBreadcrumbFormat TBreadcrumbFormat
} from "@app/components/v2"; } from "@app/components/v2";
import { import {
useProjectPermission,
ProjectPermissionActions, ProjectPermissionActions,
ProjectPermissionSub, ProjectPermissionSub,
useProjectPermission,
useSubscription, useSubscription,
useWorkspace useWorkspace
} from "@app/context"; } from "@app/context";
@@ -15,43 +15,18 @@ import {
TextArea TextArea
} from "@app/components/v2"; } from "@app/components/v2";
import { useWorkspace } from "@app/context"; 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 { UsePopUpState } from "@app/hooks/usePopUp";
import { CertificateContent } from "./CertificateContent"; import { CertificateContent } from "./CertificateContent";
const schema = z.object({ const schema = z.object({
certificatePem: z.string().trim().min(1, "Certificate PEM is required"), certificatePem: z.string().trim().min(1, "Certificate PEM is required"),
privateKeyPem: z.string().trim().optional(), privateKeyPem: z.string().trim(),
chainPem: z.string().trim().optional(), chainPem: z.string().trim(),
friendlyName: z.string(), friendlyName: z.string(),
collectionId: z.string().optional() 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<typeof schema>; export type FormData = z.infer<typeof schema>;
@@ -195,33 +170,14 @@ export const CertificateImportModal = ({ popUp, handlePopUpToggle }: Props) => {
name="certificatePem" name="certificatePem"
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => (
<FormControl <FormControl
label="Certificate PEM" label="Leaf Certificate PEM"
isError={Boolean(error)} isError={Boolean(error)}
errorText={error?.message}
isRequired isRequired
errorText={error?.message}
> >
<TextArea <TextArea
{...field} {...field}
placeholder="TODO(andrey): Pem placeholder" placeholder={"-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"}
isDisabled={Boolean(cert)}
/>
</FormControl>
)}
/>
<Controller
control={control}
defaultValue=""
name="chainPem"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Certificate Chain PEM"
isError={Boolean(error)}
isOptional
errorText={error?.message}
>
<TextArea
{...field}
placeholder="TODO(andrey): Pem placeholder"
isDisabled={Boolean(cert)} isDisabled={Boolean(cert)}
/> />
</FormControl> </FormControl>
@@ -235,12 +191,33 @@ export const CertificateImportModal = ({ popUp, handlePopUpToggle }: Props) => {
<FormControl <FormControl
label="Private Key PEM" label="Private Key PEM"
isError={Boolean(error)} isError={Boolean(error)}
isOptional
errorText={error?.message} errorText={error?.message}
isRequired
> >
<TextArea <TextArea
{...field} {...field}
placeholder="TODO(andrey): Pem placeholder" placeholder={
"-----BEGIN EC PRIVATE KEY-----\n...\n-----END EC PRIVATE KEY-----"
}
isDisabled={Boolean(cert)}
/>
</FormControl>
)}
/>
<Controller
control={control}
defaultValue=""
name="chainPem"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Certificate Chain PEM"
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<TextArea
{...field}
placeholder={"-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"}
isDisabled={Boolean(cert)} isDisabled={Boolean(cert)}
/> />
</FormControl> </FormControl>
@@ -9,10 +9,10 @@ import { useDeleteCert } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp"; import { usePopUp } from "@app/hooks/usePopUp";
import { CertificateCertModal } from "./CertificateCertModal"; import { CertificateCertModal } from "./CertificateCertModal";
import { CertificateImportModal } from "./CertificateImportModal";
import { CertificateModal } from "./CertificateModal"; import { CertificateModal } from "./CertificateModal";
import { CertificateRevocationModal } from "./CertificateRevocationModal"; import { CertificateRevocationModal } from "./CertificateRevocationModal";
import { CertificatesTable } from "./CertificatesTable"; import { CertificatesTable } from "./CertificatesTable";
import { CertificateImportModal } from "./CertificateImportModal";
export const CertificatesSection = () => { export const CertificatesSection = () => {
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();