230 lines
6.9 KiB
PHP
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);
|
|
}
|
|
}
|
|
}
|