mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 11:29:42 +00:00
343 lines
9.3 KiB
TypeScript
343 lines
9.3 KiB
TypeScript
import * as x509 from "@peculiar/x509";
|
|
|
|
import { crypto } from "@app/lib/crypto/cryptography";
|
|
import { NotFoundError } from "@app/lib/errors";
|
|
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
|
|
|
|
import { CertKeyAlgorithm, CertStatus } from "../certificate/certificate-types";
|
|
import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal";
|
|
import {
|
|
TDNParts,
|
|
TGetCaCertChainDTO,
|
|
TGetCaCertChainsDTO,
|
|
TGetCaCredentialsDTO,
|
|
TRebuildCaCrlDTO
|
|
} from "./internal/internal-certificate-authority-types";
|
|
|
|
/* eslint-disable no-bitwise */
|
|
export const createSerialNumber = () => {
|
|
const randomBytes = crypto.randomBytes(20); // 20 bytes = 160 bits
|
|
randomBytes[0] &= 0x7f; // ensure the first bit is 0
|
|
return randomBytes.toString("hex");
|
|
};
|
|
|
|
export const createDistinguishedName = (parts: TDNParts) => {
|
|
const dnParts = [];
|
|
if (parts.country) dnParts.push(`C=${parts.country}`);
|
|
if (parts.organization) dnParts.push(`O=${parts.organization}`);
|
|
if (parts.ou) dnParts.push(`OU=${parts.ou}`);
|
|
if (parts.province) dnParts.push(`ST=${parts.province}`);
|
|
if (parts.commonName) dnParts.push(`CN=${parts.commonName}`);
|
|
if (parts.locality) dnParts.push(`L=${parts.locality}`);
|
|
return dnParts.join(", ");
|
|
};
|
|
|
|
export const parseDistinguishedName = (dn: string): TDNParts => {
|
|
const parts: TDNParts = {};
|
|
const dnParts = dn.split(/,\s*/);
|
|
|
|
for (const part of dnParts) {
|
|
const [key, value] = part.split("=");
|
|
switch (key.toUpperCase()) {
|
|
case "C":
|
|
parts.country = value;
|
|
break;
|
|
case "O":
|
|
parts.organization = value;
|
|
break;
|
|
case "OU":
|
|
parts.ou = value;
|
|
break;
|
|
case "ST":
|
|
parts.province = value;
|
|
break;
|
|
case "CN":
|
|
parts.commonName = value;
|
|
break;
|
|
case "L":
|
|
parts.locality = value;
|
|
break;
|
|
default:
|
|
// Ignore unrecognized keys
|
|
break;
|
|
}
|
|
}
|
|
|
|
return parts;
|
|
};
|
|
|
|
export const keyAlgorithmToAlgCfg = (keyAlgorithm: CertKeyAlgorithm) => {
|
|
switch (keyAlgorithm) {
|
|
case CertKeyAlgorithm.RSA_4096:
|
|
return {
|
|
name: "RSASSA-PKCS1-v1_5",
|
|
hash: "SHA-256",
|
|
publicExponent: new Uint8Array([1, 0, 1]),
|
|
modulusLength: 4096
|
|
};
|
|
case CertKeyAlgorithm.ECDSA_P256:
|
|
return {
|
|
name: "ECDSA",
|
|
namedCurve: "P-256",
|
|
hash: "SHA-256"
|
|
};
|
|
case CertKeyAlgorithm.ECDSA_P384:
|
|
return {
|
|
name: "ECDSA",
|
|
namedCurve: "P-384",
|
|
hash: "SHA-384"
|
|
};
|
|
default: {
|
|
// RSA_2048
|
|
return {
|
|
name: "RSASSA-PKCS1-v1_5",
|
|
hash: "SHA-256",
|
|
publicExponent: new Uint8Array([1, 0, 1]),
|
|
modulusLength: 2048
|
|
};
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Return the public and private key of CA with id [caId]
|
|
* Note: credentials are returned as crypto.webcrypto.CryptoKey
|
|
* suitable for use with @peculiar/x509 module
|
|
*
|
|
* TODO: Update to get latest CA Secret once support for CA renewal with new key pair is added
|
|
*/
|
|
export const getCaCredentials = async ({
|
|
caId,
|
|
certificateAuthorityDAL,
|
|
certificateAuthoritySecretDAL,
|
|
projectDAL,
|
|
kmsService
|
|
}: TGetCaCredentialsDTO) => {
|
|
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId);
|
|
if (!ca?.internalCa?.id) throw new NotFoundError({ message: `Internal CA with ID '${caId}' not found` });
|
|
|
|
const caSecret = await certificateAuthoritySecretDAL.findOne({ caId });
|
|
if (!caSecret) throw new NotFoundError({ message: `CA secret for CA with ID '${caId}' not found` });
|
|
|
|
const keyId = await getProjectKmsCertificateKeyId({
|
|
projectId: ca.projectId,
|
|
projectDAL,
|
|
kmsService
|
|
});
|
|
|
|
const kmsDecryptor = await kmsService.decryptWithKmsKey({
|
|
kmsId: keyId
|
|
});
|
|
const decryptedPrivateKey = await kmsDecryptor({
|
|
cipherTextBlob: caSecret.encryptedPrivateKey
|
|
});
|
|
|
|
const alg = keyAlgorithmToAlgCfg(ca.internalCa.keyAlgorithm as CertKeyAlgorithm);
|
|
const skObj = crypto.nativeCrypto.createPrivateKey({ key: decryptedPrivateKey, format: "der", type: "pkcs8" });
|
|
const caPrivateKey = await crypto.nativeCrypto.subtle.importKey(
|
|
"pkcs8",
|
|
skObj.export({ format: "der", type: "pkcs8" }),
|
|
alg,
|
|
true,
|
|
["sign"]
|
|
);
|
|
|
|
const pkObj = crypto.nativeCrypto.createPublicKey(skObj);
|
|
const caPublicKey = await crypto.nativeCrypto.subtle.importKey(
|
|
"spki",
|
|
pkObj.export({ format: "der", type: "spki" }),
|
|
alg,
|
|
true,
|
|
["verify"]
|
|
);
|
|
|
|
return {
|
|
caSecret,
|
|
caPrivateKey,
|
|
caPublicKey
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Return the list of decrypted pem-encoded certificates and certificate chains
|
|
* for CA with id [caId].
|
|
*/
|
|
export const getCaCertChains = async ({
|
|
caId,
|
|
certificateAuthorityDAL,
|
|
certificateAuthorityCertDAL,
|
|
projectDAL,
|
|
kmsService
|
|
}: TGetCaCertChainsDTO) => {
|
|
const ca = await certificateAuthorityDAL.findById(caId);
|
|
if (!ca) throw new NotFoundError({ message: `CA with ID '${caId}' not found` });
|
|
|
|
const keyId = await getProjectKmsCertificateKeyId({
|
|
projectId: ca.projectId,
|
|
projectDAL,
|
|
kmsService
|
|
});
|
|
|
|
const kmsDecryptor = await kmsService.decryptWithKmsKey({
|
|
kmsId: keyId
|
|
});
|
|
|
|
const caCerts = await certificateAuthorityCertDAL.find({ caId: ca.id }, { sort: [["version", "asc"]] });
|
|
|
|
const decryptedChains = await Promise.all(
|
|
caCerts.map(async (caCert) => {
|
|
const decryptedCaCert = await kmsDecryptor({
|
|
cipherTextBlob: caCert.encryptedCertificate
|
|
});
|
|
const caCertObj = new x509.X509Certificate(decryptedCaCert);
|
|
const decryptedChain = await kmsDecryptor({
|
|
cipherTextBlob: caCert.encryptedCertificateChain
|
|
});
|
|
return {
|
|
certificate: caCertObj.toString("pem"),
|
|
certificateChain: decryptedChain.toString("utf-8"),
|
|
serialNumber: caCertObj.serialNumber,
|
|
version: caCert.version
|
|
};
|
|
})
|
|
);
|
|
|
|
return decryptedChains;
|
|
};
|
|
|
|
/**
|
|
* Return the decrypted pem-encoded certificate and certificate chain
|
|
* corresponding to CA certificate with id [caCertId].
|
|
*/
|
|
export const getCaCertChain = async ({
|
|
caCertId,
|
|
certificateAuthorityDAL,
|
|
certificateAuthorityCertDAL,
|
|
projectDAL,
|
|
kmsService
|
|
}: TGetCaCertChainDTO) => {
|
|
const caCert = await certificateAuthorityCertDAL.findById(caCertId);
|
|
if (!caCert) throw new NotFoundError({ message: "CA certificate not found" });
|
|
const ca = await certificateAuthorityDAL.findById(caCert.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);
|
|
|
|
const decryptedChain = await kmsDecryptor({
|
|
cipherTextBlob: caCert.encryptedCertificateChain
|
|
});
|
|
|
|
return {
|
|
caCert: caCertObj.toString("pem"),
|
|
caCertChain: decryptedChain.toString("utf-8"),
|
|
serialNumber: caCertObj.serialNumber
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Rebuilds the certificate revocation list (CRL)
|
|
* for CA with id [caId]
|
|
*/
|
|
export const rebuildCaCrl = async ({
|
|
caId,
|
|
certificateAuthorityDAL,
|
|
certificateAuthorityCrlDAL,
|
|
certificateAuthoritySecretDAL,
|
|
projectDAL,
|
|
certificateDAL,
|
|
kmsService
|
|
}: TRebuildCaCrlDTO) => {
|
|
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId);
|
|
if (!ca?.internalCa?.id) throw new NotFoundError({ message: `Internal CA with ID '${caId}' not found` });
|
|
|
|
const caSecret = await certificateAuthoritySecretDAL.findOne({ caId: ca.id });
|
|
|
|
const alg = keyAlgorithmToAlgCfg(ca.internalCa.keyAlgorithm as CertKeyAlgorithm);
|
|
|
|
const keyId = await getProjectKmsCertificateKeyId({
|
|
projectId: ca.projectId,
|
|
projectDAL,
|
|
kmsService
|
|
});
|
|
|
|
const kmsDecryptor = await kmsService.decryptWithKmsKey({
|
|
kmsId: keyId
|
|
});
|
|
|
|
const privateKey = await kmsDecryptor({
|
|
cipherTextBlob: caSecret.encryptedPrivateKey
|
|
});
|
|
|
|
const skObj = crypto.nativeCrypto.createPrivateKey({ key: privateKey, format: "der", type: "pkcs8" });
|
|
const sk = await crypto.nativeCrypto.subtle.importKey(
|
|
"pkcs8",
|
|
skObj.export({ format: "der", type: "pkcs8" }),
|
|
alg,
|
|
true,
|
|
["sign"]
|
|
);
|
|
|
|
const revokedCerts = await certificateDAL.find({
|
|
caId: ca.id,
|
|
status: CertStatus.REVOKED
|
|
});
|
|
|
|
const crl = await x509.X509CrlGenerator.create({
|
|
issuer: ca.internalCa.dn,
|
|
thisUpdate: new Date(),
|
|
nextUpdate: new Date("2025/12/12"),
|
|
entries: revokedCerts.map((revokedCert) => {
|
|
const revocationDate = new Date(revokedCert.revokedAt as Date);
|
|
return {
|
|
serialNumber: revokedCert.serialNumber,
|
|
revocationDate,
|
|
reason: revokedCert.revocationReason as number
|
|
};
|
|
}),
|
|
signingAlgorithm: alg,
|
|
signingKey: sk
|
|
});
|
|
|
|
const kmsEncryptor = await kmsService.encryptWithKmsKey({
|
|
kmsId: keyId
|
|
});
|
|
const { cipherTextBlob: encryptedCrl } = await kmsEncryptor({
|
|
plainText: Buffer.from(new Uint8Array(crl.rawData))
|
|
});
|
|
|
|
await certificateAuthorityCrlDAL.update(
|
|
{
|
|
caId: ca.id
|
|
},
|
|
{
|
|
encryptedCrl
|
|
}
|
|
);
|
|
};
|
|
|
|
export const expandInternalCa = (
|
|
ca: Awaited<ReturnType<TCertificateAuthorityDALFactory["findByIdWithAssociatedCa"]>>
|
|
) => {
|
|
if (!ca.internalCa) {
|
|
throw new Error("Internal CA must be defined");
|
|
}
|
|
return {
|
|
...ca.internalCa,
|
|
...ca,
|
|
requireTemplateForIssuance: !ca.enableDirectIssuance
|
|
} as const;
|
|
};
|