@hashgraph/solo
Version:
An opinionated CLI tool to deploy and manage private Hedera Networks.
442 lines • 19.8 kB
JavaScript
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
var KeyManager_1;
/**
* SPDX-License-Identifier: Apache-2.0
*/
import * as x509 from '@peculiar/x509';
import crypto from 'crypto';
import fs from 'fs';
import path from 'path';
import { SoloError, IllegalArgumentError, MissingArgumentError } from './errors.js';
import * as constants from './constants.js';
import { Templates } from './templates.js';
import * as helpers from './helpers.js';
import chalk from 'chalk';
import { inject, injectable } from 'tsyringe-neo';
import { patchInject } from './dependency_injection/container_helper.js';
import { InjectTokens } from './dependency_injection/inject_tokens.js';
// @ts-ignore
x509.cryptoProvider.set(crypto);
let KeyManager = class KeyManager {
static { KeyManager_1 = this; }
logger;
static SigningKeyAlgo = {
name: 'RSASSA-PKCS1-v1_5',
hash: 'SHA-384',
publicExponent: new Uint8Array([1, 0, 1]),
modulusLength: 3072,
};
static SigningKeyUsage = ['sign', 'verify'];
static TLSKeyAlgo = {
name: 'RSASSA-PKCS1-v1_5',
hash: 'SHA-384',
publicExponent: new Uint8Array([1, 0, 1]),
modulusLength: 4096,
};
static TLSKeyUsage = ['sign', 'verify'];
static TLSCertKeyUsages = x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment | x509.KeyUsageFlags.dataEncipherment;
static TLSCertKeyExtendedUsages = [x509.ExtendedKeyUsage.serverAuth, x509.ExtendedKeyUsage.clientAuth];
static ECKeyAlgo = {
name: 'ECDSA',
namedCurve: 'P-384',
hash: 'SHA-384',
};
constructor(logger) {
this.logger = logger;
this.logger = patchInject(logger, InjectTokens.SoloLogger, this.constructor.name);
}
/** Convert CryptoKey into PEM string */
async convertPrivateKeyToPem(privateKey) {
const ab = await crypto.subtle.exportKey('pkcs8', privateKey);
return x509.PemConverter.encode(ab, 'PRIVATE KEY');
}
/**
* Convert PEM private key into CryptoKey
* @param pemStr - PEM string
* @param algo - key algorithm
* @param [keyUsages]
*/
async convertPemToPrivateKey(pemStr, algo, keyUsages = ['sign']) {
if (!algo)
throw new MissingArgumentError('algo is required');
const items = x509.PemConverter.decode(pemStr);
// Since pem file may include multiple PEM data, the decoder returns an array
// However for private key there should be a single item.
// So, we just being careful here to pick the last item (similar to how last PEM data represents the actual cert in
// a certificate bundle)
const lastItem = items[items.length - 1];
return await crypto.subtle.importKey('pkcs8', lastItem, algo, false, keyUsages);
}
/**
* Return file names for node key
* @param nodeAlias
* @param keysDir - directory where keys and certs are stored
*/
prepareNodeKeyFilePaths(nodeAlias, keysDir) {
if (!nodeAlias)
throw new MissingArgumentError('nodeAlias is required');
if (!keysDir)
throw new MissingArgumentError('keysDir is required');
const keyFile = path.join(keysDir, Templates.renderGossipPemPrivateKeyFile(nodeAlias));
const certFile = path.join(keysDir, Templates.renderGossipPemPublicKeyFile(nodeAlias));
return {
privateKeyFile: keyFile,
certificateFile: certFile,
};
}
/**
* Return file names for TLS key
* @param nodeAlias
* @param keysDir - directory where keys and certs are stored
*/
prepareTLSKeyFilePaths(nodeAlias, keysDir) {
if (!nodeAlias)
throw new MissingArgumentError('nodeAlias is required');
if (!keysDir)
throw new MissingArgumentError('keysDir is required');
const keyFile = path.join(keysDir, `hedera-${nodeAlias}.key`);
const certFile = path.join(keysDir, `hedera-${nodeAlias}.crt`);
return {
privateKeyFile: keyFile,
certificateFile: certFile,
};
}
/**
* Store node keys and certs as PEM files
* @param nodeAlias
* @param nodeKey
* @param keysDir - directory where keys and certs are stored
* @param nodeKeyFiles
* @param [keyName] - optional key type name for logging
* @returns a Promise that saves the keys and certs as PEM files
*/
async storeNodeKey(nodeAlias, nodeKey, keysDir, nodeKeyFiles, keyName = '') {
if (!nodeAlias) {
throw new MissingArgumentError('nodeAlias is required');
}
if (!nodeKey || !nodeKey.privateKey) {
throw new MissingArgumentError('nodeKey.ed25519PrivateKey is required');
}
if (!nodeKey || !nodeKey.certificateChain) {
throw new MissingArgumentError('nodeKey.certificateChain is required');
}
if (!keysDir) {
throw new MissingArgumentError('keysDir is required');
}
if (!nodeKeyFiles || !nodeKeyFiles.privateKeyFile) {
throw new MissingArgumentError('nodeKeyFiles.privateKeyFile is required');
}
if (!nodeKeyFiles || !nodeKeyFiles.certificateFile) {
throw new MissingArgumentError('nodeKeyFiles.certificateFile is required');
}
const keyPem = await this.convertPrivateKeyToPem(nodeKey.privateKey);
const certPems = [];
nodeKey.certificateChain.forEach(cert => {
certPems.push(cert.toString('pem'));
});
const self = this;
return new Promise((resolve, reject) => {
try {
this.logger.debug(`Storing ${keyName} key for node: ${nodeAlias}`, { nodeKeyFiles });
fs.writeFileSync(nodeKeyFiles.privateKeyFile, keyPem);
// remove if the certificate file exists already as otherwise we'll keep appending to the last
if (fs.existsSync(nodeKeyFiles.certificateFile)) {
fs.rmSync(nodeKeyFiles.certificateFile);
}
certPems.forEach(certPem => {
fs.writeFileSync(nodeKeyFiles.certificateFile, certPem + '\n', { flag: 'a' });
});
self.logger.debug(`Stored ${keyName} key for node: ${nodeAlias}`, {
nodeKeyFiles,
});
resolve(nodeKeyFiles);
}
catch (e) {
reject(e);
}
});
}
/**
* Load node keys and certs from PEM files
* @param nodeAlias
* @param keysDir - directory where keys and certs are stored
* @param algo - algorithm used for key
* @param nodeKeyFiles an object stores privateKeyFile and certificateFile
* @param [keyName] - optional key type name for logging
* @returns
*/
async loadNodeKey(nodeAlias, keysDir, algo, nodeKeyFiles, keyName = '') {
if (!nodeAlias) {
throw new MissingArgumentError('nodeAlias is required');
}
if (!keysDir) {
throw new MissingArgumentError('keysDir is required');
}
if (!algo) {
throw new MissingArgumentError('algo is required');
}
if (!nodeKeyFiles || !nodeKeyFiles.privateKeyFile) {
throw new MissingArgumentError('nodeKeyFiles.privateKeyFile is required');
}
if (!nodeKeyFiles || !nodeKeyFiles.certificateFile) {
throw new MissingArgumentError('nodeKeyFiles.certificateFile is required');
}
this.logger.debug(`Loading ${keyName}-keys for node: ${nodeAlias}`, { nodeKeyFiles });
const keyBytes = fs.readFileSync(nodeKeyFiles.privateKeyFile);
const keyPem = keyBytes.toString();
const key = await this.convertPemToPrivateKey(keyPem, algo);
const certBytes = fs.readFileSync(nodeKeyFiles.certificateFile);
const certPems = x509.PemConverter.decode(certBytes.toString());
const certs = [];
certPems.forEach(certPem => {
const cert = new x509.X509Certificate(certPem);
certs.push(cert);
});
const certChain = await new x509.X509ChainBuilder({ certificates: certs.slice(1) }).build(certs[0]);
this.logger.debug(`Loaded ${keyName}-key for node: ${nodeAlias}`, {
nodeKeyFiles,
cert: certs[0].toString('pem'),
});
return {
privateKey: key,
certificate: certs[0],
certificateChain: certChain,
};
}
/** Generate signing key and certificate */
async generateSigningKey(nodeAlias) {
try {
const keyPrefix = constants.SIGNING_KEY_PREFIX;
const curDate = new Date();
const friendlyName = Templates.renderNodeFriendlyName(keyPrefix, nodeAlias);
this.logger.debug(`generating ${keyPrefix}-key for node: ${nodeAlias}`, { friendlyName });
const keypair = await crypto.subtle.generateKey(KeyManager_1.SigningKeyAlgo, true, KeyManager_1.SigningKeyUsage);
const cert = await x509.X509CertificateGenerator.createSelfSigned({
serialNumber: '01',
name: `CN=${friendlyName}`,
notBefore: curDate,
// @ts-ignore
notAfter: new Date().setFullYear(curDate.getFullYear() + constants.CERTIFICATE_VALIDITY_YEARS),
keys: keypair,
extensions: [
new x509.BasicConstraintsExtension(true, 1, true),
new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage.serverAuth, x509.ExtendedKeyUsage.clientAuth], true),
new x509.KeyUsagesExtension(x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.cRLSign, true),
await x509.SubjectKeyIdentifierExtension.create(keypair.publicKey),
],
});
const certChain = await new x509.X509ChainBuilder().build(cert);
this.logger.debug(`generated ${keyPrefix}-key for node: ${nodeAlias}`, { cert: cert.toString('pem') });
return {
privateKey: keypair.privateKey,
certificate: cert,
certificateChain: certChain,
};
}
catch (e) {
throw new SoloError(`failed to generate signing key: ${e.message}`, e);
}
}
/**
* Store signing key and certificate
* @param nodeAlias
* @param nodeKey - an object containing privateKeyPem, certificatePem data
* @param keysDir - directory where keys and certs are stored
* @returns returns a Promise that saves the keys and certs as PEM files
*/
storeSigningKey(nodeAlias, nodeKey, keysDir) {
const nodeKeyFiles = this.prepareNodeKeyFilePaths(nodeAlias, keysDir);
return this.storeNodeKey(nodeAlias, nodeKey, keysDir, nodeKeyFiles, 'signing');
}
/**
* Load signing key and certificate
* @param nodeAlias
* @param keysDir - directory path where pem files are stored
*/
loadSigningKey(nodeAlias, keysDir) {
const nodeKeyFiles = this.prepareNodeKeyFilePaths(nodeAlias, keysDir);
return this.loadNodeKey(nodeAlias, keysDir, KeyManager_1.SigningKeyAlgo, nodeKeyFiles, 'signing');
}
/**
* Generate gRPC TLS key
*
* It generates TLS keys in PEM format such as below:
* hedera-<nodeAlias>.key
* hedera-<nodeAlias>.crt
*
* @param nodeAlias
* @param distinguishedName distinguished name as: new x509.Name(`CN=${nodeAlias},ST=${state},L=${locality},O=${org},OU=${orgUnit},C=${country}`)
*/
async generateGrpcTlsKey(nodeAlias, distinguishedName = new x509.Name(`CN=${nodeAlias}`)) {
if (!nodeAlias)
throw new MissingArgumentError('nodeAlias is required');
if (!distinguishedName)
throw new MissingArgumentError('distinguishedName is required');
try {
const curDate = new Date();
this.logger.debug(`generating gRPC TLS for node: ${nodeAlias}`, { distinguishedName });
const keypair = await crypto.subtle.generateKey(KeyManager_1.TLSKeyAlgo, true, KeyManager_1.TLSKeyUsage);
const cert = await x509.X509CertificateGenerator.createSelfSigned({
serialNumber: '01',
name: distinguishedName,
notBefore: curDate,
// @ts-ignore
notAfter: new Date().setFullYear(curDate.getFullYear() + constants.CERTIFICATE_VALIDITY_YEARS),
keys: keypair,
extensions: [
new x509.BasicConstraintsExtension(false, 0, true),
new x509.KeyUsagesExtension(KeyManager_1.TLSCertKeyUsages, true),
new x509.ExtendedKeyUsageExtension(KeyManager_1.TLSCertKeyExtendedUsages, true),
await x509.SubjectKeyIdentifierExtension.create(keypair.publicKey, false),
await x509.AuthorityKeyIdentifierExtension.create(keypair.publicKey, false),
],
});
const certChain = await new x509.X509ChainBuilder().build(cert);
this.logger.debug(`generated gRPC TLS for node: ${nodeAlias}`, { cert: cert.toString('pem') });
return {
privateKey: keypair.privateKey,
certificate: cert,
certificateChain: certChain,
};
}
catch (e) {
throw new SoloError(`failed to generate gRPC TLS key: ${e.message}`, e);
}
}
/**
* Store TLS key and certificate
* @param nodeAlias
* @param nodeKey
* @param keysDir - directory where keys and certs are stored
* @returns a Promise that saves the keys and certs as PEM files
*/
storeTLSKey(nodeAlias, nodeKey, keysDir) {
const nodeKeyFiles = this.prepareTLSKeyFilePaths(nodeAlias, keysDir);
return this.storeNodeKey(nodeAlias, nodeKey, keysDir, nodeKeyFiles, 'gRPC TLS');
}
/**
* Load TLS key and certificate
* @param nodeAlias
* @param keysDir - directory path where pem files are stored
*/
loadTLSKey(nodeAlias, keysDir) {
const nodeKeyFiles = this.prepareTLSKeyFilePaths(nodeAlias, keysDir);
return this.loadNodeKey(nodeAlias, keysDir, KeyManager_1.TLSKeyAlgo, nodeKeyFiles, 'gRPC TLS');
}
copyNodeKeysToStaging(nodeKey, destDir) {
for (const keyFile of [nodeKey.privateKeyFile, nodeKey.certificateFile]) {
if (!fs.existsSync(keyFile)) {
throw new SoloError(`file (${keyFile}) is missing`);
}
const fileName = path.basename(keyFile);
fs.cpSync(keyFile, path.join(destDir, fileName));
}
}
copyGossipKeysToStaging(keysDir, stagingKeysDir, nodeAliases) {
// copy gossip keys to the staging
for (const nodeAlias of nodeAliases) {
const signingKeyFiles = this.prepareNodeKeyFilePaths(nodeAlias, keysDir);
this.copyNodeKeysToStaging(signingKeyFiles, stagingKeysDir);
}
}
/**
* Return a list of subtasks to generate gossip keys
*
* WARNING: These tasks MUST run in sequence.
*
* @param nodeAliases
* @param keysDir - keys directory
* @param curDate - current date
* @param [allNodeAliases] - includes the nodeAliases to get new keys as well as existing nodeAliases that will be included in the public.pfx file
* @return a list of subtasks
*/
taskGenerateGossipKeys(nodeAliases, keysDir, curDate = new Date(), allNodeAliases = null) {
allNodeAliases = allNodeAliases || nodeAliases; // TODO: unused variable
if (!Array.isArray(nodeAliases) || !nodeAliases.every(nodeAlias => typeof nodeAlias === 'string')) {
throw new IllegalArgumentError('nodeAliases must be an array of strings, nodeAliases = ' + JSON.stringify(nodeAliases));
}
const self = this;
const subTasks = [];
subTasks.push({
title: 'Backup old files',
task: () => helpers.backupOldPemKeys(nodeAliases, keysDir, curDate),
});
for (const nodeAlias of nodeAliases) {
subTasks.push({
title: `Gossip key for node: ${chalk.yellow(nodeAlias)}`,
task: async () => {
const signingKey = await self.generateSigningKey(nodeAlias);
const signingKeyFiles = await self.storeSigningKey(nodeAlias, signingKey, keysDir);
this.logger.debug(`generated Gossip signing keys for node ${nodeAlias}`, { keyFiles: signingKeyFiles });
},
});
}
return subTasks;
}
/**
* Return a list of subtasks to generate gRPC TLS keys
*
* WARNING: These tasks should run in sequence
*
* @param nodeAliases
* @param keysDir keys directory
* @param curDate current date
* @return return a list of subtasks
*/
taskGenerateTLSKeys(nodeAliases, keysDir, curDate = new Date()) {
// check if nodeAliases is an array of strings
if (!Array.isArray(nodeAliases) || !nodeAliases.every(nodeAlias => typeof nodeAlias === 'string')) {
throw new SoloError('nodeAliases must be an array of strings');
}
const self = this;
const nodeKeyFiles = new Map();
const subTasks = [];
subTasks.push({
title: 'Backup old files',
task: () => helpers.backupOldTlsKeys(nodeAliases, keysDir, curDate),
});
for (const nodeAlias of nodeAliases) {
subTasks.push({
title: `TLS key for node: ${chalk.yellow(nodeAlias)}`,
task: async () => {
const tlsKey = await self.generateGrpcTlsKey(nodeAlias);
const tlsKeyFiles = await self.storeTLSKey(nodeAlias, tlsKey, keysDir);
nodeKeyFiles.set(nodeAlias, {
tlsKeyFiles,
});
},
});
}
return subTasks;
}
/**
* Given the path to the PEM certificate (Base64 ASCII), will return the DER (raw binary) bytes
* @param pemCertFullPath
*/
getDerFromPemCertificate(pemCertFullPath) {
const certPem = fs.readFileSync(pemCertFullPath).toString();
const decodedDers = x509.PemConverter.decode(certPem);
if (!decodedDers || decodedDers.length === 0) {
throw new SoloError('unable to load perm key: ' + pemCertFullPath);
}
return new Uint8Array(decodedDers[0]);
}
};
KeyManager = KeyManager_1 = __decorate([
injectable(),
__param(0, inject(InjectTokens.SoloLogger)),
__metadata("design:paramtypes", [Function])
], KeyManager);
export { KeyManager };
//# sourceMappingURL=key_manager.js.map