Continue cert mgmt mvp

This commit is contained in:
Tuan Dang
2024-05-28 17:00:48 -07:00
parent 26ea949a4e
commit b8516da90f
57 changed files with 3339 additions and 467 deletions
@@ -1,10 +1,47 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex";
export type TCertificateAuthorityDALFactory = ReturnType<typeof certificateAuthorityDALFactory>;
export const certificateAuthorityDALFactory = (db: TDbClient) => {
const caOrm = ormify(db, TableName.CertificateAuthority);
return caOrm;
const buildCertificateChain = async (caId: string) => {
try {
const result: {
caId: string;
parentCaId?: string;
certificate: string;
}[] = await db
.withRecursive("cte", (cte) => {
void cte
.select("ca.id as caId", "ca.parentCaId", "cert.certificate")
.from({ ca: TableName.CertificateAuthority })
.leftJoin({ cert: TableName.CertificateAuthorityCert }, "ca.id", "cert.caId")
.where("ca.id", caId)
.unionAll((builder) => {
void builder
.select("ca.id as caId", "ca.parentCaId", "cert.certificate")
.from({ ca: TableName.CertificateAuthority })
.leftJoin({ cert: TableName.CertificateAuthorityCert }, "ca.id", "cert.caId")
.innerJoin("cte", "cte.parentCaId", "ca.id");
});
})
.select("*")
.from("cte");
// Extract certificates and reverse the order to have the root CA at the end
const certChain: string[] = result.map((row) => row.certificate);
return certChain;
} catch (error) {
throw new DatabaseError({ error, name: "BuildCertificateChain" });
}
};
return {
...caOrm,
buildCertificateChain
};
};
@@ -0,0 +1,12 @@
import { TDNParts } from "./certificate-authority-types";
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(", ");
};
@@ -1,18 +1,31 @@
import { ForbiddenError } from "@casl/ability";
import * as x509 from "@peculiar/x509";
import crypto, { KeyObject } from "crypto";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { BadRequestError } from "@app/lib/errors";
import { TCertificateCertDALFactory } from "@app/services/certificate/certificate-cert-dal";
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TCertificateAuthorityCertDALFactory } from "./certificate-authority-cert-dal";
import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal";
import { createDistinguishedName } from "./certificate-authority-fns";
import { TCertificateAuthoritySkDALFactory } from "./certificate-authority-sk-dal";
import {
// TIssueCertFromCaDTO,
CAType,
CaStatus,
CaType,
TCreateCaDTO,
TDeleteCaDTO,
TGetCaCertDTO,
TGetCaCsrDTO
TGetCaCsrDTO,
TGetCaDTO,
TImportCertToCaDTO,
TIssueCertFromCaDTO,
TSignIntermediateDTO,
TUpdateCaDTO
} from "./certificate-authority-types";
type TCertificateAuthorityServiceFactoryDep = {
@@ -20,41 +33,27 @@ type TCertificateAuthorityServiceFactoryDep = {
certificateAuthorityDAL: TCertificateAuthorityDALFactory;
certificateAuthorityCertDAL: TCertificateAuthorityCertDALFactory;
certificateAuthoritySkDAL: TCertificateAuthoritySkDALFactory;
certificateDAL: TCertificateDALFactory;
certificateCertDAL: TCertificateCertDALFactory;
certificateSecretDAL: TCertificateSecretDALFactory;
projectDAL: TProjectDALFactory;
permissionService: TPermissionServiceFactory;
};
export type TCertificateAuthorityServiceFactory = ReturnType<typeof certificateAuthorityServiceFactory>;
type DNParts = {
commonName?: string;
organization?: string;
ou?: string;
country?: string;
province?: string;
locality?: string;
};
function createDistinguishedName(parts: DNParts) {
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 certificateAuthorityServiceFactory = ({
certificateAuthorityDAL,
certificateAuthorityCertDAL,
certificateAuthoritySkDAL,
projectDAL
certificateDAL,
certificateCertDAL,
certificateSecretDAL,
projectDAL,
permissionService
}: TCertificateAuthorityServiceFactoryDep) => {
/**
* Generates a new root or intermediate CA
* @param param0
* @returns
*/
const createCa = async ({
projectSlug,
@@ -65,14 +64,30 @@ export const certificateAuthorityServiceFactory = ({
country,
province,
locality,
// actorId,
// actorAuthMethod,
// actor,
notBefore,
notAfter,
maxPathLength,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TCreateCaDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new BadRequestError({ message: "Project not found" });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
project.id,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.CertificateAuthorities
);
const dn = createDistinguishedName({
commonName,
organization,
@@ -97,25 +112,41 @@ export const certificateAuthorityServiceFactory = ({
const pk = pkObj.export({ format: "pem", type: "spki" }) as string;
const newCa = await certificateAuthorityDAL.transaction(async (tx) => {
const notBeforeDate = notBefore ? new Date(notBefore) : new Date();
// if undefined, set [notAfterDate] to 10 years from now
const notAfterDate = notAfter
? new Date(notAfter)
: new Date(new Date().setFullYear(new Date().getFullYear() + 10));
const ca = await certificateAuthorityDAL.create(
{
projectId: project.id,
type,
dn
organization,
ou,
country,
province,
locality,
commonName,
status: type === CaType.ROOT ? CaStatus.ACTIVE : CaStatus.PENDING_CERTIFICATE,
dn,
...(type === CaType.ROOT && { maxPathLength, notBefore: notBeforeDate, notAfter: notAfterDate })
},
tx
);
if (type === CAType.ROOT) {
if (type === CaType.ROOT) {
// note: self-signed cert only applicable for root CA
const cert = await x509.X509CertificateGenerator.createSelfSigned({
name: dn,
notBefore: new Date("2020/01/01"),
notAfter: new Date("2020/01/02"),
notBefore: notBeforeDate,
notAfter: notAfterDate,
signingAlgorithm: alg,
keys,
extensions: [
new x509.BasicConstraintsExtension(true, 2, true),
new x509.BasicConstraintsExtension(true, maxPathLength === -1 ? undefined : maxPathLength, true),
new x509.ExtendedKeyUsageExtension(["1.2.3.4.5.6.7", "2.3.4.5.6.7.8"], true),
// eslint-disable-next-line no-bitwise
new x509.KeyUsagesExtension(x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.cRLSign, true),
@@ -148,15 +179,94 @@ export const certificateAuthorityServiceFactory = ({
return newCa;
};
const getCaById = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCaDTO) => {
const ca = await certificateAuthorityDAL.findById(caId);
if (!ca) throw new BadRequestError({ message: "CA not found" });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
ca.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.CertificateAuthorities
);
return ca;
};
const updateCaById = async ({ caId, status, actorId, actorAuthMethod, actor, actorOrgId }: TUpdateCaDTO) => {
const ca = await certificateAuthorityDAL.findById(caId);
if (!ca) throw new BadRequestError({ message: "CA not found" });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
ca.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.CertificateAuthorities
);
const updatedCa = await certificateAuthorityDAL.updateById(caId, { status });
return updatedCa;
};
const deleteCaById = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TDeleteCaDTO) => {
const ca = await certificateAuthorityDAL.findById(caId);
if (!ca) throw new BadRequestError({ message: "CA not found" });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
ca.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.CertificateAuthorities
);
const deletedCa = await certificateAuthorityDAL.deleteById(caId);
return deletedCa;
};
/**
* Generates a CSR for a CA
*/
const getCaCsr = async ({ caId }: TGetCaCsrDTO) => {
const getCaCsr = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCaCsrDTO) => {
const ca = await certificateAuthorityDAL.findById(caId);
if (!ca) throw new BadRequestError({ message: "CA not found" });
// TODO: permissioning
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
ca.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.CertificateAuthorities
);
if (ca.type === CaType.ROOT) throw new BadRequestError({ message: "Root CA cannot generate CSR" });
const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id });
if (caCert) throw new BadRequestError({ message: "CA already has a certificate installed" });
const caKeys = await certificateAuthoritySkDAL.findOne({ caId: ca.id });
const alg = {
@@ -176,10 +286,8 @@ export const certificateAuthorityServiceFactory = ({
"verify"
]);
const cert = new x509.X509Certificate(caCert.certificate);
const csr = await x509.Pkcs10CertificateRequestGenerator.create({
name: cert.subject || ca.dn,
const csrObj = await x509.Pkcs10CertificateRequestGenerator.create({
name: ca.dn,
keys: { privateKey: sk, publicKey: pk },
signingAlgorithm: alg,
extensions: [
@@ -189,80 +297,389 @@ export const certificateAuthorityServiceFactory = ({
attributes: [new x509.ChallengePasswordAttribute("password")]
});
return csr.toString("base64");
return csrObj.toString("pem");
};
/**
* Return certificate and certificate chain for CA
*/
const getCaCert = async ({ caId }: TGetCaCertDTO) => {
const getCaCert = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCaCertDTO) => {
const ca = await certificateAuthorityDAL.findById(caId);
if (!ca) throw new BadRequestError({ message: "CA not found" });
// TODO: permissioning
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
ca.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.CertificateAuthorities
);
const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id });
const certObj = new x509.X509Certificate(caCert.certificate);
return {
certificate: caCert.certificate,
certificateChain: caCert.certificateChain
certificateChain: caCert.certificateChain,
serialNumber: certObj.serialNumber
};
};
/**
* Issue new certificate
* Issue certificate to be imported back in for intermediate CA
* TODO: cannot chain to self
*/
const issueCertFromCa = async () => {
// WIP: parse publicKey from CSR
// const csrArrayBuffer = Uint8Array.from(atob(csr), (c) => c.charCodeAt(0));
// const csrR = new x509.Pkcs10CertificateRequest(csrArrayBuffer);
const signIntermediate = async ({
caId,
actorId,
actorAuthMethod,
actor,
actorOrgId,
csr,
notBefore,
notAfter,
maxPathLength
}: TSignIntermediateDTO) => {
const ca = await certificateAuthorityDAL.findById(caId);
if (!ca) throw new BadRequestError({ message: "CA not found" });
// const ca = await certificateAuthorityDAL.findById(caId);
// if (!ca) throw new BadRequestError({ message: "CA not found" });
// // TODO: permissioning
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
ca.projectId,
actorAuthMethod,
actorOrgId
);
// const alg = {
// name: "RSASSA-PKCS1-v1_5",
// hash: "SHA-256",
// publicExponent: new Uint8Array([1, 0, 1]),
// modulusLength: 2048
// };
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.CertificateAuthorities
);
// const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id });
// const caKeys = await certificateAuthoritySkDAL.findOne({ caId: ca.id });
if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" });
// const skObj = crypto.createPrivateKey({ key: caKeys.sk, format: "pem", type: "pkcs8" });
// const sk = await crypto.subtle.importKey("pkcs8", skObj.export({ format: "der", type: "pkcs8" }), alg, true, [
// "sign"
// ]);
const alg = {
name: "RSASSA-PKCS1-v1_5",
hash: "SHA-256",
publicExponent: new Uint8Array([1, 0, 1]),
modulusLength: 2048
};
// const cert = new x509.X509Certificate(caCert.certificate);
const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id });
const caKeys = await certificateAuthoritySkDAL.findOne({ caId: ca.id });
// const leafCert = await x509.X509CertificateGenerator.create({
// // serialNumber: "03",
// subject: "CN=Leaf", // TODO: make dynamic
// issuer: cert.subject,
// notBefore: new Date(notBefore),
// notAfter: new Date(notAfter),
// signingKey: sk,
// publicKey: leafKeys.publicKey,
// signingAlgorithm: alg,
// extensions: [
// new x509.KeyUsagesExtension(x509.KeyUsageFlags.dataEncipherment, true),
// new x509.BasicConstraintsExtension(false),
// await x509.AuthorityKeyIdentifierExtension.create(cert, false),
// await x509.SubjectKeyIdentifierExtension.create(leafKeys.publicKey)
// ]
// });
const skObj = crypto.createPrivateKey({ key: caKeys.sk, format: "pem", type: "pkcs8" });
const sk = await crypto.subtle.importKey("pkcs8", skObj.export({ format: "der", type: "pkcs8" }), alg, true, [
"sign"
]);
// console.log("leafCert: ", leafCert);
const certObj = new x509.X509Certificate(caCert.certificate);
const csrObj = new x509.Pkcs10CertificateRequest(csr);
return "";
// check path length constraint
const caPathLength = certObj.getExtension(x509.BasicConstraintsExtension)?.pathLength;
if (caPathLength !== undefined) {
if (caPathLength === 0)
throw new BadRequestError({
message: "Failed to issue intermediate certificate due to CA path length constraint"
});
if (maxPathLength >= caPathLength || (maxPathLength === -1 && caPathLength !== -1))
throw new BadRequestError({
message: "The requested path length constraint exceeds the CA's allowed path length"
});
}
const notBeforeDate = notBefore ? new Date(notBefore) : new Date();
const notAfterDate = new Date(notAfter);
const caCertNotBeforeDate = new Date(certObj.notBefore);
const caCertNotAfterDate = new Date(certObj.notAfter);
// check not before constraint
if (notBeforeDate < caCertNotBeforeDate) {
throw new BadRequestError({ message: "notBefore date is before CA certificate's notBefore date" });
}
if (notBeforeDate > notAfterDate) throw new BadRequestError({ message: "notBefore date is after notAfter date" });
// check not after constraint
if (notAfterDate > caCertNotAfterDate) {
throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" });
}
const intermediateCert = await x509.X509CertificateGenerator.create({
// serialNumber: "03",
subject: csrObj.subject,
issuer: certObj.subject,
notBefore: notBeforeDate,
notAfter: notAfterDate,
signingKey: sk,
publicKey: csrObj.publicKey,
signingAlgorithm: alg,
extensions: [
new x509.KeyUsagesExtension(x509.KeyUsageFlags.dataEncipherment, true),
new x509.BasicConstraintsExtension(true, maxPathLength === -1 ? undefined : maxPathLength, true),
await x509.AuthorityKeyIdentifierExtension.create(certObj, false),
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
]
});
const chain = await certificateAuthorityDAL.buildCertificateChain(caId);
return {
certificate: intermediateCert.toString("pem"),
issuingCaCertificate: caCert.certificate,
certificateChain: chain.join("\n"),
serialNumber: intermediateCert.serialNumber
};
};
const importCertToCa = async ({
caId,
actorId,
actorAuthMethod,
actor,
actorOrgId,
certificate,
certificateChain
}: TImportCertToCaDTO) => {
const ca = await certificateAuthorityDAL.findById(caId);
if (!ca) throw new BadRequestError({ message: "CA not found" });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
ca.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.CertificateAuthorities
);
const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id });
if (caCert) throw new BadRequestError({ message: "CA has already imported a certificate" });
const certObj = new x509.X509Certificate(certificate);
const maxPathLength = certObj.getExtension(x509.BasicConstraintsExtension)?.pathLength;
// validate imported certificate and certificate chain
const certificates = certificateChain
.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" });
const chain = new x509.X509ChainBuilder({
certificates
});
const chainItems = await chain.build(certObj);
// chain.build() implicitly verifies the chain
if (chainItems.length !== certificates.length + 1)
throw new BadRequestError({ message: "Invalid certificate chain" });
const parentCertObj = chainItems[1];
const parentCertSubject = parentCertObj.subject;
const parentCa = await certificateAuthorityDAL.findOne({
projectId: ca.projectId,
dn: parentCertSubject
});
await certificateAuthorityCertDAL.transaction(async (tx) => {
await certificateAuthorityCertDAL.create(
{
caId: ca.id,
certificate, // TODO: encrypt
certificateChain // TODO: encrypt
},
tx
);
await certificateAuthorityDAL.updateById(
ca.id,
{
status: CaStatus.ACTIVE,
maxPathLength: maxPathLength === undefined ? -1 : maxPathLength,
notBefore: new Date(certObj.notBefore),
notAfter: new Date(certObj.notAfter),
parentCaId: parentCa?.id
},
tx
);
});
};
const issueCertFromCa = async ({
caId,
commonName,
ttl,
notBefore,
notAfter,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TIssueCertFromCaDTO) => {
const ca = await certificateAuthorityDAL.findById(caId);
if (!ca) throw new BadRequestError({ message: "CA not found" });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
ca.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates);
const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id });
if (!caCert) throw new BadRequestError({ message: "CA does not have a certificate installed" });
if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" });
const caCertObj = new x509.X509Certificate(caCert.certificate);
const alg = {
name: "RSASSA-PKCS1-v1_5",
hash: "SHA-256",
publicExponent: new Uint8Array([1, 0, 1]),
modulusLength: 2048
};
const caKeys = await certificateAuthoritySkDAL.findOne({ caId: ca.id });
const caSkObj = crypto.createPrivateKey({ key: caKeys.sk, format: "pem", type: "pkcs8" });
const caSk = await crypto.subtle.importKey("pkcs8", caSkObj.export({ format: "der", type: "pkcs8" }), alg, true, [
"sign"
]);
const leafKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
const csrObj = await x509.Pkcs10CertificateRequestGenerator.create({
name: `CN=${commonName}`,
keys: leafKeys,
signingAlgorithm: alg,
extensions: [
// eslint-disable-next-line no-bitwise
new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment)
],
attributes: [new x509.ChallengePasswordAttribute("password")]
});
const notBeforeDate = notBefore ? new Date(notBefore) : new Date();
let notAfterDate = new Date(new Date().setFullYear(new Date().getFullYear() + 1));
if (notAfter) {
notAfterDate = new Date(notAfter);
} else if (ttl) {
// ttl in seconds
notAfterDate = new Date(new Date().getTime() + ttl * 1000);
}
const caCertNotBeforeDate = new Date(caCertObj.notBefore);
const caCertNotAfterDate = new Date(caCertObj.notAfter);
// check not before constraint
if (notBeforeDate < caCertNotBeforeDate) {
throw new BadRequestError({ message: "notBefore date is before CA certificate's notBefore date" });
}
if (notBeforeDate > notAfterDate) throw new BadRequestError({ message: "notBefore date is after notAfter date" });
// check not after constraint
if (notAfterDate > caCertNotAfterDate) {
throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" });
}
const leafCert = await x509.X509CertificateGenerator.create({
// serialNumber: "03",
subject: csrObj.subject,
issuer: caCertObj.subject,
notBefore: notBeforeDate,
notAfter: notAfterDate,
signingKey: caSk,
publicKey: csrObj.publicKey,
signingAlgorithm: alg,
extensions: [
new x509.KeyUsagesExtension(x509.KeyUsageFlags.dataEncipherment, true),
new x509.BasicConstraintsExtension(false),
await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
]
});
const skLeafObj = KeyObject.from(leafKeys.privateKey);
const skLeaf = skLeafObj.export({ format: "pem", type: "pkcs8" }) as string;
const chain = await certificateAuthorityDAL.buildCertificateChain(caId);
// https://nodejs.org/api/crypto.html#static-method-keyobjectfromkey
const skObj = KeyObject.from(leafKeys.privateKey);
const sk = skObj.export({ format: "pem", type: "pkcs8" }) as string;
const pkObj = KeyObject.from(leafKeys.publicKey);
const pk = pkObj.export({ format: "pem", type: "spki" }) as string;
await certificateDAL.transaction(async (tx) => {
const cert = await certificateDAL.create(
{
caId: ca.id,
commonName,
notBefore: notBeforeDate,
notAfter: notAfterDate
},
tx
);
await certificateCertDAL.create(
{
certId: cert.id,
certificate: leafCert.toString("pem"), // TODO: encrypt
certificateChain: chain.join("\n") // TODO: encrypt
},
tx
);
await certificateSecretDAL.create(
{
certId: cert.id,
pk, // TODO: encrypt
sk // TODO: encrypt
},
tx
);
return cert;
});
return {
certificate: leafCert.toString("pem"),
certificateChain: chain.join("\n"),
issuingCaCertificate: caCert.certificate,
privateKey: skLeaf,
serialNumber: leafCert.serialNumber
};
};
return {
createCa,
getCaById,
updateCaById,
deleteCaById,
getCaCsr,
getCaCert,
signIntermediate,
importCertToCa,
issueCertFromCa
};
};
@@ -1,21 +1,43 @@
import { TProjectPermission } from "@app/lib/types";
export enum CAType {
export enum CaType {
ROOT = "root",
INTERMEDIATE = "intermediate"
}
export enum CaStatus {
ACTIVE = "active",
DISABLED = "disabled",
PENDING_CERTIFICATE = "pending-certificate"
}
// TODO: attach permissions after draft impl
export type TCreateCaDTO = {
projectSlug: string;
type: CAType;
type: CaType;
commonName: string;
organization: string;
ou: string;
country: string;
province: string;
locality: string;
notBefore?: string;
notAfter?: string;
maxPathLength?: number;
} & Omit<TProjectPermission, "projectId">;
export type TGetCaDTO = {
caId: string;
} & Omit<TProjectPermission, "projectId">;
export type TUpdateCaDTO = {
caId: string;
status?: CaStatus;
} & Omit<TProjectPermission, "projectId">;
export type TDeleteCaDTO = {
caId: string;
} & Omit<TProjectPermission, "projectId">;
export type TGetCaCsrDTO = {
@@ -26,9 +48,33 @@ export type TGetCaCertDTO = {
caId: string;
} & Omit<TProjectPermission, "projectId">;
export type TIssueCertFromCaDTO = {
export type TSignIntermediateDTO = {
caId: string;
csr: string;
notBefore: string;
notBefore?: string;
notAfter: string;
maxPathLength: number;
} & Omit<TProjectPermission, "projectId">;
export type TImportCertToCaDTO = {
caId: string;
certificate: string;
certificateChain: string;
} & Omit<TProjectPermission, "projectId">;
export type TIssueCertFromCaDTO = {
caId: string;
commonName: string;
ttl?: number;
notBefore?: string;
notAfter?: string;
} & Omit<TProjectPermission, "projectId">;
export type TDNParts = {
commonName?: string;
organization?: string;
ou?: string;
country?: string;
province?: string;
locality?: string;
};
@@ -0,0 +1,8 @@
import { z } from "zod";
const isValidDate = (dateString: string) => {
const date = new Date(dateString);
return !Number.isNaN(date.getTime());
};
export const validateCaDateField = z.string().trim().refine(isValidDate, { message: "Invalid date format" });
@@ -0,0 +1,10 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TCertificateCertDALFactory = ReturnType<typeof certificateCertDALFactory>;
export const certificateCertDALFactory = (db: TDbClient) => {
const certificateCertOrm = ormify(db, TableName.CertificateCert);
return certificateCertOrm;
};
@@ -0,0 +1,10 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TCertificateDALFactory = ReturnType<typeof certificateDALFactory>;
export const certificateDALFactory = (db: TDbClient) => {
const certificateOrm = ormify(db, TableName.Certificate);
return certificateOrm;
};
@@ -0,0 +1,10 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TCertificateSecretDALFactory = ReturnType<typeof certificateSecretDALFactory>;
export const certificateSecretDALFactory = (db: TDbClient) => {
const certificateSecretOrm = ormify(db, TableName.CertificateSecret);
return certificateSecretOrm;
};
@@ -0,0 +1,102 @@
import { ForbiddenError } from "@casl/ability";
import * as x509 from "@peculiar/x509";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { TCertificateCertDALFactory } from "@app/services/certificate/certificate-cert-dal";
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
import { TDeleteCertDTO, TGetCertCertDTO, TGetCertDTO } from "./certificate-types";
type TCertificateServiceFactoryDep = {
// TODO: Pick
certificateDAL: TCertificateDALFactory;
certificateCertDAL: TCertificateCertDALFactory;
certificateSecretDAL: TCertificateSecretDALFactory;
certificateAuthorityDAL: TCertificateAuthorityDALFactory;
certificateAuthorityCertDAL: TCertificateAuthorityCertDALFactory;
permissionService: TPermissionServiceFactory;
};
export type TCertificateServiceFactory = ReturnType<typeof certificateServiceFactory>;
export const certificateServiceFactory = ({
certificateDAL,
certificateCertDAL,
certificateSecretDAL,
certificateAuthorityDAL,
certificateAuthorityCertDAL,
permissionService
}: TCertificateServiceFactoryDep) => {
const getCertById = async ({ certId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCertDTO) => {
const cert = await certificateDAL.findById(certId);
const ca = await certificateAuthorityDAL.findById(cert.caId);
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
ca.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
return cert;
};
const deleteCertById = async ({ certId, actorId, actorAuthMethod, actor, actorOrgId }: TDeleteCertDTO) => {
const cert = await certificateDAL.findById(certId);
const ca = await certificateAuthorityDAL.findById(cert.caId);
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
ca.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates);
const deletedCert = await certificateDAL.deleteById(cert.id);
return deletedCert;
};
const getCertCert = async ({ certId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCertCertDTO) => {
const cert = await certificateDAL.findById(certId);
const ca = await certificateAuthorityDAL.findById(cert.caId);
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
ca.projectId,
actorAuthMethod,
actorOrgId
);
// TODO: re-evaluate this permission
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates);
const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id });
const certCert = await certificateCertDAL.findOne({ certId });
const certSecret = await certificateSecretDAL.findOne({ certId });
const certObj = new x509.X509Certificate(certCert.certificate);
return {
certificate: certCert.certificate,
certificateChain: certCert.certificateChain,
issuingCaCertificate: caCert.certificate,
privateKey: certSecret.sk,
serialNumber: certObj.serialNumber
};
};
return {
getCertById,
deleteCertById,
getCertCert
};
};
@@ -0,0 +1,13 @@
import { TProjectPermission } from "@app/lib/types";
export type TGetCertDTO = {
certId: string;
} & Omit<TProjectPermission, "projectId">;
export type TDeleteCertDTO = {
certId: string;
} & Omit<TProjectPermission, "projectId">;
export type TGetCertCertDTO = {
certId: string;
} & Omit<TProjectPermission, "projectId">;
+61 -14
View File
@@ -16,6 +16,7 @@ import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TProjectPermission } from "@app/lib/types";
import { ActorType } from "../auth/auth-type";
import { TCertificateDALFactory } from "../certificate/certificate-dal";
import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal";
@@ -51,6 +52,7 @@ export const DEFAULT_PROJECT_ENVS = [
];
type TProjectServiceFactoryDep = {
// TODO: Pick
projectDAL: TProjectDALFactory;
projectQueue: TProjectQueueFactory;
userDAL: TUserDALFactory;
@@ -65,6 +67,7 @@ type TProjectServiceFactoryDep = {
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">;
secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "create">;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "find">;
certificateDAL: TCertificateDALFactory;
permissionService: TPermissionServiceFactory;
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
@@ -93,6 +96,7 @@ export const projectServiceFactory = ({
projectUserMembershipRoleDAL,
identityProjectMembershipRoleDAL,
certificateAuthorityDAL,
certificateDAL,
keyStore
}: TProjectServiceFactoryDep) => {
/*
@@ -500,24 +504,66 @@ export const projectServiceFactory = ({
* Return list of CAs for project
*/
const listProjectCas = async ({
// actorId,
// actorOrgId,
// actorAuthMethod,
filter // actor
status,
actorId,
actorOrgId,
actorAuthMethod,
filter,
actor
}: TListProjectCasDTO) => {
const project = await projectDAL.findProjectByFilter(filter);
// const { permission } = await permissionService.getProjectPermission(
// actor,
// actorId,
// project.id,
// actorAuthMethod,
// actorOrgId
// );
// TODO: add permissioning
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
project.id,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.CertificateAuthorities
);
const cas = await certificateAuthorityDAL.find({
projectId: project.id,
...(status && { status })
});
return cas;
};
/**
* Return list of certificates for project
*/
const listProjectCertificates = async ({
actorId,
actorOrgId,
actorAuthMethod,
filter,
actor
}: TListProjectCasDTO) => {
const project = await projectDAL.findProjectByFilter(filter);
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
project.id,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
const cas = await certificateAuthorityDAL.find({ projectId: project.id });
return cas;
const certificates = await certificateDAL.find({
$in: {
caId: cas.map((ca) => ca.id)
}
});
return certificates;
};
return {
@@ -530,6 +576,7 @@ export const projectServiceFactory = ({
toggleAutoCapitalization,
updateName,
upgradeProject,
listProjectCas
listProjectCas,
listProjectCertificates
};
};
@@ -2,6 +2,7 @@ import { ProjectMembershipRole, TProjectKeys } from "@app/db/schemas";
import { TProjectPermission } from "@app/lib/types";
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
import { CaStatus } from "../certificate-authority/certificate-authority-types";
export enum ProjectFilterType {
ID = "id",
@@ -77,5 +78,6 @@ export type AddUserToWsDTO = {
};
export type TListProjectCasDTO = {
status?: CaStatus;
filter: Filter;
} & Omit<TProjectPermission, "projectId">;