You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

144 lines
4.8 KiB

import {
unlinkSync as rm,
readFileSync as readFile,
writeFileSync as writeFile
} from 'fs';
import createDebug from 'debug';
import {
domainsDir,
rootCADir,
ensureConfigDirs,
getLegacyConfigDir,
rootCAKeyPath,
rootCACertPath,
caSelfSignConfig,
opensslSerialFilePath,
opensslDatabaseFilePath,
caVersionFile
} from './constants';
import currentPlatform from './platforms';
import { openssl, mktmp } from './utils';
import { generateKey } from './certificates';
import { Options } from './index';
const debug = createDebug('devcert:certificate-authority');
/**
* Install the once-per-machine trusted root CA. We'll use this CA to sign
* per-app certs.
*/
export default async function installCertificateAuthority(options: Options = {}): Promise<void> {
debug(`Uninstalling existing certificates, which will be void once any existing CA is gone`);
uninstall();
ensureConfigDirs();
debug(`Making a temp working directory for files to copied in`);
let rootKeyPath = mktmp();
debug(`Generating the OpenSSL configuration needed to setup the certificate authority`);
seedConfigFiles();
debug(`Generating a private key`);
generateKey(rootKeyPath);
debug(`Generating a CA certificate`);
openssl(['req', '-new', '-x509', '-config', caSelfSignConfig, '-key', rootKeyPath, '-out', rootCACertPath, '-days', '825']);
debug('Saving certificate authority credentials');
await saveCertificateAuthorityCredentials(rootKeyPath);
debug(`Adding the root certificate authority to trust stores`);
await currentPlatform.addToTrustStores(rootCACertPath, options);
}
/**
* Initializes the files OpenSSL needs to sign certificates as a certificate
* authority, as well as 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) {
debug(`Retrieving devcert's certificate authority credentials`);
let tmpCAKeyPath = mktmp();
let caKey = await currentPlatform.readProtectedFile(rootCAKeyPath);
writeFile(tmpCAKeyPath, caKey);
await cb({ caKeyPath: tmpCAKeyPath, caCertPath: rootCACertPath });
rm(tmpCAKeyPath);
}
async function saveCertificateAuthorityCredentials(keypath: string) {
debug(`Saving devcert's certificate authority credentials`);
let key = readFile(keypath, 'utf-8');
await currentPlatform.writeProtectedFile(rootCAKeyPath, key);
}
function certErrors(): string {
try {
openssl(['x509', '-in', rootCACertPath, '-noout']);
return '';
} catch (e) {
return e.toString();
}
}
// This function helps to migrate from v1.0.x to >= v1.1.0.
/**
* Smoothly migrate the certificate storage from v1.0.x to >= v1.1.0.
* In v1.1.0 there are new options for retrieving the CA cert directly,
* to help third-party Node apps trust the root CA.
*
* If a v1.0.x cert already exists, then devcert has written it with
* platform.writeProtectedFile(), so an unprivileged readFile cannot access it.
* Pre-detect and remedy this; it should only happen once per installation.
*/
export async function ensureCACertReadable(options: Options = {}): Promise<void> {
if (!certErrors()) {
return;
}
/**
* on windows, writeProtectedFile left the cert encrypted on *nix, the cert
* has no read permissions either way, openssl will fail and that means we
* have to fix it
*/
try {
const caFileContents = await currentPlatform.readProtectedFile(rootCACertPath);
currentPlatform.deleteProtectedFiles(rootCACertPath);
writeFile(rootCACertPath, caFileContents);
} catch (e) {
return installCertificateAuthority(options);
}
// double check that we have a live one
const remainingErrors = certErrors();
if (remainingErrors) {
return installCertificateAuthority(options);
}
}
/**
* Remove as much of the devcert files and state as we can. This is necessary
* when generating a new root certificate, and should be available to API
* consumers as well.
*
* Not all of it will be removable. If certutil is not installed, we'll leave
* Firefox alone. We try to remove files with maximum permissions, and if that
* fails, we'll silently fail.
*
* It's also possible that the command to untrust will not work, and we'll
* silently fail that as well; with no existing certificates anymore, the
* security exposure there is minimal.
*/
export function uninstall(): void {
currentPlatform.removeFromTrustStores(rootCACertPath);
currentPlatform.deleteProtectedFiles(domainsDir);
currentPlatform.deleteProtectedFiles(rootCADir);
currentPlatform.deleteProtectedFiles(getLegacyConfigDir());
}