Skip to content

Commit

Permalink
feat: added --private-key-file option to CommandStart.ts and `Com…
Browse files Browse the repository at this point in the history
…mandBootstrap.ts`

This should allow us to override the keypair generation with the provided private key. this will speed up agent starting.

Note that the key is provided as a Pem. The `PrivateKey` type contains functions that get destroyed somewhere between `commandStart.ts` and `keyManager.createKeyManager`. So I'm using the Pem string to keep the type primitive

Related #404
  • Loading branch information
tegefaulkes committed Jul 18, 2022
1 parent 4e210ca commit 898dd08
Show file tree
Hide file tree
Showing 11 changed files with 178 additions and 23 deletions.
6 changes: 6 additions & 0 deletions src/bin/agent/CommandStart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ class CommandStart extends CommandPolykey {
this.addOption(binOptions.backgroundOutFile);
this.addOption(binOptions.backgroundErrFile);
this.addOption(binOptions.fresh);
this.addOption(binOptions.privateKeyFile);
this.action(async (options) => {
options.clientHost =
options.clientHost ?? config.defaults.networkConfig.clientHost;
Expand Down Expand Up @@ -88,12 +89,17 @@ class CommandStart extends CommandPolykey {
const [seedNodes, defaults] = options.seedNodes;
let seedNodes_ = seedNodes;
if (defaults) seedNodes_ = { ...options.network, ...seedNodes };
const privateKeyPem =
options.privateKeyFile != null
? await binProcessors.processPrivateKeyFile(options.privateKeyFile)
: undefined;
const agentConfig = {
password,
nodePath: options.nodePath,
keysConfig: {
rootKeyPairBits: options.rootKeyPairBits,
recoveryCode: recoveryCodeIn,
privateKeyPemOverride: privateKeyPem,
},
proxyConfig: {
connConnectTime: options.connectionTimeout,
Expand Down
8 changes: 7 additions & 1 deletion src/bin/bootstrap/CommandBootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class CommandBootstrap extends CommandPolykey {
this.addOption(binOptions.recoveryCodeFile);
this.addOption(binOptions.rootKeyPairBits);
this.addOption(binOptions.fresh);
this.addOption(binOptions.privateKeyFile);
this.action(async (options) => {
const bootstrapUtils = await import('../../bootstrap/utils');
const password = await binProcessors.processNewPassword(
Expand All @@ -21,19 +22,24 @@ class CommandBootstrap extends CommandPolykey {
options.recoveryCodeFile,
this.fs,
);
const privateKeyPem =
options.privateKeyFile != null
? await binProcessors.processPrivateKeyFile(options.privateKeyFile)
: undefined;
const recoveryCodeOut = await bootstrapUtils.bootstrapState({
password,
nodePath: options.nodePath,
keysConfig: {
rootKeyPairBits: options.rootKeyPairBits,
recoveryCode: recoveryCodeIn,
privateKeyPemOverride: privateKeyPem,
},
fresh: options.fresh,
fs: this.fs,
logger: this.logger,
});
this.logger.info(`Bootstrapped ${options.nodePath}`);
process.stdout.write(recoveryCodeOut + '\n');
if (recoveryCodeOut != null) process.stdout.write(recoveryCodeOut + '\n');
});
}
}
Expand Down
6 changes: 6 additions & 0 deletions src/bin/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ class ErrorCLIRecoveryCodeFileRead<T> extends ErrorCLI<T> {
exitCode = sysexits.NOINPUT;
}

class ErrorCLIPrivateKeyFileRead<T> extends ErrorCLI<T> {
static description = 'Failed to read private key Pem file';
exitCode = sysexits.NOINPUT;
}

class ErrorCLIFileRead<T> extends ErrorCLI<T> {
static description = 'Failed to read file';
exitCode = sysexits.NOINPUT;
Expand Down Expand Up @@ -61,6 +66,7 @@ export {
ErrorCLIPasswordMissing,
ErrorCLIPasswordFileRead,
ErrorCLIRecoveryCodeFileRead,
ErrorCLIPrivateKeyFileRead,
ErrorCLIFileRead,
ErrorCLIPolykeyAgentStatus,
ErrorCLIPolykeyAgentProcess,
Expand Down
8 changes: 8 additions & 0 deletions src/bin/utils/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,13 @@ const noPing = new commander.Option('--no-ping', 'Skip ping step').default(
true,
);

const privateKeyFile = new commander.Option(
'--private-key-file <privateKeyFile>',
'Override key generation with a private key Pem from a file.',
)
.env('PK_PRIVATE_KEY_FILE')
.default(undefined);

export {
nodePath,
format,
Expand All @@ -187,4 +194,5 @@ export {
pullVault,
forceNodeAdd,
noPing,
privateKeyFile,
};
26 changes: 25 additions & 1 deletion src/bin/utils/processors.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { FileSystem } from '../../types';
import type { RecoveryCode } from '../../keys/types';
import type { RecoveryCode, PrivateKeyPem } from '../../keys/types';
import type { NodeId } from '../../nodes/types';
import type { Host, Port } from '../../network/types';
import type {
Expand Down Expand Up @@ -403,6 +403,29 @@ async function processAuthentication(
return meta;
}

async function processPrivateKeyFile(
privateKeyFile: string,
fs: FileSystem = require('fs'),
): Promise<PrivateKeyPem> {
let privateKeyPem: string;
try {
privateKeyPem = (
await fs.promises.readFile(privateKeyFile, 'utf-8')
).trim();
} catch (e) {
throw new binErrors.ErrorCLIPrivateKeyFileRead(e.message, {
data: {
errno: e.errno,
syscall: e.syscall,
code: e.code,
path: e.path,
},
cause: e,
});
}
return privateKeyPem;
}

export {
promptPassword,
promptNewPassword,
Expand All @@ -412,4 +435,5 @@ export {
processClientOptions,
processClientStatus,
processAuthentication,
processPrivateKeyFile,
};
5 changes: 3 additions & 2 deletions src/bootstrap/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { FileSystem } from '../types';
import type { RecoveryCode } from '../keys/types';
import type { RecoveryCode, PrivateKeyPem } from '../keys/types';
import path from 'path';
import Logger from '@matrixai/logger';
import { DB } from '@matrixai/db';
Expand Down Expand Up @@ -40,11 +40,12 @@ async function bootstrapState({
rootCertDuration?: number;
dbKeyBits?: number;
recoveryCode?: RecoveryCode;
privateKeyPemOverride?: PrivateKeyPem;
};
fresh?: boolean;
fs?: FileSystem;
logger?: Logger;
}): Promise<RecoveryCode> {
}): Promise<RecoveryCode | undefined> {
const umask = 0o077;
logger.info(`Setting umask to ${umask.toString(8).padStart(3, '0')}`);
process.umask(umask);
Expand Down
29 changes: 15 additions & 14 deletions src/keys/KeyManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type {
CertificatePemChain,
RecoveryCode,
KeyManagerChangeData,
PrivateKey,
PrivateKeyPem,
} from './types';
import type { FileSystem } from '../types';
import type { NodeId } from '../nodes/types';
Expand Down Expand Up @@ -41,7 +41,7 @@ class KeyManager {
fs = require('fs'),
logger = new Logger(this.name),
recoveryCode,
privateKeyOverride,
privateKeyPemOverride,
fresh = false,
}: {
keysPath: string;
Expand All @@ -53,7 +53,7 @@ class KeyManager {
fs?: FileSystem;
logger?: Logger;
recoveryCode?: RecoveryCode;
privateKeyOverride?: PrivateKey;
privateKeyPemOverride?: PrivateKeyPem;
fresh?: boolean;
}): Promise<KeyManager> {
logger.info(`Creating ${this.name}`);
Expand All @@ -70,7 +70,7 @@ class KeyManager {
await keyManager.start({
password,
recoveryCode,
privateKeyOverride,
privateKeyPemOverride,
fresh,
});
logger.info(`Created ${this.name}`);
Expand Down Expand Up @@ -138,12 +138,12 @@ class KeyManager {
public async start({
password,
recoveryCode,
privateKeyOverride,
privateKeyPemOverride,
fresh = false,
}: {
password: string;
recoveryCode?: RecoveryCode;
privateKeyOverride?: PrivateKey;
privateKeyPemOverride?: PrivateKeyPem;
fresh?: boolean;
}): Promise<void> {
this.logger.info(`Starting ${this.constructor.name}`);
Expand All @@ -166,7 +166,7 @@ class KeyManager {
password,
this.rootKeyPairBits,
recoveryCode,
privateKeyOverride,
privateKeyPemOverride,
);
const rootCert = await this.setupRootCert(
rootKeyPair,
Expand Down Expand Up @@ -597,18 +597,18 @@ class KeyManager {

/**
* Generates and writes the encrypted keypair to a the root key file.
* If privateKeyOverride is provided then key generation is skipped in favor of the provided key.
* If state already exists the privateKeyOverride is ignored.
* If privateKeyPemOverride is provided then key generation is skipped in favor of the provided key.
* If state already exists the privateKeyPemOverride is ignored.
* @param password
* @param bits - Bit-width of the generated key.
* @param recoveryCode - Code to generate the key from.
* @param privateKeyOverride - Override generation with a provided private key.
* @param privateKeyPemOverride - Override generation with a provided private key.
*/
protected async setupRootKeyPair(
password: string,
bits: number = 4096,
recoveryCode: RecoveryCode | undefined,
privateKeyOverride: PrivateKey | undefined,
privateKeyPemOverride: PrivateKeyPem | undefined,
): Promise<[KeyPair, RecoveryCode | undefined]> {
let rootKeyPair: KeyPair;
let recoveryCodeNew: RecoveryCode | undefined;
Expand All @@ -627,10 +627,11 @@ class KeyManager {
}
return [rootKeyPair, undefined];
} else {
if (privateKeyOverride != null) {
if (privateKeyPemOverride != null) {
this.logger.info('Using provided root key pair');
const publicKey = keysUtils.publicKeyFromPrivateKey(privateKeyOverride);
rootKeyPair = { privateKey: privateKeyOverride, publicKey };
const privateKey = keysUtils.privateKeyFromPem(privateKeyPemOverride);
const publicKey = keysUtils.publicKeyFromPrivateKey(privateKey);
rootKeyPair = { privateKey, publicKey };
await this.writeRootKeyPair(rootKeyPair, password);
return [rootKeyPair, undefined];
}
Expand Down
18 changes: 18 additions & 0 deletions src/validation/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ import type { GestaltAction, GestaltId } from '../gestalts/types';
import type { VaultAction, VaultId } from '../vaults/types';
import type { Host, Hostname, Port } from '../network/types';
import type { ClaimId } from '../claims/types';
import type { PrivateKey } from '../keys/types';
import * as validationErrors from './errors';
import * as nodesUtils from '../nodes/utils';
import * as gestaltsUtils from '../gestalts/utils';
import * as vaultsUtils from '../vaults/utils';
import * as networkUtils from '../network/utils';
import * as claimsUtils from '../claims/utils';
import * as keysUtils from '../keys/utils';
import config from '../config';

function parseInteger(data: any): number {
Expand Down Expand Up @@ -259,6 +261,21 @@ function parseSeedNodes(data: any): [SeedNodes, boolean] {
return [seedNodes, defaults];
}

function parsePrivateKeyPem(data: any): PrivateKey {
if (typeof data !== 'string') {
throw new validationErrors.ErrorParse('Private key Pem must be a string');
}
let privateKey: PrivateKey;
try {
privateKey = keysUtils.privateKeyFromPem(data);
} catch (e) {
throw new validationErrors.ErrorParse(
'Must provide a valid private key Pem',
);
}
return privateKey;
}

export {
parseInteger,
parseNumber,
Expand All @@ -276,4 +293,5 @@ export {
parsePort,
parseNetwork,
parseSeedNodes,
parsePrivateKeyPem,
};
48 changes: 48 additions & 0 deletions tests/bin/agent/start.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import PolykeyAgent from '@/PolykeyAgent';
import Status from '@/status/Status';
import * as statusErrors from '@/status/errors';
import config from '@/config';
import * as keysUtils from '@/keys/utils';
import * as testBinUtils from '../utils';
import * as testUtils from '../../utils';
import { runDescribeIfPlatforms, runTestIfPlatforms } from '../../utils';
Expand Down Expand Up @@ -740,6 +741,53 @@ describe('start', () => {
},
global.defaultTimeout * 2,
);
runTestIfPlatforms('linux', 'docker')(
'start with --private-key override',
async () => {
const status = new Status({
statusPath: path.join(dataDir, 'polykey', config.defaults.statusBase),
statusLockPath: path.join(
dataDir,
'polykey',
config.defaults.statusLockBase,
),
fs,
logger,
});
const password = 'abc123';
// Make sure these ports are not occupied
const rootKeys = await keysUtils.generateKeyPair(4096);
const privateKeyPem = keysUtils.privateKeyToPem(rootKeys.privateKey);
const nodeId = keysUtils.publicKeyToNodeId(rootKeys.publicKey);
const privateKeyPath = path.join(dataDir, 'private.pem');
await fs.promises.writeFile(privateKeyPath, privateKeyPem, {
encoding: 'utf-8',
});
const agentProcess = await testBinUtils.pkSpawnSwitch(global.testCmd)(
[
'agent',
'start',
'--workers',
'0',
'--verbose',
'--private-key-file',
privateKeyPath,
],
{
PK_NODE_PATH: path.join(dataDir, 'polykey'),
PK_PASSWORD: password,
},
dataDir,
logger,
);
const statusInfo = await status.waitFor('LIVE');
expect(nodeId.equals(statusInfo.data.nodeId)).toBe(true);
agentProcess.kill('SIGINT');
// Check for graceful exit
await status.waitFor('DEAD');
},
global.defaultTimeout * 2,
);
runDescribeIfPlatforms('linux')('start with global agent', () => {
let globalAgentStatus: StatusLive;
let globalAgentClose;
Expand Down
Loading

0 comments on commit 898dd08

Please sign in to comment.