Preliminary scaffolding for CA structures

This commit is contained in:
Tuan Dang
2024-05-22 21:33:06 -07:00
parent 303edadb1e
commit 26ea949a4e
41 changed files with 1656 additions and 8 deletions
@@ -0,0 +1,10 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TCertificateAuthorityCertDALFactory = ReturnType<typeof certificateAuthorityCertDALFactory>;
export const certificateAuthorityCertDALFactory = (db: TDbClient) => {
const caCertOrm = ormify(db, TableName.CertificateAuthorityCert);
return caCertOrm;
};
@@ -0,0 +1,10 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
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;
};
@@ -0,0 +1,268 @@
import * as x509 from "@peculiar/x509";
import crypto, { KeyObject } from "crypto";
import { BadRequestError } from "@app/lib/errors";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TCertificateAuthorityCertDALFactory } from "./certificate-authority-cert-dal";
import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal";
import { TCertificateAuthoritySkDALFactory } from "./certificate-authority-sk-dal";
import {
// TIssueCertFromCaDTO,
CAType,
TCreateCaDTO,
TGetCaCertDTO,
TGetCaCsrDTO
} from "./certificate-authority-types";
type TCertificateAuthorityServiceFactoryDep = {
// TODO: Pick
certificateAuthorityDAL: TCertificateAuthorityDALFactory;
certificateAuthorityCertDAL: TCertificateAuthorityCertDALFactory;
certificateAuthoritySkDAL: TCertificateAuthoritySkDALFactory;
projectDAL: TProjectDALFactory;
};
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
}: TCertificateAuthorityServiceFactoryDep) => {
/**
* Generates a new root or intermediate CA
* @param param0
* @returns
*/
const createCa = async ({
projectSlug,
type,
commonName,
organization,
ou,
country,
province,
locality,
// actorId,
// actorAuthMethod,
// actor,
actorOrgId
}: TCreateCaDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new BadRequestError({ message: "Project not found" });
const dn = createDistinguishedName({
commonName,
organization,
ou,
country,
province,
locality
});
const alg = {
name: "RSASSA-PKCS1-v1_5",
hash: "SHA-256",
publicExponent: new Uint8Array([1, 0, 1]),
modulusLength: 2048
};
const keys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
// https://nodejs.org/api/crypto.html#static-method-keyobjectfromkey
const skObj = KeyObject.from(keys.privateKey);
const sk = skObj.export({ format: "pem", type: "pkcs8" }) as string;
const pkObj = KeyObject.from(keys.publicKey);
const pk = pkObj.export({ format: "pem", type: "spki" }) as string;
const newCa = await certificateAuthorityDAL.transaction(async (tx) => {
const ca = await certificateAuthorityDAL.create(
{
projectId: project.id,
type,
dn
},
tx
);
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"),
signingAlgorithm: alg,
keys,
extensions: [
new x509.BasicConstraintsExtension(true, 2, 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),
await x509.SubjectKeyIdentifierExtension.create(keys.publicKey)
]
});
const certificate = cert.toString("pem");
await certificateAuthorityCertDAL.create(
{
caId: ca.id,
certificate, // TODO: encrypt
certificateChain: "" // TODO: encrypt
},
tx
);
}
await certificateAuthoritySkDAL.create(
{
caId: ca.id,
pk, // TODO: encrypt
sk // TODO: encrypt
},
tx
);
return ca;
});
return newCa;
};
/**
* Generates a CSR for a CA
*/
const getCaCsr = async ({ caId }: TGetCaCsrDTO) => {
const ca = await certificateAuthorityDAL.findById(caId);
if (!ca) throw new BadRequestError({ message: "CA not found" });
// TODO: permissioning
const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id });
const caKeys = await certificateAuthoritySkDAL.findOne({ caId: ca.id });
const alg = {
name: "RSASSA-PKCS1-v1_5",
hash: "SHA-256",
publicExponent: new Uint8Array([1, 0, 1]),
modulusLength: 2048
};
const skObj = crypto.createPrivateKey({ key: caKeys.sk, format: "pem", type: "pkcs8" });
const pkObj = crypto.createPublicKey({ key: caKeys.pk, format: "pem", type: "spki" });
const sk = await crypto.subtle.importKey("pkcs8", skObj.export({ format: "der", type: "pkcs8" }), alg, true, [
"sign"
]);
const pk = await crypto.subtle.importKey("spki", pkObj.export({ format: "der", type: "spki" }), alg, true, [
"verify"
]);
const cert = new x509.X509Certificate(caCert.certificate);
const csr = await x509.Pkcs10CertificateRequestGenerator.create({
name: cert.subject || ca.dn,
keys: { privateKey: sk, publicKey: pk },
signingAlgorithm: alg,
extensions: [
// eslint-disable-next-line no-bitwise
new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment)
],
attributes: [new x509.ChallengePasswordAttribute("password")]
});
return csr.toString("base64");
};
/**
* Return certificate and certificate chain for CA
*/
const getCaCert = async ({ caId }: TGetCaCertDTO) => {
const ca = await certificateAuthorityDAL.findById(caId);
if (!ca) throw new BadRequestError({ message: "CA not found" });
// TODO: permissioning
const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id });
return {
certificate: caCert.certificate,
certificateChain: caCert.certificateChain
};
};
/**
* Issue new certificate
*/
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 ca = await certificateAuthorityDAL.findById(caId);
// if (!ca) throw new BadRequestError({ message: "CA not found" });
// // TODO: permissioning
// const alg = {
// name: "RSASSA-PKCS1-v1_5",
// hash: "SHA-256",
// publicExponent: new Uint8Array([1, 0, 1]),
// modulusLength: 2048
// };
// const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id });
// const caKeys = await certificateAuthoritySkDAL.findOne({ caId: ca.id });
// 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 cert = new x509.X509Certificate(caCert.certificate);
// 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)
// ]
// });
// console.log("leafCert: ", leafCert);
return "";
};
return {
createCa,
getCaCsr,
getCaCert,
issueCertFromCa
};
};
@@ -0,0 +1,10 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TCertificateAuthoritySkDALFactory = ReturnType<typeof certificateAuthoritySkDALFactory>;
export const certificateAuthoritySkDALFactory = (db: TDbClient) => {
const caSkOrm = ormify(db, TableName.CertificateAuthoritySk);
return caSkOrm;
};
@@ -0,0 +1,34 @@
import { TProjectPermission } from "@app/lib/types";
export enum CAType {
ROOT = "root",
INTERMEDIATE = "intermediate"
}
// TODO: attach permissions after draft impl
export type TCreateCaDTO = {
projectSlug: string;
type: CAType;
commonName: string;
organization: string;
ou: string;
country: string;
province: string;
locality: string;
} & Omit<TProjectPermission, "projectId">;
export type TGetCaCsrDTO = {
caId: string;
} & Omit<TProjectPermission, "projectId">;
export type TGetCaCertDTO = {
caId: string;
} & Omit<TProjectPermission, "projectId">;
export type TIssueCertFromCaDTO = {
caId: string;
csr: string;
notBefore: string;
notAfter: string;
} & Omit<TProjectPermission, "projectId">;
@@ -16,6 +16,7 @@ import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TProjectPermission } from "@app/lib/types";
import { ActorType } from "../auth/auth-type";
import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal";
import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal";
@@ -36,6 +37,7 @@ import {
TCreateProjectDTO,
TDeleteProjectDTO,
TGetProjectDTO,
TListProjectCasDTO,
TToggleProjectAutoCapitalizationDTO,
TUpdateProjectDTO,
TUpdateProjectNameDTO,
@@ -62,6 +64,7 @@ type TProjectServiceFactoryDep = {
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "create" | "findProjectGhostUser" | "findOne">;
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">;
secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "create">;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "find">;
permissionService: TPermissionServiceFactory;
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
@@ -89,6 +92,7 @@ export const projectServiceFactory = ({
licenseService,
projectUserMembershipRoleDAL,
identityProjectMembershipRoleDAL,
certificateAuthorityDAL,
keyStore
}: TProjectServiceFactoryDep) => {
/*
@@ -492,6 +496,30 @@ export const projectServiceFactory = ({
return project.upgradeStatus || null;
};
/**
* Return list of CAs for project
*/
const listProjectCas = async ({
// 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 cas = await certificateAuthorityDAL.find({ projectId: project.id });
return cas;
};
return {
createProject,
deleteProject,
@@ -501,6 +529,7 @@ export const projectServiceFactory = ({
getAProject,
toggleAutoCapitalization,
updateName,
upgradeProject
upgradeProject,
listProjectCas
};
};
@@ -75,3 +75,7 @@ export type AddUserToWsDTO = {
userPublicKey: string;
}[];
};
export type TListProjectCasDTO = {
filter: Filter;
} & Omit<TProjectPermission, "projectId">;