misc: addressed review comments

This commit is contained in:
Sheen Capadngan
2024-08-26 21:10:29 +08:00
parent f560534493
commit 00f86cfd00
7 changed files with 93 additions and 54 deletions

View File

@@ -12,7 +12,7 @@ export async function up(knex: Knex): Promise<void> {
tb.foreign("certificateTemplateId").references("id").inTable(TableName.CertificateTemplate).onDelete("CASCADE"); tb.foreign("certificateTemplateId").references("id").inTable(TableName.CertificateTemplate).onDelete("CASCADE");
tb.binary("encryptedCaChain").notNullable(); tb.binary("encryptedCaChain").notNullable();
tb.string("hashedPassphrase").notNullable(); tb.string("hashedPassphrase").notNullable();
tb.boolean("isEnabled"); tb.boolean("isEnabled").notNullable();
tb.timestamps(true, true, true); tb.timestamps(true, true, true);
}); });

View File

@@ -1554,9 +1554,6 @@ export const certificateAuthorityServiceFactory = ({
return { return {
certificate: leafCert, certificate: leafCert,
// certificate: leafCert.toString("pem"),
// certificateObj: leafCert,
// rawCertificate: leafCert.rawData,
certificateChain: `${issuingCaCertificate}\n${caCertChain}`.trim(), certificateChain: `${issuingCaCertificate}\n${caCertChain}`.trim(),
issuingCaCertificate, issuingCaCertificate,
serialNumber, serialNumber,

View File

@@ -0,0 +1,22 @@
import { Certificate, ContentInfo, EncapsulatedContentInfo, SignedData } from "pkijs";
export const convertRawCertsToPkcs7 = (rawCertificate: ArrayBuffer[]) => {
const certs = rawCertificate.map((rawCert) => Certificate.fromBER(rawCert));
const cmsSigned = new SignedData({
encapContentInfo: new EncapsulatedContentInfo({
eContentType: "1.2.840.113549.1.7.1" // not encrypted and not compressed data
}),
certificates: certs
});
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;
};

View File

@@ -2,6 +2,7 @@ import * as x509 from "@peculiar/x509";
import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { isCertChainValid } from "../certificate/certificate-fns";
import { TCertificateAuthorityCertDALFactory } from "../certificate-authority/certificate-authority-cert-dal"; import { TCertificateAuthorityCertDALFactory } from "../certificate-authority/certificate-authority-cert-dal";
import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal"; import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal";
import { getCaCertChain, getCaCertChains } from "../certificate-authority/certificate-authority-fns"; import { getCaCertChain, getCaCertChains } from "../certificate-authority/certificate-authority-fns";
@@ -10,6 +11,7 @@ import { TCertificateTemplateDALFactory } from "../certificate-template/certific
import { TCertificateTemplateServiceFactory } from "../certificate-template/certificate-template-service"; import { TCertificateTemplateServiceFactory } from "../certificate-template/certificate-template-service";
import { TKmsServiceFactory } from "../kms/kms-service"; import { TKmsServiceFactory } from "../kms/kms-service";
import { TProjectDALFactory } from "../project/project-dal"; import { TProjectDALFactory } from "../project/project-dal";
import { convertRawCertsToPkcs7 } from "./certificate-est-fns";
type TCertificateEstServiceFactoryDep = { type TCertificateEstServiceFactoryDep = {
certificateAuthorityService: Pick<TCertificateAuthorityServiceFactory, "signCertFromCa">; certificateAuthorityService: Pick<TCertificateAuthorityServiceFactory, "signCertFromCa">;
@@ -62,15 +64,7 @@ export const certificateEstServiceFactory = ({
throw new UnauthorizedError({ message: "Missing client certificate" }); throw new UnauthorizedError({ message: "Missing client certificate" });
} }
const clientCertBody = leafCertificate const cert = new x509.X509Certificate(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 // We have to assert that the client certificate provided can be traced back to the Root CA
const caCertChains = await getCaCertChains({ const caCertChains = await getCaCertChains({
caId: certTemplate.caId, caId: certTemplate.caId,
@@ -80,21 +74,15 @@ export const certificateEstServiceFactory = ({
kmsService kmsService
}); });
const caChainBuilders = caCertChains.map((chain) => {
const caCert = new x509.X509Certificate(chain.certificate);
const caChain =
chain.certificateChain
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
?.map((c) => new x509.X509Certificate(c)) || [];
return new x509.X509ChainBuilder({
certificates: [caCert, ...caChain]
});
});
const verifiedChains = await Promise.all( const verifiedChains = await Promise.all(
caChainBuilders.map(async (caChainBuilder) => { caCertChains.map((chain) => {
const chainItems = await caChainBuilder.build(cert); const caCert = new x509.X509Certificate(chain.certificate);
return chainItems.length === caChainBuilder.certificates.length + 1; const caChain =
chain.certificateChain
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
?.map((c) => new x509.X509Certificate(c)) || [];
return isCertChainValid([cert, caCert, ...caChain]);
}) })
); );
@@ -138,8 +126,7 @@ export const certificateEstServiceFactory = ({
csr csr
}); });
const certs = new x509.X509Certificates([certificate]); return convertRawCertsToPkcs7([certificate.rawData]);
return certs.export("base64");
}; };
const simpleEnroll = async ({ const simpleEnroll = async ({
@@ -173,10 +160,6 @@ export const certificateEstServiceFactory = ({
if (!caCerts) throw new BadRequestError({ message: "Failed to parse certificate chain" }); if (!caCerts) throw new BadRequestError({ message: "Failed to parse certificate chain" });
const caChain = new x509.X509ChainBuilder({
certificates: caCerts
});
const leafCertificate = decodeURIComponent(sslClientCert).match( const leafCertificate = decodeURIComponent(sslClientCert).match(
/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g /-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g
)?.[0]; )?.[0];
@@ -185,17 +168,10 @@ export const certificateEstServiceFactory = ({
throw new BadRequestError({ message: "Missing client certificate" }); throw new BadRequestError({ message: "Missing client certificate" });
} }
const clientCertBody = leafCertificate const certObj = new x509.X509Certificate(leafCertificate);
.replace("-----BEGIN CERTIFICATE-----", "") if (!(await isCertChainValid([certObj, ...caCerts]))) {
.replace("-----END CERTIFICATE-----", "") throw new BadRequestError({ message: "Invalid certificate chain" });
.replace(/\n/g, "") }
.replace(/ /g, "")
.trim();
const certObj = new x509.X509Certificate(clientCertBody);
const chainItems = await caChain.build(certObj);
if (chainItems.length !== caCerts.length + 1) throw new BadRequestError({ message: "Invalid certificate chain" });
const { certificate } = await certificateAuthorityService.signCertFromCa({ const { certificate } = await certificateAuthorityService.signCertFromCa({
isInternal: true, isInternal: true,
@@ -203,8 +179,7 @@ export const certificateEstServiceFactory = ({
csr csr
}); });
const certs = new x509.X509Certificates([certificate]); return convertRawCertsToPkcs7([certificate.rawData]);
return certs.export("base64");
}; };
/** /**
@@ -240,16 +215,13 @@ export const certificateEstServiceFactory = ({
if (!certificates) throw new BadRequestError({ message: "Failed to parse certificate chain" }); if (!certificates) throw new BadRequestError({ message: "Failed to parse certificate chain" });
const chain = new x509.X509ChainBuilder({ const caCertificate = new x509.X509Certificate(caCert);
certificates
});
const chainItems = await chain.build(new x509.X509Certificate(caCert)); if (!(await isCertChainValid([caCertificate, ...certificates]))) {
if (chainItems.length !== certificates.length + 1)
throw new BadRequestError({ message: "Invalid certificate chain" }); throw new BadRequestError({ message: "Invalid certificate chain" });
}
return chainItems.export("base64"); return convertRawCertsToPkcs7([caCertificate.rawData, ...certificates.map((cert) => cert.rawData)]);
}; };
return { return {

View File

@@ -1,4 +1,5 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import * as x509 from "@peculiar/x509";
import bcrypt from "bcrypt"; import bcrypt from "bcrypt";
import { TCertificateTemplateEstConfigsUpdate } from "@app/db/schemas"; import { TCertificateTemplateEstConfigsUpdate } from "@app/db/schemas";
@@ -7,6 +8,7 @@ import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { isCertChainValid } from "../certificate/certificate-fns";
import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal"; import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal";
import { TKmsServiceFactory } from "../kms/kms-service"; import { TKmsServiceFactory } from "../kms/kms-service";
import { TProjectDALFactory } from "../project/project-dal"; import { TProjectDALFactory } from "../project/project-dal";
@@ -241,6 +243,19 @@ export const certificateTemplateServiceFactory = ({
kmsService kmsService
}); });
// validate CA chain
const certificates = caChain
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
?.map((cert) => new x509.X509Certificate(cert));
if (!certificates) {
throw new BadRequestError({ message: "Failed to parse certificate chain" });
}
if (!(await isCertChainValid(certificates))) {
throw new BadRequestError({ message: "Invalid certificate chain" });
}
const kmsEncryptor = await kmsService.encryptWithKmsKey({ const kmsEncryptor = await kmsService.encryptWithKmsKey({
kmsId: certificateManagerKmsId kmsId: certificateManagerKmsId
}); });
@@ -313,6 +328,18 @@ export const certificateTemplateServiceFactory = ({
}; };
if (caChain) { if (caChain) {
const certificates = caChain
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
?.map((cert) => new x509.X509Certificate(cert));
if (!certificates) {
throw new BadRequestError({ message: "Failed to parse certificate chain" });
}
if (!(await isCertChainValid(certificates))) {
throw new BadRequestError({ message: "Invalid certificate chain" });
}
const kmsEncryptor = await kmsService.encryptWithKmsKey({ const kmsEncryptor = await kmsService.encryptWithKmsKey({
kmsId: certificateManagerKmsId kmsId: certificateManagerKmsId
}); });

View File

@@ -24,3 +24,24 @@ export const revocationReasonToCrlCode = (crlReason: CrlReason) => {
return x509.X509CrlReason.unspecified; return x509.X509CrlReason.unspecified;
} }
}; };
export const isCertChainValid = async (certificates: x509.X509Certificate[]) => {
if (certificates.length === 1) {
return true;
}
// check for self-signed
if (certificates.length === 2 && certificates[0].equal(certificates[1])) {
return true;
}
const leafCert = certificates[0];
const chain = new x509.X509ChainBuilder({
certificates: certificates.slice(1)
});
const chainItems = await chain.build(leafCert);
// chain.build() implicitly verifies the chain
return chainItems.length === certificates.length;
};

View File

@@ -54,7 +54,7 @@ export const CertificateTemplatesSection = () => {
<p className="text-xl font-semibold text-mineshaft-100">Certificate Templates</p> <p className="text-xl font-semibold text-mineshaft-100">Certificate Templates</p>
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Create} I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.Certificates} a={ProjectPermissionSub.CertificateTemplates}
> >
{(isAllowed) => ( {(isAllowed) => (
<Button <Button