generate static method

Future<GeneratedCertificates> generate({
  1. required String outputDir,
  2. List<String> hosts = const [],
  3. String commonName = 'localhost',
  4. String caCommonName = 'OmnyShell Dev CA',
  5. int caDays = 3650,
  6. int serverDays = 825,
  7. bool force = false,
})

Generate a CA plus a Hub server certificate into outputDir.

hosts are extra SAN DNS entries; localhost and 127.0.0.1 are always included. commonName is the server certificate's CN, caCommonName the CA's. caDays/serverDays set validity. If <outputDir>/server.crt already exists, the call throws unless force is set.

Implementation

static Future<GeneratedCertificates> generate({
  required String outputDir,
  List<String> hosts = const [],
  String commonName = 'localhost',
  String caCommonName = 'OmnyShell Dev CA',
  int caDays = 3650,
  int serverDays = 825,
  bool force = false,
}) async {
  final caCert = '$outputDir/ca.crt';
  final caKey = '$outputDir/ca.key';
  final serverCert = '$outputDir/server.crt';
  final serverKey = '$outputDir/server.key';
  final serverCsr = '$outputDir/server.csr';
  final serverLeaf = '$outputDir/server-leaf.crt';
  final caSerial = '$outputDir/ca.srl';

  if (!force && File(serverCert).existsSync()) {
    throw const CertGeneratorException(
      'certificates already exist in the output directory — '
      'pass --force to regenerate',
    );
  }

  await _requireOpenssl();
  Directory(outputDir).createSync(recursive: true);

  // Subject Alternative Names the server certificate is valid for.
  final san = StringBuffer('DNS:localhost,IP:127.0.0.1');
  for (final host in hosts) {
    if (host.isNotEmpty) san.write(',DNS:$host');
  }

  // 1. Local CA.
  await _openssl([
    'req',
    '-x509',
    '-newkey',
    'rsa:2048',
    '-nodes',
    '-keyout',
    caKey,
    '-out',
    caCert,
    '-days',
    '$caDays',
    '-subj',
    '/CN=$caCommonName',
    '-addext',
    'basicConstraints=critical,CA:TRUE',
    '-addext',
    'keyUsage=critical,keyCertSign,cRLSign',
  ]);

  // 2. Server key + CSR.
  await _openssl([
    'req',
    '-newkey',
    'rsa:2048',
    '-nodes',
    '-keyout',
    serverKey,
    '-out',
    serverCsr,
    '-subj',
    '/CN=$commonName',
  ]);

  // 3. Sign the server certificate with the CA. The extensions go in a temp
  //    file (bash process substitution has no Dart equivalent).
  final extFile = File(
    '${Directory.systemTemp.path}/omnyshell-cert-ext-$pid.cnf',
  );
  try {
    extFile.writeAsStringSync(
      'subjectAltName=$san\n'
      'basicConstraints=critical,CA:FALSE\n'
      'keyUsage=critical,digitalSignature,keyEncipherment\n'
      'extendedKeyUsage=serverAuth\n',
    );
    await _openssl([
      'x509',
      '-req',
      '-in',
      serverCsr,
      '-CA',
      caCert,
      '-CAkey',
      caKey,
      '-CAcreateserial',
      '-out',
      serverLeaf,
      '-days',
      '$serverDays',
      '-extfile',
      extFile.path,
    ]);
  } finally {
    if (extFile.existsSync()) extFile.deleteSync();
  }

  // 4. The Hub presents the full chain (leaf + CA) so clients can build the
  //    verification path.
  File(serverCert).writeAsStringSync(
    File(serverLeaf).readAsStringSync() + File(caCert).readAsStringSync(),
  );

  // 5. Clean up intermediates.
  for (final path in [serverCsr, serverLeaf, caSerial]) {
    final f = File(path);
    if (f.existsSync()) f.deleteSync();
  }

  return GeneratedCertificates(
    caCert: caCert,
    caKey: caKey,
    serverCert: serverCert,
    serverKey: serverKey,
  );
}