Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(ssl): replace openssl dependency with node-forge #29

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
25 changes: 0 additions & 25 deletions openssl-configurations/certificate-authority-self-signing.conf

This file was deleted.

25 changes: 0 additions & 25 deletions openssl-configurations/domain-certificate-signing-requests.conf

This file was deleted.

39 changes: 0 additions & 39 deletions openssl-configurations/domain-certificates.conf

This file was deleted.

2 changes: 2 additions & 0 deletions package.json
Expand Up @@ -37,6 +37,7 @@
"@types/lodash": "^4.14.92",
"@types/mkdirp": "^0.5.2",
"@types/node": "^8.5.7",
"@types/node-forge": "^0.7.6",
"@types/rimraf": "^2.0.2",
"@types/tmp": "^0.0.33",
"application-config-path": "^0.1.0",
Expand All @@ -48,6 +49,7 @@
"glob": "^7.1.2",
"lodash": "^4.17.4",
"mkdirp": "^0.5.1",
"node-forge": "^0.7.6",
"password-prompt": "^1.0.4",
"rimraf": "^2.6.2",
"sudo-prompt": "^8.2.0",
Expand Down
17 changes: 5 additions & 12 deletions src/certificate-authority.ts
Expand Up @@ -11,16 +11,12 @@ import createDebug from 'debug';
import {
rootCAKeyPath,
rootCACertPath,
caSelfSignConfig,
opensslSerialFilePath,
opensslDatabaseFilePath,
isWindows,
isLinux,
caVersionFile
} from './constants';
import currentPlatform from './platforms';
import { openssl, mktmp } from './utils';
import { generateKey } from './certificates';
import { generateKey, generateCACertificate, mktmp } from './utils';
import { Options } from './index';

const debug = createDebug('devcert:certificate-authority');
Expand All @@ -35,16 +31,17 @@ export default async function installCertificateAuthority(options: Options = {})

debug(`Generating a root certificate authority`);
let rootKeyPath = mktmp();
let rootPublicKeyPath = mktmp();
let rootCertPath = mktmp();

debug(`Generating the OpenSSL configuration needed to setup the certificate authority`);
seedConfigFiles();

debug(`Generating a private key`);
generateKey(rootKeyPath);
await generateKey(rootKeyPath, rootPublicKeyPath);

debug(`Generating a CA certificate`);
openssl(`req -new -x509 -config "${ caSelfSignConfig }" -key "${ rootKeyPath }" -out "${ rootCertPath }"`);
await generateCACertificate(rootKeyPath, rootPublicKeyPath, rootCertPath);

debug('Saving certificate authority credentials');
await saveCertificateAuthorityCredentials(rootKeyPath, rootCertPath);
Expand Down Expand Up @@ -87,15 +84,11 @@ function scrubOldInsecureVersions() {
}

/**
* Initializes the files OpenSSL needs to sign certificates as a certificate
* authority, as well as our CA setup version
* Initializes our CA setup version
*/
function seedConfigFiles() {
// This is v2 of the devcert certificate authority setup
writeFile(caVersionFile, '2');
// OpenSSL CA files
writeFile(opensslDatabaseFilePath, '');
writeFile(opensslSerialFilePath, '01');
}

