mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-08 13:28:27 +00:00
feat: added simplereenroll
This commit is contained in:
@@ -83,7 +83,7 @@ export const registerCertificateEstRouter = async (server: FastifyZodProvider) =
|
|||||||
certificateTemplateId: z.string().min(1)
|
certificateTemplateId: z.string().min(1)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({})
|
200: z.string()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handler: async (req, res) => {
|
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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -660,7 +660,12 @@ export const registerRoutes = async (
|
|||||||
|
|
||||||
const certificateEstService = certificateEstServiceFactory({
|
const certificateEstService = certificateEstServiceFactory({
|
||||||
certificateAuthorityService,
|
certificateAuthorityService,
|
||||||
certificateTemplateService
|
certificateTemplateService,
|
||||||
|
certificateTemplateDAL,
|
||||||
|
certificateAuthorityCertDAL,
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
});
|
});
|
||||||
|
|
||||||
const pkiAlertService = pkiAlertServiceFactory({
|
const pkiAlertService = pkiAlertServiceFactory({
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -1,22 +1,163 @@
|
|||||||
import * as x509 from "@peculiar/x509";
|
import * as x509 from "@peculiar/x509";
|
||||||
import { Certificate, ContentInfo, EncapsulatedContentInfo, SignedData } from "pkijs";
|
|
||||||
|
|
||||||
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
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 { TCertificateAuthorityServiceFactory } from "../certificate-authority/certificate-authority-service";
|
||||||
|
import { TCertificateTemplateDALFactory } from "../certificate-template/certificate-template-dal";
|
||||||
import { TCertificateTemplateServiceFactory } from "../certificate-template/certificate-template-service";
|
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 = {
|
type TCertificateEstServiceFactoryDep = {
|
||||||
certificateAuthorityService: Pick<TCertificateAuthorityServiceFactory, "signCertFromCa">;
|
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 type TCertificateEstServiceFactory = ReturnType<typeof certificateEstServiceFactory>;
|
||||||
|
|
||||||
export const certificateEstServiceFactory = ({
|
export const certificateEstServiceFactory = ({
|
||||||
certificateAuthorityService,
|
certificateAuthorityService,
|
||||||
certificateTemplateService
|
certificateTemplateService,
|
||||||
|
certificateTemplateDAL,
|
||||||
|
certificateAuthorityCertDAL,
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
}: TCertificateEstServiceFactoryDep) => {
|
}: 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 ({
|
const simpleEnroll = async ({
|
||||||
csr,
|
csr,
|
||||||
certificateTemplateId,
|
certificateTemplateId,
|
||||||
@@ -55,7 +196,6 @@ export const certificateEstServiceFactory = ({
|
|||||||
.replace(/ /g, "")
|
.replace(/ /g, "")
|
||||||
.trim();
|
.trim();
|
||||||
|
|
||||||
// validate SSL client cert against configured CA
|
|
||||||
const chainCerts = estConfig.caChain
|
const chainCerts = estConfig.caChain
|
||||||
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
|
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
|
||||||
?.map((cert) => {
|
?.map((cert) => {
|
||||||
@@ -74,23 +214,9 @@ export const certificateEstServiceFactory = ({
|
|||||||
throw new BadRequestError({ message: "Failed to parse certificate chain" });
|
throw new BadRequestError({ message: "Failed to parse certificate chain" });
|
||||||
}
|
}
|
||||||
|
|
||||||
let isSslClientCertValid = true;
|
const cert = new x509.X509Certificate(clientCertBody);
|
||||||
let certToVerify = new x509.X509Certificate(clientCertBody);
|
|
||||||
|
|
||||||
for await (const issuerCert of chainCerts) {
|
if (!(await checkCertValidityAgainstChain(cert, 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({
|
throw new UnauthorizedError({
|
||||||
message: "Invalid client certificate"
|
message: "Invalid client certificate"
|
||||||
});
|
});
|
||||||
@@ -102,26 +228,10 @@ export const certificateEstServiceFactory = ({
|
|||||||
csr
|
csr
|
||||||
});
|
});
|
||||||
|
|
||||||
const cert = Certificate.fromBER(rawCertificate);
|
return convertRawCertToPkcs7(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 {
|
return {
|
||||||
simpleEnroll
|
simpleEnroll,
|
||||||
|
simpleReenroll
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user