feat: added simplereenroll

This commit is contained in:
Sheen Capadngan
2024-08-20 23:56:27 +08:00
parent 2d7ff66246
commit 760a1e917a
4 changed files with 225 additions and 41 deletions

View File

@@ -83,7 +83,7 @@ export const registerCertificateEstRouter = async (server: FastifyZodProvider) =
certificateTemplateId: z.string().min(1)
}),
response: {
200: z.object({})
200: z.string()
}
},
handler: async (req, res) => {
@@ -97,4 +97,31 @@ export const registerCertificateEstRouter = async (server: FastifyZodProvider) =
});
}
});
server.route({
method: "POST",
url: "/:certificateTemplateId/simplereenroll",
config: {
rateLimit: writeLimit
},
schema: {
body: z.string().min(1),
params: z.object({
certificateTemplateId: z.string().min(1)
}),
response: {
200: z.string()
}
},
handler: async (req, res) => {
void res.header("Content-Type", "application/pkcs7-mime; smime-type=certs-only");
void res.header("Content-Transfer-Encoding", "base64");
return server.services.certificateEst.simpleReenroll({
csr: req.body,
certificateTemplateId: req.params.certificateTemplateId,
sslClientCert: req.headers["x-ssl-client-cert"] as string
});
}
});
};

View File

