check for php-curl installed when cron_letsencrypt runs; format source
Signed-off-by: Michael Kaufmann (d00p) <d00p@froxlor.org>
This commit is contained in:
@@ -4,14 +4,14 @@
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are met:
|
||||
// * Redistributions of source code must retain the above copyright
|
||||
// notice, this list of conditions and the following disclaimer.
|
||||
// * Redistributions in binary form must reproduce the above copyright
|
||||
// notice, this list of conditions and the following disclaimer in the
|
||||
// documentation and/or other materials provided with the distribution.
|
||||
// * Neither the name of the <organization> nor the
|
||||
// names of its contributors may be used to endorse or promote products
|
||||
// derived from this software without specific prior written permission.
|
||||
// * Redistributions of source code must retain the above copyright
|
||||
// notice, this list of conditions and the following disclaimer.
|
||||
// * Redistributions in binary form must reproduce the above copyright
|
||||
// notice, this list of conditions and the following disclaimer in the
|
||||
// documentation and/or other materials provided with the distribution.
|
||||
// * Neither the name of the <organization> nor the
|
||||
// names of its contributors may be used to endorse or promote products
|
||||
// derived from this software without specific prior written permission.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
@@ -28,274 +28,285 @@
|
||||
// and modified to work without files and integrate in Froxlor
|
||||
class lescript
|
||||
{
|
||||
public $license = 'https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf';
|
||||
|
||||
private $logger;
|
||||
private $client;
|
||||
private $accountKey;
|
||||
public $license = 'https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf';
|
||||
|
||||
public function __construct($logger)
|
||||
{
|
||||
$this->logger = $logger;
|
||||
if (Settings::Get('system.letsencryptca') == 'production') {
|
||||
$ca = 'https://acme-v01.api.letsencrypt.org';
|
||||
} else {
|
||||
$ca = 'https://acme-staging.api.letsencrypt.org';
|
||||
}
|
||||
$this->client = new Client($ca);
|
||||
$this->log("Using '$ca' to generate certificate");
|
||||
}
|
||||
private $logger;
|
||||
|
||||
public function initAccount($certrow)
|
||||
{
|
||||
// Let's see if we have the private accountkey
|
||||
$this->accountKey = $certrow['leprivatekey'];
|
||||
if (!$this->accountKey || $this->accountKey == 'unset' || Settings::Get('system.letsencryptca') != 'production') {
|
||||
private $client;
|
||||
|
||||
// generate and save new private key for account
|
||||
// ---------------------------------------------
|
||||
private $accountKey;
|
||||
|
||||
$this->log('Starting new account registration');
|
||||
$keys = $this->generateKey();
|
||||
// Only store the accountkey in production, in staging always generate a new key
|
||||
if (Settings::Get('system.letsencryptca') == 'production') {
|
||||
$upd_stmt = Database::prepare("
|
||||
UPDATE `".TABLE_PANEL_CUSTOMERS."` SET `lepublickey` = :public, `leprivatekey` = :private WHERE `customerid` = :customerid;
|
||||
");
|
||||
Database::pexecute($upd_stmt, array('public' => $keys['public'], 'private' => $keys['private'], 'customerid' => $certrow['customerid']));
|
||||
}
|
||||
$this->accountKey = $keys['private'];
|
||||
$this->postNewReg();
|
||||
$this->log('New account certificate registered');
|
||||
|
||||
} else {
|
||||
|
||||
$this->log('Account already registered. Continuing.');
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public function signDomains(array $domains, $domainkey = null, $csr = null)
|
||||
{
|
||||
|
||||
if (!$this->accountKey) {
|
||||
throw new \RuntimeException("Account not initiated");
|
||||
}
|
||||
|
||||
$this->log('Starting certificate generation process for domains');
|
||||
|
||||
$privateAccountKey = openssl_pkey_get_private($this->accountKey);
|
||||
$accountKeyDetails = openssl_pkey_get_details($privateAccountKey);
|
||||
|
||||
// start domains authentication
|
||||
// ----------------------------
|
||||
|
||||
foreach($domains as $domain) {
|
||||
|
||||
// 1. getting available authentication options
|
||||
// -------------------------------------------
|
||||
|
||||
$this->log("Requesting challenge for $domain");
|
||||
|
||||
$response = $this->signedRequest(
|
||||
"/acme/new-authz",
|
||||
array("resource" => "new-authz", "identifier" => array("type" => "dns", "value" => $domain))
|
||||
);
|
||||
|
||||
// if response is not an array but a string, it's most likely a server-error, e.g.
|
||||
// <HTML><HEAD><TITLE>Error</TITLE></HEAD><BODY>An error occurred while processing your request.
|
||||
// <p>Reference #179.d8be1402.1458059103.3613c4db</BODY></HTML>
|
||||
if (!is_array($response)) {
|
||||
throw new RuntimeException("Invalid response from LE for domain $domain. Whole response: ".$response);
|
||||
}
|
||||
|
||||
if (!array_key_exists('challenges', $response)) {
|
||||
throw new RuntimeException("No challenges received for $domain. Whole response: ".json_encode($response));
|
||||
}
|
||||
|
||||
// choose http-01 challenge only
|
||||
$challenge = array_reduce($response['challenges'], function($v, $w) { return $v ? $v : ($w['type'] == 'http-01' ? $w : false); });
|
||||
if(!$challenge) throw new RuntimeException("HTTP Challenge for $domain is not available. Whole response: ".json_encode($response));
|
||||
|
||||
$this->log("Got challenge token for $domain");
|
||||
$location = $this->client->getLastLocation();
|
||||
|
||||
|
||||
// 2. saving authentication token for web verification
|
||||
// ---------------------------------------------------
|
||||
|
||||
$directory = Settings::Get('system.letsencryptchallengepath').'/.well-known/acme-challenge';
|
||||
$tokenPath = $directory.'/'.$challenge['token'];
|
||||
|
||||
if(!file_exists($directory) && !@mkdir($directory, 0755, true)) {
|
||||
throw new \RuntimeException("Couldn't create directory to expose challenge: ${tokenPath}");
|
||||
}
|
||||
|
||||
$header = array(
|
||||
// need to be in precise order!
|
||||
"e" => Base64UrlSafeEncoder::encode($accountKeyDetails["rsa"]["e"]),
|
||||
"kty" => "RSA",
|
||||
"n" => Base64UrlSafeEncoder::encode($accountKeyDetails["rsa"]["n"])
|
||||
|
||||
);
|
||||
$payload = $challenge['token'] . '.' . Base64UrlSafeEncoder::encode(hash('sha256', json_encode($header), true));
|
||||
|
||||
file_put_contents($tokenPath, $payload);
|
||||
chmod($tokenPath, 0644);
|
||||
|
||||
// 3. verification process itself
|
||||
// -------------------------------
|
||||
|
||||
$uri = "http://${domain}/.well-known/acme-challenge/${challenge['token']}";
|
||||
|
||||
$this->log("Token for $domain saved at $tokenPath and should be available at $uri");
|
||||
|
||||
// simple self check
|
||||
if($payload !== trim(@file_get_contents($uri))) {
|
||||
$errmsg = json_encode(error_get_last());
|
||||
if ($errmsg != "null") {
|
||||
$errmsg = "; PHP error: " . $errmsg;
|
||||
public function __construct($logger)
|
||||
{
|
||||
$this->logger = $logger;
|
||||
if (Settings::Get('system.letsencryptca') == 'production') {
|
||||
$ca = 'https://acme-v01.api.letsencrypt.org';
|
||||
} else {
|
||||
$errmsg = "";
|
||||
$ca = 'https://acme-staging.api.letsencrypt.org';
|
||||
}
|
||||
@unlink($tokenPath);
|
||||
throw new \RuntimeException("Please check $uri - token not available" . $errmsg);
|
||||
}
|
||||
$this->client = new Client($ca);
|
||||
$this->log("Using '$ca' to generate certificate");
|
||||
}
|
||||
|
||||
$this->log("Sending request to challenge");
|
||||
public function initAccount($certrow)
|
||||
{
|
||||
// Let's see if we have the private accountkey
|
||||
$this->accountKey = $certrow['leprivatekey'];
|
||||
if (! $this->accountKey || $this->accountKey == 'unset' || Settings::Get('system.letsencryptca') != 'production') {
|
||||
|
||||
// send request to challenge
|
||||
$result = $this->signedRequest(
|
||||
$challenge['uri'],
|
||||
array(
|
||||
"resource" => "challenge",
|
||||
"type" => "http-01",
|
||||
"keyAuthorization" => $payload,
|
||||
"token" => $challenge['token']
|
||||
)
|
||||
);
|
||||
// generate and save new private key for account
|
||||
// ---------------------------------------------
|
||||
|
||||
// waiting loop
|
||||
// we wait for a maximum of 30 seconds to avoid endless loops
|
||||
$count = 0;
|
||||
do {
|
||||
if(empty($result['status']) || $result['status'] == "invalid") {
|
||||
@unlink($tokenPath);
|
||||
throw new \RuntimeException("Verification ended with error: ".json_encode($result));
|
||||
}
|
||||
$ended = !($result['status'] === "pending");
|
||||
$this->log('Starting new account registration');
|
||||
$keys = $this->generateKey();
|
||||
// Only store the accountkey in production, in staging always generate a new key
|
||||
if (Settings::Get('system.letsencryptca') == 'production') {
|
||||
$upd_stmt = Database::prepare("
|
||||
UPDATE `" . TABLE_PANEL_CUSTOMERS . "` SET `lepublickey` = :public, `leprivatekey` = :private WHERE `customerid` = :customerid;
|
||||
");
|
||||
Database::pexecute($upd_stmt, array(
|
||||
'public' => $keys['public'],
|
||||
'private' => $keys['private'],
|
||||
'customerid' => $certrow['customerid']
|
||||
));
|
||||
}
|
||||
$this->accountKey = $keys['private'];
|
||||
$this->postNewReg();
|
||||
$this->log('New account certificate registered');
|
||||
} else {
|
||||
|
||||
if(!$ended) {
|
||||
$this->log("Verification pending, sleeping 1s");
|
||||
sleep(1);
|
||||
$count++;
|
||||
}
|
||||
$this->log('Account already registered. Continuing.');
|
||||
}
|
||||
}
|
||||
|
||||
$result = $this->client->get($location);
|
||||
public function signDomains(array $domains, $domainkey = null, $csr = null)
|
||||
{
|
||||
if (! $this->accountKey) {
|
||||
throw new \RuntimeException("Account not initiated");
|
||||
}
|
||||
|
||||
} while (!$ended && $count < 30);
|
||||
$this->log('Starting certificate generation process for domains');
|
||||
|
||||
$this->log("Verification ended with status: ${result['status']}");
|
||||
@unlink($tokenPath);
|
||||
}
|
||||
$privateAccountKey = openssl_pkey_get_private($this->accountKey);
|
||||
$accountKeyDetails = openssl_pkey_get_details($privateAccountKey);
|
||||
|
||||
// requesting certificate
|
||||
// ----------------------
|
||||
// start domains authentication
|
||||
// ----------------------------
|
||||
|
||||
// generate private key for domain if not exist
|
||||
if(empty($domainkey) || Settings::Get('system.letsencryptreuseold') == 0) {
|
||||
$keys = $this->generateKey();
|
||||
$domainkey = $keys['private'];
|
||||
}
|
||||
foreach ($domains as $domain) {
|
||||
|
||||
// load domain key
|
||||
$privateDomainKey = openssl_pkey_get_private($domainkey);
|
||||
// 1. getting available authentication options
|
||||
// -------------------------------------------
|
||||
|
||||
$this->client->getLastLinks();
|
||||
$this->log("Requesting challenge for $domain");
|
||||
|
||||
if (empty($csrfile) || Settings::Get('system.letsencryptreuseold') == 0) {
|
||||
$csr = $this->generateCSR($privateDomainKey, $domains);
|
||||
}
|
||||
$response = $this->signedRequest("/acme/new-authz", array(
|
||||
"resource" => "new-authz",
|
||||
"identifier" => array(
|
||||
"type" => "dns",
|
||||
"value" => $domain
|
||||
)
|
||||
));
|
||||
|
||||
// request certificates creation
|
||||
$result = $this->signedRequest(
|
||||
"/acme/new-cert",
|
||||
array('resource' => 'new-cert', 'csr' => $csr)
|
||||
);
|
||||
if ($this->client->getLastCode() !== 201) {
|
||||
throw new \RuntimeException("Invalid response code: ".$this->client->getLastCode().", ".json_encode($result));
|
||||
}
|
||||
$location = $this->client->getLastLocation();
|
||||
// if response is not an array but a string, it's most likely a server-error, e.g.
|
||||
// <HTML><HEAD><TITLE>Error</TITLE></HEAD><BODY>An error occurred while processing your request.
|
||||
// <p>Reference #179.d8be1402.1458059103.3613c4db</BODY></HTML>
|
||||
if (! is_array($response)) {
|
||||
throw new RuntimeException("Invalid response from LE for domain $domain. Whole response: " . $response);
|
||||
}
|
||||
|
||||
// waiting loop
|
||||
$certificates = array();
|
||||
while(1) {
|
||||
$this->client->getLastLinks();
|
||||
if (! array_key_exists('challenges', $response)) {
|
||||
throw new RuntimeException("No challenges received for $domain. Whole response: " . json_encode($response));
|
||||
}
|
||||
|
||||
$result = $this->client->get($location);
|
||||
// choose http-01 challenge only
|
||||
$challenge = array_reduce($response['challenges'], function ($v, $w) {
|
||||
return $v ? $v : ($w['type'] == 'http-01' ? $w : false);
|
||||
});
|
||||
if (! $challenge)
|
||||
throw new RuntimeException("HTTP Challenge for $domain is not available. Whole response: " . json_encode($response));
|
||||
|
||||
if($this->client->getLastCode() == 202) {
|
||||
$this->log("Got challenge token for $domain");
|
||||
$location = $this->client->getLastLocation();
|
||||
|
||||
$this->log("Certificate generation pending, sleeping 1s");
|
||||
sleep(1);
|
||||
// 2. saving authentication token for web verification
|
||||
// ---------------------------------------------------
|
||||
|
||||
} else if ($this->client->getLastCode() == 200) {
|
||||
$directory = Settings::Get('system.letsencryptchallengepath') . '/.well-known/acme-challenge';
|
||||
$tokenPath = $directory . '/' . $challenge['token'];
|
||||
|
||||
$this->log("Got certificate! YAY!");
|
||||
$certificates[] = $this->parsePemFromBody($result);
|
||||
if (! file_exists($directory) && ! @mkdir($directory, 0755, true)) {
|
||||
throw new \RuntimeException("Couldn't create directory to expose challenge: ${tokenPath}");
|
||||
}
|
||||
|
||||
$header = array(
|
||||
// need to be in precise order!
|
||||
"e" => Base64UrlSafeEncoder::encode($accountKeyDetails["rsa"]["e"]),
|
||||
"kty" => "RSA",
|
||||
"n" => Base64UrlSafeEncoder::encode($accountKeyDetails["rsa"]["n"])
|
||||
)
|
||||
;
|
||||
$payload = $challenge['token'] . '.' . Base64UrlSafeEncoder::encode(hash('sha256', json_encode($header), true));
|
||||
|
||||
foreach($this->client->getLastLinks() as $link) {
|
||||
$this->log("Requesting chained cert at $link");
|
||||
$result = $this->client->get($link);
|
||||
$certificates[] = $this->parsePemFromBody($result);
|
||||
}
|
||||
file_put_contents($tokenPath, $payload);
|
||||
chmod($tokenPath, 0644);
|
||||
|
||||
break;
|
||||
} else {
|
||||
// 3. verification process itself
|
||||
// -------------------------------
|
||||
|
||||
throw new \RuntimeException("Can't get certificate: HTTP code ".$this->client->getLastCode());
|
||||
$uri = "http://${domain}/.well-known/acme-challenge/${challenge['token']}";
|
||||
|
||||
}
|
||||
}
|
||||
$this->log("Token for $domain saved at $tokenPath and should be available at $uri");
|
||||
|
||||
if(empty($certificates)) throw new \RuntimeException('No certificates generated');
|
||||
// simple self check
|
||||
if ($payload !== trim(@file_get_contents($uri))) {
|
||||
$errmsg = json_encode(error_get_last());
|
||||
if ($errmsg != "null") {
|
||||
$errmsg = "; PHP error: " . $errmsg;
|
||||
} else {
|
||||
$errmsg = "";
|
||||
}
|
||||
@unlink($tokenPath);
|
||||
throw new \RuntimeException("Please check $uri - token not available" . $errmsg);
|
||||
}
|
||||
|
||||
$fullchain = implode("\n", $certificates);
|
||||
$crt = array_shift($certificates);
|
||||
$chain = implode("\n", $certificates);
|
||||
$this->log("Sending request to challenge");
|
||||
|
||||
$this->log("Done, returning new certificates and key");
|
||||
return array('fullchain' => $fullchain, 'crt' => $crt, 'chain' => $chain, 'key' => $domainkey, 'csr' => $csr);
|
||||
}
|
||||
// send request to challenge
|
||||
$result = $this->signedRequest($challenge['uri'], array(
|
||||
"resource" => "challenge",
|
||||
"type" => "http-01",
|
||||
"keyAuthorization" => $payload,
|
||||
"token" => $challenge['token']
|
||||
));
|
||||
|
||||
private function parsePemFromBody($body)
|
||||
{
|
||||
$pem = chunk_split(base64_encode($body), 64, "\n");
|
||||
return "-----BEGIN CERTIFICATE-----\n" . $pem . "-----END CERTIFICATE-----\n";
|
||||
}
|
||||
// waiting loop
|
||||
// we wait for a maximum of 30 seconds to avoid endless loops
|
||||
$count = 0;
|
||||
do {
|
||||
if (empty($result['status']) || $result['status'] == "invalid") {
|
||||
@unlink($tokenPath);
|
||||
throw new \RuntimeException("Verification ended with error: " . json_encode($result));
|
||||
}
|
||||
$ended = ! ($result['status'] === "pending");
|
||||
|
||||
private function postNewReg()
|
||||
{
|
||||
$this->log('Sending registration to letsencrypt server');
|
||||
if (! $ended) {
|
||||
$this->log("Verification pending, sleeping 1s");
|
||||
sleep(1);
|
||||
$count ++;
|
||||
}
|
||||
|
||||
return $this->signedRequest(
|
||||
'/acme/new-reg',
|
||||
array('resource' => 'new-reg', 'agreement' => $this->license)
|
||||
);
|
||||
}
|
||||
$result = $this->client->get($location);
|
||||
} while (! $ended && $count < 30);
|
||||
|
||||
private function generateCSR($privateKey, array $domains)
|
||||
{
|
||||
$domain = reset($domains);
|
||||
$san = implode(",", array_map(function ($dns) { return "DNS:" . $dns; }, $domains));
|
||||
$tmpConf = tmpfile();
|
||||
$tmpConfMeta = stream_get_meta_data($tmpConf);
|
||||
$tmpConfPath = $tmpConfMeta["uri"];
|
||||
$this->log("Verification ended with status: ${result['status']}");
|
||||
@unlink($tokenPath);
|
||||
}
|
||||
|
||||
// workaround to get SAN working
|
||||
fwrite($tmpConf,
|
||||
'HOME = .
|
||||
// requesting certificate
|
||||
// ----------------------
|
||||
|
||||
// generate private key for domain if not exist
|
||||
if (empty($domainkey) || Settings::Get('system.letsencryptreuseold') == 0) {
|
||||
$keys = $this->generateKey();
|
||||
$domainkey = $keys['private'];
|
||||
}
|
||||
|
||||
// load domain key
|
||||
$privateDomainKey = openssl_pkey_get_private($domainkey);
|
||||
|
||||
$this->client->getLastLinks();
|
||||
|
||||
if (empty($csrfile) || Settings::Get('system.letsencryptreuseold') == 0) {
|
||||
$csr = $this->generateCSR($privateDomainKey, $domains);
|
||||
}
|
||||
|
||||
// request certificates creation
|
||||
$result = $this->signedRequest("/acme/new-cert", array(
|
||||
'resource' => 'new-cert',
|
||||
'csr' => $csr
|
||||
));
|
||||
if ($this->client->getLastCode() !== 201) {
|
||||
throw new \RuntimeException("Invalid response code: " . $this->client->getLastCode() . ", " . json_encode($result));
|
||||
}
|
||||
$location = $this->client->getLastLocation();
|
||||
|
||||
// waiting loop
|
||||
$certificates = array();
|
||||
while (1) {
|
||||
$this->client->getLastLinks();
|
||||
|
||||
$result = $this->client->get($location);
|
||||
|
||||
if ($this->client->getLastCode() == 202) {
|
||||
|
||||
$this->log("Certificate generation pending, sleeping 1s");
|
||||
sleep(1);
|
||||
} else
|
||||
if ($this->client->getLastCode() == 200) {
|
||||
|
||||
$this->log("Got certificate! YAY!");
|
||||
$certificates[] = $this->parsePemFromBody($result);
|
||||
|
||||
foreach ($this->client->getLastLinks() as $link) {
|
||||
$this->log("Requesting chained cert at $link");
|
||||
$result = $this->client->get($link);
|
||||
$certificates[] = $this->parsePemFromBody($result);
|
||||
}
|
||||
|
||||
break;
|
||||
} else {
|
||||
|
||||
throw new \RuntimeException("Can't get certificate: HTTP code " . $this->client->getLastCode());
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($certificates))
|
||||
throw new \RuntimeException('No certificates generated');
|
||||
|
||||
$fullchain = implode("\n", $certificates);
|
||||
$crt = array_shift($certificates);
|
||||
$chain = implode("\n", $certificates);
|
||||
|
||||
$this->log("Done, returning new certificates and key");
|
||||
return array(
|
||||
'fullchain' => $fullchain,
|
||||
'crt' => $crt,
|
||||
'chain' => $chain,
|
||||
'key' => $domainkey,
|
||||
'csr' => $csr
|
||||
);
|
||||
}
|
||||
|
||||
private function parsePemFromBody($body)
|
||||
{
|
||||
$pem = chunk_split(base64_encode($body), 64, "\n");
|
||||
return "-----BEGIN CERTIFICATE-----\n" . $pem . "-----END CERTIFICATE-----\n";
|
||||
}
|
||||
|
||||
private function postNewReg()
|
||||
{
|
||||
$this->log('Sending registration to letsencrypt server');
|
||||
|
||||
return $this->signedRequest('/acme/new-reg', array(
|
||||
'resource' => 'new-reg',
|
||||
'agreement' => $this->license
|
||||
));
|
||||
}
|
||||
|
||||
private function generateCSR($privateKey, array $domains)
|
||||
{
|
||||
$domain = reset($domains);
|
||||
$san = implode(",", array_map(function ($dns) {
|
||||
return "DNS:" . $dns;
|
||||
}, $domains));
|
||||
$tmpConf = tmpfile();
|
||||
$tmpConfMeta = stream_get_meta_data($tmpConf);
|
||||
$tmpConfPath = $tmpConfMeta["uri"];
|
||||
|
||||
// workaround to get SAN working
|
||||
fwrite($tmpConf, 'HOME = .
|
||||
RANDFILE = $ENV::HOME/.rnd
|
||||
[ req ]
|
||||
default_bits = ' . Settings::Get('system.letsencryptkeysize') . '
|
||||
@@ -306,197 +317,202 @@ req_extensions = v3_req
|
||||
countryName = Country Name (2 letter code)
|
||||
[ v3_req ]
|
||||
basicConstraints = CA:FALSE
|
||||
subjectAltName = '.$san.'
|
||||
subjectAltName = ' . $san . '
|
||||
keyUsage = nonRepudiation, digitalSignature, keyEncipherment');
|
||||
|
||||
$csr = openssl_csr_new(
|
||||
array(
|
||||
"CN" => $domain,
|
||||
"ST" => Settings::Get('system.letsencryptstate'),
|
||||
"C" => Settings::Get('system.letsencryptcountrycode'),
|
||||
"O" => "Unknown",
|
||||
),
|
||||
$privateKey,
|
||||
array(
|
||||
"config" => $tmpConfPath,
|
||||
"digest_alg" => "sha256"
|
||||
)
|
||||
);
|
||||
$csr = openssl_csr_new(array(
|
||||
"CN" => $domain,
|
||||
"ST" => Settings::Get('system.letsencryptstate'),
|
||||
"C" => Settings::Get('system.letsencryptcountrycode'),
|
||||
"O" => "Unknown"
|
||||
), $privateKey, array(
|
||||
"config" => $tmpConfPath,
|
||||
"digest_alg" => "sha256"
|
||||
));
|
||||
|
||||
if (!$csr) throw new \RuntimeException("CSR couldn't be generated! ".openssl_error_string());
|
||||
if (! $csr)
|
||||
throw new \RuntimeException("CSR couldn't be generated! " . openssl_error_string());
|
||||
|
||||
openssl_csr_export($csr, $csr);
|
||||
fclose($tmpConf);
|
||||
openssl_csr_export($csr, $csr);
|
||||
fclose($tmpConf);
|
||||
|
||||
preg_match('~REQUEST-----(.*)-----END~s', $csr, $matches);
|
||||
preg_match('~REQUEST-----(.*)-----END~s', $csr, $matches);
|
||||
|
||||
return trim(Base64UrlSafeEncoder::encode(base64_decode($matches[1])));
|
||||
}
|
||||
return trim(Base64UrlSafeEncoder::encode(base64_decode($matches[1])));
|
||||
}
|
||||
|
||||
private function generateKey()
|
||||
{
|
||||
$res = openssl_pkey_new(array(
|
||||
"private_key_type" => OPENSSL_KEYTYPE_RSA,
|
||||
"private_key_bits" => (int)Settings::Get('system.letsencryptkeysize'),
|
||||
));
|
||||
private function generateKey()
|
||||
{
|
||||
$res = openssl_pkey_new(array(
|
||||
"private_key_type" => OPENSSL_KEYTYPE_RSA,
|
||||
"private_key_bits" => (int) Settings::Get('system.letsencryptkeysize')
|
||||
));
|
||||
|
||||
if(!openssl_pkey_export($res, $privateKey)) {
|
||||
throw new \RuntimeException("Key export failed!");
|
||||
}
|
||||
if (! openssl_pkey_export($res, $privateKey)) {
|
||||
throw new \RuntimeException("Key export failed!");
|
||||
}
|
||||
|
||||
$details = openssl_pkey_get_details($res);
|
||||
$details = openssl_pkey_get_details($res);
|
||||
|
||||
return array('private' => $privateKey, 'public' => $details['key']);
|
||||
}
|
||||
return array(
|
||||
'private' => $privateKey,
|
||||
'public' => $details['key']
|
||||
);
|
||||
}
|
||||
|
||||
private function signedRequest($uri, array $payload)
|
||||
{
|
||||
$privateKey = openssl_pkey_get_private($this->accountKey);
|
||||
$details = openssl_pkey_get_details($privateKey);
|
||||
private function signedRequest($uri, array $payload)
|
||||
{
|
||||
$privateKey = openssl_pkey_get_private($this->accountKey);
|
||||
$details = openssl_pkey_get_details($privateKey);
|
||||
|
||||
$header = array(
|
||||
"alg" => "RS256",
|
||||
"jwk" => array(
|
||||
"kty" => "RSA",
|
||||
"n" => Base64UrlSafeEncoder::encode($details["rsa"]["n"]),
|
||||
"e" => Base64UrlSafeEncoder::encode($details["rsa"]["e"]),
|
||||
)
|
||||
);
|
||||
$header = array(
|
||||
"alg" => "RS256",
|
||||
"jwk" => array(
|
||||
"kty" => "RSA",
|
||||
"n" => Base64UrlSafeEncoder::encode($details["rsa"]["n"]),
|
||||
"e" => Base64UrlSafeEncoder::encode($details["rsa"]["e"])
|
||||
)
|
||||
);
|
||||
|
||||
$protected = $header;
|
||||
$protected["nonce"] = $this->client->getLastNonce();
|
||||
$protected = $header;
|
||||
$protected["nonce"] = $this->client->getLastNonce();
|
||||
|
||||
$payload64 = Base64UrlSafeEncoder::encode(str_replace('\\/', '/', json_encode($payload)));
|
||||
$protected64 = Base64UrlSafeEncoder::encode(json_encode($protected));
|
||||
|
||||
$payload64 = Base64UrlSafeEncoder::encode(str_replace('\\/', '/', json_encode($payload)));
|
||||
$protected64 = Base64UrlSafeEncoder::encode(json_encode($protected));
|
||||
openssl_sign($protected64 . '.' . $payload64, $signed, $privateKey, "SHA256");
|
||||
|
||||
openssl_sign($protected64.'.'.$payload64, $signed, $privateKey, "SHA256");
|
||||
$signed64 = Base64UrlSafeEncoder::encode($signed);
|
||||
|
||||
$signed64 = Base64UrlSafeEncoder::encode($signed);
|
||||
$data = array(
|
||||
'header' => $header,
|
||||
'protected' => $protected64,
|
||||
'payload' => $payload64,
|
||||
'signature' => $signed64
|
||||
);
|
||||
|
||||
$data = array(
|
||||
'header' => $header,
|
||||
'protected' => $protected64,
|
||||
'payload' => $payload64,
|
||||
'signature' => $signed64
|
||||
);
|
||||
$this->log("Sending signed request to $uri");
|
||||
|
||||
$this->log("Sending signed request to $uri");
|
||||
return $this->client->post($uri, json_encode($data));
|
||||
}
|
||||
|
||||
return $this->client->post($uri, json_encode($data));
|
||||
}
|
||||
|
||||
protected function log($message)
|
||||
{
|
||||
$this->logger->logAction(CRON_ACTION, LOG_INFO, "letsencrypt " . $message);
|
||||
}
|
||||
protected function log($message)
|
||||
{
|
||||
$this->logger->logAction(CRON_ACTION, LOG_INFO, "letsencrypt " . $message);
|
||||
}
|
||||
}
|
||||
|
||||
class Client
|
||||
{
|
||||
private $lastCode;
|
||||
private $lastHeader;
|
||||
|
||||
private $base;
|
||||
private $lastCode;
|
||||
|
||||
public function __construct($base)
|
||||
{
|
||||
$this->base = $base;
|
||||
}
|
||||
private $lastHeader;
|
||||
|
||||
private function curl($method, $url, $data = null)
|
||||
{
|
||||
$headers = array('Accept: application/json', 'Content-Type: application/json');
|
||||
$handle = curl_init();
|
||||
curl_setopt($handle, CURLOPT_URL, preg_match('~^http~', $url) ? $url : $this->base.$url);
|
||||
curl_setopt($handle, CURLOPT_HTTPHEADER, $headers);
|
||||
curl_setopt($handle, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($handle, CURLOPT_HEADER, true);
|
||||
private $base;
|
||||
|
||||
// DO NOT DO THAT!
|
||||
// curl_setopt($handle, CURLOPT_SSL_VERIFYHOST, false);
|
||||
// curl_setopt($handle, CURLOPT_SSL_VERIFYPEER, false);
|
||||
public function __construct($base)
|
||||
{
|
||||
$this->base = $base;
|
||||
}
|
||||
|
||||
switch ($method) {
|
||||
case 'GET':
|
||||
break;
|
||||
case 'POST':
|
||||
curl_setopt($handle, CURLOPT_POST, true);
|
||||
curl_setopt($handle, CURLOPT_POSTFIELDS, $data);
|
||||
break;
|
||||
}
|
||||
$response = curl_exec($handle);
|
||||
private function curl($method, $url, $data = null)
|
||||
{
|
||||
$headers = array(
|
||||
'Accept: application/json',
|
||||
'Content-Type: application/json'
|
||||
);
|
||||
$handle = curl_init();
|
||||
curl_setopt($handle, CURLOPT_URL, preg_match('~^http~', $url) ? $url : $this->base . $url);
|
||||
curl_setopt($handle, CURLOPT_HTTPHEADER, $headers);
|
||||
curl_setopt($handle, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($handle, CURLOPT_HEADER, true);
|
||||
|
||||
if(curl_errno($handle)) {
|
||||
throw new \RuntimeException('Curl: '.curl_error($handle));
|
||||
}
|
||||
// DO NOT DO THAT!
|
||||
// curl_setopt($handle, CURLOPT_SSL_VERIFYHOST, false);
|
||||
// curl_setopt($handle, CURLOPT_SSL_VERIFYPEER, false);
|
||||
|
||||
$header_size = curl_getinfo($handle, CURLINFO_HEADER_SIZE);
|
||||
switch ($method) {
|
||||
case 'GET':
|
||||
break;
|
||||
case 'POST':
|
||||
curl_setopt($handle, CURLOPT_POST, true);
|
||||
curl_setopt($handle, CURLOPT_POSTFIELDS, $data);
|
||||
break;
|
||||
}
|
||||
$response = curl_exec($handle);
|
||||
|
||||
$header = substr($response, 0, $header_size);
|
||||
$body = substr($response, $header_size);
|
||||
if (curl_errno($handle)) {
|
||||
throw new \RuntimeException('Curl: ' . curl_error($handle));
|
||||
}
|
||||
|
||||
$this->lastHeader = $header;
|
||||
$this->lastCode = curl_getinfo($handle, CURLINFO_HTTP_CODE);
|
||||
$header_size = curl_getinfo($handle, CURLINFO_HEADER_SIZE);
|
||||
|
||||
$data = json_decode($body, true);
|
||||
return $data === null ? $body : $data;
|
||||
}
|
||||
$header = substr($response, 0, $header_size);
|
||||
$body = substr($response, $header_size);
|
||||
|
||||
public function post($url, $data)
|
||||
{
|
||||
return $this->curl('POST', $url, $data);
|
||||
}
|
||||
$this->lastHeader = $header;
|
||||
$this->lastCode = curl_getinfo($handle, CURLINFO_HTTP_CODE);
|
||||
|
||||
public function get($url)
|
||||
{
|
||||
return $this->curl('GET', $url);
|
||||
}
|
||||
$data = json_decode($body, true);
|
||||
return $data === null ? $body : $data;
|
||||
}
|
||||
|
||||
public function getLastNonce()
|
||||
{
|
||||
if(preg_match('~Replay\-Nonce: (.+)~i', $this->lastHeader, $matches)) {
|
||||
return trim($matches[1]);
|
||||
}
|
||||
public function post($url, $data)
|
||||
{
|
||||
return $this->curl('POST', $url, $data);
|
||||
}
|
||||
|
||||
$this->curl('GET', '/directory');
|
||||
return $this->getLastNonce();
|
||||
}
|
||||
public function get($url)
|
||||
{
|
||||
return $this->curl('GET', $url);
|
||||
}
|
||||
|
||||
public function getLastLocation()
|
||||
{
|
||||
if(preg_match('~Location: (.+)~i', $this->lastHeader, $matches)) {
|
||||
return trim($matches[1]);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
public function getLastNonce()
|
||||
{
|
||||
if (preg_match('~Replay\-Nonce: (.+)~i', $this->lastHeader, $matches)) {
|
||||
return trim($matches[1]);
|
||||
}
|
||||
|
||||
public function getLastCode()
|
||||
{
|
||||
return $this->lastCode;
|
||||
}
|
||||
$this->curl('GET', '/directory');
|
||||
return $this->getLastNonce();
|
||||
}
|
||||
|
||||
public function getLastLinks()
|
||||
{
|
||||
preg_match_all('~Link: <(.+)>;rel="up"~', $this->lastHeader, $matches);
|
||||
return $matches[1];
|
||||
}
|
||||
public function getLastLocation()
|
||||
{
|
||||
if (preg_match('~Location: (.+)~i', $this->lastHeader, $matches)) {
|
||||
return trim($matches[1]);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getLastCode()
|
||||
{
|
||||
return $this->lastCode;
|
||||
}
|
||||
|
||||
public function getLastLinks()
|
||||
{
|
||||
preg_match_all('~Link: <(.+)>;rel="up"~', $this->lastHeader, $matches);
|
||||
return $matches[1];
|
||||
}
|
||||
}
|
||||
|
||||
class Base64UrlSafeEncoder
|
||||
{
|
||||
public static function encode($input)
|
||||
{
|
||||
return str_replace('=', '', strtr(base64_encode($input), '+/', '-_'));
|
||||
}
|
||||
|
||||
public static function decode($input)
|
||||
{
|
||||
$remainder = strlen($input) % 4;
|
||||
if ($remainder) {
|
||||
$padlen = 4 - $remainder;
|
||||
$input .= str_repeat('=', $padlen);
|
||||
}
|
||||
return base64_decode(strtr($input, '-_', '+/'));
|
||||
}
|
||||
public static function encode($input)
|
||||
{
|
||||
return str_replace('=', '', strtr(base64_encode($input), '+/', '-_'));
|
||||
}
|
||||
|
||||
public static function decode($input)
|
||||
{
|
||||
$remainder = strlen($input) % 4;
|
||||
if ($remainder) {
|
||||
$padlen = 4 - $remainder;
|
||||
$input .= str_repeat('=', $padlen);
|
||||
}
|
||||
return base64_decode(strtr($input, '-_', '+/'));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user