mirror of
https://github.com/Infisical/infisical.git
synced 2025-03-31 22:09:57 +00:00
misc: moved est logic to service
This commit is contained in:
backend/src
@types
server/routes
services/certificate-est
2
backend/src/@types/fastify.d.ts
vendored
2
backend/src/@types/fastify.d.ts
vendored
@ -36,6 +36,7 @@ import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
|
||||
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
||||
import { TCertificateServiceFactory } from "@app/services/certificate/certificate-service";
|
||||
import { TCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service";
|
||||
import { TCertificateEstServiceFactory } from "@app/services/certificate-est/certificate-est-service";
|
||||
import { TCertificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
|
||||
import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service";
|
||||
import { TIdentityServiceFactory } from "@app/services/identity/identity-service";
|
||||
@ -160,6 +161,7 @@ declare module "fastify" {
|
||||
certificateTemplate: TCertificateTemplateServiceFactory;
|
||||
certificateAuthority: TCertificateAuthorityServiceFactory;
|
||||
certificateAuthorityCrl: TCertificateAuthorityCrlServiceFactory;
|
||||
certificateEst: TCertificateEstServiceFactory;
|
||||
pkiCollection: TPkiCollectionServiceFactory;
|
||||
secretScanning: TSecretScanningServiceFactory;
|
||||
license: TLicenseServiceFactory;
|
||||
|
@ -1,6 +1,4 @@
|
||||
import * as x509 from "@peculiar/x509";
|
||||
import bcrypt from "bcrypt";
|
||||
import { Certificate, ContentInfo, EncapsulatedContentInfo, SignedData } from "pkijs";
|
||||
import { z } from "zod";
|
||||
|
||||
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||
@ -17,7 +15,7 @@ export const registerCertificateEstRouter = async (server: FastifyZodProvider) =
|
||||
}
|
||||
});
|
||||
|
||||
// Authenticate EST client
|
||||
// Authenticate EST client using Passphrase
|
||||
server.addHook("onRequest", async (req, res) => {
|
||||
const { authorization } = req.headers;
|
||||
if (!authorization) {
|
||||
@ -47,64 +45,7 @@ export const registerCertificateEstRouter = async (server: FastifyZodProvider) =
|
||||
|
||||
if (!estConfig.isEnabled) {
|
||||
throw new BadRequestError({
|
||||
message: "EST enrollment is disabled"
|
||||
});
|
||||
}
|
||||
|
||||
const sslClientCert = req.headers["x-ssl-client-cert"] as string;
|
||||
const leafCertificate = decodeURIComponent(sslClientCert).match(
|
||||
/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g
|
||||
)?.[0];
|
||||
|
||||
if (!sslClientCert || !leafCertificate) {
|
||||
throw new UnauthorizedError({ message: "Missing client certificate" });
|
||||
}
|
||||
|
||||
const clientCertBody = leafCertificate
|
||||
.replace("-----BEGIN CERTIFICATE-----", "")
|
||||
.replace("-----END CERTIFICATE-----", "")
|
||||
.replace(/\n/g, "")
|
||||
.replace(/ /g, "")
|
||||
.trim();
|
||||
|
||||
// validate SSL client cert against configured CA
|
||||
const chainCerts = estConfig.caChain
|
||||
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
|
||||
?.map((cert) => {
|
||||
const processedBody = cert
|
||||
.replace("-----BEGIN CERTIFICATE-----", "")
|
||||
.replace("-----END CERTIFICATE-----", "")
|
||||
.replace(/\n/g, "")
|
||||
.replace(/ /g, "")
|
||||
.trim();
|
||||
|
||||
const certificateBuffer = Buffer.from(processedBody, "base64");
|
||||
return new x509.X509Certificate(certificateBuffer);
|
||||
});
|
||||
|
||||
if (!chainCerts) {
|
||||
throw new BadRequestError({ message: "Failed to parse certificate chain" });
|
||||
}
|
||||
|
||||
let isSslClientCertValid = true;
|
||||
let certToVerify = new x509.X509Certificate(clientCertBody);
|
||||
|
||||
for await (const issuerCert of chainCerts) {
|
||||
if (
|
||||
await certToVerify.verify({
|
||||
publicKey: issuerCert.publicKey,
|
||||
date: new Date()
|
||||
})
|
||||
) {
|
||||
certToVerify = issuerCert; // Move to the next certificate in the chain
|
||||
} else {
|
||||
isSslClientCertValid = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isSslClientCertValid) {
|
||||
throw new UnauthorizedError({
|
||||
message: "Invalid client certificate"
|
||||
message: "EST is disabled"
|
||||
});
|
||||
}
|
||||
|
||||
@ -137,42 +78,23 @@ export const registerCertificateEstRouter = async (server: FastifyZodProvider) =
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
body: z.string(),
|
||||
body: z.string().min(1),
|
||||
params: z.object({
|
||||
certificateTemplateId: z.string()
|
||||
certificateTemplateId: z.string().min(1)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({})
|
||||
}
|
||||
},
|
||||
handler: async (req, res) => {
|
||||
const { rawCertificate } = await server.services.certificateAuthority.signCertFromCa({
|
||||
isInternal: true,
|
||||
certificateTemplateId: req.params.certificateTemplateId,
|
||||
csr: req.body
|
||||
});
|
||||
|
||||
void res.header("Content-Type", "application/pkcs7-mime; smime-type=certs-only");
|
||||
void res.header("Content-Transfer-Encoding", "base64");
|
||||
|
||||
const cert = Certificate.fromBER(rawCertificate);
|
||||
const cmsSigned = new SignedData({
|
||||
encapContentInfo: new EncapsulatedContentInfo({
|
||||
eContentType: "1.2.840.113549.1.7.1" // not encrypted and not compressed data
|
||||
}),
|
||||
certificates: [cert]
|
||||
return server.services.certificateEst.simpleEnroll({
|
||||
csr: req.body,
|
||||
certificateTemplateId: req.params.certificateTemplateId,
|
||||
sslClientCert: req.headers["x-ssl-client-cert"] as string
|
||||
});
|
||||
|
||||
const cmsContent = new ContentInfo({
|
||||
contentType: "1.2.840.113549.1.7.2", // SignedData
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
content: cmsSigned.toSchema()
|
||||
});
|
||||
|
||||
const derBuffer = cmsContent.toSchema().toBER(false);
|
||||
const base64Pkcs7 = Buffer.from(derBuffer).toString("base64");
|
||||
|
||||
return base64Pkcs7;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -89,6 +89,7 @@ import { certificateAuthorityDALFactory } from "@app/services/certificate-author
|
||||
import { certificateAuthorityQueueFactory } from "@app/services/certificate-authority/certificate-authority-queue";
|
||||
import { certificateAuthoritySecretDALFactory } from "@app/services/certificate-authority/certificate-authority-secret-dal";
|
||||
import { certificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service";
|
||||
import { certificateEstServiceFactory } from "@app/services/certificate-est/certificate-est-service";
|
||||
import { certificateTemplateDALFactory } from "@app/services/certificate-template/certificate-template-dal";
|
||||
import { certificateTemplateEstConfigDALFactory } from "@app/services/certificate-template/certificate-template-est-config-dal";
|
||||
import { certificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
|
||||
@ -657,6 +658,11 @@ export const registerRoutes = async (
|
||||
projectDAL
|
||||
});
|
||||
|
||||
const certificateEstService = certificateEstServiceFactory({
|
||||
certificateAuthorityService,
|
||||
certificateTemplateService
|
||||
});
|
||||
|
||||
const pkiAlertService = pkiAlertServiceFactory({
|
||||
pkiAlertDAL,
|
||||
pkiCollectionDAL,
|
||||
@ -1179,6 +1185,7 @@ export const registerRoutes = async (
|
||||
certificateAuthority: certificateAuthorityService,
|
||||
certificateTemplate: certificateTemplateService,
|
||||
certificateAuthorityCrl: certificateAuthorityCrlService,
|
||||
certificateEst: certificateEstService,
|
||||
pkiAlert: pkiAlertService,
|
||||
pkiCollection: pkiCollectionService,
|
||||
secretScanning: secretScanningService,
|
||||
|
127
backend/src/services/certificate-est/certificate-est-service.ts
Normal file
127
backend/src/services/certificate-est/certificate-est-service.ts
Normal file
@ -0,0 +1,127 @@
|
||||
import * as x509 from "@peculiar/x509";
|
||||
import { Certificate, ContentInfo, EncapsulatedContentInfo, SignedData } from "pkijs";
|
||||
|
||||
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||
|
||||
import { TCertificateAuthorityServiceFactory } from "../certificate-authority/certificate-authority-service";
|
||||
import { TCertificateTemplateServiceFactory } from "../certificate-template/certificate-template-service";
|
||||
|
||||
type TCertificateEstServiceFactoryDep = {
|
||||
certificateAuthorityService: Pick<TCertificateAuthorityServiceFactory, "signCertFromCa">;
|
||||
certificateTemplateService: Pick<TCertificateTemplateServiceFactory, "getEstConfiguration">;
|
||||
};
|
||||
|
||||
export type TCertificateEstServiceFactory = ReturnType<typeof certificateEstServiceFactory>;
|
||||
|
||||
export const certificateEstServiceFactory = ({
|
||||
certificateAuthorityService,
|
||||
certificateTemplateService
|
||||
}: TCertificateEstServiceFactoryDep) => {
|
||||
const simpleEnroll = async ({
|
||||
csr,
|
||||
certificateTemplateId,
|
||||
sslClientCert
|
||||
}: {
|
||||
csr: string;
|
||||
certificateTemplateId: string;
|
||||
sslClientCert: string;
|
||||
}) => {
|
||||
/* We first have to assert that the client certificate provided can be traced back to the attached
|
||||
CA chain in the EST configuration
|
||||
*/
|
||||
const estConfig = await certificateTemplateService.getEstConfiguration({
|
||||
isInternal: true,
|
||||
certificateTemplateId
|
||||
});
|
||||
|
||||
if (!estConfig.isEnabled) {
|
||||
throw new BadRequestError({
|
||||
message: "EST is disabled"
|
||||
});
|
||||
}
|
||||
|
||||
const leafCertificate = decodeURIComponent(sslClientCert).match(
|
||||
/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g
|
||||
)?.[0];
|
||||
|
||||
if (!sslClientCert || !leafCertificate) {
|
||||
throw new UnauthorizedError({ message: "Missing client certificate" });
|
||||
}
|
||||
|
||||
const clientCertBody = leafCertificate
|
||||
.replace("-----BEGIN CERTIFICATE-----", "")
|
||||
.replace("-----END CERTIFICATE-----", "")
|
||||
.replace(/\n/g, "")
|
||||
.replace(/ /g, "")
|
||||
.trim();
|
||||
|
||||
// validate SSL client cert against configured CA
|
||||
const chainCerts = estConfig.caChain
|
||||
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
|
||||
?.map((cert) => {
|
||||
const processedBody = cert
|
||||
.replace("-----BEGIN CERTIFICATE-----", "")
|
||||
.replace("-----END CERTIFICATE-----", "")
|
||||
.replace(/\n/g, "")
|
||||
.replace(/ /g, "")
|
||||
.trim();
|
||||
|
||||
const certificateBuffer = Buffer.from(processedBody, "base64");
|
||||
return new x509.X509Certificate(certificateBuffer);
|
||||
});
|
||||
|
||||
if (!chainCerts) {
|
||||
throw new BadRequestError({ message: "Failed to parse certificate chain" });
|
||||
}
|
||||
|
||||
let isSslClientCertValid = true;
|
||||
let certToVerify = new x509.X509Certificate(clientCertBody);
|
||||
|
||||
for await (const issuerCert of chainCerts) {
|
||||
if (
|
||||
await certToVerify.verify({
|
||||
publicKey: issuerCert.publicKey,
|
||||
date: new Date()
|
||||
})
|
||||
) {
|
||||
certToVerify = issuerCert; // Move to the next certificate in the chain
|
||||
} else {
|
||||
isSslClientCertValid = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isSslClientCertValid) {
|
||||
throw new UnauthorizedError({
|
||||
message: "Invalid client certificate"
|
||||
});
|
||||
}
|
||||
|
||||
const { rawCertificate } = await certificateAuthorityService.signCertFromCa({
|
||||
isInternal: true,
|
||||
certificateTemplateId,
|
||||
csr
|
||||
});
|
||||
|
||||
const cert = Certificate.fromBER(rawCertificate);
|
||||
const cmsSigned = new SignedData({
|
||||
encapContentInfo: new EncapsulatedContentInfo({
|
||||
eContentType: "1.2.840.113549.1.7.1" // not encrypted and not compressed data
|
||||
}),
|
||||
certificates: [cert]
|
||||
});
|
||||
|
||||
const cmsContent = new ContentInfo({
|
||||
contentType: "1.2.840.113549.1.7.2", // SignedData
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
content: cmsSigned.toSchema()
|
||||
});
|
||||
|
||||
const derBuffer = cmsContent.toSchema().toBER(false);
|
||||
const base64Pkcs7 = Buffer.from(derBuffer).toString("base64");
|
||||
|
||||
return base64Pkcs7;
|
||||
};
|
||||
return {
|
||||
simpleEnroll
|
||||
};
|
||||
};
|
Reference in New Issue
Block a user