diff --git a/backend/src/ee/routes/v1/certificate-authority-crl-router.ts b/backend/src/ee/routes/v1/certificate-authority-crl-router.ts index 468981c0e..f61706025 100644 --- a/backend/src/ee/routes/v1/certificate-authority-crl-router.ts +++ b/backend/src/ee/routes/v1/certificate-authority-crl-router.ts @@ -11,6 +11,30 @@ export const registerCaCrlRouter = async (server: FastifyZodProvider) => { config: { rateLimit: readLimit }, + schema: { + description: "Get CRL in DER format (deprecated)", + params: z.object({ + crlId: z.string().trim().describe(CA_CRLS.GET.crlId) + }), + response: { + 200: z.instanceof(Buffer) + } + }, + handler: async (req, res) => { + const { crl } = await server.services.certificateAuthorityCrl.getCrlById(req.params.crlId); + + res.header("Content-Type", "application/pkix-crl"); + + return Buffer.from(crl); + } + }); + + server.route({ + method: "GET", + url: "/:crlId/der", + config: { + rateLimit: readLimit + }, schema: { description: "Get CRL in DER format", params: z.object({ diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index ca71e5d41..5ed7ed8f2 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -1083,6 +1083,10 @@ export const CERTIFICATE_AUTHORITIES = { certificateChain: "The certificate chain of the CA", serialNumber: "The serial number of the CA certificate" }, + GET_CERT_BY_ID: { + caId: "The ID of the CA to get the CA certificate from", + caCertId: "The ID of the CA certificate to get" + }, GET_CA_CERTS: { caId: "The ID of the CA to get the CA certificates for", certificate: "The certificate body of the CA certificate", diff --git a/backend/src/server/routes/v1/certificate-authority-router.ts b/backend/src/server/routes/v1/certificate-authority-router.ts index 188eb28f3..88ec8500e 100644 --- a/backend/src/server/routes/v1/certificate-authority-router.ts +++ b/backend/src/server/routes/v1/certificate-authority-router.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-floating-promises */ import ms from "ms"; import { z } from "zod"; @@ -139,6 +140,33 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { } }); + // this endpoint will be used to serve the CA certificate when a client makes a request + // against the Authority Information Access CA Issuer URL + server.route({ + method: "GET", + url: "/:caId/certificates/:caCertId/der", + config: { + rateLimit: readLimit + }, + schema: { + description: "Get DER-encoded certificate of CA", + params: z.object({ + caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET_CERT_BY_ID.caId), + caCertId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET_CERT_BY_ID.caCertId) + }), + response: { + 200: z.instanceof(Buffer) + } + }, + handler: async (req, res) => { + const caCert = await server.services.certificateAuthority.getCaCertById(req.params); + + res.header("Content-Type", "application/pkix-cert"); + + return Buffer.from(caCert.rawData); + } + }); + server.route({ method: "PATCH", url: "/:caId", diff --git a/backend/src/services/certificate-authority/certificate-authority-fns.ts b/backend/src/services/certificate-authority/certificate-authority-fns.ts index 7330f029b..65ba57f07 100644 --- a/backend/src/services/certificate-authority/certificate-authority-fns.ts +++ b/backend/src/services/certificate-authority/certificate-authority-fns.ts @@ -15,7 +15,7 @@ import { /* eslint-disable no-bitwise */ export const createSerialNumber = () => { - const randomBytes = crypto.randomBytes(32); + const randomBytes = crypto.randomBytes(20); randomBytes[0] &= 0x7f; // ensure the first bit is 0 return randomBytes.toString("hex"); }; diff --git a/backend/src/services/certificate-authority/certificate-authority-service.ts b/backend/src/services/certificate-authority/certificate-authority-service.ts index f2a58922e..c7d32f1d3 100644 --- a/backend/src/services/certificate-authority/certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/certificate-authority-service.ts @@ -768,6 +768,39 @@ export const certificateAuthorityServiceFactory = ({ }; }; + /** + * Return CA certificate object by ID + */ + const getCaCertById = async ({ caId, caCertId }: { caId: string; caCertId: string }) => { + const caCert = await certificateAuthorityCertDAL.findOne({ + caId, + id: caCertId + }); + + if (!caCert) { + throw new NotFoundError({ message: "CA certificate not found" }); + } + + const ca = await certificateAuthorityDAL.findById(caId); + const keyId = await getProjectKmsCertificateKeyId({ + projectId: ca.projectId, + projectDAL, + kmsService + }); + + const kmsDecryptor = await kmsService.decryptWithKmsKey({ + kmsId: keyId + }); + + const decryptedCaCert = await kmsDecryptor({ + cipherTextBlob: caCert.encryptedCertificate + }); + + const caCertObj = new x509.X509Certificate(decryptedCaCert); + + return caCertObj; + }; + /** * Issue certificate to be imported back in for intermediate CA */ @@ -782,6 +815,7 @@ export const certificateAuthorityServiceFactory = ({ notAfter, maxPathLength }: TSignIntermediateDTO) => { + const appCfg = getConfig(); const ca = await certificateAuthorityDAL.findById(caId); if (!ca) throw new BadRequestError({ message: "CA not found" }); @@ -856,7 +890,7 @@ export const certificateAuthorityServiceFactory = ({ throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" }); } - const { caPrivateKey } = await getCaCredentials({ + const { caPrivateKey, caSecret } = await getCaCredentials({ caId: ca.id, certificateAuthorityDAL, certificateAuthoritySecretDAL, @@ -865,6 +899,11 @@ export const certificateAuthorityServiceFactory = ({ }); const serialNumber = createSerialNumber(); + + const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id }); + const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/pki/crl/${caCrl.id}/der`; + + const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/pki/ca/${ca.id}/certificates/${caCert.id}/der`; const intermediateCert = await x509.X509CertificateGenerator.create({ serialNumber, subject: csrObj.subject, @@ -884,7 +923,11 @@ export const certificateAuthorityServiceFactory = ({ ), new x509.BasicConstraintsExtension(true, maxPathLength === -1 ? undefined : maxPathLength, true), await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false), - await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey) + await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey), + new x509.CRLDistributionPointsExtension([distributionPointUrl]), + new x509.AuthorityInfoAccessExtension({ + caIssuers: new x509.GeneralName("url", caIssuerUrl) + }) ] }); @@ -1176,12 +1219,18 @@ export const certificateAuthorityServiceFactory = ({ const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id }); const appCfg = getConfig(); - const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/pki/crl/${caCrl.id}`; + const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/pki/crl/${caCrl.id}/der`; + const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/pki/ca/${ca.id}/certificates/${caCert.id}/der`; + const extensions: x509.Extension[] = [ new x509.BasicConstraintsExtension(false), new x509.CRLDistributionPointsExtension([distributionPointUrl]), await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false), - await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey) + await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey), + new x509.AuthorityInfoAccessExtension({ + caIssuers: new x509.GeneralName("url", caIssuerUrl) + }), + new x509.CertificatePolicyExtension(["2.5.29.32.0"]) // anyPolicy ]; // handle key usages @@ -1366,6 +1415,7 @@ export const certificateAuthorityServiceFactory = ({ * Note: CSR is generated externally and submitted to Infisical. */ const signCertFromCa = async (dto: TSignCertFromCaDTO) => { + const appCfg = getConfig(); let ca: TCertificateAuthorities | undefined; let certificateTemplate: TCertificateTemplates | undefined; @@ -1492,7 +1542,7 @@ export const certificateAuthorityServiceFactory = ({ message: "A common name (CN) is required in the CSR or as a parameter to this endpoint" }); - const { caPrivateKey } = await getCaCredentials({ + const { caPrivateKey, caSecret } = await getCaCredentials({ caId: ca.id, certificateAuthorityDAL, certificateAuthoritySecretDAL, @@ -1500,10 +1550,19 @@ export const certificateAuthorityServiceFactory = ({ kmsService }); + const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id }); + const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/pki/crl/${caCrl.id}/der`; + + const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/pki/ca/${ca.id}/certificates/${caCert.id}/der`; const extensions: x509.Extension[] = [ new x509.BasicConstraintsExtension(false), await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false), - await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey) + await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey), + new x509.CRLDistributionPointsExtension([distributionPointUrl]), + new x509.AuthorityInfoAccessExtension({ + caIssuers: new x509.GeneralName("url", caIssuerUrl) + }), + new x509.CertificatePolicyExtension(["2.5.29.32.0"]) // anyPolicy ]; // handle key usages @@ -1783,6 +1842,7 @@ export const certificateAuthorityServiceFactory = ({ renewCaCert, getCaCerts, getCaCert, + getCaCertById, signIntermediate, importCertToCa, issueCertFromCa,