@@ -660,7 +660,12 @@ export const registerRoutes = async (
const certificateEstService = certificateEstServiceFactory({
certificateAuthorityService,
certificateTemplateService
certificateTemplateService,
certificateTemplateDAL,
certificateAuthorityCertDAL,
certificateAuthorityDAL,
projectDAL,
kmsService
});
const pkiAlertService = pkiAlertServiceFactory({

View File

@@ -0,0 +1,42 @@
import { X509Certificate } from "@peculiar/x509";
import { Certificate, ContentInfo, EncapsulatedContentInfo, SignedData } from "pkijs";
export const convertRawCertToPkcs7 = (rawCertificate: ArrayBuffer) => {
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;
};
export const checkCertValidityAgainstChain = async (cert: X509Certificate, chainCerts: X509Certificate[]) => {
let isSslClientCertValid = true;
let certToVerify = cert;
for await (const issuerCert of chainCerts) {
if (
await certToVerify.verify({
publicKey: issuerCert.publicKey
})
) {
certToVerify = issuerCert; // Move to the next certificate in the chain
} else {
isSslClientCertValid = false;
}
}
return isSslClientCertValid;
};

View File

@@ -1,22 +1,163 @@
import * as x509 from "@peculiar/x509";
import { Certificate, ContentInfo, EncapsulatedContentInfo, SignedData } from "pkijs";
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
import { TCertificateAuthorityCertDALFactory } from "../certificate-authority/certificate-authority-cert-dal";
import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal";
import { getCaCertChains } from "../certificate-authority/certificate-authority-fns";
import { TCertificateAuthorityServiceFactory } from "../certificate-authority/certificate-authority-service";
import { TCertificateTemplateDALFactory } from "../certificate-template/certificate-template-dal";
import { TCertificateTemplateServiceFactory } from "../certificate-template/certificate-template-service";
import { TKmsServiceFactory } from "../kms/kms-service";
import { TProjectDALFactory } from "../project/project-dal";
import { checkCertValidityAgainstChain, convertRawCertToPkcs7 } from "./certificate-est-fns";
type TCertificateEstServiceFactoryDep = {
certificateAuthorityService: Pick<TCertificateAuthorityServiceFactory, "signCertFromCa">;
certificateTemplateService: Pick<TCertificateTemplateServiceFactory, "getEstConfiguration">;
certificateTemplateService: Pick<TCertificateTemplateServiceFactory, "getEstConfiguration" | "getCertTemplate">;
certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "findById">;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "find">;
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
};
export type TCertificateEstServiceFactory = ReturnType<typeof certificateEstServiceFactory>;
export const certificateEstServiceFactory = ({
certificateAuthorityService,
certificateTemplateService
certificateTemplateService,
certificateTemplateDAL,
certificateAuthorityCertDAL,
certificateAuthorityDAL,
projectDAL,
kmsService
}: TCertificateEstServiceFactoryDep) => {
const simpleReenroll = async ({
csr,
certificateTemplateId,
sslClientCert
}: {
csr: string;
certificateTemplateId: string;
sslClientCert: string;
}) => {
const estConfig = await certificateTemplateService.getEstConfiguration({
isInternal: true,
certificateTemplateId
});
if (!estConfig.isEnabled) {
throw new BadRequestError({
message: "EST is disabled"
});
}
const certTemplate = await certificateTemplateDAL.findById(certificateTemplateId);
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();
const cert = new x509.X509Certificate(clientCertBody);
// We have to assert that the client certificate provided can be traced back to the Root CA
const caCertChains = await getCaCertChains({
caId: certTemplate.caId,
certificateAuthorityCertDAL,
certificateAuthorityDAL,
projectDAL,
kmsService
});
const parsedChains = caCertChains
// we need the full chain from the CA certificate to the root
.map((chain) => chain.certificate + chain.certificateChain)
.map(
(certificateChain) =>
certificateChain.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)?.map((certEntry) => {
const processedBody = certEntry
.replace("-----BEGIN CERTIFICATE-----", "")
.replace("-----END CERTIFICATE-----", "")
.replace(/\n/g, "")
.replace(/ /g, "")
.trim();
const certificateBuffer = Buffer.from(processedBody, "base64");
return new x509.X509Certificate(certificateBuffer);
})
);
if (!parsedChains || !parsedChains.length) {
throw new BadRequestError({
message: "Error parsing CA chain"
});
}
const certValidityAgainstChains = await Promise.all(
parsedChains.map(async (chain) => {
if (!chain) {
return false;
}
return checkCertValidityAgainstChain(cert, chain);
})
);
if (certValidityAgainstChains.every((isCertValid) => !isCertValid)) {
throw new BadRequestError({
message: "Invalid client certificate"
});
}
// We ensure that the Subject and SubjectAltNames of the CSR and the existing certificate are exactly the same
const csrObj = new x509.Pkcs10CertificateRequest(csr);
if (csrObj.subject !== cert.subject) {
throw new BadRequestError({
message: "Subject mismatch"
});
}
let csrSanSet: Set<string> = new Set();
const csrSanExtension = csrObj.extensions.find((ext) => ext.type === "2.5.29.17");
if (csrSanExtension) {
const sanNames = new x509.GeneralNames(csrSanExtension.value);
csrSanSet = new Set([...sanNames.items.map((name) => `${name.type}-${name.value}`)]);
}
let certSanSet: Set<string> = new Set();
const certSanExtension = cert.extensions.find((ext) => ext.type === "2.5.29.17");
if (certSanExtension) {
const sanNames = new x509.GeneralNames(certSanExtension.value);
certSanSet = new Set([...sanNames.items.map((name) => `${name.type}-${name.value}`)]);
}
if (csrSanSet.size !== certSanSet.size || ![...csrSanSet].every((element) => certSanSet.has(element))) {
throw new BadRequestError({
message: "Subject alternative names mismatch"
});
}
const { rawCertificate } = await certificateAuthorityService.signCertFromCa({
isInternal: true,
certificateTemplateId,
csr
});
return convertRawCertToPkcs7(rawCertificate);
};
const simpleEnroll = async ({
csr,
certificateTemplateId,
@@ -55,7 +196,6 @@ export const certificateEstServiceFactory = ({
.replace(/ /g, "")
.trim();
// validate SSL client cert against configured CA
const chainCerts = estConfig.caChain
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
?.map((cert) => {
@@ -74,23 +214,9 @@ export const certificateEstServiceFactory = ({
throw new BadRequestError({ message: "Failed to parse certificate chain" });
}
let isSslClientCertValid = true;
let certToVerify = new x509.X509Certificate(clientCertBody);
const cert = 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) {
if (!(await checkCertValidityAgainstChain(cert, chainCerts))) {
throw new UnauthorizedError({
message: "Invalid client certificate"
});
@@ -102,26 +228,10 @@ export const certificateEstServiceFactory = ({
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 convertRawCertToPkcs7(rawCertificate);
};
return {
simpleEnroll
simpleEnroll,
simpleReenroll
};
};