1import {
2  generateKeyPair,
3  generateSelfSignedCodeSigningCertificate,
4  convertCertificateToCertificatePEM,
5  convertKeyPairToPEM,
6} from '@expo/code-signing-certificates';
7import assert from 'assert';
8import { promises as fs } from 'fs';
9import path from 'path';
10
11import { ensureDirAsync } from './utils/dir';
12import { log } from './utils/log';
13
14type Options = {
15  certificateValidityDurationYears: number;
16  keyOutput: string;
17  certificateOutput: string;
18  certificateCommonName: string;
19};
20
21export async function generateCodeSigningAsync(
22  projectRoot: string,
23  { certificateValidityDurationYears, keyOutput, certificateOutput, certificateCommonName }: Options
24) {
25  const validityDurationYears = Math.floor(certificateValidityDurationYears);
26
27  const certificateOutputDir = path.resolve(projectRoot, certificateOutput);
28  const keyOutputDir = path.resolve(projectRoot, keyOutput);
29  await Promise.all([ensureDirAsync(certificateOutputDir), ensureDirAsync(keyOutputDir)]);
30
31  const [certificateOutputDirContents, keyOutputDirContents] = await Promise.all([
32    fs.readdir(certificateOutputDir),
33    fs.readdir(keyOutputDir),
34  ]);
35  assert(certificateOutputDirContents.length === 0, 'Certificate output directory must be empty');
36  assert(keyOutputDirContents.length === 0, 'Key output directory must be empty');
37
38  const keyPair = generateKeyPair();
39  const validityNotBefore = new Date();
40  const validityNotAfter = new Date();
41  validityNotAfter.setFullYear(validityNotAfter.getFullYear() + validityDurationYears);
42  const certificate = generateSelfSignedCodeSigningCertificate({
43    keyPair,
44    validityNotBefore,
45    validityNotAfter,
46    commonName: certificateCommonName,
47  });
48
49  const keyPairPEM = convertKeyPairToPEM(keyPair);
50  const certificatePEM = convertCertificateToCertificatePEM(certificate);
51
52  await Promise.all([
53    fs.writeFile(path.join(keyOutputDir, 'public-key.pem'), keyPairPEM.publicKeyPEM),
54    fs.writeFile(path.join(keyOutputDir, 'private-key.pem'), keyPairPEM.privateKeyPEM),
55    fs.writeFile(path.join(certificateOutputDir, 'certificate.pem'), certificatePEM),
56  ]);
57
58  log(
59    `Generated public and private keys output in ${keyOutputDir}. Remember to add them to .gitignore or to encrypt them. (e.g. with git-crypt)`
60  );
61  log(`Generated code signing certificate output in ${certificateOutputDir}.`);
62  log(
63    `To automatically configure this project for code signing, run \`yarn expo-updates codesigning:configure --certificate-input-directory=${certificateOutput} --key-input-directory=${keyOutput}\`.`
64  );
65}
66