public function signDomains(array $domains, $reuseCsr = false)
{
$this->log('Starting certificate generation process for domains');
$privateAccountKey = $this->readPrivateKey($this->accountKeyPath);
$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 (empty($response['challenges'])) {
throw new \RuntimeException("HTTP Challenge for {$domain} is not available. Whole response: " . json_encode($response));
}
$self = $this;
$challenge = array_reduce($response['challenges'], function ($v, $w) use(&$self) {
return $v ? $v : ($w['type'] == $self->challenge ? $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 = $this->webRootDir . '/.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("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))) {
throw new \RuntimeException("Please check {$uri} - token not available");
}
$this->log("Sending request to challenge");
// send request to challenge
$result = $this->signedRequest($challenge['uri'], array("resource" => "challenge", "type" => $this->challenge, "keyAuthorization" => $payload, "token" => $challenge['token']));
// waiting loop
do {
if (empty($result['status']) || $result['status'] == "invalid") {
throw new \RuntimeException("Verification ended with error: " . json_encode($result));
}
$ended = !($result['status'] === "pending");
if (!$ended) {
$this->log("Verification pending, sleeping 1s");
sleep(1);
}
$result = $this->client->get($location);
} while (!$ended);
$this->log("Verification ended with status: {$result['status']}");
@unlink($tokenPath);
}
// requesting certificate
// ----------------------
$domainPath = $this->getDomainPath(reset($domains));
// generate private key for domain if not exist
if (!is_dir($domainPath) || !is_file($domainPath . '/private.pem')) {
$this->generateKey($domainPath);
}
// load domain key
$privateDomainKey = $this->readPrivateKey($domainPath . '/private.pem');
$this->client->getLastLinks();
$csr = $reuseCsr && is_file($domainPath . "/last.csr") ? $this->getCsrContent($domainPath . "/last.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');
}
$this->log("Saving fullchain.pem");
file_put_contents($domainPath . '/fullchain.pem', implode("\n", $certificates));
$this->log("Saving cert.pem");
file_put_contents($domainPath . '/cert.pem', array_shift($certificates));
$this->log("Saving chain.pem");
file_put_contents($domainPath . "/chain.pem", implode("\n", $certificates));
$this->log("Done !!§§!");
}