orbit/src/Orbit/Cert.php
2020-09-06 02:32:56 -05:00

230 lines
6.9 KiB
PHP

<?php declare(strict_types=1);
namespace Orbit;
use Monolog\Logger;
/**
* Cert
*
* @package Orbit
*/
class Cert
{
public $hostname = '';
public $tls_certfile = '';
public $tls_keyfile = '';
public $key_passphrase = '';
/**
* __construct
*
* @param Config $config
* @param Logger $logger
* @return void
*/
public function __construct(Config $config, ?Logger $logger = null)
{
$this->hostname = $config->hostname;
$this->tls_certfile = $config->tls_certfile;
$this->tls_keyfile = $config->tls_keyfile;
$this->key_passphrase = $config->key_passphrase;
if ($logger !== null) {
$this->logger = $logger;
} else {
$this->logger = new Logger('orbit-cert');
}
if ($config->getIsDevelopmentServer()) {
$this->initDevelopment();
} else {
$this->initProduction();
}
}
/**
* Initialize for development mode
*
* If the cert files do not exist, generate a new self-signed cert
*
* @return bool
*/
public function initDevelopment(): bool
{
$this->logger->debug("Initialize cert for development mode.");
if ($this->tls_certfile == '') {
$this->tls_certfile = sprintf("certs/%s.cert.pem", $this->hostname);
}
if ($this->tls_keyfile == '') {
$this->tls_keyfile = sprintf("certs/%s.key.pem", $this->hostname);
}
if (file_exists($this->tls_certfile) && file_exists($this->tls_keyfile)) {
$this->logger->info(sprintf("Using existing cert file '%s'", $this->tls_certfile));
$this->logger->info(sprintf("Using existing key file '%s'", $this->tls_keyfile));
} else {
$this->logger->info(sprintf("Generating new cert file '%s'", $this->tls_certfile));
$this->logger->info(sprintf("Generating new key file '%s'", $this->tls_keyfile));
if (file_exists($this->tls_certfile)) {
$this->logger->warning(sprintf("Warning! May overwrite existing cert file '%s'", $this->tls_certfile));
}
if (file_exists($this->tls_keyfile)) {
$this->logger->warning(sprintf("Warning! May overwrite existing key file '%s'", $this->tls_keyfile));
}
$this->generateCert();
}
return true;
}
/**
* Initialize for production mode
*
* Cert/key files must be provided. If fails, generates exception
*
* @return bool
*/
public function initProduction(): bool
{
$this->logger->debug("Initialize cert for production mode.");
$errors = [];
if ($this->tls_certfile == '') {
$errors[] = "Missing required cert file: use --tls-cert to specify;";
}
if ($this->tls_keyfile == '') {
$errors[] = "Missing required key file: use --tls-key to specify;";
}
if ($this->tls_certfile && !file_exists($this->tls_certfile)) {
$errors[] = sprintf("Cert file '%s' does not exist or is not readable!", $this->tls_certfile);
}
if ($this->tls_keyfile && !file_exists($this->tls_keyfile)) {
$errors[] = sprintf("Key file '%s' does not exist or is not readable!", $this->tls_keyfile);
}
if (count($errors)) {
$errors[] = "\nTry running with --dev to generate a self-signed cert automatically.";
$this->logger->alert(implode("\n", $errors));
throw new \Exception("\n" . implode("\n", $errors));
}
$this->logger->debug(sprintf("Using cert file '%s'", $this->tls_certfile));
$this->logger->debug(sprintf("Using key file '%s'", $this->tls_keyfile));
return true;
}
/**
* Generate a self-signed cert
*
* @return void
*/
private function generateCert(): void
{
// Certificate data
$dn = [
"countryName" => "UK",
"stateOrProvinceName" => "X",
"localityName" => "X",
"organizationName" => "X",
"organizationalUnitName" => "X",
"commonName" => $this->hostname,
"emailAddress" => "X",
];
$days_valid = 365;
$san_domains = ["DNS:" . $this->hostname, "IP:127.0.0.1", "IP:0.0.0.0", "IP:::1"];
$ssl_config = $this->createOpenSslConf($san_domains);
$csr_config = ['digest_alg' => 'sha256', 'req_extensions' => 'v3_req', 'config' => $ssl_config];
$cert_config = ['digest_alg' => 'sha256', 'x509_extensions' => 'usr_cert', 'config' => $ssl_config];
// Generate certificate
$private_key = openssl_pkey_new([
'private_key_type' => OPENSSL_KEYTYPE_RSA,
'private_key_bits' => 2048
]);
$cert = openssl_csr_new($dn, $private_key, $csr_config);
$cert = openssl_csr_sign($cert, null, $private_key, $days_valid, $cert_config);
// Generate PEM files
$pem = [];
openssl_x509_export($cert, $pem[0]);
openssl_pkey_export($private_key, $pem[1], $this->key_passphrase);
// Ensure dir exists for cert files
$this->ensureDirExists($this->tls_certfile);
$this->ensureDirExists($this->tls_keyfile);
// Save PEM files
file_put_contents($this->tls_certfile, $pem[0]);
file_put_contents($this->tls_keyfile, $pem[1]);
// Remove temp sslconf file
unlink($ssl_config);
}
/**
* createOpenSslConf
*
* Creating this temp file to be used in the CSR and the cert signing is
* required to add csr request and x509 extensions into the cert in order
* to include the subject alternative names
*
* @param array $san_domains
* @return string Filename
*/
private function createOpenSslConf(array $san_domains = []): string
{
$san_domains_string = implode(",", $san_domains);
$str = <<<EOS
[ req ]
distinguished_name = req_distinguished_name
req_extensions = v3_req
[ req_distinguished_name ]
[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
subjectAltName = $san_domains_string
[ usr_cert ]
basicConstraints=CA:FALSE
nsComment = "Generated Certificate by php unicorn"
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid,issuer
subjectAltName = $san_domains_string
EOS;
$temp_filename = tempnam("/tmp", "orbit-sslconf-");
file_put_contents($temp_filename, $str);
return $temp_filename;
}
/**
* Ensure directory for a given filename exists
*
* Will recursively create parent directories if they don't exist
*
* @param string $filename
* @return void
*/
private function ensureDirExists(string $filename): void
{
$dir = dirname($filename);
if (!is_dir($dir)) {
// Recursively make directory if necessary
mkdir($dir, 0777, true);
}
}
}