export async function withCertificateAuthorityCredentials(cb: ({ caKeyPath, caCertPath }: { caKeyPath: string, caCertPath: string }) => Promise<void> | void) {
Expand Down
36 changes: 12 additions & 24 deletions src/certificates.ts
@@ -1,9 +1,9 @@
// import path from 'path';
import createDebug from 'debug';
import { unlinkSync as rm } from 'fs';
import { sync as mkdirp } from 'mkdirp';
import { chmodSync as chmod } from 'fs';
import { pathForDomain, withDomainSigningRequestConfig, withDomainCertificateConfig } from './constants';
import { openssl } from './utils';
import { pathForDomain } from './constants';
import { generateCertificateWithCA, generateKey, mktmp } from './utils';
import { withCertificateAuthorityCredentials } from './certificate-authority';

const debug = createDebug('devcert:certificates');
Expand All @@ -18,29 +18,17 @@ const debug = createDebug('devcert:certificates');
export default async function generateDomainCertificate(domain: string): Promise<void> {
mkdirp(pathForDomain(domain));

debug(`Generating private key for ${ domain }`);
let domainKeyPath = pathForDomain(domain, 'private-key.key');
generateKey(domainKeyPath);
debug(`Generating key pair for ${ domain }`);
let domainPublicKeyPath = mktmp();
let domainPrivateKeyPath = pathForDomain(domain, 'private-key.key')
await generateKey(domainPrivateKeyPath, domainPublicKeyPath);

debug(`Generating certificate signing request for ${ domain }`);
let csrFile = pathForDomain(domain, `certificate-signing-request.csr`);
withDomainSigningRequestConfig(domain, (configpath) => {
openssl(`req -new -config "${ configpath }" -key "${ domainKeyPath }" -out "${ csrFile }"`);
});

debug(`Generating certificate for ${ domain } from signing request and signing with root CA`);
debug(`Generating certificate for ${ domain } and signing with root CA`);
let domainCertPath = pathForDomain(domain, `certificate.crt`);

await withCertificateAuthorityCredentials(({ caKeyPath, caCertPath }) => {
withDomainCertificateConfig(domain, (domainCertConfigPath) => {
openssl(`ca -config "${ domainCertConfigPath }" -in "${ csrFile }" -out "${ domainCertPath }" -keyfile "${ caKeyPath }" -cert "${ caCertPath }" -days 7000 -batch`)
});
});
await withCertificateAuthorityCredentials(({ caKeyPath, caCertPath }) =>
generateCertificateWithCA(domain, domainCertPath, domainPublicKeyPath, domainPrivateKeyPath, caKeyPath, caCertPath)
);
rm(domainPublicKeyPath);
}

// Generate a cryptographic key, used to sign certificates or certificate signing requests.
export function generateKey(filename: string): void {
debug(`generateKey: ${ filename }`);
openssl(`genrsa -out "${ filename }" 2048`);
chmod(filename, 400);
}
41 changes: 5 additions & 36 deletions src/constants.ts
@@ -1,10 +1,11 @@
import path from 'path';
import { unlinkSync as rm, writeFileSync as writeFile, readFileSync as readFile } from 'fs';
import { sync as mkdirp } from 'mkdirp';
import { template as makeTemplate } from 'lodash';
import applicationConfigPath = require('application-config-path');
import eol from 'eol';
import { mktmp } from './utils';

export function daysToMs(days: number): number {
return days * 24 * 60 * 60 * 1000;
}
export const defaultDays = daysToMs(7000);

// Platform shortcuts
export const isMac = process.platform === 'darwin';
Expand All @@ -19,38 +20,6 @@ export const domainsDir = configPath('domains');
export const pathForDomain: (domain: string, ...pathSegments: string[]) => string = path.join.bind(path, domainsDir)

export const caVersionFile = configPath('devcert-ca-version');
export const opensslSerialFilePath = configPath('certificate-authority', 'serial');
export const opensslDatabaseFilePath = configPath('certificate-authority', 'index.txt');
export const caSelfSignConfig = path.join(__dirname, '../openssl-configurations/certificate-authority-self-signing.conf');

export function withDomainSigningRequestConfig(domain: string, cb: (filepath: string) => void) {
let tmpFile = mktmp();
let source = readFile(path.join(__dirname, '../openssl-configurations/domain-certificate-signing-requests.conf'), 'utf-8');
let template = makeTemplate(source);
let result = template({ domain });
writeFile(tmpFile, eol.auto(result));
cb(tmpFile);
rm(tmpFile);
}

export function withDomainCertificateConfig(domain: string, cb: (filepath: string) => void) {
let tmpFile = mktmp();
let source = readFile(path.join(__dirname, '../openssl-configurations/domain-certificates.conf'), 'utf-8');
let template = makeTemplate(source);
let result = template({
domain,
serialFile: opensslSerialFilePath,
databaseFile: opensslDatabaseFilePath,
domainDir: pathForDomain(domain)
});
writeFile(tmpFile, eol.auto(result));
cb(tmpFile);
rm(tmpFile);
}

// confTemplate = confTemplate.replace(/DATABASE_PATH/, configPath('index.txt').replace(/\\/g, '\\\\'));
// confTemplate = confTemplate.replace(/SERIAL_PATH/, configPath('serial').replace(/\\/g, '\\\\'));
// confTemplate = eol.auto(confTemplate);

export const rootCADir = configPath('certificate-authority');
export const rootCAKeyPath = configPath('certificate-authority', 'private-key.key');
Expand Down
9 changes: 2 additions & 7 deletions src/index.ts
@@ -1,14 +1,13 @@
import { readFileSync as readFile, readdirSync as readdir, existsSync as exists } from 'fs';
import createDebug from 'debug';
import { sync as commandExists } from 'command-exists';
import rimraf from 'rimraf';
import {
isMac,
isLinux,
isWindows,
pathForDomain,
domainsDir,
rootCAKeyPath
rootCACertPath
} from './constants';
import currentPlatform from './platforms';
import installCertificateAuthority from './certificate-authority';
Expand Down Expand Up @@ -46,14 +45,10 @@ export async function certificateFor(domain: string, options: Options = {}) {
throw new Error(`Platform not supported: "${ process.platform }"`);
}

if (!commandExists('openssl')) {
throw new Error('OpenSSL not found: OpenSSL is required to generate SSL certificates - make sure it is installed and available in your PATH');
}

let domainKeyPath = pathForDomain(domain, `private-key.key`);
let domainCertPath = pathForDomain(domain, `certificate.crt`);

if (!exists(rootCAKeyPath)) {
if (!exists(rootCACertPath)) {
debug('Root CA is not installed yet, so it must be our first run. Installing root CA ...');
await installCertificateAuthority(options);
}
Expand Down
18 changes: 18 additions & 0 deletions src/types.d.ts
@@ -1,7 +1,25 @@
/// <reference types="node-forge" />
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not super familiar with this syntax, but I was under the impression it was no longer necessary?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was the way I got TypeScript to refer to DefinitelyTyped node-forge types in this file. If I used an import declaration, then TypeScript stopped regarding the file as a definition file and would error on compilation.

declare module "command-exists";
declare module "eol";
declare module "sudo-prompt";
declare module "password-prompt";
declare module "application-config-path" {
export = (appName: string) => string;
}

/**
* The @types/node-forge package is missing these definitions.
*/
declare module "node-forge" {
namespace random {
function getBytes(numBytes: int, callback: (err: Error, bytes: Bytes) => any): Bytes;
}
namespace pki {
interface CertificateAuthorityStore {
addCertificate: (cert: Certificate) => void;
}
function createCertificationRequest(): Certificate;
function createCaStore(): CertificateAuthorityStore;
function verifyCertificateChain(caStore: CertificateAuthorityStore, certs: Certificate[]): void;
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest filing a PR with the DefinitelyTyped repo to land these types for everyone to use!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's a good idea! I haven't opened that PR yet--would you strongly prefer to wait until DefinitelyTyped approves, merges and releases such a PR before merging this?

}