Add ssh project default cas

This commit is contained in:
Tuan Dang
2025-04-06 14:22:17 -07:00
parent 5671cd5cef
commit c2f2a038ad
141 changed files with 696 additions and 400 deletions
@@ -1,21 +1,29 @@
import { execFile } from "child_process";
import crypto from "crypto";
import { promises as fs } from "fs";
import { Knex } from "knex";
import os from "os";
import path from "path";
import { promisify } from "util";
import { TSshCertificateTemplates } from "@app/db/schemas";
import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certificate-types";
import { BadRequestError } from "@app/lib/errors";
import { ms } from "@app/lib/ms";
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
import { KmsDataKey } from "@app/services/kms/kms-types";
import {
isValidHostPattern,
isValidUserPattern
} from "../ssh-certificate-template/ssh-certificate-template-validators";
import { SshCertType, TCreateSshCertDTO } from "./ssh-certificate-authority-types";
import {
SshCaKeySource,
SshCaStatus,
SshCertType,
TCreateSshCaHelperDTO,
TCreateSshCertDTO
} from "./ssh-certificate-authority-types";
const execFileAsync = promisify(execFile);
@@ -31,31 +39,35 @@ export const createSshCertSerialNumber = () => {
* Return a pair of SSH CA keys based on the specified key algorithm [keyAlgorithm].
* We use this function because the key format generated by `ssh-keygen` is unique.
*/
export const createSshKeyPair = async (keyAlgorithm: CertKeyAlgorithm) => {
export const createSshKeyPair = async (keyAlgorithm: SshCertKeyAlgorithm) => {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "ssh-key-"));
const privateKeyFile = path.join(tempDir, "id_key");
const publicKeyFile = `${privateKeyFile}.pub`;
let keyType: string;
let keyBits: string;
let keyBits: string | null;
switch (keyAlgorithm) {
case CertKeyAlgorithm.RSA_2048:
case SshCertKeyAlgorithm.RSA_2048:
keyType = "rsa";
keyBits = "2048";
break;
case CertKeyAlgorithm.RSA_4096:
case SshCertKeyAlgorithm.RSA_4096:
keyType = "rsa";
keyBits = "4096";
break;
case CertKeyAlgorithm.ECDSA_P256:
case SshCertKeyAlgorithm.ECDSA_P256:
keyType = "ecdsa";
keyBits = "256";
break;
case CertKeyAlgorithm.ECDSA_P384:
case SshCertKeyAlgorithm.ECDSA_P384:
keyType = "ecdsa";
keyBits = "384";
break;
case SshCertKeyAlgorithm.ED25519:
keyType = "ed25519";
keyBits = null;
break;
default:
throw new BadRequestError({
message: "Failed to produce SSH CA key pair generation command due to unrecognized key algorithm"
@@ -63,10 +75,16 @@ export const createSshKeyPair = async (keyAlgorithm: CertKeyAlgorithm) => {
}
try {
const args = ["-t", keyType];
if (keyBits !== null) {
args.push("-b", keyBits);
}
args.push("-f", privateKeyFile, "-N", "");
// Generate the SSH key pair
// The "-N ''" sets an empty passphrase
// The keys are created in the temporary directory
await execFileAsync("ssh-keygen", ["-t", keyType, "-b", keyBits, "-f", privateKeyFile, "-N", ""], {
await execFileAsync("ssh-keygen", args, {
timeout: EXEC_TIMEOUT_MS
});
@@ -322,17 +340,22 @@ const validateSshPublicKey = async (publicKey: string) => {
}
};
export const getKeyAlgorithmFromFingerprintOutput = (output: string): CertKeyAlgorithm | undefined => {
export const getKeyAlgorithmFromFingerprintOutput = (output: string): SshCertKeyAlgorithm | undefined => {
const parts = output.trim().split(" ");
const bitsInt = parseInt(parts[0], 10);
const keyTypeRaw = parts.at(-1)?.replace(/[()]/g, ""); // remove surrounding parentheses
if (keyTypeRaw === "RSA") {
return bitsInt === 2048 ? CertKeyAlgorithm.RSA_2048 : CertKeyAlgorithm.RSA_4096;
return bitsInt === 2048 ? SshCertKeyAlgorithm.RSA_2048 : SshCertKeyAlgorithm.RSA_4096;
}
if (keyTypeRaw === "ECDSA") {
return bitsInt === 256 ? CertKeyAlgorithm.ECDSA_P256 : CertKeyAlgorithm.ECDSA_P384;
return bitsInt === 256 ? SshCertKeyAlgorithm.ECDSA_P256 : SshCertKeyAlgorithm.ECDSA_P384;
}
if (keyTypeRaw === "ED25519") {
// TODO: test
return SshCertKeyAlgorithm.ED25519;
}
return undefined;
@@ -473,3 +496,68 @@ export const createSshCert = async ({
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
}
};
export const createSshCaHelper = async ({
projectId,
friendlyName,
keyAlgorithm: requestedKeyAlgorithm,
keySource,
externalPk,
externalSk,
sshCertificateAuthorityDAL,
sshCertificateAuthoritySecretDAL,
kmsService,
tx: outerTx
}: TCreateSshCaHelperDTO) => {
// Function to handle the actual creation logic
const processCreation = async (tx: Knex) => {
let publicKey: string;
let privateKey: string;
let keyAlgorithm: SshCertKeyAlgorithm = requestedKeyAlgorithm;
if (keySource === SshCaKeySource.INTERNAL) {
// generate SSH CA key pair internally
({ publicKey, privateKey } = await createSshKeyPair(requestedKeyAlgorithm));
} else {
// use external SSH CA key pair
if (!externalPk || !externalSk) {
throw new BadRequestError({
message: "Public and private keys are required if generateSigningKey is false"
});
}
publicKey = externalPk;
privateKey = externalSk;
keyAlgorithm = await validateExternalSshCaKeyPair(publicKey, privateKey);
}
const ca = await sshCertificateAuthorityDAL.create(
{
projectId,
friendlyName,
status: SshCaStatus.ACTIVE,
keyAlgorithm,
keySource
},
tx
);
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey(
{
type: KmsDataKey.SecretManager,
projectId
},
tx
);
await sshCertificateAuthoritySecretDAL.create(
{
sshCaId: ca.id,
encryptedPrivateKey: secretManagerEncryptor({ plainText: Buffer.from(privateKey, "utf8") }).cipherTextBlob
},
tx
);
return { ...ca, publicKey };
};
if (outerTx) {
return processCreation(outerTx);
}
return sshCertificateAuthorityDAL.transaction(processCreation);
};