mirror of
https://github.com/netcccyun/dnsmgr.git
synced 2026-02-21 15:31:12 +08:00
684 lines
20 KiB
PHP
684 lines
20 KiB
PHP
<?php
|
|
|
|
namespace app\lib\acme;
|
|
|
|
use Exception;
|
|
use stdClass;
|
|
|
|
/**
|
|
* ACMECert
|
|
* https://github.com/skoerfgen/ACMECert
|
|
*/
|
|
class ACMECert extends ACMEv2
|
|
{
|
|
private $alternate_chains = array();
|
|
|
|
public function register($termsOfServiceAgreed = false, $contacts = array())
|
|
{
|
|
return $this->_register($termsOfServiceAgreed, $contacts);
|
|
}
|
|
|
|
public function registerEAB($termsOfServiceAgreed, $eab_kid, $eab_hmac, $contacts = array())
|
|
{
|
|
if (!$this->resources) $this->readDirectory();
|
|
|
|
$protected = array(
|
|
'alg' => 'HS256',
|
|
'kid' => $eab_kid,
|
|
'url' => $this->unproxiedURL($this->resources['newAccount'])
|
|
);
|
|
$payload = $this->jwk_header['jwk'];
|
|
|
|
$protected64 = $this->base64url(json_encode($protected, JSON_UNESCAPED_SLASHES));
|
|
$payload64 = $this->base64url(json_encode($payload, JSON_UNESCAPED_SLASHES));
|
|
|
|
$signature = hash_hmac('sha256', $protected64 . '.' . $payload64, $this->base64url_decode($eab_hmac), true);
|
|
|
|
return $this->_register($termsOfServiceAgreed, $contacts, array(
|
|
'externalAccountBinding' => array(
|
|
'protected' => $protected64,
|
|
'payload' => $payload64,
|
|
'signature' => $this->base64url($signature)
|
|
)
|
|
));
|
|
}
|
|
|
|
private function _register($termsOfServiceAgreed = false, $contacts = array(), $extra = array())
|
|
{
|
|
$this->log('Registering account');
|
|
|
|
$ret = $this->request('newAccount', array(
|
|
'termsOfServiceAgreed' => (bool)$termsOfServiceAgreed,
|
|
'contact' => $this->make_contacts_array($contacts)
|
|
) + $extra);
|
|
$this->log($ret['code'] == 201 ? 'Account registered' : 'Account already registered');
|
|
return $this->kid_header['kid'];
|
|
}
|
|
|
|
public function update($contacts = array())
|
|
{
|
|
$this->log('Updating account');
|
|
$ret = $this->request($this->getAccountID(), array(
|
|
'contact' => $this->make_contacts_array($contacts)
|
|
));
|
|
$this->log('Account updated');
|
|
return $ret['body'];
|
|
}
|
|
|
|
public function getAccount()
|
|
{
|
|
$ret = parent::getAccount();
|
|
return $this->kid_header['kid'];
|
|
}
|
|
|
|
public function setAccount($kid)
|
|
{
|
|
$this->kid_header['kid'] = $kid;
|
|
}
|
|
|
|
public function deactivateAccount()
|
|
{
|
|
$this->log('Deactivating account');
|
|
$ret = $this->deactivate($this->getAccountID());
|
|
$this->log('Account deactivated');
|
|
return $ret;
|
|
}
|
|
|
|
public function deactivate($url)
|
|
{
|
|
$this->log('Deactivating resource: ' . $url);
|
|
$ret = $this->request($url, array('status' => 'deactivated'));
|
|
$this->log('Resource deactivated');
|
|
return $ret['body'];
|
|
}
|
|
|
|
public function getTermsURL()
|
|
{
|
|
if (!$this->resources) $this->readDirectory();
|
|
if (!isset($this->resources['meta']['termsOfService'])) {
|
|
throw new Exception('Failed to get Terms Of Service URL');
|
|
}
|
|
return $this->resources['meta']['termsOfService'];
|
|
}
|
|
|
|
public function getCAAIdentities()
|
|
{
|
|
if (!$this->resources) $this->readDirectory();
|
|
if (!isset($this->resources['meta']['caaIdentities'])) {
|
|
throw new Exception('Failed to get CAA Identities');
|
|
}
|
|
return $this->resources['meta']['caaIdentities'];
|
|
}
|
|
|
|
public function keyChange($new_account_key_pem)
|
|
{ // account key roll-over
|
|
$this->loadAccountKey($new_account_key_pem);
|
|
$account = $this->getAccountID();
|
|
$this->resources = $this->resources;
|
|
|
|
$this->log('Account Key Roll-Over');
|
|
|
|
$ret = $this->request(
|
|
'keyChange',
|
|
$this->jws_encapsulate('keyChange', array(
|
|
'account' => $account,
|
|
'oldKey' => $this->jwk_header['jwk']
|
|
), true)
|
|
);
|
|
$this->log('Account Key Roll-Over successful');
|
|
|
|
$this->loadAccountKey($new_account_key_pem);
|
|
return $ret['body'];
|
|
}
|
|
|
|
public function revoke($pem)
|
|
{
|
|
if (false === ($res = openssl_x509_read($pem))) {
|
|
throw new Exception('Could not load certificate: ' . $pem . ' (' . $this->get_openssl_error() . ')');
|
|
}
|
|
if (false === (openssl_x509_export($res, $certificate))) {
|
|
throw new Exception('Could not export certificate: ' . $pem . ' (' . $this->get_openssl_error() . ')');
|
|
}
|
|
|
|
$this->log('Revoking certificate');
|
|
$this->request('revokeCert', array(
|
|
'certificate' => $this->base64url($this->pem2der($certificate))
|
|
));
|
|
$this->log('Certificate revoked');
|
|
}
|
|
|
|
public function createOrder($domain_config, $settings = array())
|
|
{
|
|
$settings = $this->parseSettings($settings);
|
|
|
|
$domain_config = array_change_key_case($domain_config, CASE_LOWER);
|
|
$domains = array_keys($domain_config);
|
|
$authz_deactivated = false;
|
|
|
|
// === Order ===
|
|
$this->log('Creating Order');
|
|
$ret = $this->request('newOrder', $this->makeOrder($domains, $settings));
|
|
$order = $ret['body'];
|
|
$order_location = $ret['headers']['location'];
|
|
$this->log('Order created: ' . $order_location);
|
|
$order['location'] = $order_location;
|
|
|
|
// === Authorization ===
|
|
if ($order['status'] === 'ready' && $settings['authz_reuse']) {
|
|
$this->log('All authorizations already valid, skipping validation altogether');
|
|
} else {
|
|
$auth_count = count($order['authorizations']);
|
|
$challenges = array();
|
|
|
|
foreach ($order['authorizations'] as $idx => $auth_url) {
|
|
$this->log('Fetching authorization ' . ($idx + 1) . ' of ' . $auth_count);
|
|
$ret = $this->request($auth_url, '');
|
|
$authorization = $ret['body'];
|
|
|
|
// wildcard authorization identifiers have no leading *.
|
|
$domain = ( // get domain and add leading *. if wildcard is used
|
|
isset($authorization['wildcard']) &&
|
|
$authorization['wildcard'] ?
|
|
'*.' : ''
|
|
) . $authorization['identifier']['value'];
|
|
|
|
if ($authorization['status'] === 'valid') {
|
|
if ($settings['authz_reuse']) {
|
|
$this->log('Authorization of ' . $domain . ' already valid, skipping validation');
|
|
} else {
|
|
$this->log('Authorization of ' . $domain . ' already valid, deactivating authorization');
|
|
$this->deactivate($auth_url);
|
|
$authz_deactivated = true;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if(!isset($domain_config[$domain])) {
|
|
$this->log('Domain ' . $domain . ' not found in domain_config');
|
|
continue;
|
|
}
|
|
|
|
$config = $domain_config[$domain];
|
|
$type = $config['challenge'];
|
|
|
|
$challenge = $this->parse_challenges($authorization, $type, $challenge_url);
|
|
|
|
$opts = array(
|
|
'domain' => $domain,
|
|
'type' => $type,
|
|
'auth_url' => $auth_url,
|
|
'challenge_url' => $challenge_url
|
|
);
|
|
list($opts['key'], $opts['value']) = $challenge;
|
|
|
|
$challenges[] = $opts;
|
|
}
|
|
|
|
if ($authz_deactivated) {
|
|
$this->log('Restarting Order after deactivating already valid authorizations');
|
|
$settings['authz_reuse'] = true;
|
|
return $this->createOrder($domain_config, $settings);
|
|
}
|
|
|
|
$order['challenges'] = $challenges;
|
|
}
|
|
return $order;
|
|
}
|
|
|
|
public function authOrder($order)
|
|
{
|
|
if ($order['status'] != 'pending' && $order['status'] != 'ready' && empty($order['challenges'])) {
|
|
throw new Exception('No challenges available');
|
|
}
|
|
|
|
// === Challenge ===
|
|
if (!empty($order['challenges'])){
|
|
foreach ($order['challenges'] as $opts) {
|
|
$this->log('Notifying server for validation of ' . $opts['domain']);
|
|
$this->request($opts['challenge_url'], new stdClass);
|
|
|
|
$this->log('Waiting for server challenge validation');
|
|
sleep(1);
|
|
|
|
if (!$this->poll('pending', $opts['auth_url'], $body)) {
|
|
$this->log('Validation failed: ' . $opts['domain']);
|
|
|
|
$error = $body['challenges'][0]['error'];
|
|
throw $this->create_ACME_Exception(
|
|
$error['type'],
|
|
'Challenge validation failed: ' . $error['detail']
|
|
);
|
|
} else {
|
|
$this->log('Validation successful: ' . $opts['domain']);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public function finalizeOrder($domains, $order, $pem)
|
|
{
|
|
// autodetect if Private Key or CSR is used
|
|
if ($key = openssl_pkey_get_private($pem)) { // Private Key detected
|
|
if (PHP_MAJOR_VERSION < 8) openssl_free_key($key);
|
|
$this->log('Generating CSR');
|
|
$csr = $this->generateCSR($pem, $domains);
|
|
} elseif (openssl_csr_get_subject($pem)) { // CSR detected
|
|
$this->log('Using provided CSR');
|
|
if (0 === strpos($pem, 'file://')) {
|
|
$csr = file_get_contents(substr($pem, 7));
|
|
if (false === $csr) {
|
|
throw new Exception('Failed to read CSR from ' . $pem . ' (' . $this->get_openssl_error() . ')');
|
|
}
|
|
} else {
|
|
$csr = $pem;
|
|
}
|
|
} else {
|
|
throw new Exception('Could not load Private Key or CSR (' . $this->get_openssl_error() . '): ' . $pem);
|
|
}
|
|
|
|
$this->log('Finalizing Order');
|
|
|
|
$ret = $this->request($order['finalize'], array(
|
|
'csr' => $this->base64url($this->pem2der($csr))
|
|
));
|
|
$ret = $ret['body'];
|
|
|
|
if (isset($ret['certificate'])) {
|
|
return $this->request_certificate($ret);
|
|
}
|
|
|
|
if ($this->poll('processing', $order['location'], $ret)) {
|
|
return $this->request_certificate($ret);
|
|
}
|
|
|
|
throw new Exception('Order failed');
|
|
}
|
|
|
|
public function finalizeOrders($domains, $order, $pem)
|
|
{
|
|
$default_chain = $this->finalizeOrder($domains, $order, $pem);
|
|
|
|
$out = array();
|
|
$out[$this->getTopIssuerCN($default_chain)] = $default_chain;
|
|
|
|
foreach ($this->alternate_chains as $link) {
|
|
$chain = $this->request_certificate(array('certificate' => $link), true);
|
|
$out[$this->getTopIssuerCN($chain)] = $chain;
|
|
}
|
|
|
|
$this->log('Received ' . count($out) . ' chain(s): ' . implode(', ', array_keys($out)));
|
|
return $out;
|
|
}
|
|
|
|
public function generateCSR($domain_key_pem, $domains)
|
|
{
|
|
if (false === ($domain_key = openssl_pkey_get_private($domain_key_pem))) {
|
|
throw new Exception('Could not load domain key: ' . $domain_key_pem . ' (' . $this->get_openssl_error() . ')');
|
|
}
|
|
|
|
$fn = $this->tmp_ssl_cnf($domains);
|
|
$cn = reset($domains);
|
|
$dn = array();
|
|
if (strlen($cn) <= 64) {
|
|
$dn['commonName'] = $cn;
|
|
}
|
|
$csr = openssl_csr_new($dn, $domain_key, array(
|
|
'config' => $fn,
|
|
'req_extensions' => 'SAN',
|
|
'digest_alg' => 'sha512'
|
|
));
|
|
unlink($fn);
|
|
if (PHP_MAJOR_VERSION < 8) openssl_free_key($domain_key);
|
|
|
|
if (false === $csr) {
|
|
throw new Exception('Could not generate CSR ! (' . $this->get_openssl_error() . ')');
|
|
}
|
|
if (false === openssl_csr_export($csr, $out)) {
|
|
throw new Exception('Could not export CSR ! (' . $this->get_openssl_error() . ')');
|
|
}
|
|
|
|
return $out;
|
|
}
|
|
|
|
private function generateKey($opts)
|
|
{
|
|
$fn = $this->tmp_ssl_cnf();
|
|
$config = array('config' => $fn) + $opts;
|
|
if (false === ($key = openssl_pkey_new($config))) {
|
|
throw new Exception('Could not generate new private key ! (' . $this->get_openssl_error() . ')');
|
|
}
|
|
if (false === openssl_pkey_export($key, $pem, null, $config)) {
|
|
throw new Exception('Could not export private key ! (' . $this->get_openssl_error() . ')');
|
|
}
|
|
unlink($fn);
|
|
if (PHP_MAJOR_VERSION < 8) openssl_free_key($key);
|
|
return $pem;
|
|
}
|
|
|
|
public function generateRSAKey($bits = 2048)
|
|
{
|
|
return $this->generateKey(array(
|
|
'private_key_bits' => (int)$bits,
|
|
'private_key_type' => OPENSSL_KEYTYPE_RSA
|
|
));
|
|
}
|
|
|
|
public function generateECKey($curve_name = '384')
|
|
{
|
|
if (version_compare(PHP_VERSION, '7.1.0') < 0) throw new Exception('PHP >= 7.1.0 required for EC keys !');
|
|
$map = array('256' => 'prime256v1', '384' => 'secp384r1', '521' => 'secp521r1');
|
|
if (isset($map[$curve_name])) $curve_name = $map[$curve_name];
|
|
return $this->generateKey(array(
|
|
'curve_name' => $curve_name,
|
|
'private_key_type' => OPENSSL_KEYTYPE_EC
|
|
));
|
|
}
|
|
|
|
public function parseCertificate($cert_pem)
|
|
{
|
|
if (false === ($ret = openssl_x509_read($cert_pem))) {
|
|
throw new Exception('Could not load certificate: ' . $cert_pem . ' (' . $this->get_openssl_error() . ')');
|
|
}
|
|
if (!is_array($ret = openssl_x509_parse($ret, true))) {
|
|
throw new Exception('Could not parse certificate (' . $this->get_openssl_error() . ')');
|
|
}
|
|
return $ret;
|
|
}
|
|
|
|
public function getSAN($pem)
|
|
{
|
|
$ret = $this->parseCertificate($pem);
|
|
if (!isset($ret['extensions']['subjectAltName'])) {
|
|
throw new Exception('No Subject Alternative Name (SAN) found in certificate');
|
|
}
|
|
$out = array();
|
|
foreach (explode(',', $ret['extensions']['subjectAltName']) as $line) {
|
|
list($type, $name) = array_map('trim', explode(':', $line));
|
|
if ($type === 'DNS') {
|
|
$out[] = $name;
|
|
}
|
|
}
|
|
return $out;
|
|
}
|
|
|
|
public function getRemainingDays($cert_pem)
|
|
{
|
|
$ret = $this->parseCertificate($cert_pem);
|
|
return ($ret['validTo_time_t'] - time()) / 86400;
|
|
}
|
|
|
|
public function getRemainingPercent($cert_pem)
|
|
{
|
|
$ret = $this->parseCertificate($cert_pem);
|
|
$total = $ret['validTo_time_t'] - $ret['validFrom_time_t'];
|
|
$used = time() - $ret['validFrom_time_t'];
|
|
return (1 - max(0, min(1, $used / $total))) * 100;
|
|
}
|
|
|
|
public function generateALPNCertificate($domain_key_pem, $domain, $token)
|
|
{
|
|
$domains = array($domain);
|
|
$csr = $this->generateCSR($domain_key_pem, $domains);
|
|
|
|
$fn = $this->tmp_ssl_cnf($domains, '1.3.6.1.5.5.7.1.31=critical,DER:0420' . $token . "\n");
|
|
$config = array(
|
|
'config' => $fn,
|
|
'x509_extensions' => 'SAN',
|
|
'digest_alg' => 'sha512'
|
|
);
|
|
$cert = openssl_csr_sign($csr, null, $domain_key_pem, 1, $config);
|
|
unlink($fn);
|
|
if (false === $cert) {
|
|
throw new Exception('Could not generate self signed certificate ! (' . $this->get_openssl_error() . ')');
|
|
}
|
|
if (false === openssl_x509_export($cert, $out)) {
|
|
throw new Exception('Could not export self signed certificate ! (' . $this->get_openssl_error() . ')');
|
|
}
|
|
return $out;
|
|
}
|
|
|
|
public function getARI($pem, &$ari_cert_id = null)
|
|
{
|
|
$ari_cert_id = null;
|
|
$id = $this->getARICertID($pem);
|
|
|
|
if (!$this->resources) $this->readDirectory();
|
|
if (!isset($this->resources['renewalInfo'])) throw new Exception('ARI not supported');
|
|
|
|
$ret = $this->http_request($this->resources['renewalInfo'] . '/' . $id);
|
|
|
|
if (!is_array($ret['body']['suggestedWindow'])) throw new Exception('ARI suggestedWindow not present');
|
|
|
|
$sw = &$ret['body']['suggestedWindow'];
|
|
|
|
if (!isset($sw['start'])) throw new Exception('ARI suggestedWindow start not present');
|
|
if (!isset($sw['end'])) throw new Exception('ARI suggestedWindow end not present');
|
|
|
|
$sw = array_map(array($this, 'parseDate'), $sw);
|
|
|
|
$ari_cert_id = $id;
|
|
return $ret['body'];
|
|
}
|
|
|
|
private function getARICertID($pem)
|
|
{
|
|
if (version_compare(PHP_VERSION, '7.1.2', '<')) {
|
|
throw new Exception('PHP Version >= 7.1.2 required for ARI'); // serialNumberHex - https://github.com/php/php-src/pull/1755
|
|
}
|
|
$ret = $this->parseCertificate($pem);
|
|
|
|
if (!isset($ret['extensions']['authorityKeyIdentifier'])) {
|
|
throw new Exception('authorityKeyIdentifier missing');
|
|
}
|
|
$aki = hex2bin(str_replace(':', '', substr(trim($ret['extensions']['authorityKeyIdentifier']), 6)));
|
|
if (!$aki) throw new Exception('Failed to parse authorityKeyIdentifier');
|
|
|
|
if (!isset($ret['serialNumberHex'])) {
|
|
throw new Exception('serialNumberHex missing');
|
|
}
|
|
$ser = hex2bin(trim($ret['serialNumberHex']));
|
|
if (!$ser) throw new Exception('Failed to parse serialNumberHex');
|
|
|
|
return $this->base64url($aki) . '.' . $this->base64url($ser);
|
|
}
|
|
|
|
private function parseDate($str)
|
|
{
|
|
$ret = strtotime(preg_replace('/(\.\d\d)\d+/', '$1', $str));
|
|
if ($ret === false) throw new Exception('Failed to parse date: ' . $str);
|
|
return $ret;
|
|
}
|
|
|
|
private function parseSettings($opts)
|
|
{
|
|
// authz_reuse: backwards compatibility to ACMECert v3.1.2 or older
|
|
if (!is_array($opts)) $opts = array('authz_reuse' => (bool)$opts);
|
|
if (!isset($opts['authz_reuse'])) $opts['authz_reuse'] = true;
|
|
|
|
$diff = array_diff_key(
|
|
$opts,
|
|
array_flip(array('authz_reuse', 'notAfter', 'notBefore', 'replaces'))
|
|
);
|
|
|
|
if (!empty($diff)) {
|
|
throw new Exception('getCertificateChain(s): Invalid option "' . key($diff) . '"');
|
|
}
|
|
|
|
return $opts;
|
|
}
|
|
|
|
private function setRFC3339Date(&$out, $key, $opts)
|
|
{
|
|
if (isset($opts[$key])) {
|
|
$out[$key] = is_string($opts[$key]) ?
|
|
$opts[$key] :
|
|
date(DATE_RFC3339, $opts[$key]);
|
|
}
|
|
}
|
|
|
|
private function makeOrder($domains, $opts)
|
|
{
|
|
$order = array(
|
|
'identifiers' => array_map(
|
|
function ($domain) {
|
|
return array('type' => 'dns', 'value' => $domain);
|
|
},
|
|
$domains
|
|
)
|
|
);
|
|
$this->setRFC3339Date($order, 'notAfter', $opts);
|
|
$this->setRFC3339Date($order, 'notBefore', $opts);
|
|
|
|
if (isset($opts['replaces'])) { // ARI
|
|
$order['replaces'] = $opts['replaces'];
|
|
$this->log('Replacing Certificate: ' . $opts['replaces']);
|
|
}
|
|
|
|
return $order;
|
|
}
|
|
|
|
private function parse_challenges($authorization, $type, &$url)
|
|
{
|
|
foreach ($authorization['challenges'] as $challenge) {
|
|
if ($challenge['type'] != $type) continue;
|
|
|
|
$url = $challenge['url'];
|
|
|
|
switch ($challenge['type']) {
|
|
case 'dns-01':
|
|
return array(
|
|
'_acme-challenge.' . $authorization['identifier']['value'],
|
|
$this->base64url(hash('sha256', $this->keyAuthorization($challenge['token']), true))
|
|
);
|
|
break;
|
|
case 'http-01':
|
|
return array(
|
|
'/.well-known/acme-challenge/' . $challenge['token'],
|
|
$this->keyAuthorization($challenge['token'])
|
|
);
|
|
break;
|
|
case 'tls-alpn-01':
|
|
return array(null, hash('sha256', $this->keyAuthorization($challenge['token'])));
|
|
break;
|
|
}
|
|
}
|
|
throw new Exception(
|
|
'Challenge type: "' . $type . '" not available, for this challenge use ' .
|
|
implode(' or ', array_map(
|
|
function ($a) {
|
|
return '"' . $a['type'] . '"';
|
|
},
|
|
$authorization['challenges']
|
|
))
|
|
);
|
|
}
|
|
|
|
private function poll($initial, $type, &$ret)
|
|
{
|
|
$max_tries = 10; // ~ 5 minutes
|
|
for ($i = 0; $i < $max_tries; $i++) {
|
|
$ret = $this->request($type);
|
|
$ret = $ret['body'];
|
|
if ($ret['status'] !== $initial) return $ret['status'] === 'valid';
|
|
$s = pow(2, min($i, 6));
|
|
if ($i !== $max_tries - 1) {
|
|
$this->log('Retrying in ' . ($s) . 's');
|
|
sleep($s);
|
|
}
|
|
}
|
|
throw new Exception('Aborted after ' . $max_tries . ' tries');
|
|
}
|
|
|
|
private function request_certificate($ret, $alternate = false)
|
|
{
|
|
$this->log('Requesting ' . ($alternate ? 'alternate' : 'default') . ' certificate-chain');
|
|
$ret = $this->request($ret['certificate'], '');
|
|
if ($ret['headers']['content-type'] !== 'application/pem-certificate-chain') {
|
|
throw new Exception('Unexpected content-type: ' . $ret['headers']['content-type']);
|
|
}
|
|
|
|
$chain = array();
|
|
foreach ($this->splitChain($ret['body']) as $cert) {
|
|
$info = $this->parseCertificate($cert);
|
|
$chain[] = '[' . $info['issuer']['CN'] . ']';
|
|
}
|
|
|
|
if (!$alternate) {
|
|
if (isset($ret['headers']['link']['alternate'])) {
|
|
$this->alternate_chains = $ret['headers']['link']['alternate'];
|
|
} else {
|
|
$this->alternate_chains = array();
|
|
}
|
|
}
|
|
|
|
$this->log(($alternate ? 'Alternate' : 'Default') . ' certificate-chain retrieved: ' . implode(' -> ', array_reverse($chain, true)));
|
|
return $ret['body'];
|
|
}
|
|
|
|
private function tmp_ssl_cnf($domains = null, $extension = '')
|
|
{
|
|
if (false === ($fn = tempnam(sys_get_temp_dir(), "CNF_"))) {
|
|
throw new Exception('Failed to create temp file !');
|
|
}
|
|
if (false === @file_put_contents(
|
|
$fn,
|
|
'HOME = .' . "\n" .
|
|
'RANDFILE=$ENV::HOME/.rnd' . "\n" .
|
|
'[v3_ca]' . "\n" .
|
|
'[req]' . "\n" .
|
|
'default_bits=2048' . "\n" .
|
|
($domains ?
|
|
'distinguished_name=req_distinguished_name' . "\n" .
|
|
'[req_distinguished_name]' . "\n" .
|
|
'[v3_req]' . "\n" .
|
|
'[SAN]' . "\n" .
|
|
'subjectAltName=' .
|
|
implode(',', array_map(function ($domain) {
|
|
return 'DNS:' . $domain;
|
|
}, $domains)) . "\n"
|
|
:
|
|
''
|
|
) . $extension
|
|
)) {
|
|
throw new Exception('Failed to write tmp file: ' . $fn);
|
|
}
|
|
return $fn;
|
|
}
|
|
|
|
private function pem2der($pem)
|
|
{
|
|
return base64_decode(implode('', array_slice(
|
|
array_map('trim', explode("\n", trim($pem))),
|
|
1,
|
|
-1
|
|
)));
|
|
}
|
|
|
|
private function make_contacts_array($contacts)
|
|
{
|
|
if (!is_array($contacts)) {
|
|
$contacts = $contacts ? array($contacts) : array();
|
|
}
|
|
return array_map(function ($contact) {
|
|
return 'mailto:' . $contact;
|
|
}, $contacts);
|
|
}
|
|
|
|
private function getTopIssuerCN($chain)
|
|
{
|
|
$tmp = $this->splitChain($chain);
|
|
$ret = $this->parseCertificate(end($tmp));
|
|
return $ret['issuer']['CN'];
|
|
}
|
|
|
|
public function splitChain($chain)
|
|
{
|
|
$delim = '-----END CERTIFICATE-----';
|
|
return array_map(function ($item) use ($delim) {
|
|
return trim($item . $delim);
|
|
}, array_filter(explode($delim, $chain), function ($item) {
|
|
return strpos($item, '-----BEGIN CERTIFICATE-----') !== false;
|
|
}));
|
|
}
|
|
}
|