save
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
composer.lock
|
||||
vendor/
|
||||
+404
@@ -0,0 +1,404 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core;
|
||||
|
||||
use AcmePhp\Core\Exception\AcmeCoreClientException;
|
||||
use AcmePhp\Core\Exception\AcmeCoreServerException;
|
||||
use AcmePhp\Core\Exception\Protocol\CertificateRequestFailedException;
|
||||
use AcmePhp\Core\Exception\Protocol\CertificateRevocationException;
|
||||
use AcmePhp\Core\Exception\Protocol\ChallengeFailedException;
|
||||
use AcmePhp\Core\Exception\Protocol\ChallengeNotSupportedException;
|
||||
use AcmePhp\Core\Exception\Protocol\ChallengeTimedOutException;
|
||||
use AcmePhp\Core\Http\SecureHttpClient;
|
||||
use AcmePhp\Core\Protocol\AuthorizationChallenge;
|
||||
use AcmePhp\Core\Protocol\CertificateOrder;
|
||||
use AcmePhp\Core\Protocol\ExternalAccount;
|
||||
use AcmePhp\Core\Protocol\ResourcesDirectory;
|
||||
use AcmePhp\Core\Protocol\RevocationReason;
|
||||
use AcmePhp\Ssl\Certificate;
|
||||
use AcmePhp\Ssl\CertificateRequest;
|
||||
use AcmePhp\Ssl\CertificateResponse;
|
||||
use AcmePhp\Ssl\Signer\CertificateRequestSigner;
|
||||
use GuzzleHttp\Psr7\Utils;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* ACME protocol client implementation.
|
||||
*
|
||||
* @author Titouan Galopin <galopintitouan@gmail.com>
|
||||
*/
|
||||
class AcmeClient implements AcmeClientInterface
|
||||
{
|
||||
/**
|
||||
* @var SecureHttpClient
|
||||
*/
|
||||
private $uninitializedHttpClient;
|
||||
|
||||
/**
|
||||
* @var SecureHttpClient
|
||||
*/
|
||||
private $initializedHttpClient;
|
||||
|
||||
/**
|
||||
* @var CertificateRequestSigner
|
||||
*/
|
||||
private $csrSigner;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $directoryUrl;
|
||||
|
||||
/**
|
||||
* @var ResourcesDirectory
|
||||
*/
|
||||
private $directory;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $account;
|
||||
|
||||
public function __construct(SecureHttpClient $httpClient, string $directoryUrl, CertificateRequestSigner $csrSigner = null)
|
||||
{
|
||||
$this->uninitializedHttpClient = $httpClient;
|
||||
$this->directoryUrl = $directoryUrl;
|
||||
$this->csrSigner = $csrSigner ?: new CertificateRequestSigner();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function registerAccount(string $email = null, ExternalAccount $externalAccount = null): array
|
||||
{
|
||||
$client = $this->getHttpClient();
|
||||
|
||||
$payload = [
|
||||
'termsOfServiceAgreed' => true,
|
||||
'contact' => [],
|
||||
];
|
||||
|
||||
if ($email) {
|
||||
$payload['contact'][] = 'mailto:'.$email;
|
||||
}
|
||||
|
||||
if ($externalAccount) {
|
||||
$payload['externalAccountBinding'] = $client->createExternalAccountPayload(
|
||||
$externalAccount,
|
||||
$this->getResourceUrl(ResourcesDirectory::NEW_ACCOUNT)
|
||||
);
|
||||
}
|
||||
|
||||
$this->requestResource('POST', ResourcesDirectory::NEW_ACCOUNT, $payload);
|
||||
$account = $this->getResourceAccount();
|
||||
|
||||
return $client->request('POST', $account, $client->signKidPayload($account, $account, null));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function requestOrder(array $domains): CertificateOrder
|
||||
{
|
||||
Assert::allStringNotEmpty($domains, 'requestOrder::$domains expected a list of strings. Got: %s');
|
||||
|
||||
$payload = [
|
||||
'identifiers' => array_map(
|
||||
static function ($domain) {
|
||||
return [
|
||||
'type' => 'dns',
|
||||
'value' => $domain,
|
||||
];
|
||||
},
|
||||
array_values($domains)
|
||||
),
|
||||
];
|
||||
|
||||
$client = $this->getHttpClient();
|
||||
$resourceUrl = $this->getResourceUrl(ResourcesDirectory::NEW_ORDER);
|
||||
$response = $client->request('POST', $resourceUrl, $client->signKidPayload($resourceUrl, $this->getResourceAccount(), $payload));
|
||||
if (!isset($response['authorizations']) || !$response['authorizations']) {
|
||||
throw new ChallengeNotSupportedException();
|
||||
}
|
||||
|
||||
$authorizationsChallenges = [];
|
||||
$orderEndpoint = $client->getLastLocation();
|
||||
foreach ($response['authorizations'] as $authorizationEndpoint) {
|
||||
$authorizationsResponse = $client->request('POST', $authorizationEndpoint, $client->signKidPayload($authorizationEndpoint, $this->getResourceAccount(), null));
|
||||
$domain = (empty($authorizationsResponse['wildcard']) ? '' : '*.').$authorizationsResponse['identifier']['value'];
|
||||
foreach ($authorizationsResponse['challenges'] as $challenge) {
|
||||
$authorizationsChallenges[$domain][] = $this->createAuthorizationChallenge($authorizationsResponse['identifier']['value'], $challenge);
|
||||
}
|
||||
}
|
||||
|
||||
return new CertificateOrder($authorizationsChallenges, $orderEndpoint, $response['status']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function reloadOrder(CertificateOrder $order): CertificateOrder
|
||||
{
|
||||
$client = $this->getHttpClient();
|
||||
$orderEndpoint = $order->getOrderEndpoint();
|
||||
$response = $client->request('POST', $orderEndpoint, $client->signKidPayload($orderEndpoint, $this->getResourceAccount(), null));
|
||||
|
||||
if (!isset($response['authorizations']) || !$response['authorizations']) {
|
||||
throw new ChallengeNotSupportedException();
|
||||
}
|
||||
|
||||
$authorizationsChallenges = [];
|
||||
foreach ($response['authorizations'] as $authorizationEndpoint) {
|
||||
$authorizationsResponse = $client->request('POST', $authorizationEndpoint, $client->signKidPayload($authorizationEndpoint, $this->getResourceAccount(), null));
|
||||
$domain = (empty($authorizationsResponse['wildcard']) ? '' : '*.').$authorizationsResponse['identifier']['value'];
|
||||
foreach ($authorizationsResponse['challenges'] as $challenge) {
|
||||
$authorizationsChallenges[$domain][] = $this->createAuthorizationChallenge($authorizationsResponse['identifier']['value'], $challenge);
|
||||
}
|
||||
}
|
||||
|
||||
return new CertificateOrder($authorizationsChallenges, $orderEndpoint, $response['status']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function finalizeOrder(CertificateOrder $order, CertificateRequest $csr, int $timeout = 180, bool $returnAlternateCertificateIfAvailable = false): CertificateResponse
|
||||
{
|
||||
$endTime = time() + $timeout;
|
||||
$client = $this->getHttpClient();
|
||||
$orderEndpoint = $order->getOrderEndpoint();
|
||||
$response = $client->request('POST', $orderEndpoint, $client->signKidPayload($orderEndpoint, $this->getResourceAccount(), null));
|
||||
if (\in_array($response['status'], ['pending', 'processing', 'ready'])) {
|
||||
$humanText = ['-----BEGIN CERTIFICATE REQUEST-----', '-----END CERTIFICATE REQUEST-----'];
|
||||
|
||||
$csrContent = $this->csrSigner->signCertificateRequest($csr);
|
||||
$csrContent = trim(str_replace($humanText, '', $csrContent));
|
||||
$csrContent = trim($client->getBase64Encoder()->encode(base64_decode($csrContent)));
|
||||
|
||||
$response = $client->request('POST', $response['finalize'], $client->signKidPayload($response['finalize'], $this->getResourceAccount(), ['csr' => $csrContent]));
|
||||
}
|
||||
|
||||
// Waiting loop
|
||||
while (time() <= $endTime && (!isset($response['status']) || \in_array($response['status'], ['pending', 'processing', 'ready']))) {
|
||||
sleep(1);
|
||||
$response = $client->request('POST', $orderEndpoint, $client->signKidPayload($orderEndpoint, $this->getResourceAccount(), null));
|
||||
}
|
||||
|
||||
if ('valid' !== $response['status']) {
|
||||
throw new CertificateRequestFailedException('The order has not been validated');
|
||||
}
|
||||
|
||||
$response = $client->rawRequest('POST', $response['certificate'], $client->signKidPayload($response['certificate'], $this->getResourceAccount(), null));
|
||||
$responseHeaders = $response->getHeaders();
|
||||
|
||||
if ($returnAlternateCertificateIfAvailable && isset($responseHeaders['Link'][1])) {
|
||||
$matches = [];
|
||||
preg_match('/<(http.*)>;rel="alternate"/', $responseHeaders['Link'][1], $matches);
|
||||
|
||||
// If response headers include a valid alternate certificate link, return that certificate instead
|
||||
if (isset($matches[1])) {
|
||||
return $this->createCertificateResponse(
|
||||
$csr,
|
||||
$client->request('POST', $matches[1], $client->signKidPayload($matches[1], $this->getResourceAccount(), null), false)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->createCertificateResponse($csr, Utils::copyToString($response->getBody()));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function requestAuthorization(string $domain): array
|
||||
{
|
||||
$order = $this->requestOrder([$domain]);
|
||||
|
||||
try {
|
||||
return $order->getAuthorizationChallenges($domain);
|
||||
} catch (AcmeCoreClientException $e) {
|
||||
throw new ChallengeNotSupportedException();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function reloadAuthorization(AuthorizationChallenge $challenge): AuthorizationChallenge
|
||||
{
|
||||
$client = $this->getHttpClient();
|
||||
$challengeUrl = $challenge->getUrl();
|
||||
$response = (array) $client->request('POST', $challengeUrl, $client->signKidPayload($challengeUrl, $this->getResourceAccount(), null));
|
||||
|
||||
return $this->createAuthorizationChallenge($challenge->getDomain(), $response);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function challengeAuthorization(AuthorizationChallenge $challenge, int $timeout = 180): array
|
||||
{
|
||||
$endTime = time() + $timeout;
|
||||
$client = $this->getHttpClient();
|
||||
$challengeUrl = $challenge->getUrl();
|
||||
$response = (array) $client->request('POST', $challengeUrl, $client->signKidPayload($challengeUrl, $this->getResourceAccount(), null));
|
||||
if ('pending' === $response['status'] || 'processing' === $response['status']) {
|
||||
$response = (array) $client->request('POST', $challengeUrl, $client->signKidPayload($challengeUrl, $this->getResourceAccount(), []));
|
||||
}
|
||||
|
||||
// Waiting loop
|
||||
while (time() <= $endTime && (!isset($response['status']) || 'pending' === $response['status'] || 'processing' === $response['status'])) {
|
||||
sleep(1);
|
||||
$response = (array) $client->request('POST', $challengeUrl, $client->signKidPayload($challengeUrl, $this->getResourceAccount(), null));
|
||||
}
|
||||
|
||||
if (isset($response['status']) && ('pending' === $response['status'] || 'processing' === $response['status'])) {
|
||||
throw new ChallengeTimedOutException($response);
|
||||
}
|
||||
if (!isset($response['status']) || 'valid' !== $response['status']) {
|
||||
throw new ChallengeFailedException($response);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function requestCertificate(string $domain, CertificateRequest $csr, int $timeout = 180, bool $returnAlternateCertificateIfAvailable = false): CertificateResponse
|
||||
{
|
||||
$order = $this->requestOrder(array_unique(array_merge([$domain], $csr->getDistinguishedName()->getSubjectAlternativeNames())));
|
||||
|
||||
return $this->finalizeOrder($order, $csr, $timeout, $returnAlternateCertificateIfAvailable);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function revokeCertificate(Certificate $certificate, RevocationReason $revocationReason = null)
|
||||
{
|
||||
if (!$endpoint = $this->getResourceUrl(ResourcesDirectory::REVOKE_CERT)) {
|
||||
throw new CertificateRevocationException('This ACME server does not support certificate revocation.');
|
||||
}
|
||||
|
||||
if (null === $revocationReason) {
|
||||
$revocationReason = RevocationReason::createDefaultReason();
|
||||
}
|
||||
|
||||
openssl_x509_export(openssl_x509_read($certificate->getPEM()), $formattedPem);
|
||||
|
||||
$formattedPem = str_ireplace('-----BEGIN CERTIFICATE-----', '', $formattedPem);
|
||||
$formattedPem = str_ireplace('-----END CERTIFICATE-----', '', $formattedPem);
|
||||
$client = $this->getHttpClient();
|
||||
$formattedPem = $client->getBase64Encoder()->encode(base64_decode(trim($formattedPem)));
|
||||
|
||||
try {
|
||||
$client->request(
|
||||
'POST',
|
||||
$endpoint,
|
||||
$client->signKidPayload($endpoint, $this->getResourceAccount(), ['certificate' => $formattedPem, 'reason' => $revocationReason->getReasonType()]),
|
||||
false
|
||||
);
|
||||
} catch (AcmeCoreServerException $e) {
|
||||
throw new CertificateRevocationException($e->getMessage(), $e);
|
||||
} catch (AcmeCoreClientException $e) {
|
||||
throw new CertificateRevocationException($e->getMessage(), $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a resource URL from the Certificate Authority.
|
||||
*/
|
||||
public function getResourceUrl(string $resource): string
|
||||
{
|
||||
if (!$this->directory) {
|
||||
$this->directory = new ResourcesDirectory(
|
||||
$this->getHttpClient()->request('GET', $this->directoryUrl)
|
||||
);
|
||||
}
|
||||
|
||||
return $this->directory->getResourceUrl($resource);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request a resource (URL is found using ACME server directory).
|
||||
*
|
||||
* @throws AcmeCoreServerException when the ACME server returns an error HTTP status code
|
||||
* @throws AcmeCoreClientException when an error occured during response parsing
|
||||
*
|
||||
* @return array|string
|
||||
*/
|
||||
protected function requestResource(string $method, string $resource, array $payload, bool $returnJson = true)
|
||||
{
|
||||
$client = $this->getHttpClient();
|
||||
$endpoint = $this->getResourceUrl($resource);
|
||||
|
||||
return $client->request(
|
||||
$method,
|
||||
$endpoint,
|
||||
$client->signJwkPayload($endpoint, $payload),
|
||||
$returnJson
|
||||
);
|
||||
}
|
||||
|
||||
private function createCertificateResponse(CertificateRequest $csr, string $certificate): CertificateResponse
|
||||
{
|
||||
$certificateHeader = '-----BEGIN CERTIFICATE-----';
|
||||
$certificatesChain = null;
|
||||
|
||||
foreach (array_reverse(explode($certificateHeader, $certificate)) as $pem) {
|
||||
if ('' !== \trim($pem)) {
|
||||
$certificatesChain = new Certificate($certificateHeader.$pem, $certificatesChain);
|
||||
}
|
||||
}
|
||||
|
||||
return new CertificateResponse($csr, $certificatesChain);
|
||||
}
|
||||
|
||||
private function getResourceAccount(): string
|
||||
{
|
||||
if (!$this->account) {
|
||||
$payload = [
|
||||
'onlyReturnExisting' => true,
|
||||
];
|
||||
|
||||
$this->requestResource('POST', ResourcesDirectory::NEW_ACCOUNT, $payload);
|
||||
$this->account = $this->getHttpClient()->getLastLocation();
|
||||
}
|
||||
|
||||
return $this->account;
|
||||
}
|
||||
|
||||
private function createAuthorizationChallenge($domain, array $response): AuthorizationChallenge
|
||||
{
|
||||
$base64encoder = $this->getHttpClient()->getBase64Encoder();
|
||||
|
||||
return new AuthorizationChallenge(
|
||||
$domain,
|
||||
$response['status'],
|
||||
$response['type'],
|
||||
$response['url'],
|
||||
$response['token'],
|
||||
$response['token'].'.'.$base64encoder->encode($this->getHttpClient()->getJWKThumbprint())
|
||||
);
|
||||
}
|
||||
|
||||
private function getHttpClient(): SecureHttpClient
|
||||
{
|
||||
if (!$this->initializedHttpClient) {
|
||||
$this->initializedHttpClient = $this->uninitializedHttpClient;
|
||||
$this->initializedHttpClient->setNonceEndpoint($this->getResourceUrl(ResourcesDirectory::NEW_NONCE));
|
||||
}
|
||||
|
||||
return $this->initializedHttpClient;
|
||||
}
|
||||
}
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core;
|
||||
|
||||
use AcmePhp\Core\Exception\AcmeCoreClientException;
|
||||
use AcmePhp\Core\Exception\AcmeCoreServerException;
|
||||
use AcmePhp\Core\Exception\Protocol\CertificateRequestFailedException;
|
||||
use AcmePhp\Core\Exception\Protocol\CertificateRequestTimedOutException;
|
||||
use AcmePhp\Core\Exception\Protocol\CertificateRevocationException;
|
||||
use AcmePhp\Core\Exception\Protocol\ChallengeFailedException;
|
||||
use AcmePhp\Core\Exception\Protocol\ChallengeNotSupportedException;
|
||||
use AcmePhp\Core\Exception\Protocol\ChallengeTimedOutException;
|
||||
use AcmePhp\Core\Protocol\AuthorizationChallenge;
|
||||
use AcmePhp\Core\Protocol\CertificateOrder;
|
||||
use AcmePhp\Core\Protocol\ExternalAccount;
|
||||
use AcmePhp\Core\Protocol\RevocationReason;
|
||||
use AcmePhp\Ssl\Certificate;
|
||||
use AcmePhp\Ssl\CertificateRequest;
|
||||
use AcmePhp\Ssl\CertificateResponse;
|
||||
|
||||
/**
|
||||
* ACME protocol client interface.
|
||||
*
|
||||
* @author Titouan Galopin <galopintitouan@gmail.com>
|
||||
*/
|
||||
interface AcmeClientInterface
|
||||
{
|
||||
/**
|
||||
* Register the local account KeyPair in the Certificate Authority.
|
||||
*
|
||||
* @param string|null $email an optionnal e-mail to associate with the account
|
||||
* @param ExternalAccount|null $externalAccount an optionnal External Account to use for External Account Binding
|
||||
*
|
||||
* @throws AcmeCoreServerException when the ACME server returns an error HTTP status code
|
||||
* (the exception will be more specific if detail is provided)
|
||||
* @throws AcmeCoreClientException when an error occured during response parsing
|
||||
*
|
||||
* @return array the Certificate Authority response decoded from JSON into an array
|
||||
*/
|
||||
public function registerAccount(string $email = null, ExternalAccount $externalAccount = null): array;
|
||||
|
||||
/**
|
||||
* Request authorization challenge data for a list of domains.
|
||||
*
|
||||
* An AuthorizationChallenge is an association between a URI, a token and a payload.
|
||||
* The Certificate Authority will create this challenge data and you will then have
|
||||
* to expose the payload for the verification (see challengeAuthorization).
|
||||
*
|
||||
* @param string[] $domains the domains to challenge
|
||||
*
|
||||
* @throws AcmeCoreServerException when the ACME server returns an error HTTP status code
|
||||
* (the exception will be more specific if detail is provided)
|
||||
* @throws AcmeCoreClientException when an error occured during response parsing
|
||||
* @throws ChallengeNotSupportedException when the HTTP challenge is not supported by the server
|
||||
*
|
||||
* @return CertificateOrder the Order returned by the Certificate Authority
|
||||
*/
|
||||
public function requestOrder(array $domains): CertificateOrder;
|
||||
|
||||
/**
|
||||
* Request the current status of a certificate order.
|
||||
*/
|
||||
public function reloadOrder(CertificateOrder $order): CertificateOrder;
|
||||
|
||||
/**
|
||||
* Request a certificate for the given domain.
|
||||
*
|
||||
* This method should be called only if a previous authorization challenge has
|
||||
* been successful for the asked domain.
|
||||
*
|
||||
* WARNING : This method SHOULD NOT BE USED in a web action. It will
|
||||
* wait for the Certificate Authority to validate the certificate and
|
||||
* this operation could be long.
|
||||
*
|
||||
* @param CertificateOrder $order the Order returned by the Certificate Authority
|
||||
* @param CertificateRequest $csr the Certificate Signing Request (informations for the certificate)
|
||||
* @param int $timeout the timeout period
|
||||
* @param bool $returnAlternateCertificateIfAvailable whether the alternate certificate provided by
|
||||
* the CA should be returned instead of the main one.
|
||||
* This is especially useful following
|
||||
* https://letsencrypt.org/2019/04/15/transitioning-to-isrg-root.html.
|
||||
*
|
||||
* @throws AcmeCoreServerException when the ACME server returns an error HTTP status code
|
||||
* (the exception will be more specific if detail is provided)
|
||||
* @throws AcmeCoreClientException when an error occured during response parsing
|
||||
* @throws CertificateRequestFailedException when the certificate request failed
|
||||
* @throws CertificateRequestTimedOutException when the certificate request timed out
|
||||
*
|
||||
* @return CertificateResponse the certificate data to save it somewhere you want
|
||||
*/
|
||||
public function finalizeOrder(CertificateOrder $order, CertificateRequest $csr, int $timeout = 180, bool $returnAlternateCertificateIfAvailable = false): CertificateResponse;
|
||||
|
||||
/**
|
||||
* Request authorization challenge data for a given domain.
|
||||
*
|
||||
* An AuthorizationChallenge is an association between a URI, a token and a payload.
|
||||
* The Certificate Authority will create this challenge data and you will then have
|
||||
* to expose the payload for the verification (see challengeAuthorization).
|
||||
*
|
||||
* @param string $domain the domain to challenge
|
||||
*
|
||||
* @throws AcmeCoreServerException when the ACME server returns an error HTTP status code
|
||||
* (the exception will be more specific if detail is provided)
|
||||
* @throws AcmeCoreClientException when an error occured during response parsing
|
||||
* @throws ChallengeNotSupportedException when the HTTP challenge is not supported by the server
|
||||
*
|
||||
* @return AuthorizationChallenge[] the list of challenges data returned by the Certificate Authority
|
||||
*/
|
||||
public function requestAuthorization(string $domain): array;
|
||||
|
||||
/**
|
||||
* Request the current status of an authorization challenge.
|
||||
*
|
||||
* @param AuthorizationChallenge $challenge The challenge to request
|
||||
*
|
||||
* @return AuthorizationChallenge A new instance of the challenge
|
||||
*/
|
||||
public function reloadAuthorization(AuthorizationChallenge $challenge): AuthorizationChallenge;
|
||||
|
||||
/**
|
||||
* Ask the Certificate Authority to challenge a given authorization.
|
||||
*
|
||||
* This check will generally consists of requesting over HTTP the domain
|
||||
* at a specific URL. This URL should return the raw payload generated
|
||||
* by requestAuthorization.
|
||||
*
|
||||
* WARNING : This method SHOULD NOT BE USED in a web action. It will
|
||||
* wait for the Certificate Authority to validate the challenge and this
|
||||
* operation could be long.
|
||||
*
|
||||
* @param AuthorizationChallenge $challenge the challenge data to check
|
||||
* @param int $timeout the timeout period
|
||||
*
|
||||
* @throws AcmeCoreServerException when the ACME server returns an error HTTP status code
|
||||
* (the exception will be more specific if detail is provided)
|
||||
* @throws AcmeCoreClientException when an error occured during response parsing
|
||||
* @throws ChallengeTimedOutException when the challenge timed out
|
||||
* @throws ChallengeFailedException when the challenge failed
|
||||
*
|
||||
* @return array the validate challenge response
|
||||
*/
|
||||
public function challengeAuthorization(AuthorizationChallenge $challenge, int $timeout = 180): array;
|
||||
|
||||
/**
|
||||
* Request a certificate for the given domain.
|
||||
*
|
||||
* This method should be called only if a previous authorization challenge has
|
||||
* been successful for the asked domain.
|
||||
*
|
||||
* WARNING : This method SHOULD NOT BE USED in a web action. It will
|
||||
* wait for the Certificate Authority to validate the certificate and
|
||||
* this operation could be long.
|
||||
*
|
||||
* @param string $domain the domain to request a certificate for
|
||||
* @param CertificateRequest $csr the Certificate Signing Request (informations for the certificate)
|
||||
* @param int $timeout the timeout period
|
||||
* @param bool $returnAlternateCertificateIfAvailable whether the alternate certificate provided by
|
||||
* the CA should be returned instead of the main one.
|
||||
* This is especially useful following
|
||||
* https://letsencrypt.org/2019/04/15/transitioning-to-isrg-root.html.
|
||||
*
|
||||
* @throws AcmeCoreServerException when the ACME server returns an error HTTP status code
|
||||
* (the exception will be more specific if detail is provided)
|
||||
* @throws AcmeCoreClientException when an error occured during response parsing
|
||||
* @throws CertificateRequestFailedException when the certificate request failed
|
||||
* @throws CertificateRequestTimedOutException when the certificate request timed out
|
||||
*
|
||||
* @return CertificateResponse the certificate data to save it somewhere you want
|
||||
*/
|
||||
public function requestCertificate(string $domain, CertificateRequest $csr, int $timeout = 180, bool $returnAlternateCertificateIfAvailable = false): CertificateResponse;
|
||||
|
||||
/**
|
||||
* Revoke a given certificate from the Certificate Authority.
|
||||
*
|
||||
* @throws CertificateRevocationException
|
||||
*/
|
||||
public function revokeCertificate(Certificate $certificate, RevocationReason $revocationReason = null);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Challenge;
|
||||
|
||||
use AcmePhp\Core\Exception\Protocol\ChallengeNotSupportedException;
|
||||
use AcmePhp\Core\Protocol\AuthorizationChallenge;
|
||||
|
||||
/**
|
||||
* A strategy ACME validator.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class ChainValidator implements ValidatorInterface
|
||||
{
|
||||
/** @var ValidatorInterface[] */
|
||||
private $validators;
|
||||
|
||||
/**
|
||||
* @param ValidatorInterface[] $validators
|
||||
*/
|
||||
public function __construct(array $validators)
|
||||
{
|
||||
$this->validators = $validators;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function supports(AuthorizationChallenge $authorizationChallenge, SolverInterface $solver): bool
|
||||
{
|
||||
foreach ($this->validators as $validator) {
|
||||
if ($validator->supports($authorizationChallenge, $solver)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isValid(AuthorizationChallenge $authorizationChallenge, SolverInterface $solver): bool
|
||||
{
|
||||
foreach ($this->validators as $validator) {
|
||||
if ($validator->supports($authorizationChallenge, $solver)) {
|
||||
return $validator->isValid($authorizationChallenge, $solver);
|
||||
}
|
||||
}
|
||||
|
||||
throw new ChallengeNotSupportedException();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Challenge;
|
||||
|
||||
/**
|
||||
* ACME challenge solver.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
interface ConfigurableServiceInterface
|
||||
{
|
||||
/**
|
||||
* Configure the service with a set of configuration.
|
||||
*/
|
||||
public function configure(array $config);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Challenge\Dns;
|
||||
|
||||
use AcmePhp\Core\Http\Base64SafeEncoder;
|
||||
use AcmePhp\Core\Protocol\AuthorizationChallenge;
|
||||
|
||||
/**
|
||||
* Extract data needed to solve DNS challenges.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class DnsDataExtractor
|
||||
{
|
||||
/** @var Base64SafeEncoder */
|
||||
private $encoder;
|
||||
|
||||
public function __construct(Base64SafeEncoder $encoder = null)
|
||||
{
|
||||
$this->encoder = $encoder ?: new Base64SafeEncoder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the name of the TXT record to register.
|
||||
*/
|
||||
public function getRecordName(AuthorizationChallenge $authorizationChallenge): string
|
||||
{
|
||||
return sprintf('_acme-challenge.%s.', $authorizationChallenge->getDomain());
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the value of the TXT record to register.
|
||||
*/
|
||||
public function getRecordValue(AuthorizationChallenge $authorizationChallenge): string
|
||||
{
|
||||
return $this->encoder->encode(hash('sha256', $authorizationChallenge->getPayload(), true));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Challenge\Dns;
|
||||
|
||||
/**
|
||||
* Resolves DNS.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
interface DnsResolverInterface
|
||||
{
|
||||
/**
|
||||
* Return whether or not the Resolver is supported.
|
||||
*/
|
||||
public static function isSupported(): bool;
|
||||
|
||||
/**
|
||||
* Retrieves the list of TXT entries for the given domain.
|
||||
*/
|
||||
public function getTxtEntries(string $domain): array;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Challenge\Dns;
|
||||
|
||||
use AcmePhp\Core\Challenge\SolverInterface;
|
||||
use AcmePhp\Core\Challenge\ValidatorInterface;
|
||||
use AcmePhp\Core\Exception\AcmeDnsResolutionException;
|
||||
use AcmePhp\Core\Protocol\AuthorizationChallenge;
|
||||
|
||||
/**
|
||||
* Validator for DNS challenges.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class DnsValidator implements ValidatorInterface
|
||||
{
|
||||
/**
|
||||
* @var DnsDataExtractor
|
||||
*/
|
||||
private $extractor;
|
||||
|
||||
/**
|
||||
* @var DnsResolverInterface
|
||||
*/
|
||||
private $dnsResolver;
|
||||
|
||||
public function __construct(DnsDataExtractor $extractor = null, DnsResolverInterface $dnsResolver = null)
|
||||
{
|
||||
$this->extractor = $extractor ?: new DnsDataExtractor();
|
||||
|
||||
$this->dnsResolver = $dnsResolver;
|
||||
if (!$this->dnsResolver) {
|
||||
$this->dnsResolver = LibDnsResolver::isSupported() ? new LibDnsResolver() : new SimpleDnsResolver();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function supports(AuthorizationChallenge $authorizationChallenge, SolverInterface $solver): bool
|
||||
{
|
||||
return 'dns-01' === $authorizationChallenge->getType();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isValid(AuthorizationChallenge $authorizationChallenge, SolverInterface $solver): bool
|
||||
{
|
||||
$recordName = $this->extractor->getRecordName($authorizationChallenge);
|
||||
$recordValue = $this->extractor->getRecordValue($authorizationChallenge);
|
||||
|
||||
try {
|
||||
return \in_array($recordValue, $this->dnsResolver->getTxtEntries($recordName), false);
|
||||
} catch (AcmeDnsResolutionException $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Challenge\Dns;
|
||||
|
||||
use AcmePhp\Core\Challenge\ConfigurableServiceInterface;
|
||||
use AcmePhp\Core\Challenge\MultipleChallengesSolverInterface;
|
||||
use AcmePhp\Core\Protocol\AuthorizationChallenge;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\ClientInterface;
|
||||
use Psr\Log\LoggerAwareTrait;
|
||||
use Psr\Log\NullLogger;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* ACME DNS solver with automate configuration of a Gandi.Net.
|
||||
*
|
||||
* @author Alexander Obuhovich <aik.bold@gmail.com>
|
||||
*/
|
||||
class GandiSolver implements MultipleChallengesSolverInterface, ConfigurableServiceInterface
|
||||
{
|
||||
use LoggerAwareTrait;
|
||||
|
||||
/**
|
||||
* @var DnsDataExtractor
|
||||
*/
|
||||
private $extractor;
|
||||
|
||||
/**
|
||||
* @var ClientInterface
|
||||
*/
|
||||
private $client;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $cacheZones;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $apiKey;
|
||||
|
||||
public function __construct(DnsDataExtractor $extractor = null, ClientInterface $client = null)
|
||||
{
|
||||
$this->extractor = $extractor ?: new DnsDataExtractor();
|
||||
$this->client = $client ?: new Client();
|
||||
$this->logger = new NullLogger();
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the service with a set of configuration.
|
||||
*/
|
||||
public function configure(array $config)
|
||||
{
|
||||
$this->apiKey = $config['api_key'];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function supports(AuthorizationChallenge $authorizationChallenge): bool
|
||||
{
|
||||
return 'dns-01' === $authorizationChallenge->getType();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function solve(AuthorizationChallenge $authorizationChallenge)
|
||||
{
|
||||
return $this->solveAll([$authorizationChallenge]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function solveAll(array $authorizationChallenges)
|
||||
{
|
||||
Assert::allIsInstanceOf($authorizationChallenges, AuthorizationChallenge::class);
|
||||
|
||||
foreach ($authorizationChallenges as $authorizationChallenge) {
|
||||
$topLevelDomain = $this->getTopLevelDomain($authorizationChallenge->getDomain());
|
||||
$recordName = $this->extractor->getRecordName($authorizationChallenge);
|
||||
$recordValue = $this->extractor->getRecordValue($authorizationChallenge);
|
||||
|
||||
$subDomain = \str_replace('.'.$topLevelDomain.'.', '', $recordName);
|
||||
|
||||
$this->client->request(
|
||||
'PUT',
|
||||
'https://dns.api.gandi.net/api/v5/domains/'.$topLevelDomain.'/records/'.$subDomain.'/TXT',
|
||||
[
|
||||
'headers' => [
|
||||
'X-Api-Key' => $this->apiKey,
|
||||
],
|
||||
'json' => [
|
||||
'rrset_type' => 'TXT',
|
||||
'rrset_ttl' => 600,
|
||||
'rrset_name' => $subDomain,
|
||||
'rrset_values' => [$recordValue],
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function cleanup(AuthorizationChallenge $authorizationChallenge)
|
||||
{
|
||||
return $this->cleanupAll([$authorizationChallenge]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function cleanupAll(array $authorizationChallenges)
|
||||
{
|
||||
Assert::allIsInstanceOf($authorizationChallenges, AuthorizationChallenge::class);
|
||||
|
||||
foreach ($authorizationChallenges as $authorizationChallenge) {
|
||||
$topLevelDomain = $this->getTopLevelDomain($authorizationChallenge->getDomain());
|
||||
$recordName = $this->extractor->getRecordName($authorizationChallenge);
|
||||
|
||||
$subDomain = \str_replace('.'.$topLevelDomain.'.', '', $recordName);
|
||||
|
||||
$this->client->request(
|
||||
'DELETE',
|
||||
'https://dns.api.gandi.net/api/v5/domains/'.$topLevelDomain.'/records/'.$subDomain.'/TXT',
|
||||
[
|
||||
'headers' => [
|
||||
'X-Api-Key' => $this->apiKey,
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected function getTopLevelDomain(string $domain): string
|
||||
{
|
||||
return \implode('.', \array_slice(\explode('.', $domain), -2));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Challenge\Dns;
|
||||
|
||||
use AcmePhp\Core\Exception\AcmeDnsResolutionException;
|
||||
use LibDNS\Decoder\Decoder;
|
||||
use LibDNS\Decoder\DecoderFactory;
|
||||
use LibDNS\Encoder\Encoder;
|
||||
use LibDNS\Encoder\EncoderFactory;
|
||||
use LibDNS\Messages\MessageFactory;
|
||||
use LibDNS\Messages\MessageTypes;
|
||||
use LibDNS\Records\QuestionFactory;
|
||||
use LibDNS\Records\ResourceTypes;
|
||||
use Psr\Log\LoggerAwareTrait;
|
||||
use Psr\Log\NullLogger;
|
||||
|
||||
/**
|
||||
* Resolves DNS with LibDNS (pass over internal DNS cache and check several nameservers).
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class LibDnsResolver implements DnsResolverInterface
|
||||
{
|
||||
use LoggerAwareTrait;
|
||||
|
||||
/**
|
||||
* @var QuestionFactory
|
||||
*/
|
||||
private $questionFactory;
|
||||
|
||||
/**
|
||||
* @var MessageFactory
|
||||
*/
|
||||
private $messageFactory;
|
||||
|
||||
/**
|
||||
* @var Encoder
|
||||
*/
|
||||
private $encoder;
|
||||
|
||||
/**
|
||||
* @var Decoder
|
||||
*/
|
||||
private $decoder;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $nameServer;
|
||||
|
||||
public function __construct(
|
||||
QuestionFactory $questionFactory = null,
|
||||
MessageFactory $messageFactory = null,
|
||||
Encoder $encoder = null,
|
||||
Decoder $decoder = null,
|
||||
$nameServer = '8.8.8.8'
|
||||
) {
|
||||
$this->questionFactory = $questionFactory ?: new QuestionFactory();
|
||||
$this->messageFactory = $messageFactory ?: new MessageFactory();
|
||||
$this->encoder = $encoder ?: (new EncoderFactory())->create();
|
||||
$this->decoder = $decoder ?: (new DecoderFactory())->create();
|
||||
$this->nameServer = $nameServer;
|
||||
$this->logger = new NullLogger();
|
||||
}
|
||||
|
||||
/**
|
||||
* @{@inheritdoc}
|
||||
*/
|
||||
public static function isSupported(): bool
|
||||
{
|
||||
return class_exists(ResourceTypes::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @{@inheritdoc}
|
||||
*/
|
||||
public function getTxtEntries($domain): array
|
||||
{
|
||||
$domain = rtrim($domain, '.');
|
||||
$nameServers = $this->getNameServers($domain);
|
||||
$this->logger->debug('Fetched TXT records for domain', ['nsDomain' => $domain, 'servers' => $nameServers]);
|
||||
$identicalEntries = [];
|
||||
foreach ($nameServers as $nameServer) {
|
||||
$ipNameServer = gethostbynamel($nameServer);
|
||||
if (empty($ipNameServer)) {
|
||||
throw new AcmeDnsResolutionException(sprintf('Unable to find domain %s on nameserver %s', $domain, $nameServer));
|
||||
}
|
||||
try {
|
||||
$response = $this->request($domain, ResourceTypes::TXT, $ipNameServer[0]);
|
||||
} catch (\Exception $e) {
|
||||
throw new AcmeDnsResolutionException(sprintf('Unable to find domain %s on nameserver %s', $domain, $nameServer));
|
||||
}
|
||||
$entries = [];
|
||||
foreach ($response->getAnswerRecords() as $record) {
|
||||
foreach ($record->getData() as $recordData) {
|
||||
$entries[] = (string) $recordData;
|
||||
}
|
||||
}
|
||||
|
||||
$identicalEntries[json_encode($entries)][] = $nameServer;
|
||||
}
|
||||
|
||||
$this->logger->info('DNS records fetched', ['mapping' => $identicalEntries]);
|
||||
if (1 !== \count($identicalEntries)) {
|
||||
throw new AcmeDnsResolutionException('Dns not fully propagated');
|
||||
}
|
||||
|
||||
return json_decode(key($identicalEntries));
|
||||
}
|
||||
|
||||
private function getNameServers($domain)
|
||||
{
|
||||
if ('' === $domain) {
|
||||
return [$this->nameServer];
|
||||
}
|
||||
|
||||
$parentNameServers = $this->getNameServers(implode('.', \array_slice(explode('.', $domain), 1)));
|
||||
$itemNameServers = [];
|
||||
$this->logger->debug('Fetched NS in charge of domain', ['nsDomain' => $domain, 'servers' => $parentNameServers]);
|
||||
foreach ($parentNameServers as $parentNameServer) {
|
||||
$ipNameServer = gethostbynamel($parentNameServer);
|
||||
if (empty($ipNameServer)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
$response = $this->request(
|
||||
$domain,
|
||||
ResourceTypes::NS,
|
||||
$ipNameServer[0]
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
// ignore errors
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($response->getAnswerRecords() as $record) {
|
||||
try {
|
||||
$field = $record->getData()->getFieldByName('nsdname');
|
||||
$itemNameServers[] = $field->getValue();
|
||||
} catch (\OutOfBoundsException $e) {
|
||||
}
|
||||
}
|
||||
foreach ($response->getAuthorityRecords() as $record) {
|
||||
try {
|
||||
$field = $record->getData()->getFieldByName('nsdname');
|
||||
$itemNameServers[] = $field->getValue();
|
||||
} catch (\OutOfBoundsException $e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
$itemNameServers = array_unique($itemNameServers);
|
||||
if (empty($itemNameServers)) {
|
||||
return $parentNameServers;
|
||||
}
|
||||
|
||||
return $itemNameServers;
|
||||
}
|
||||
|
||||
private function request($domain, $type, $nameServer)
|
||||
{
|
||||
$question = $this->questionFactory->create($type);
|
||||
$question->setName($domain);
|
||||
|
||||
// Create request message
|
||||
$request = $this->messageFactory->create(MessageTypes::QUERY);
|
||||
$request->getQuestionRecords()->add($question);
|
||||
$request->isRecursionDesired(true);
|
||||
|
||||
// Send request
|
||||
$socket = stream_socket_client(sprintf('udp://'.$nameServer.':53'));
|
||||
stream_socket_sendto($socket, $this->encoder->encode($request));
|
||||
|
||||
$r = [$socket];
|
||||
$w = $e = [];
|
||||
if (!stream_select($r, $w, $e, 3)) {
|
||||
throw new AcmeDnsResolutionException(sprintf('Timeout reached when requesting ServerName %s', $nameServer));
|
||||
}
|
||||
|
||||
// Decode response message
|
||||
try {
|
||||
$response = $this->decoder->decode(fread($socket, 1 << 20));
|
||||
} catch (\Exception $e) {
|
||||
throw new AcmeDnsResolutionException('Failed to decode server response', $e);
|
||||
}
|
||||
if (0 !== $response->getResponseCode()) {
|
||||
throw new AcmeDnsResolutionException(sprintf('ServerName respond with error code "%d"', $response->getResponseCode()));
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Challenge\Dns;
|
||||
|
||||
use AcmePhp\Core\Challenge\MultipleChallengesSolverInterface;
|
||||
use AcmePhp\Core\Exception\Protocol\ChallengeFailedException;
|
||||
use AcmePhp\Core\Protocol\AuthorizationChallenge;
|
||||
use Aws\Route53\Route53Client;
|
||||
use Psr\Log\LoggerAwareTrait;
|
||||
use Psr\Log\NullLogger;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* ACME DNS solver with automate configuration of a AWS route53.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class Route53Solver implements MultipleChallengesSolverInterface
|
||||
{
|
||||
use LoggerAwareTrait;
|
||||
|
||||
/**
|
||||
* @var DnsDataExtractor
|
||||
*/
|
||||
private $extractor;
|
||||
|
||||
/**
|
||||
* @var Route53Client
|
||||
*/
|
||||
private $client;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $cacheZones;
|
||||
|
||||
public function __construct(DnsDataExtractor $extractor = null, Route53Client $client = null)
|
||||
{
|
||||
$this->extractor = $extractor ?: new DnsDataExtractor();
|
||||
$this->client = $client ?: new Route53Client([]);
|
||||
$this->logger = new NullLogger();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function supports(AuthorizationChallenge $authorizationChallenge): bool
|
||||
{
|
||||
return 'dns-01' === $authorizationChallenge->getType();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function solve(AuthorizationChallenge $authorizationChallenge)
|
||||
{
|
||||
return $this->solveAll([$authorizationChallenge]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function solveAll(array $authorizationChallenges)
|
||||
{
|
||||
Assert::allIsInstanceOf($authorizationChallenges, AuthorizationChallenge::class);
|
||||
|
||||
$changesPerZone = [];
|
||||
$authorizationChallengesPerDomain = $this->groupAuthorizationChallengesPerDomain($authorizationChallenges);
|
||||
foreach ($authorizationChallengesPerDomain as $domain => $authorizationChallengesForDomain) {
|
||||
$zone = $this->getZone($authorizationChallengesForDomain[0]->getDomain());
|
||||
|
||||
$authorizationChallengesPerRecordName = $this->groupAuthorizationChallengesPerRecordName($authorizationChallengesForDomain);
|
||||
foreach ($authorizationChallengesPerRecordName as $recordName => $authorizationChallengesForRecordName) {
|
||||
$challengeValues = array_unique(array_map([$this->extractor, 'getRecordValue'], $authorizationChallengesForRecordName));
|
||||
$recordIndex = $this->getPreviousRecordIndex($zone['Id'], $recordName);
|
||||
|
||||
if (0 === \count(array_diff($challengeValues, array_keys($recordIndex)))) {
|
||||
$this->logger->debug('Record already defined', ['recordName' => $recordName]);
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($challengeValues as $recordValue) {
|
||||
$recordIndex[$recordValue] = time();
|
||||
}
|
||||
|
||||
$changesPerZone[$zone['Id']][] = $this->getSaveRecordQuery($recordName, $recordIndex);
|
||||
}
|
||||
}
|
||||
|
||||
$records = [];
|
||||
foreach ($changesPerZone as $zoneId => $changes) {
|
||||
$this->logger->info('Updating route 53 DNS', ['zone' => $zoneId]);
|
||||
$records[$zoneId] = $this->client->changeResourceRecordSets(
|
||||
[
|
||||
'ChangeBatch' => [
|
||||
'Changes' => $changes,
|
||||
],
|
||||
'HostedZoneId' => $zoneId,
|
||||
]
|
||||
);
|
||||
}
|
||||
foreach ($records as $zoneId => $record) {
|
||||
$this->logger->info('Waiting for Route 53 changes', ['zone' => $zoneId]);
|
||||
$this->client->waitUntil('ResourceRecordSetsChanged', ['Id' => $record['ChangeInfo']['Id']]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function cleanup(AuthorizationChallenge $authorizationChallenge)
|
||||
{
|
||||
return $this->cleanupAll([$authorizationChallenge]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function cleanupAll(array $authorizationChallenges)
|
||||
{
|
||||
Assert::allIsInstanceOf($authorizationChallenges, AuthorizationChallenge::class);
|
||||
|
||||
$changesPerZone = [];
|
||||
$authorizationChallengesPerDomain = $this->groupAuthorizationChallengesPerDomain($authorizationChallenges);
|
||||
foreach ($authorizationChallengesPerDomain as $domain => $authorizationChallengesForDomain) {
|
||||
$zone = $this->getZone($authorizationChallengesForDomain[0]->getDomain());
|
||||
|
||||
$authorizationChallengesPerRecordName = $this->groupAuthorizationChallengesPerRecordName($authorizationChallengesForDomain);
|
||||
foreach ($authorizationChallengesPerRecordName as $recordName => $authorizationChallengesForRecordName) {
|
||||
$challengeValues = array_unique(array_map([$this->extractor, 'getRecordValue'], $authorizationChallengesForRecordName));
|
||||
$recordIndex = $this->getPreviousRecordIndex($zone['Id'], $recordName);
|
||||
|
||||
foreach ($challengeValues as $recordValue) {
|
||||
unset($recordIndex[$recordValue]);
|
||||
}
|
||||
$changesPerZone[$zone['Id']][] = $this->getSaveRecordQuery($recordName, $recordIndex);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($changesPerZone as $zoneId => $changes) {
|
||||
$this->logger->info('Updating route 53 DNS', ['zone' => $zoneId]);
|
||||
$this->client->changeResourceRecordSets(
|
||||
[
|
||||
'ChangeBatch' => [
|
||||
'Changes' => $changes,
|
||||
],
|
||||
'HostedZoneId' => $zoneId,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function getPreviousRecordIndex($zoneId, $recordName)
|
||||
{
|
||||
$previousRecordSets = $this->client->listResourceRecordSets([
|
||||
'HostedZoneId' => $zoneId,
|
||||
'StartRecordName' => $recordName,
|
||||
'StartRecordType' => 'TXT',
|
||||
]);
|
||||
$recordSets = array_filter(
|
||||
$previousRecordSets['ResourceRecordSets'],
|
||||
function ($recordSet) use ($recordName) {
|
||||
return $recordSet['Name'] === $recordName && 'TXT' === $recordSet['Type'];
|
||||
}
|
||||
);
|
||||
$recordIndex = [];
|
||||
foreach ($recordSets as $previousRecordSet) {
|
||||
$previousTxt = array_map(function ($resourceRecord) {
|
||||
return stripslashes(trim($resourceRecord['Value'], '"'));
|
||||
}, $previousRecordSet['ResourceRecords']);
|
||||
// Search the special Index
|
||||
foreach ($previousTxt as $index => $recordValue) {
|
||||
if (null !== $previousIndex = json_decode($recordValue, true)) {
|
||||
$recordIndex = $previousIndex;
|
||||
unset($previousTxt[$index]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Set default value
|
||||
foreach ($previousTxt as $recordValue) {
|
||||
if (!isset($recordIndex[$recordValue])) {
|
||||
$recordIndex[$recordValue] = time();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $recordIndex;
|
||||
}
|
||||
|
||||
private function getSaveRecordQuery($recordName, array $recordIndex)
|
||||
{
|
||||
//remove old indexes
|
||||
$limitTime = time() - 86400;
|
||||
foreach ($recordIndex as $recordValue => $time) {
|
||||
if ($time < $limitTime) {
|
||||
unset($recordIndex[$recordValue]);
|
||||
}
|
||||
}
|
||||
|
||||
$recordValues = array_keys($recordIndex);
|
||||
$recordValues[] = json_encode($recordIndex);
|
||||
|
||||
return [
|
||||
'Action' => 'UPSERT',
|
||||
'ResourceRecordSet' => [
|
||||
'Name' => $recordName,
|
||||
'ResourceRecords' => array_map(function ($recordValue) {
|
||||
return [
|
||||
'Value' => sprintf('"%s"', addslashes($recordValue)),
|
||||
];
|
||||
}, $recordValues),
|
||||
'TTL' => 5,
|
||||
'Type' => 'TXT',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param AuthorizationChallenge[] $authorizationChallenges
|
||||
*
|
||||
* @return AuthorizationChallenge[][]
|
||||
*/
|
||||
private function groupAuthorizationChallengesPerDomain(array $authorizationChallenges)
|
||||
{
|
||||
$groups = [];
|
||||
foreach ($authorizationChallenges as $authorizationChallenge) {
|
||||
$groups[$authorizationChallenge->getDomain()][] = $authorizationChallenge;
|
||||
}
|
||||
|
||||
return $groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param AuthorizationChallenge[] $authorizationChallenges
|
||||
*
|
||||
* @return AuthorizationChallenge[][]
|
||||
*/
|
||||
private function groupAuthorizationChallengesPerRecordName(array $authorizationChallenges)
|
||||
{
|
||||
$groups = [];
|
||||
foreach ($authorizationChallenges as $authorizationChallenge) {
|
||||
$groups[$this->extractor->getRecordName($authorizationChallenge)][] = $authorizationChallenge;
|
||||
}
|
||||
|
||||
return $groups;
|
||||
}
|
||||
|
||||
private function getZone($domain)
|
||||
{
|
||||
$domainParts = explode('.', $domain);
|
||||
$domains = array_reverse(array_map(
|
||||
function ($index) use ($domainParts) {
|
||||
return implode('.', \array_slice($domainParts, \count($domainParts) - $index));
|
||||
},
|
||||
range(0, \count($domainParts))
|
||||
));
|
||||
|
||||
$zones = $this->getZones();
|
||||
foreach ($domains as $cursorDomain) {
|
||||
if (isset($zones[$cursorDomain.'.'])) {
|
||||
return $zones[$cursorDomain.'.'];
|
||||
}
|
||||
}
|
||||
|
||||
throw new ChallengeFailedException(sprintf('Unable to find a zone for the domain "%s"', $domain));
|
||||
}
|
||||
|
||||
private function getZones()
|
||||
{
|
||||
if (null !== $this->cacheZones) {
|
||||
return $this->cacheZones;
|
||||
}
|
||||
|
||||
$zones = [];
|
||||
$args = [];
|
||||
do {
|
||||
$resp = $this->client->listHostedZones($args);
|
||||
$zones = array_merge($zones, $resp['HostedZones']);
|
||||
$args = ['Marker' => $resp['NextMarker']];
|
||||
} while ($resp['IsTruncated']);
|
||||
|
||||
$this->cacheZones = array_column($zones, null, 'Name');
|
||||
|
||||
return $this->cacheZones;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Challenge\Dns;
|
||||
|
||||
/**
|
||||
* Resolves DNS through dns_get_record.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class SimpleDnsResolver implements DnsResolverInterface
|
||||
{
|
||||
/**
|
||||
* @{@inheritdoc}
|
||||
*/
|
||||
public static function isSupported(): bool
|
||||
{
|
||||
return \function_exists('dns_get_record');
|
||||
}
|
||||
|
||||
/**
|
||||
* @{@inheritdoc}
|
||||
*/
|
||||
public function getTxtEntries($domain): array
|
||||
{
|
||||
$entries = [];
|
||||
foreach (dns_get_record($domain, DNS_TXT) as $record) {
|
||||
$entries = array_merge($entries, $record['entries']);
|
||||
}
|
||||
|
||||
sort($entries);
|
||||
|
||||
return array_unique($entries);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Challenge\Dns;
|
||||
|
||||
use AcmePhp\Core\Challenge\SolverInterface;
|
||||
use AcmePhp\Core\Protocol\AuthorizationChallenge;
|
||||
use Symfony\Component\Console\Output\NullOutput;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* ACME DNS solver with manual intervention.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class SimpleDnsSolver implements SolverInterface
|
||||
{
|
||||
/**
|
||||
* @var DnsDataExtractor
|
||||
*/
|
||||
private $extractor;
|
||||
|
||||
/**
|
||||
* @var OutputInterface
|
||||
*/
|
||||
protected $output;
|
||||
|
||||
/**
|
||||
* @param DnsDataExtractor $extractor
|
||||
* @param OutputInterface $output
|
||||
*/
|
||||
public function __construct(DnsDataExtractor $extractor = null, OutputInterface $output = null)
|
||||
{
|
||||
$this->extractor = $extractor ?: new DnsDataExtractor();
|
||||
$this->output = $output ?: new NullOutput();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function supports(AuthorizationChallenge $authorizationChallenge): bool
|
||||
{
|
||||
return 'dns-01' === $authorizationChallenge->getType();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function solve(AuthorizationChallenge $authorizationChallenge)
|
||||
{
|
||||
$recordName = $this->extractor->getRecordName($authorizationChallenge);
|
||||
$recordValue = $this->extractor->getRecordValue($authorizationChallenge);
|
||||
|
||||
$this->output->writeln(
|
||||
sprintf(
|
||||
<<<'EOF'
|
||||
Add the following TXT record to your DNS zone
|
||||
Domain: %s
|
||||
TXT value: %s
|
||||
|
||||
<comment>Wait for the propagation before moving to the next step</comment>
|
||||
Tips: Use the following command to check the propagation
|
||||
|
||||
host -t TXT %s
|
||||
|
||||
EOF
|
||||
,
|
||||
$recordName,
|
||||
$recordValue,
|
||||
$recordName
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function cleanup(AuthorizationChallenge $authorizationChallenge)
|
||||
{
|
||||
$recordName = $this->extractor->getRecordName($authorizationChallenge);
|
||||
|
||||
$this->output->writeln(
|
||||
sprintf(
|
||||
<<<'EOF'
|
||||
You can now cleanup your DNS by removing the domain <comment>_acme-challenge.%s.</comment>
|
||||
EOF
|
||||
,
|
||||
$recordName
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Challenge\Http;
|
||||
|
||||
use AcmePhp\Core\Challenge\ConfigurableServiceInterface;
|
||||
use AcmePhp\Core\Challenge\SolverInterface;
|
||||
use AcmePhp\Core\Filesystem\Adapter\NullAdapter;
|
||||
use AcmePhp\Core\Filesystem\FilesystemFactoryInterface;
|
||||
use AcmePhp\Core\Filesystem\FilesystemInterface;
|
||||
use AcmePhp\Core\Protocol\AuthorizationChallenge;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Symfony\Component\DependencyInjection\ServiceLocator;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* ACME HTTP solver through ftp upload.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class FilesystemSolver implements SolverInterface, ConfigurableServiceInterface
|
||||
{
|
||||
/**
|
||||
* @var ContainerInterface
|
||||
*/
|
||||
private $filesystemFactoryLocator;
|
||||
|
||||
/**
|
||||
* @var FilesystemInterface
|
||||
*/
|
||||
private $filesystem;
|
||||
|
||||
/**
|
||||
* @var HttpDataExtractor
|
||||
*/
|
||||
private $extractor;
|
||||
|
||||
public function __construct(ContainerInterface $filesystemFactoryLocator = null, HttpDataExtractor $extractor = null)
|
||||
{
|
||||
$this->filesystemFactoryLocator = $filesystemFactoryLocator ?: new ServiceLocator([]);
|
||||
$this->extractor = $extractor ?: new HttpDataExtractor();
|
||||
$this->filesystem = new NullAdapter();
|
||||
}
|
||||
|
||||
public function configure(array $config)
|
||||
{
|
||||
Assert::keyExists($config, 'adapter', 'configure::$config expected an array with the key %s.');
|
||||
|
||||
/** @var FilesystemFactoryInterface $factory */
|
||||
$factory = $this->filesystemFactoryLocator->get($config['adapter']);
|
||||
$this->filesystem = $factory->create($config);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function supports(AuthorizationChallenge $authorizationChallenge): bool
|
||||
{
|
||||
return 'http-01' === $authorizationChallenge->getType();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function solve(AuthorizationChallenge $authorizationChallenge)
|
||||
{
|
||||
$checkPath = $this->extractor->getCheckPath($authorizationChallenge);
|
||||
$checkContent = $this->extractor->getCheckContent($authorizationChallenge);
|
||||
|
||||
$this->filesystem->write($checkPath, $checkContent);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function cleanup(AuthorizationChallenge $authorizationChallenge)
|
||||
{
|
||||
$checkPath = $this->extractor->getCheckPath($authorizationChallenge);
|
||||
|
||||
$this->filesystem->delete($checkPath);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Challenge\Http;
|
||||
|
||||
use AcmePhp\Core\Protocol\AuthorizationChallenge;
|
||||
|
||||
/**
|
||||
* Extract data needed to solve HTTP challenges.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class HttpDataExtractor
|
||||
{
|
||||
/**
|
||||
* Retrieves the absolute URL called by the CA.
|
||||
*/
|
||||
public function getCheckUrl(AuthorizationChallenge $authorizationChallenge): string
|
||||
{
|
||||
return sprintf(
|
||||
'http://%s%s',
|
||||
$authorizationChallenge->getDomain(),
|
||||
$this->getCheckPath($authorizationChallenge)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the absolute path called by the CA.
|
||||
*/
|
||||
public function getCheckPath(AuthorizationChallenge $authorizationChallenge): string
|
||||
{
|
||||
return sprintf(
|
||||
'/.well-known/acme-challenge/%s',
|
||||
$authorizationChallenge->getToken()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the content that should be returned in the response.
|
||||
*/
|
||||
public function getCheckContent(AuthorizationChallenge $authorizationChallenge): string
|
||||
{
|
||||
return $authorizationChallenge->getPayload();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Challenge\Http;
|
||||
|
||||
use AcmePhp\Core\Challenge\SolverInterface;
|
||||
use AcmePhp\Core\Challenge\ValidatorInterface;
|
||||
use AcmePhp\Core\Protocol\AuthorizationChallenge;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\ClientException;
|
||||
|
||||
/**
|
||||
* Validator for HTTP challenges.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class HttpValidator implements ValidatorInterface
|
||||
{
|
||||
/**
|
||||
* @var HttpDataExtractor
|
||||
*/
|
||||
private $extractor;
|
||||
|
||||
/**
|
||||
* @var Client
|
||||
*/
|
||||
private $client;
|
||||
|
||||
public function __construct(HttpDataExtractor $extractor = null, Client $client = null)
|
||||
{
|
||||
$this->extractor = $extractor ?: new HttpDataExtractor();
|
||||
$this->client = $client ?: new Client();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function supports(AuthorizationChallenge $authorizationChallenge, SolverInterface $solver): bool
|
||||
{
|
||||
return 'http-01' === $authorizationChallenge->getType() && !$solver instanceof MockServerHttpSolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isValid(AuthorizationChallenge $authorizationChallenge, SolverInterface $solver): bool
|
||||
{
|
||||
$checkUrl = $this->extractor->getCheckUrl($authorizationChallenge);
|
||||
$checkContent = $this->extractor->getCheckContent($authorizationChallenge);
|
||||
|
||||
try {
|
||||
return $checkContent === trim($this->client->get($checkUrl, ['verify' => false])->getBody()->getContents());
|
||||
} catch (ClientException $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Challenge\Http;
|
||||
|
||||
use AcmePhp\Core\Challenge\SolverInterface;
|
||||
use AcmePhp\Core\Challenge\ValidatorInterface;
|
||||
use AcmePhp\Core\Protocol\AuthorizationChallenge;
|
||||
|
||||
/**
|
||||
* Validator for pebble-challtestsrv.
|
||||
*
|
||||
* @author Titouan Galopin <galopintitouan@gmail.com>
|
||||
*/
|
||||
class MockHttpValidator implements ValidatorInterface
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function supports(AuthorizationChallenge $authorizationChallenge, SolverInterface $solver): bool
|
||||
{
|
||||
return 'http-01' === $authorizationChallenge->getType() && $solver instanceof MockServerHttpSolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isValid(AuthorizationChallenge $authorizationChallenge, SolverInterface $solver): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Challenge\Http;
|
||||
|
||||
use AcmePhp\Core\Challenge\SolverInterface;
|
||||
use AcmePhp\Core\Protocol\AuthorizationChallenge;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\RequestOptions;
|
||||
|
||||
/**
|
||||
* ACME HTTP solver talking to pebble-challtestsrv.
|
||||
*
|
||||
* @author Titouan Galopin <galopintitouan@gmail.com>
|
||||
*/
|
||||
class MockServerHttpSolver implements SolverInterface
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function supports(AuthorizationChallenge $authorizationChallenge): bool
|
||||
{
|
||||
return 'http-01' === $authorizationChallenge->getType();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function solve(AuthorizationChallenge $authorizationChallenge)
|
||||
{
|
||||
(new Client())->post('http://localhost:8055/add-http01', [
|
||||
RequestOptions::JSON => [
|
||||
'token' => $authorizationChallenge->getToken(),
|
||||
'content' => $authorizationChallenge->getPayload(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function cleanup(AuthorizationChallenge $authorizationChallenge)
|
||||
{
|
||||
(new Client())->post('http://localhost:8055/del-http01', [
|
||||
RequestOptions::JSON => [
|
||||
'token' => $authorizationChallenge->getToken(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Challenge\Http;
|
||||
|
||||
use AcmePhp\Core\Challenge\SolverInterface;
|
||||
use AcmePhp\Core\Protocol\AuthorizationChallenge;
|
||||
use Symfony\Component\Console\Output\NullOutput;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* ACME HTTP solver with manual intervention.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class SimpleHttpSolver implements SolverInterface
|
||||
{
|
||||
/**
|
||||
* @var HttpDataExtractor
|
||||
*/
|
||||
private $extractor;
|
||||
|
||||
/**
|
||||
* @var OutputInterface
|
||||
*/
|
||||
private $output;
|
||||
|
||||
public function __construct(HttpDataExtractor $extractor = null, OutputInterface $output = null)
|
||||
{
|
||||
$this->extractor = $extractor ?: new HttpDataExtractor();
|
||||
$this->output = $output ?: new NullOutput();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function supports(AuthorizationChallenge $authorizationChallenge): bool
|
||||
{
|
||||
return 'http-01' === $authorizationChallenge->getType();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function solve(AuthorizationChallenge $authorizationChallenge)
|
||||
{
|
||||
$checkUrl = $this->extractor->getCheckUrl($authorizationChallenge);
|
||||
$checkContent = $this->extractor->getCheckContent($authorizationChallenge);
|
||||
|
||||
$this->output->writeln(
|
||||
sprintf(
|
||||
<<<'EOF'
|
||||
Create a text file accessible on URL %s
|
||||
containing the following content:
|
||||
|
||||
%s
|
||||
|
||||
Check in your browser that the URL %s returns
|
||||
the authorization token above.
|
||||
|
||||
EOF
|
||||
,
|
||||
$checkUrl,
|
||||
$checkContent,
|
||||
$checkContent
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function cleanup(AuthorizationChallenge $authorizationChallenge)
|
||||
{
|
||||
$checkUrl = $this->extractor->getCheckUrl($authorizationChallenge);
|
||||
|
||||
$this->output->writeln(
|
||||
sprintf(
|
||||
<<<'EOF'
|
||||
You can now safely remove the challenge's file at %s
|
||||
|
||||
EOF
|
||||
,
|
||||
$checkUrl
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Challenge;
|
||||
|
||||
use AcmePhp\Core\Protocol\AuthorizationChallenge;
|
||||
|
||||
/**
|
||||
* ACME challenge solver able to solve several challenges at once.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
interface MultipleChallengesSolverInterface extends SolverInterface
|
||||
{
|
||||
/**
|
||||
* Solve the given list of authorization challenge.
|
||||
*
|
||||
* @param AuthorizationChallenge[] $authorizationChallenges
|
||||
*/
|
||||
public function solveAll(array $authorizationChallenges);
|
||||
|
||||
/**
|
||||
* Cleanup the environments after all challenges.
|
||||
*
|
||||
* @param AuthorizationChallenge[] $authorizationChallenges
|
||||
*/
|
||||
public function cleanupAll(array $authorizationChallenges);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Challenge;
|
||||
|
||||
use AcmePhp\Core\Protocol\AuthorizationChallenge;
|
||||
|
||||
/**
|
||||
* ACME challenge solver.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
interface SolverInterface
|
||||
{
|
||||
/**
|
||||
* Determines whether or not the solver supports a given Challenge.
|
||||
*
|
||||
* @return bool The solver supports the given challenge's type
|
||||
*/
|
||||
public function supports(AuthorizationChallenge $authorizationChallenge): bool;
|
||||
|
||||
/**
|
||||
* Solve the given authorization challenge.
|
||||
*/
|
||||
public function solve(AuthorizationChallenge $authorizationChallenge);
|
||||
|
||||
/**
|
||||
* Cleanup the environments after a successful challenge.
|
||||
*/
|
||||
public function cleanup(AuthorizationChallenge $authorizationChallenge);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Challenge;
|
||||
|
||||
use AcmePhp\Core\Protocol\AuthorizationChallenge;
|
||||
|
||||
/**
|
||||
* ACME challenge pre validator.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
interface ValidatorInterface
|
||||
{
|
||||
/**
|
||||
* Determines whether or not the validator supports a given Challenge.
|
||||
*
|
||||
* @return bool The validator supports the given challenge's type
|
||||
*/
|
||||
public function supports(AuthorizationChallenge $authorizationChallenge, SolverInterface $solver): bool;
|
||||
|
||||
/**
|
||||
* Internally validate the challenge by performing the same kind of test than the CA.
|
||||
*
|
||||
* @return bool The challenge is valid
|
||||
*/
|
||||
public function isValid(AuthorizationChallenge $authorizationChallenge, SolverInterface $solver): bool;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Challenge;
|
||||
|
||||
use AcmePhp\Core\Protocol\AuthorizationChallenge;
|
||||
|
||||
/**
|
||||
* ACME Challenge validator who implement a retry strategy till the decorated validator successfully validate the
|
||||
* challenge or the timeout is reached.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class WaitingValidator implements ValidatorInterface
|
||||
{
|
||||
/** @var ValidatorInterface */
|
||||
private $validator;
|
||||
|
||||
/** @var int */
|
||||
private $timeout;
|
||||
|
||||
public function __construct(ValidatorInterface $validator, int $timeout = 180)
|
||||
{
|
||||
$this->validator = $validator;
|
||||
$this->timeout = $timeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function supports(AuthorizationChallenge $authorizationChallenge, SolverInterface $solver): bool
|
||||
{
|
||||
return $this->validator->supports($authorizationChallenge, $solver);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isValid(AuthorizationChallenge $authorizationChallenge, SolverInterface $solver): bool
|
||||
{
|
||||
$limitEndTime = time() + $this->timeout;
|
||||
|
||||
do {
|
||||
if ($this->validator->isValid($authorizationChallenge, $solver)) {
|
||||
return true;
|
||||
}
|
||||
sleep(3);
|
||||
} while ($limitEndTime > time());
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Exception;
|
||||
|
||||
/**
|
||||
* Error reported by the client.
|
||||
*
|
||||
* @author Titouan Galopin <galopintitouan@gmail.com>
|
||||
*/
|
||||
class AcmeCoreClientException extends AcmeCoreException
|
||||
{
|
||||
public function __construct($message, \Exception $previous = null)
|
||||
{
|
||||
parent::__construct($message, 0, $previous);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Exception;
|
||||
|
||||
/**
|
||||
* @author Titouan Galopin <galopintitouan@gmail.com>
|
||||
*/
|
||||
class AcmeCoreException extends \RuntimeException
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Exception;
|
||||
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
|
||||
/**
|
||||
* Error reported by the server.
|
||||
*
|
||||
* @author Titouan Galopin <galopintitouan@gmail.com>
|
||||
*/
|
||||
class AcmeCoreServerException extends AcmeCoreException
|
||||
{
|
||||
public function __construct(RequestInterface $request, $message, \Exception $previous = null)
|
||||
{
|
||||
parent::__construct($message, $previous ? $previous->getCode() : 0, $previous);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Exception;
|
||||
|
||||
/**
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class AcmeDnsResolutionException extends AcmeCoreException
|
||||
{
|
||||
public function __construct($message, \Exception $previous = null)
|
||||
{
|
||||
parent::__construct(null === $message ? 'An exception was thrown during resolution of DNS' : $message, 0, $previous);
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Exception\Protocol;
|
||||
|
||||
/**
|
||||
* @author Titouan Galopin <galopintitouan@gmail.com>
|
||||
*/
|
||||
class CertificateRequestFailedException extends ProtocolException
|
||||
{
|
||||
public function __construct(string $response)
|
||||
{
|
||||
parent::__construct(sprintf('Certificate request failed (response: %s)', $response));
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Exception\Protocol;
|
||||
|
||||
/**
|
||||
* @author Titouan Galopin <galopintitouan@gmail.com>
|
||||
*/
|
||||
class CertificateRequestTimedOutException extends ProtocolException
|
||||
{
|
||||
public function __construct(string $response)
|
||||
{
|
||||
parent::__construct(sprintf('Certificate request timed out (response: %s)', $response));
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Exception\Protocol;
|
||||
|
||||
use AcmePhp\Core\Exception\AcmeCoreClientException;
|
||||
|
||||
class CertificateRevocationException extends AcmeCoreClientException
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Exception\Protocol;
|
||||
|
||||
/**
|
||||
* @author Titouan Galopin <galopintitouan@gmail.com>
|
||||
*/
|
||||
class ChallengeFailedException extends ProtocolException
|
||||
{
|
||||
private $response;
|
||||
|
||||
public function __construct($response, \Exception $previous = null)
|
||||
{
|
||||
parent::__construct(
|
||||
sprintf('Challenge failed (response: %s).', json_encode($response)),
|
||||
$previous
|
||||
);
|
||||
|
||||
$this->response = $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getResponse()
|
||||
{
|
||||
return $this->response;
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Exception\Protocol;
|
||||
|
||||
/**
|
||||
* @author Titouan Galopin <galopintitouan@gmail.com>
|
||||
*/
|
||||
class ChallengeNotSupportedException extends ProtocolException
|
||||
{
|
||||
public function __construct(\Exception $previous = null)
|
||||
{
|
||||
parent::__construct('This ACME server does not expose supported challenge.', $previous);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Exception\Protocol;
|
||||
|
||||
/**
|
||||
* @author Titouan Galopin <galopintitouan@gmail.com>
|
||||
*/
|
||||
class ChallengeTimedOutException extends ProtocolException
|
||||
{
|
||||
private $response;
|
||||
|
||||
public function __construct($response, \Exception $previous = null)
|
||||
{
|
||||
parent::__construct(
|
||||
sprintf('Challenge timed out (response: %s).', json_encode($response)),
|
||||
$previous
|
||||
);
|
||||
|
||||
$this->response = $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getResponse()
|
||||
{
|
||||
return $this->response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Exception\Protocol;
|
||||
|
||||
/**
|
||||
* @author Titouan Galopin <galopintitouan@gmail.com>
|
||||
*/
|
||||
class ExpectedJsonException extends ProtocolException
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Exception\Protocol;
|
||||
|
||||
use AcmePhp\Core\Exception\AcmeCoreClientException;
|
||||
|
||||
/**
|
||||
* Error because the protocol was not respected.
|
||||
*
|
||||
* @author Titouan Galopin <galopintitouan@gmail.com>
|
||||
*/
|
||||
class ProtocolException extends AcmeCoreClientException
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Exception\Server;
|
||||
|
||||
use AcmePhp\Core\Exception\AcmeCoreServerException;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
|
||||
/**
|
||||
* @author Titouan Galopin <galopintitouan@gmail.com>
|
||||
*/
|
||||
class BadCsrServerException extends AcmeCoreServerException
|
||||
{
|
||||
public function __construct(RequestInterface $request, string $detail, \Exception $previous = null)
|
||||
{
|
||||
parent::__construct(
|
||||
$request,
|
||||
'[badCSR] The CSR is unacceptable (e.g., due to a short key): '.$detail,
|
||||
$previous
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Exception\Server;
|
||||
|
||||
use AcmePhp\Core\Exception\AcmeCoreServerException;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
|
||||
/**
|
||||
* @author Titouan Galopin <galopintitouan@gmail.com>
|
||||
*/
|
||||
class BadNonceServerException extends AcmeCoreServerException
|
||||
{
|
||||
public function __construct(RequestInterface $request, string $detail, \Exception $previous = null)
|
||||
{
|
||||
parent::__construct(
|
||||
$request,
|
||||
'[badNonce] The client sent an unacceptable anti-replay nonce: '.$detail,
|
||||
$previous
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Exception\Server;
|
||||
|
||||
use AcmePhp\Core\Exception\AcmeCoreServerException;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
|
||||
/**
|
||||
* @author Alex Plekhanov <alex@plekhanov.dev>
|
||||
*/
|
||||
class CaaServerException extends AcmeCoreServerException
|
||||
{
|
||||
public function __construct(RequestInterface $request, string $detail, \Exception $previous = null)
|
||||
{
|
||||
parent::__construct(
|
||||
$request,
|
||||
'[caa] Certification Authority Authorization (CAA) records forbid the CA from issuing a certificate: '.$detail,
|
||||
$previous
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Exception\Server;
|
||||
|
||||
use AcmePhp\Core\Exception\AcmeCoreServerException;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
|
||||
/**
|
||||
* @author Titouan Galopin <galopintitouan@gmail.com>
|
||||
*/
|
||||
class ConnectionServerException extends AcmeCoreServerException
|
||||
{
|
||||
public function __construct(RequestInterface $request, string $detail, \Exception $previous = null)
|
||||
{
|
||||
parent::__construct(
|
||||
$request,
|
||||
'[connection] The server could not connect to the client for DV: '.$detail,
|
||||
$previous
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Exception\Server;
|
||||
|
||||
use AcmePhp\Core\Exception\AcmeCoreServerException;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
|
||||
/**
|
||||
* @author Alex Plekhanov <alex@plekhanov.dev>
|
||||
*/
|
||||
class DnsServerException extends AcmeCoreServerException
|
||||
{
|
||||
public function __construct(RequestInterface $request, string $detail, \Exception $previous = null)
|
||||
{
|
||||
parent::__construct(
|
||||
$request,
|
||||
'[dns] There was a problem with a DNS query during identifier validation: '.$detail,
|
||||
$previous
|
||||
);
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Exception\Server;
|
||||
|
||||
use AcmePhp\Core\Exception\AcmeCoreServerException;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
|
||||
/**
|
||||
* @author Alex Plekhanov <alex@plekhanov.dev>
|
||||
*/
|
||||
class IncorrectResponseServerException extends AcmeCoreServerException
|
||||
{
|
||||
public function __construct(RequestInterface $request, string $detail, \Exception $previous = null)
|
||||
{
|
||||
parent::__construct(
|
||||
$request,
|
||||
"[incorrectResponse] Response received didn’t match the challenge's requirements: ".$detail,
|
||||
$previous
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Exception\Server;
|
||||
|
||||
use AcmePhp\Core\Exception\AcmeCoreServerException;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
|
||||
/**
|
||||
* @author Titouan Galopin <galopintitouan@gmail.com>
|
||||
*/
|
||||
class InternalServerException extends AcmeCoreServerException
|
||||
{
|
||||
public function __construct(RequestInterface $request, string $detail, \Exception $previous = null)
|
||||
{
|
||||
parent::__construct(
|
||||
$request,
|
||||
'[serverInternal] The server experienced an internal error: '.$detail,
|
||||
$previous
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Exception\Server;
|
||||
|
||||
use AcmePhp\Core\Exception\AcmeCoreServerException;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
|
||||
/**
|
||||
* @author Alex Plekhanov <alex@plekhanov.dev>
|
||||
*/
|
||||
class InvalidContactServerException extends AcmeCoreServerException
|
||||
{
|
||||
public function __construct(RequestInterface $request, string $detail, \Exception $previous = null)
|
||||
{
|
||||
parent::__construct(
|
||||
$request,
|
||||
'[invalidContact] A contact URL for an account was invalid: '.$detail,
|
||||
$previous
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Exception\Server;
|
||||
|
||||
use AcmePhp\Core\Exception\AcmeCoreServerException;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
|
||||
/**
|
||||
* @author Titouan Galopin <galopintitouan@gmail.com>
|
||||
*/
|
||||
class InvalidEmailServerException extends AcmeCoreServerException
|
||||
{
|
||||
public function __construct(RequestInterface $request, string $detail, \Exception $previous = null)
|
||||
{
|
||||
parent::__construct(
|
||||
$request,
|
||||
'[invalidEmail] This email is unacceptable (e.g., it is invalid): '.$detail,
|
||||
$previous
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Exception\Server;
|
||||
|
||||
use AcmePhp\Core\Exception\AcmeCoreServerException;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
|
||||
/**
|
||||
* @author Titouan Galopin <galopintitouan@gmail.com>
|
||||
*/
|
||||
class MalformedServerException extends AcmeCoreServerException
|
||||
{
|
||||
public function __construct(RequestInterface $request, string $detail, \Exception $previous = null)
|
||||
{
|
||||
parent::__construct(
|
||||
$request,
|
||||
'[malformed] The request message was malformed: '.$detail,
|
||||
$previous
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Exception\Server;
|
||||
|
||||
use AcmePhp\Core\Exception\AcmeCoreServerException;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
|
||||
class OrderNotReadyServerException extends AcmeCoreServerException
|
||||
{
|
||||
public function __construct(RequestInterface $request, string $detail, \Exception $previous = null)
|
||||
{
|
||||
parent::__construct(
|
||||
$request,
|
||||
'[orderNotReady] Order could not be finalized: '.$detail,
|
||||
$previous
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Exception\Server;
|
||||
|
||||
use AcmePhp\Core\Exception\AcmeCoreServerException;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
|
||||
/**
|
||||
* @author Titouan Galopin <galopintitouan@gmail.com>
|
||||
*/
|
||||
class RateLimitedServerException extends AcmeCoreServerException
|
||||
{
|
||||
public function __construct(RequestInterface $request, string $detail, \Exception $previous = null)
|
||||
{
|
||||
parent::__construct(
|
||||
$request,
|
||||
'[rateLimited] This client reached the rate limit of the server: '.$detail,
|
||||
$previous
|
||||
);
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Exception\Server;
|
||||
|
||||
use AcmePhp\Core\Exception\AcmeCoreServerException;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
|
||||
/**
|
||||
* @author Alex Plekhanov <alex@plekhanov.dev>
|
||||
*/
|
||||
class RejectedIdentifierServerException extends AcmeCoreServerException
|
||||
{
|
||||
public function __construct(RequestInterface $request, string $detail, \Exception $previous = null)
|
||||
{
|
||||
parent::__construct(
|
||||
$request,
|
||||
'[rejectedIdentifier] The server will not issue certificates for the identifier: '.$detail,
|
||||
$previous
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Exception\Server;
|
||||
|
||||
use AcmePhp\Core\Exception\AcmeCoreServerException;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
|
||||
/**
|
||||
* @author Titouan Galopin <galopintitouan@gmail.com>
|
||||
*/
|
||||
class TlsServerException extends AcmeCoreServerException
|
||||
{
|
||||
public function __construct(RequestInterface $request, string $detail, \Exception $previous = null)
|
||||
{
|
||||
parent::__construct(
|
||||
$request,
|
||||
'[tls] The server experienced a TLS error during DV: '.$detail,
|
||||
$previous
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Exception\Server;
|
||||
|
||||
use AcmePhp\Core\Exception\AcmeCoreServerException;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
|
||||
/**
|
||||
* @author Titouan Galopin <galopintitouan@gmail.com>
|
||||
*/
|
||||
class UnauthorizedServerException extends AcmeCoreServerException
|
||||
{
|
||||
public function __construct(RequestInterface $request, string $detail, \Exception $previous = null)
|
||||
{
|
||||
parent::__construct(
|
||||
$request,
|
||||
'[unauthorized] The client lacks sufficient authorization: '.$detail,
|
||||
$previous
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Exception\Server;
|
||||
|
||||
use AcmePhp\Core\Exception\AcmeCoreServerException;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
|
||||
/**
|
||||
* @author Titouan Galopin <galopintitouan@gmail.com>
|
||||
*/
|
||||
class UnknownHostServerException extends AcmeCoreServerException
|
||||
{
|
||||
public function __construct(RequestInterface $request, string $detail, \Exception $previous = null)
|
||||
{
|
||||
parent::__construct(
|
||||
$request,
|
||||
'[unknownHost] The server could not resolve a domain name: '.$detail,
|
||||
$previous
|
||||
);
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Exception\Server;
|
||||
|
||||
use AcmePhp\Core\Exception\AcmeCoreServerException;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
|
||||
/**
|
||||
* @author Alex Plekhanov <alex@plekhanov.dev>
|
||||
*/
|
||||
class UnsupportedContactServerException extends AcmeCoreServerException
|
||||
{
|
||||
public function __construct(RequestInterface $request, string $detail, \Exception $previous = null)
|
||||
{
|
||||
parent::__construct(
|
||||
$request,
|
||||
'[unsupportedContact] A contact URL for an account used an unsupported protocol scheme: '.$detail,
|
||||
$previous
|
||||
);
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Exception\Server;
|
||||
|
||||
use AcmePhp\Core\Exception\AcmeCoreServerException;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
|
||||
/**
|
||||
* @author Alex Plekhanov <alex@plekhanov.dev>
|
||||
*/
|
||||
class UnsupportedIdentifierServerException extends AcmeCoreServerException
|
||||
{
|
||||
public function __construct(RequestInterface $request, string $detail, \Exception $previous = null)
|
||||
{
|
||||
parent::__construct(
|
||||
$request,
|
||||
'[unsupportedIdentifier] An identifier is of an unsupported type: '.$detail,
|
||||
$previous
|
||||
);
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Exception\Server;
|
||||
|
||||
use AcmePhp\Core\Exception\AcmeCoreServerException;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
|
||||
/**
|
||||
* @author Alex Plekhanov <alex@plekhanov.dev>
|
||||
*/
|
||||
class UserActionRequiredServerException extends AcmeCoreServerException
|
||||
{
|
||||
public function __construct(RequestInterface $request, string $detail, \Exception $previous = null)
|
||||
{
|
||||
parent::__construct(
|
||||
$request,
|
||||
'[userActionRequired] Visit the “instance” URL and take actions specified there: '.$detail,
|
||||
$previous
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Filesystem\Adapter;
|
||||
|
||||
use AcmePhp\Core\Filesystem\FilesystemInterface;
|
||||
use League\Flysystem\FilesystemInterface as FlysystemFilesystemInterface;
|
||||
|
||||
class FlysystemAdapter implements FilesystemInterface
|
||||
{
|
||||
/**
|
||||
* @var FlysystemFilesystemInterface
|
||||
*/
|
||||
private $filesystem;
|
||||
|
||||
public function __construct(FlysystemFilesystemInterface $filesystem)
|
||||
{
|
||||
$this->filesystem = $filesystem;
|
||||
}
|
||||
|
||||
public function write(string $path, string $content)
|
||||
{
|
||||
$isOnRemote = $this->filesystem->has($path);
|
||||
if ($isOnRemote && !$this->filesystem->update($path, $content)) {
|
||||
throw $this->createRuntimeException($path, 'updated');
|
||||
}
|
||||
if (!$isOnRemote && !$this->filesystem->write($path, $content)) {
|
||||
throw $this->createRuntimeException($path, 'created');
|
||||
}
|
||||
}
|
||||
|
||||
public function delete(string $path)
|
||||
{
|
||||
$isOnRemote = $this->filesystem->has($path);
|
||||
if ($isOnRemote && !$this->filesystem->delete($path)) {
|
||||
throw $this->createRuntimeException($path, 'delete');
|
||||
}
|
||||
}
|
||||
|
||||
public function createDir(string $path)
|
||||
{
|
||||
$isOnRemote = $this->filesystem->has($path);
|
||||
if (!$isOnRemote && !$this->filesystem->createDir($path)) {
|
||||
throw $this->createRuntimeException($path, 'created');
|
||||
}
|
||||
}
|
||||
|
||||
private function createRuntimeException(string $path, string $action): \RuntimeException
|
||||
{
|
||||
return new \RuntimeException(
|
||||
sprintf(
|
||||
'File %s could not be %s because: %s',
|
||||
$path,
|
||||
$action,
|
||||
error_get_last()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Filesystem\Adapter;
|
||||
|
||||
use AcmePhp\Core\Filesystem\FilesystemFactoryInterface;
|
||||
use AcmePhp\Core\Filesystem\FilesystemInterface;
|
||||
use League\Flysystem\Adapter\Ftp;
|
||||
use League\Flysystem\Filesystem;
|
||||
|
||||
class FlysystemFtpFactory implements FilesystemFactoryInterface
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function create(array $config): FilesystemInterface
|
||||
{
|
||||
return new FlysystemAdapter(new Filesystem(new Ftp($config)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Filesystem\Adapter;
|
||||
|
||||
use AcmePhp\Core\Filesystem\FilesystemFactoryInterface;
|
||||
use AcmePhp\Core\Filesystem\FilesystemInterface;
|
||||
use League\Flysystem\Adapter\Local;
|
||||
use League\Flysystem\Filesystem;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
class FlysystemLocalFactory implements FilesystemFactoryInterface
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function create(array $config): FilesystemInterface
|
||||
{
|
||||
Assert::keyExists($config, 'root', 'create::$config expected an array with the key %s.');
|
||||
|
||||
return new FlysystemAdapter(new Filesystem(new Local($config['root'])));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Filesystem\Adapter;
|
||||
|
||||
use AcmePhp\Core\Filesystem\FilesystemFactoryInterface;
|
||||
use AcmePhp\Core\Filesystem\FilesystemInterface;
|
||||
use League\Flysystem\Filesystem;
|
||||
use League\Flysystem\Sftp\SftpAdapter;
|
||||
|
||||
class FlysystemSftpFactory implements FilesystemFactoryInterface
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function create(array $config): FilesystemInterface
|
||||
{
|
||||
return new FlysystemAdapter(new Filesystem(new SftpAdapter($config)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Filesystem\Adapter;
|
||||
|
||||
use League\Flysystem\Filesystem;
|
||||
use League\Flysystem\Memory\MemoryAdapter;
|
||||
|
||||
class NullAdapter extends FlysystemAdapter
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(new Filesystem(new MemoryAdapter()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Filesystem;
|
||||
|
||||
interface FilesystemFactoryInterface
|
||||
{
|
||||
/**
|
||||
* Create a new Filesystem.
|
||||
*/
|
||||
public function create(array $config): FilesystemInterface;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Filesystem;
|
||||
|
||||
interface FilesystemInterface
|
||||
{
|
||||
/**
|
||||
* Write content to a file.
|
||||
*/
|
||||
public function write(string $path, string $content);
|
||||
|
||||
/**
|
||||
* Delete a file.
|
||||
*/
|
||||
public function delete(string $path);
|
||||
|
||||
/**
|
||||
* Delete a directory.
|
||||
*/
|
||||
public function createDir(string $path);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Http;
|
||||
|
||||
/**
|
||||
* Encode and decode safely in base64.
|
||||
*
|
||||
* @author Titouan Galopin <galopintitouan@gmail.com>
|
||||
*/
|
||||
class Base64SafeEncoder
|
||||
{
|
||||
public function encode(string $input): string
|
||||
{
|
||||
return str_replace('=', '', strtr(base64_encode($input), '+/', '-_'));
|
||||
}
|
||||
|
||||
public function decode(string $input): string
|
||||
{
|
||||
$remainder = \strlen($input) % 4;
|
||||
|
||||
if ($remainder) {
|
||||
$padlen = 4 - $remainder;
|
||||
$input .= str_repeat('=', $padlen);
|
||||
}
|
||||
|
||||
return base64_decode(strtr($input, '-_', '+/'));
|
||||
}
|
||||
}
|
||||
+421
@@ -0,0 +1,421 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Http;
|
||||
|
||||
use AcmePhp\Core\Exception\AcmeCoreClientException;
|
||||
use AcmePhp\Core\Exception\AcmeCoreServerException;
|
||||
use AcmePhp\Core\Exception\Protocol\ExpectedJsonException;
|
||||
use AcmePhp\Core\Exception\Server\BadNonceServerException;
|
||||
use AcmePhp\Core\Protocol\ExternalAccount;
|
||||
use AcmePhp\Core\Util\JsonDecoder;
|
||||
use AcmePhp\Ssl\KeyPair;
|
||||
use AcmePhp\Ssl\Parser\KeyParser;
|
||||
use AcmePhp\Ssl\Signer\DataSigner;
|
||||
use GuzzleHttp\ClientInterface;
|
||||
use GuzzleHttp\Exception\RequestException;
|
||||
use GuzzleHttp\Psr7\Header;
|
||||
use GuzzleHttp\Psr7\Request;
|
||||
use GuzzleHttp\Psr7\Utils;
|
||||
use Lcobucci\JWT\Signer\Hmac\Sha256;
|
||||
use Lcobucci\JWT\Signer\Key\InMemory;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
/**
|
||||
* Guzzle HTTP client wrapper to send requests signed with the account KeyPair.
|
||||
*
|
||||
* @author Titouan Galopin <galopintitouan@gmail.com>
|
||||
*/
|
||||
class SecureHttpClient
|
||||
{
|
||||
/**
|
||||
* @var KeyPair
|
||||
*/
|
||||
private $accountKeyPair;
|
||||
|
||||
/**
|
||||
* @var ClientInterface
|
||||
*/
|
||||
private $httpClient;
|
||||
|
||||
/**
|
||||
* @var Base64SafeEncoder
|
||||
*/
|
||||
private $base64Encoder;
|
||||
|
||||
/**
|
||||
* @var KeyParser
|
||||
*/
|
||||
private $keyParser;
|
||||
|
||||
/**
|
||||
* @var DataSigner
|
||||
*/
|
||||
private $dataSigner;
|
||||
|
||||
/**
|
||||
* @var ServerErrorHandler
|
||||
*/
|
||||
private $errorHandler;
|
||||
|
||||
/**
|
||||
* @var ResponseInterface
|
||||
*/
|
||||
private $lastResponse;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $nonceEndpoint;
|
||||
|
||||
public function __construct(
|
||||
KeyPair $accountKeyPair,
|
||||
ClientInterface $httpClient,
|
||||
Base64SafeEncoder $base64Encoder,
|
||||
KeyParser $keyParser,
|
||||
DataSigner $dataSigner,
|
||||
ServerErrorHandler $errorHandler
|
||||
) {
|
||||
$this->accountKeyPair = $accountKeyPair;
|
||||
$this->httpClient = $httpClient;
|
||||
$this->base64Encoder = $base64Encoder;
|
||||
$this->keyParser = $keyParser;
|
||||
$this->dataSigner = $dataSigner;
|
||||
$this->errorHandler = $errorHandler;
|
||||
}
|
||||
|
||||
public function getJWK(): array
|
||||
{
|
||||
$privateKey = $this->accountKeyPair->getPrivateKey();
|
||||
$parsedKey = $this->keyParser->parse($privateKey);
|
||||
|
||||
switch ($parsedKey->getType()) {
|
||||
case OPENSSL_KEYTYPE_RSA:
|
||||
return [
|
||||
// this order matters
|
||||
'e' => $this->base64Encoder->encode($parsedKey->getDetail('e')),
|
||||
'kty' => 'RSA',
|
||||
'n' => $this->base64Encoder->encode($parsedKey->getDetail('n')),
|
||||
];
|
||||
|
||||
case OPENSSL_KEYTYPE_EC:
|
||||
return [
|
||||
// this order matters
|
||||
'crv' => 'P-'.$parsedKey->getBits(),
|
||||
'kty' => 'EC',
|
||||
'x' => $this->base64Encoder->encode($parsedKey->getDetail('x')),
|
||||
'y' => $this->base64Encoder->encode($parsedKey->getDetail('y')),
|
||||
];
|
||||
|
||||
default:
|
||||
throw new AcmeCoreClientException('Private key type not supported');
|
||||
}
|
||||
}
|
||||
|
||||
public function getJWKThumbprint(): string
|
||||
{
|
||||
return hash('sha256', json_encode($this->getJWK()), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a payload signed with account's KID.
|
||||
*
|
||||
* @param string|array|null $payload
|
||||
*/
|
||||
public function signKidPayload(string $endpoint, string $account, $payload = null, bool $withNonce = true): array
|
||||
{
|
||||
$protected = ['alg' => $this->getAlg(), 'kid' => $account, 'url' => $endpoint];
|
||||
if ($withNonce) {
|
||||
$protected['nonce'] = $this->getNonce();
|
||||
}
|
||||
|
||||
return $this->signPayload($protected, $payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a payload signed with JWK.
|
||||
*
|
||||
* @param string|array|null $payload
|
||||
*/
|
||||
public function signJwkPayload(string $endpoint, $payload = null, bool $withNonce = true): array
|
||||
{
|
||||
$protected = ['alg' => $this->getAlg(), 'jwk' => $this->getJWK(), 'url' => $endpoint];
|
||||
if ($withNonce) {
|
||||
$protected['nonce'] = $this->getNonce();
|
||||
}
|
||||
|
||||
return $this->signPayload($protected, $payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an External Account Binding payload signed with JWS.
|
||||
*
|
||||
* @param string|array|null $payload
|
||||
*/
|
||||
public function createExternalAccountPayload(ExternalAccount $externalAccount, string $url): array
|
||||
{
|
||||
$signer = new Sha256();
|
||||
|
||||
$protected = [
|
||||
'alg' => method_exists($signer, 'algorithmId') ? $signer->algorithmId() : $signer->getAlgorithmId(),
|
||||
'kid' => $externalAccount->getId(),
|
||||
'url' => $url,
|
||||
];
|
||||
|
||||
$encodedProtected = $this->base64Encoder->encode(json_encode($protected, JSON_UNESCAPED_SLASHES));
|
||||
$encodedPayload = $this->base64Encoder->encode(json_encode($this->getJWK(), JSON_UNESCAPED_SLASHES));
|
||||
|
||||
$hmacKey = $this->base64Encoder->decode($externalAccount->getHmacKey());
|
||||
$hmacKey = class_exists(InMemory::class) ? InMemory::plainText($hmacKey) : $hmacKey;
|
||||
|
||||
$signature = $this->base64Encoder->encode($signer->sign($encodedProtected.'.'.$encodedPayload, $hmacKey));
|
||||
|
||||
return [
|
||||
'protected' => $encodedProtected,
|
||||
'payload' => $encodedPayload,
|
||||
'signature' => $signature,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a request encoded in the format defined by the ACME protocol
|
||||
* and its content (optionally parsed as JSON).
|
||||
*
|
||||
* @throws AcmeCoreClientException when an error occured during response parsing
|
||||
* @throws ExpectedJsonException when $returnJson = true and the response is not valid JSON
|
||||
* @throws AcmeCoreServerException when the ACME server returns an error HTTP status code
|
||||
*
|
||||
* @return array|string Array of parsed JSON if $returnJson = true, string otherwise
|
||||
*/
|
||||
public function request(string $method, string $endpoint, array $data = [], bool $returnJson = true)
|
||||
{
|
||||
$response = $this->rawRequest($method, $endpoint, $data);
|
||||
$body = Utils::copyToString($response->getBody());
|
||||
|
||||
if (!$returnJson) {
|
||||
return $body;
|
||||
}
|
||||
|
||||
try {
|
||||
if ('' === $body) {
|
||||
throw new \InvalidArgumentException('Empty body received.');
|
||||
}
|
||||
|
||||
$data = JsonDecoder::decode($body, true);
|
||||
} catch (\InvalidArgumentException $exception) {
|
||||
throw new ExpectedJsonException(sprintf('ACME client expected valid JSON as a response to request "%s %s" (given: "%s")', $method, $endpoint, ServerErrorHandler::getResponseBodySummary($response)), $exception);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a request encoded in the format defined by the ACME protocol and return the response object.
|
||||
*
|
||||
* @throws AcmeCoreClientException when an error occured during response parsing
|
||||
* @throws ExpectedJsonException when $returnJson = true and the response is not valid JSON
|
||||
* @throws AcmeCoreServerException when the ACME server returns an error HTTP status code
|
||||
*/
|
||||
public function rawRequest(string $method, string $endpoint, array $data = []): ResponseInterface
|
||||
{
|
||||
$call = function () use ($method, $endpoint, $data) {
|
||||
$request = $this->createRequest($method, $endpoint, $data);
|
||||
try {
|
||||
$this->lastResponse = $this->httpClient->send($request);
|
||||
} catch (\Exception $exception) {
|
||||
$this->handleClientException($request, $exception);
|
||||
}
|
||||
|
||||
return $request;
|
||||
};
|
||||
|
||||
try {
|
||||
$call();
|
||||
} catch (BadNonceServerException $e) {
|
||||
$call();
|
||||
}
|
||||
|
||||
return $this->lastResponse;
|
||||
}
|
||||
|
||||
public function setAccountKeyPair(KeyPair $keyPair)
|
||||
{
|
||||
$this->accountKeyPair = $keyPair;
|
||||
}
|
||||
|
||||
public function getLastCode(): int
|
||||
{
|
||||
return $this->lastResponse->getStatusCode();
|
||||
}
|
||||
|
||||
public function getLastLocation(): string
|
||||
{
|
||||
return $this->lastResponse->getHeaderLine('Location');
|
||||
}
|
||||
|
||||
public function getLastLinks(): array
|
||||
{
|
||||
return Header::parse($this->lastResponse->getHeader('Link'));
|
||||
}
|
||||
|
||||
public function getAccountKeyPair(): KeyPair
|
||||
{
|
||||
return $this->accountKeyPair;
|
||||
}
|
||||
|
||||
public function getKeyParser(): KeyParser
|
||||
{
|
||||
return $this->keyParser;
|
||||
}
|
||||
|
||||
public function getDataSigner(): DataSigner
|
||||
{
|
||||
return $this->dataSigner;
|
||||
}
|
||||
|
||||
public function setNonceEndpoint(string $endpoint)
|
||||
{
|
||||
$this->nonceEndpoint = $endpoint;
|
||||
}
|
||||
|
||||
public function getBase64Encoder(): Base64SafeEncoder
|
||||
{
|
||||
return $this->base64Encoder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign the given Payload.
|
||||
*/
|
||||
private function signPayload(array $protected, array $payload = null): array
|
||||
{
|
||||
if (!isset($protected['alg'])) {
|
||||
throw new \InvalidArgumentException('The property "alg" is required in the protected array');
|
||||
}
|
||||
|
||||
$alg = $protected['alg'];
|
||||
|
||||
$privateKey = $this->accountKeyPair->getPrivateKey();
|
||||
list($algorithm, $format) = $this->extractSignOptionFromJWSAlg($alg);
|
||||
|
||||
$encodedProtected = $this->base64Encoder->encode(json_encode($protected, JSON_UNESCAPED_SLASHES));
|
||||
|
||||
if (null === $payload) {
|
||||
$encodedPayload = '';
|
||||
} elseif ([] === $payload) {
|
||||
$encodedPayload = $this->base64Encoder->encode('{}');
|
||||
} else {
|
||||
$encodedPayload = $this->base64Encoder->encode(json_encode($payload, JSON_UNESCAPED_SLASHES));
|
||||
}
|
||||
|
||||
$signature = $this->base64Encoder->encode(
|
||||
$this->dataSigner->signData($encodedProtected.'.'.$encodedPayload, $privateKey, $algorithm, $format)
|
||||
);
|
||||
|
||||
return [
|
||||
'protected' => $encodedProtected,
|
||||
'payload' => $encodedPayload,
|
||||
'signature' => $signature,
|
||||
];
|
||||
}
|
||||
|
||||
private function createRequest($method, $endpoint, $data)
|
||||
{
|
||||
$request = new Request($method, $endpoint);
|
||||
$request = $request->withHeader('Accept', 'application/json,application/jose+json,');
|
||||
|
||||
if ('POST' === $method && \is_array($data)) {
|
||||
$request = $request->withHeader('Content-Type', 'application/jose+json');
|
||||
$request = $request->withBody(Utils::streamFor(json_encode($data)));
|
||||
}
|
||||
|
||||
return $request;
|
||||
}
|
||||
|
||||
private function handleClientException(Request $request, \Exception $exception)
|
||||
{
|
||||
if ($exception instanceof RequestException && $exception->getResponse() instanceof ResponseInterface) {
|
||||
$this->lastResponse = $exception->getResponse();
|
||||
|
||||
throw $this->errorHandler->createAcmeExceptionForResponse($request, $this->lastResponse, $exception);
|
||||
}
|
||||
|
||||
throw new AcmeCoreClientException(sprintf('An error occured during request "%s %s"', $request->getMethod(), $request->getUri()), $exception);
|
||||
}
|
||||
|
||||
private function getNonce(): ?string
|
||||
{
|
||||
if ($this->lastResponse && $this->lastResponse->hasHeader('Replay-Nonce')) {
|
||||
return $this->lastResponse->getHeaderLine('Replay-Nonce');
|
||||
}
|
||||
|
||||
if (null !== $this->nonceEndpoint) {
|
||||
$this->request('HEAD', $this->nonceEndpoint, [], false);
|
||||
|
||||
if ($this->lastResponse->hasHeader('Replay-Nonce')) {
|
||||
return $this->lastResponse->getHeaderLine('Replay-Nonce');
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function getAlg(): string
|
||||
{
|
||||
$privateKey = $this->accountKeyPair->getPrivateKey();
|
||||
$parsedKey = $this->keyParser->parse($privateKey);
|
||||
|
||||
switch ($parsedKey->getType()) {
|
||||
case OPENSSL_KEYTYPE_RSA:
|
||||
return 'RS256';
|
||||
|
||||
case OPENSSL_KEYTYPE_EC:
|
||||
switch ($parsedKey->getBits()) {
|
||||
case 256:
|
||||
case 384:
|
||||
return 'ES'.$parsedKey->getBits();
|
||||
case 521:
|
||||
return 'ES512';
|
||||
}
|
||||
|
||||
// no break to let the default case
|
||||
default:
|
||||
throw new AcmeCoreClientException('Private key type is not supported');
|
||||
}
|
||||
}
|
||||
|
||||
private function extractSignOptionFromJWSAlg($alg): array
|
||||
{
|
||||
if (!preg_match('/^([A-Z]+)(\d+)$/', $alg, $match)) {
|
||||
throw new AcmeCoreClientException(sprintf('The given "%s" algorithm is not supported', $alg));
|
||||
}
|
||||
|
||||
if (!\defined('OPENSSL_ALGO_SHA'.$match[2])) {
|
||||
throw new AcmeCoreClientException(sprintf('The given "%s" algorithm is not supported', $alg));
|
||||
}
|
||||
|
||||
$algorithm = \constant('OPENSSL_ALGO_SHA'.$match[2]);
|
||||
|
||||
switch ($match[1]) {
|
||||
case 'RS':
|
||||
$format = DataSigner::FORMAT_DER;
|
||||
break;
|
||||
|
||||
case 'ES':
|
||||
$format = DataSigner::FORMAT_ECDSA;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new AcmeCoreClientException(sprintf('The given "%s" algorithm is not supported', $alg));
|
||||
}
|
||||
|
||||
return [$algorithm, $format];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Http;
|
||||
|
||||
use AcmePhp\Ssl\KeyPair;
|
||||
use AcmePhp\Ssl\Parser\KeyParser;
|
||||
use AcmePhp\Ssl\Signer\DataSigner;
|
||||
use GuzzleHttp\ClientInterface;
|
||||
|
||||
/**
|
||||
* Guzzle HTTP client wrapper to send requests signed with the account KeyPair.
|
||||
*
|
||||
* @author Titouan Galopin <galopintitouan@gmail.com>
|
||||
*/
|
||||
class SecureHttpClientFactory
|
||||
{
|
||||
/**
|
||||
* @var ClientInterface
|
||||
*/
|
||||
private $httpClient;
|
||||
|
||||
/**
|
||||
* @var Base64SafeEncoder
|
||||
*/
|
||||
private $base64Encoder;
|
||||
|
||||
/**
|
||||
* @var KeyParser
|
||||
*/
|
||||
private $keyParser;
|
||||
|
||||
/**
|
||||
* @var DataSigner
|
||||
*/
|
||||
private $dataSigner;
|
||||
|
||||
/**
|
||||
* @var ServerErrorHandler
|
||||
*/
|
||||
private $errorHandler;
|
||||
|
||||
public function __construct(
|
||||
ClientInterface $httpClient,
|
||||
Base64SafeEncoder $base64Encoder,
|
||||
KeyParser $keyParser,
|
||||
DataSigner $dataSigner,
|
||||
ServerErrorHandler $errorHandler
|
||||
) {
|
||||
$this->httpClient = $httpClient;
|
||||
$this->base64Encoder = $base64Encoder;
|
||||
$this->keyParser = $keyParser;
|
||||
$this->dataSigner = $dataSigner;
|
||||
$this->errorHandler = $errorHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a SecureHttpClient using a given account KeyPair.
|
||||
*/
|
||||
public function createSecureHttpClient(KeyPair $accountKeyPair): SecureHttpClient
|
||||
{
|
||||
return new SecureHttpClient(
|
||||
$accountKeyPair,
|
||||
$this->httpClient,
|
||||
$this->base64Encoder,
|
||||
$this->keyParser,
|
||||
$this->dataSigner,
|
||||
$this->errorHandler
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Http;
|
||||
|
||||
use AcmePhp\Core\Exception\AcmeCoreServerException;
|
||||
use AcmePhp\Core\Exception\Server\BadCsrServerException;
|
||||
use AcmePhp\Core\Exception\Server\BadNonceServerException;
|
||||
use AcmePhp\Core\Exception\Server\CaaServerException;
|
||||
use AcmePhp\Core\Exception\Server\ConnectionServerException;
|
||||
use AcmePhp\Core\Exception\Server\DnsServerException;
|
||||
use AcmePhp\Core\Exception\Server\IncorrectResponseServerException;
|
||||
use AcmePhp\Core\Exception\Server\InternalServerException;
|
||||
use AcmePhp\Core\Exception\Server\InvalidContactServerException;
|
||||
use AcmePhp\Core\Exception\Server\InvalidEmailServerException;
|
||||
use AcmePhp\Core\Exception\Server\MalformedServerException;
|
||||
use AcmePhp\Core\Exception\Server\OrderNotReadyServerException;
|
||||
use AcmePhp\Core\Exception\Server\RateLimitedServerException;
|
||||
use AcmePhp\Core\Exception\Server\RejectedIdentifierServerException;
|
||||
use AcmePhp\Core\Exception\Server\TlsServerException;
|
||||
use AcmePhp\Core\Exception\Server\UnauthorizedServerException;
|
||||
use AcmePhp\Core\Exception\Server\UnknownHostServerException;
|
||||
use AcmePhp\Core\Exception\Server\UnsupportedContactServerException;
|
||||
use AcmePhp\Core\Exception\Server\UnsupportedIdentifierServerException;
|
||||
use AcmePhp\Core\Exception\Server\UserActionRequiredServerException;
|
||||
use AcmePhp\Core\Util\JsonDecoder;
|
||||
use GuzzleHttp\Exception\RequestException;
|
||||
use GuzzleHttp\Psr7\Utils;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
/**
|
||||
* Create appropriate exception for given server response.
|
||||
*
|
||||
* @author Titouan Galopin <galopintitouan@gmail.com>
|
||||
*/
|
||||
class ServerErrorHandler
|
||||
{
|
||||
private static $exceptions = [
|
||||
'badCSR' => BadCsrServerException::class,
|
||||
'badNonce' => BadNonceServerException::class,
|
||||
'caa' => CaaServerException::class,
|
||||
'connection' => ConnectionServerException::class,
|
||||
'dns' => DnsServerException::class,
|
||||
'incorrectResponse' => IncorrectResponseServerException::class,
|
||||
'invalidContact' => InvalidContactServerException::class,
|
||||
'invalidEmail' => InvalidEmailServerException::class,
|
||||
'malformed' => MalformedServerException::class,
|
||||
'orderNotReady' => OrderNotReadyServerException::class,
|
||||
'rateLimited' => RateLimitedServerException::class,
|
||||
'rejectedIdentifier' => RejectedIdentifierServerException::class,
|
||||
'serverInternal' => InternalServerException::class,
|
||||
'tls' => TlsServerException::class,
|
||||
'unauthorized' => UnauthorizedServerException::class,
|
||||
'unknownHost' => UnknownHostServerException::class,
|
||||
'unsupportedContact' => UnsupportedContactServerException::class,
|
||||
'unsupportedIdentifier' => UnsupportedIdentifierServerException::class,
|
||||
'userActionRequired' => UserActionRequiredServerException::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* Get a response summary (useful for exceptions).
|
||||
* Use Guzzle method if available (Guzzle 6.1.1+).
|
||||
*/
|
||||
public static function getResponseBodySummary(ResponseInterface $response): string
|
||||
{
|
||||
// Rewind the stream if possible to allow re-reading for the summary.
|
||||
if ($response->getBody()->isSeekable()) {
|
||||
$response->getBody()->rewind();
|
||||
}
|
||||
|
||||
if (method_exists(RequestException::class, 'getResponseBodySummary')) {
|
||||
return RequestException::getResponseBodySummary($response);
|
||||
}
|
||||
|
||||
$body = Utils::copyToString($response->getBody());
|
||||
|
||||
if (\strlen($body) > 120) {
|
||||
return substr($body, 0, 120).' (truncated...)';
|
||||
}
|
||||
|
||||
return $body;
|
||||
}
|
||||
|
||||
public function createAcmeExceptionForResponse(
|
||||
RequestInterface $request,
|
||||
ResponseInterface $response,
|
||||
\Exception $previous = null
|
||||
): AcmeCoreServerException {
|
||||
$body = Utils::copyToString($response->getBody());
|
||||
|
||||
try {
|
||||
$data = JsonDecoder::decode($body, true);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
$data = null;
|
||||
}
|
||||
|
||||
if (!$data || !isset($data['type'], $data['detail'])) {
|
||||
// Not JSON: not an ACME error response
|
||||
return $this->createDefaultExceptionForResponse($request, $response, $previous);
|
||||
}
|
||||
|
||||
$type = preg_replace('/^urn:(ietf:params:)?acme:error:/i', '', $data['type']);
|
||||
|
||||
if (!isset(self::$exceptions[$type])) {
|
||||
// Unknown type: not an ACME error response
|
||||
return $this->createDefaultExceptionForResponse($request, $response, $previous);
|
||||
}
|
||||
|
||||
$exceptionClass = self::$exceptions[$type];
|
||||
|
||||
return new $exceptionClass(
|
||||
$request,
|
||||
sprintf('%s (on request "%s %s")', $data['detail'], $request->getMethod(), $request->getUri()),
|
||||
$previous
|
||||
);
|
||||
}
|
||||
|
||||
private function createDefaultExceptionForResponse(
|
||||
RequestInterface $request,
|
||||
ResponseInterface $response,
|
||||
\Exception $previous = null
|
||||
): AcmeCoreServerException {
|
||||
return new AcmeCoreServerException(
|
||||
$request,
|
||||
sprintf(
|
||||
'A non-ACME %s HTTP error occured on request "%s %s" (response body: "%s")',
|
||||
$response->getStatusCode(),
|
||||
$request->getMethod(),
|
||||
$request->getUri(),
|
||||
self::getResponseBodySummary($response)
|
||||
),
|
||||
$previous
|
||||
);
|
||||
}
|
||||
}
|
||||
Vendored
+22
@@ -0,0 +1,22 @@
|
||||
Copyright (c) 2016 Titouan Galopin
|
||||
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated documentation
|
||||
files (the "Software"), to deal in the Software without
|
||||
restriction, including without limitation the rights to use,
|
||||
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Protocol;
|
||||
|
||||
/**
|
||||
* Represent a ACME challenge.
|
||||
*
|
||||
* @author Titouan Galopin <galopintitouan@gmail.com>
|
||||
*/
|
||||
class AuthorizationChallenge
|
||||
{
|
||||
/** @var string */
|
||||
private $domain;
|
||||
|
||||
/** @var string */
|
||||
private $status;
|
||||
|
||||
/** @var string */
|
||||
private $type;
|
||||
|
||||
/** @var string */
|
||||
private $url;
|
||||
|
||||
/** @var string */
|
||||
private $token;
|
||||
|
||||
/** @var string */
|
||||
private $payload;
|
||||
|
||||
public function __construct(string $domain, string $status, string $type, string $url, string $token, string $payload)
|
||||
{
|
||||
$this->domain = $domain;
|
||||
$this->status = $status;
|
||||
$this->type = $type;
|
||||
$this->url = $url;
|
||||
$this->token = $token;
|
||||
$this->payload = $payload;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'domain' => $this->getDomain(),
|
||||
'status' => $this->getStatus(),
|
||||
'type' => $this->getType(),
|
||||
'url' => $this->getUrl(),
|
||||
'token' => $this->getToken(),
|
||||
'payload' => $this->getPayload(),
|
||||
];
|
||||
}
|
||||
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
return new self(
|
||||
$data['domain'],
|
||||
$data['status'],
|
||||
$data['type'],
|
||||
$data['url'],
|
||||
$data['token'],
|
||||
$data['payload']
|
||||
);
|
||||
}
|
||||
|
||||
public function getDomain(): string
|
||||
{
|
||||
return $this->domain;
|
||||
}
|
||||
|
||||
public function getStatus(): string
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
public function isValid(): bool
|
||||
{
|
||||
return 'valid' === $this->status;
|
||||
}
|
||||
|
||||
public function isPending(): bool
|
||||
{
|
||||
return 'pending' === $this->status || 'processing' === $this->status;
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function getUrl(): string
|
||||
{
|
||||
return $this->url;
|
||||
}
|
||||
|
||||
public function getToken(): string
|
||||
{
|
||||
return $this->token;
|
||||
}
|
||||
|
||||
public function getPayload(): string
|
||||
{
|
||||
return $this->payload;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Protocol;
|
||||
|
||||
use AcmePhp\Core\Exception\AcmeCoreClientException;
|
||||
|
||||
/**
|
||||
* Represent an ACME order.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class CertificateOrder
|
||||
{
|
||||
/** @var AuthorizationChallenge[][] */
|
||||
private $authorizationsChallenges;
|
||||
|
||||
/** @var string */
|
||||
private $orderEndpoint;
|
||||
|
||||
/** @var string */
|
||||
private $status;
|
||||
|
||||
public function __construct(array $authorizationsChallenges, string $orderEndpoint = null, string $status = null)
|
||||
{
|
||||
foreach ($authorizationsChallenges as &$authorizationChallenges) {
|
||||
foreach ($authorizationChallenges as &$authorizationChallenge) {
|
||||
if (\is_array($authorizationChallenge)) {
|
||||
$authorizationChallenge = AuthorizationChallenge::fromArray($authorizationChallenge);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->authorizationsChallenges = $authorizationsChallenges;
|
||||
$this->orderEndpoint = $orderEndpoint;
|
||||
$this->status = $status;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
$authorizationsChallenges = array_map(
|
||||
function ($challenges): array {
|
||||
return array_map(
|
||||
function ($challenge): array {
|
||||
return $challenge->toArray();
|
||||
},
|
||||
$challenges
|
||||
);
|
||||
},
|
||||
$this->getAuthorizationsChallenges()
|
||||
);
|
||||
|
||||
return [
|
||||
'authorizationsChallenges' => $authorizationsChallenges,
|
||||
'orderEndpoint' => $this->getOrderEndpoint(),
|
||||
'status' => $this->getStatus(),
|
||||
];
|
||||
}
|
||||
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
return new self($data['authorizationsChallenges'], $data['orderEndpoint']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return AuthorizationChallenge[][]
|
||||
*/
|
||||
public function getAuthorizationsChallenges()
|
||||
{
|
||||
return $this->authorizationsChallenges;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return AuthorizationChallenge[]
|
||||
*/
|
||||
public function getAuthorizationChallenges(string $domain): array
|
||||
{
|
||||
if (!isset($this->authorizationsChallenges[$domain])) {
|
||||
throw new AcmeCoreClientException('The order does not contains any authorization challenge for the domain '.$domain);
|
||||
}
|
||||
|
||||
return $this->authorizationsChallenges[$domain];
|
||||
}
|
||||
|
||||
public function getOrderEndpoint(): string
|
||||
{
|
||||
return $this->orderEndpoint;
|
||||
}
|
||||
|
||||
public function getStatus(): string
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Protocol;
|
||||
|
||||
/**
|
||||
* Represent an ACME External Account to be used for External Account Binding.
|
||||
*
|
||||
* @author Titouan Galopin <galopintitouan@gmail.com>
|
||||
*/
|
||||
class ExternalAccount
|
||||
{
|
||||
/** @var string */
|
||||
private $id;
|
||||
|
||||
/** @var string */
|
||||
private $hmacKey;
|
||||
|
||||
public function __construct(string $id, string $hmacKey)
|
||||
{
|
||||
$this->id = $id;
|
||||
$this->hmacKey = $hmacKey;
|
||||
}
|
||||
|
||||
public function getId(): string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getHmacKey(): string
|
||||
{
|
||||
return $this->hmacKey;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Protocol;
|
||||
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* Represent a ACME resources directory.
|
||||
*
|
||||
* @author Titouan Galopin <galopintitouan@gmail.com>
|
||||
*/
|
||||
class ResourcesDirectory
|
||||
{
|
||||
public const NEW_ACCOUNT = 'newAccount';
|
||||
public const NEW_ORDER = 'newOrder';
|
||||
public const NEW_NONCE = 'newNonce';
|
||||
public const REVOKE_CERT = 'revokeCert';
|
||||
|
||||
/** @var array */
|
||||
private $serverResources;
|
||||
|
||||
public function __construct(array $serverResources)
|
||||
{
|
||||
$this->serverResources = $serverResources;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public static function getResourcesNames(): array
|
||||
{
|
||||
return [
|
||||
self::NEW_ACCOUNT,
|
||||
self::NEW_ORDER,
|
||||
self::NEW_NONCE,
|
||||
self::REVOKE_CERT,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a resource URL.
|
||||
*/
|
||||
public function getResourceUrl(string $resource): string
|
||||
{
|
||||
Assert::oneOf(
|
||||
$resource,
|
||||
array_keys($this->serverResources),
|
||||
'Resource type "%s" is not supported by the ACME server (supported: %2$s)'
|
||||
);
|
||||
|
||||
return $this->serverResources[$resource];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Protocol;
|
||||
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* @url https://github.com/certbot/certbot/blob/c326c021082dede7c3b2bd411cec3aec6dff0ac5/certbot/constants.py#L124
|
||||
*/
|
||||
class RevocationReason
|
||||
{
|
||||
public const DEFAULT_REASON = self::REASON_UNSPECIFIED;
|
||||
public const REASON_UNSPECIFIED = 0;
|
||||
public const REASON_KEY_COMPROMISE = 1;
|
||||
public const REASON_AFFILLIATION_CHANGED = 3;
|
||||
public const REASON_SUPERCEDED = 4;
|
||||
public const REASON_CESSATION_OF_OPERATION = 5;
|
||||
|
||||
/** @var int|null */
|
||||
private $reasonType;
|
||||
|
||||
public function __construct(int $reasonType)
|
||||
{
|
||||
Assert::oneOf($reasonType, self::getReasons(), 'Revocation reason type "%s" is not supported by the ACME server (supported: %2$s)');
|
||||
|
||||
$this->reasonType = $reasonType;
|
||||
}
|
||||
|
||||
public function getReasonType(): int
|
||||
{
|
||||
return $this->reasonType;
|
||||
}
|
||||
|
||||
public static function createDefaultReason(): self
|
||||
{
|
||||
return new static(self::DEFAULT_REASON);
|
||||
}
|
||||
|
||||
public static function getFormattedReasons(): array
|
||||
{
|
||||
$formatted = [];
|
||||
foreach (self::getReasonLabelMap() as $reason => $label) {
|
||||
$formatted[] = $reason.' - '.$label;
|
||||
}
|
||||
|
||||
return $formatted;
|
||||
}
|
||||
|
||||
private static function getReasonLabelMap(): array
|
||||
{
|
||||
return [
|
||||
self::REASON_UNSPECIFIED => 'unspecified',
|
||||
self::REASON_KEY_COMPROMISE => 'key compromise',
|
||||
self::REASON_AFFILLIATION_CHANGED => 'affiliation changed',
|
||||
self::REASON_SUPERCEDED => 'superceded',
|
||||
self::REASON_CESSATION_OF_OPERATION => 'cessation of operation',
|
||||
];
|
||||
}
|
||||
|
||||
public static function getReasons(): array
|
||||
{
|
||||
return [
|
||||
self::REASON_UNSPECIFIED,
|
||||
self::REASON_KEY_COMPROMISE,
|
||||
self::REASON_AFFILLIATION_CHANGED,
|
||||
self::REASON_SUPERCEDED,
|
||||
self::REASON_CESSATION_OF_OPERATION,
|
||||
];
|
||||
}
|
||||
}
|
||||
Vendored
+35
@@ -0,0 +1,35 @@
|
||||
Acme PHP Core library
|
||||
=====================
|
||||
|
||||
[](https://travis-ci.org/acmephp/acmephp)
|
||||
[](https://scrutinizer-ci.com/g/acmephp/acmephp)
|
||||
[](https://styleci.io/repos/59910490)
|
||||
[](https://packagist.org/packages/acmephp/acmephp)
|
||||
[](LICENSE)
|
||||
|
||||
Acme PHP Core is the core of the Acme PHP project : it is a basis for the others more
|
||||
high-level repositories. It consists of a raw implementation of the Let's Encrypt ACME protocol.
|
||||
|
||||
> If you want to chat with us or have questions, ping
|
||||
> @tgalopin or @jderusse on the [Symfony Slack](https://symfony.com/support)!
|
||||
|
||||
## When use Acme PHP Core?
|
||||
|
||||
You usually will want to use either [the Acme PHP CLI client](https://github.com/acmephp/cli)
|
||||
or [an implementation for your application framework](https://github.com/acmephp).
|
||||
|
||||
However, in some cases, you may want to manage SSL certificates directly from your application.
|
||||
In these cases, this library will be useful to you.
|
||||
|
||||
Acme PHP Core does nothing more than implementing the [Let's Encrypt/ACME protocol](https://github.com/letsencrypt/acme-spec) :
|
||||
the generated SSL keys and certificates are stored in memory and returned to your script. You are the one in charge
|
||||
of storing them somewhere persistent.
|
||||
|
||||
## Documentation
|
||||
|
||||
Read the official [Acme PHP documentation](https://acmephp.github.io).
|
||||
|
||||
## Launch the Test suite
|
||||
|
||||
The Acme PHP test suite is located in the main repository:
|
||||
[https://github.com/acmephp/acmephp#launch-the-test-suite](https://github.com/acmephp/acmephp#launch-the-test-suite).
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Util;
|
||||
|
||||
/**
|
||||
* Guzzle HTTP client wrapper to send requests signed with the account KeyPair.
|
||||
*
|
||||
* @author Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class JsonDecoder
|
||||
{
|
||||
/**
|
||||
* Wrapper for json_decode that throws when an error occurs.
|
||||
* Extracted from Guzzle for BC.
|
||||
*
|
||||
* @param string $json JSON data to parse
|
||||
* @param bool $assoc when true, returned objects will be converted
|
||||
* into associative arrays
|
||||
* @param int $depth user specified recursion depth
|
||||
* @param int $options bitmask of JSON decode options
|
||||
*
|
||||
* @throws \InvalidArgumentException if the JSON cannot be decoded
|
||||
*
|
||||
* @return mixed
|
||||
*
|
||||
* @see http://www.php.net/manual/en/function.json-decode.php
|
||||
*/
|
||||
public static function decode(string $json, bool $assoc = false, int $depth = 512, int $options = 0)
|
||||
{
|
||||
$data = json_decode($json, $assoc, $depth, $options);
|
||||
|
||||
if (JSON_ERROR_NONE !== json_last_error()) {
|
||||
throw new \InvalidArgumentException('json_decode error: '.json_last_error_msg());
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"name": "acmephp/core",
|
||||
"description": "Raw implementation of the ACME protocol in PHP",
|
||||
"type": "library",
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/acmephp/core",
|
||||
"keywords": [
|
||||
"acmephp",
|
||||
"letsencrypt",
|
||||
"https",
|
||||
"encryption",
|
||||
"certificate",
|
||||
"ssl",
|
||||
"openssl",
|
||||
"CSR",
|
||||
"x509"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Titouan Galopin",
|
||||
"email": "galopintitouan@gmail.com",
|
||||
"homepage": "http://titouangalopin.com"
|
||||
},
|
||||
{
|
||||
"name": "Jérémy Derussé",
|
||||
"homepage": "https://twitter.com/jderusse"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=7.2.5",
|
||||
"ext-hash": "*",
|
||||
"ext-json": "*",
|
||||
"ext-openssl": "*",
|
||||
"acmephp/ssl": "^2.0",
|
||||
"guzzlehttp/guzzle": "^6.0|^7.0",
|
||||
"guzzlehttp/psr7": "^1.7|^2.1",
|
||||
"lcobucci/jwt": "^3.3|^4.0",
|
||||
"psr/http-message": "^1.0",
|
||||
"psr/log": "^1.0|^2.0|^3.0",
|
||||
"webmozart/assert": "^1.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"AcmePhp\\Core\\": "."
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"sort-packages": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
composer.lock
|
||||
vendor/
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Ssl;
|
||||
|
||||
use AcmePhp\Ssl\Exception\CertificateFormatException;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* Represent a Certificate.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class Certificate
|
||||
{
|
||||
/** @var string */
|
||||
private $certificatePEM;
|
||||
|
||||
/** @var Certificate */
|
||||
private $issuerCertificate;
|
||||
|
||||
public function __construct(string $certificatePEM, self $issuerCertificate = null)
|
||||
{
|
||||
Assert::stringNotEmpty($certificatePEM, __CLASS__.'::$certificatePEM should not be an empty string. Got %s');
|
||||
|
||||
$this->certificatePEM = $certificatePEM;
|
||||
$this->issuerCertificate = $issuerCertificate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Certificate[]
|
||||
*/
|
||||
public function getIssuerChain(): array
|
||||
{
|
||||
$chain = [];
|
||||
$issuerCertificate = $this->getIssuerCertificate();
|
||||
|
||||
while (null !== $issuerCertificate) {
|
||||
$chain[] = $issuerCertificate;
|
||||
$issuerCertificate = $issuerCertificate->getIssuerCertificate();
|
||||
}
|
||||
|
||||
return $chain;
|
||||
}
|
||||
|
||||
public function getPEM(): string
|
||||
{
|
||||
return $this->certificatePEM;
|
||||
}
|
||||
|
||||
public function getIssuerCertificate(): ?self
|
||||
{
|
||||
return $this->issuerCertificate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return resource
|
||||
*/
|
||||
public function getPublicKeyResource()
|
||||
{
|
||||
if (!$resource = openssl_pkey_get_public($this->certificatePEM)) {
|
||||
throw new CertificateFormatException(sprintf('Failed to convert certificate into public key resource: %s', openssl_error_string()));
|
||||
}
|
||||
|
||||
return $resource;
|
||||
}
|
||||
|
||||
public function getPublicKey(): PublicKey
|
||||
{
|
||||
return new PublicKey(openssl_pkey_get_details($this->getPublicKeyResource())['key']);
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Ssl;
|
||||
|
||||
/**
|
||||
* Contains data required to request a certificate.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class CertificateRequest
|
||||
{
|
||||
/** @var DistinguishedName */
|
||||
private $distinguishedName;
|
||||
|
||||
/** @var KeyPair */
|
||||
private $keyPair;
|
||||
|
||||
public function __construct(DistinguishedName $distinguishedName, KeyPair $keyPair)
|
||||
{
|
||||
$this->distinguishedName = $distinguishedName;
|
||||
$this->keyPair = $keyPair;
|
||||
}
|
||||
|
||||
public function getDistinguishedName(): DistinguishedName
|
||||
{
|
||||
return $this->distinguishedName;
|
||||
}
|
||||
|
||||
public function getKeyPair(): KeyPair
|
||||
{
|
||||
return $this->keyPair;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Ssl;
|
||||
|
||||
/**
|
||||
* Represent the response to a certificate request.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class CertificateResponse
|
||||
{
|
||||
/** @var CertificateRequest */
|
||||
private $certificateRequest;
|
||||
|
||||
/** @var Certificate */
|
||||
private $certificate;
|
||||
|
||||
public function __construct(CertificateRequest $certificateRequest, Certificate $certificate)
|
||||
{
|
||||
$this->certificateRequest = $certificateRequest;
|
||||
$this->certificate = $certificate;
|
||||
}
|
||||
|
||||
public function getCertificateRequest(): CertificateRequest
|
||||
{
|
||||
return $this->certificateRequest;
|
||||
}
|
||||
|
||||
public function getCertificate(): Certificate
|
||||
{
|
||||
return $this->certificate;
|
||||
}
|
||||
}
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Ssl;
|
||||
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* Represent a Distinguished Name.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class DistinguishedName
|
||||
{
|
||||
/** @var string */
|
||||
private $commonName;
|
||||
|
||||
/** @var string */
|
||||
private $countryName;
|
||||
|
||||
/** @var string */
|
||||
private $stateOrProvinceName;
|
||||
|
||||
/** @var string */
|
||||
private $localityName;
|
||||
|
||||
/** @var string */
|
||||
private $organizationName;
|
||||
|
||||
/** @var string */
|
||||
private $organizationalUnitName;
|
||||
|
||||
/** @var string */
|
||||
private $emailAddress;
|
||||
|
||||
/** @var array */
|
||||
private $subjectAlternativeNames;
|
||||
|
||||
public function __construct(
|
||||
string $commonName,
|
||||
string $countryName = null,
|
||||
string $stateOrProvinceName = null,
|
||||
string $localityName = null,
|
||||
string $organizationName = null,
|
||||
string $organizationalUnitName = null,
|
||||
string $emailAddress = null,
|
||||
array $subjectAlternativeNames = []
|
||||
) {
|
||||
Assert::stringNotEmpty($commonName, __CLASS__.'::$commonName expected a non empty string. Got: %s');
|
||||
Assert::allStringNotEmpty(
|
||||
$subjectAlternativeNames,
|
||||
__CLASS__.'::$subjectAlternativeNames expected an array of non empty string. Got: %s'
|
||||
);
|
||||
|
||||
$this->commonName = $commonName;
|
||||
$this->countryName = $countryName;
|
||||
$this->stateOrProvinceName = $stateOrProvinceName;
|
||||
$this->localityName = $localityName;
|
||||
$this->organizationName = $organizationName;
|
||||
$this->organizationalUnitName = $organizationalUnitName;
|
||||
$this->emailAddress = $emailAddress;
|
||||
$this->subjectAlternativeNames = array_diff(array_unique($subjectAlternativeNames), [$commonName]);
|
||||
}
|
||||
|
||||
public function getCommonName(): string
|
||||
{
|
||||
return $this->commonName;
|
||||
}
|
||||
|
||||
public function getCountryName(): ?string
|
||||
{
|
||||
return $this->countryName;
|
||||
}
|
||||
|
||||
public function getStateOrProvinceName(): ?string
|
||||
{
|
||||
return $this->stateOrProvinceName;
|
||||
}
|
||||
|
||||
public function getLocalityName(): ?string
|
||||
{
|
||||
return $this->localityName;
|
||||
}
|
||||
|
||||
public function getOrganizationName(): ?string
|
||||
{
|
||||
return $this->organizationName;
|
||||
}
|
||||
|
||||
public function getOrganizationalUnitName(): ?string
|
||||
{
|
||||
return $this->organizationalUnitName;
|
||||
}
|
||||
|
||||
public function getEmailAddress(): ?string
|
||||
{
|
||||
return $this->emailAddress;
|
||||
}
|
||||
|
||||
public function getSubjectAlternativeNames(): array
|
||||
{
|
||||
return $this->subjectAlternativeNames;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Ssl\Exception;
|
||||
|
||||
/**
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class AcmeSslException extends \RuntimeException
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Ssl\Exception;
|
||||
|
||||
/**
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class CSRSigningException extends SigningException
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Ssl\Exception;
|
||||
|
||||
/**
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class CertificateFormatException extends ParsingException
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Ssl\Exception;
|
||||
|
||||
/**
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class CertificateParsingException extends ParsingException
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Ssl\Exception;
|
||||
|
||||
/**
|
||||
* @author Titouan Galopin <galopintitouan@gmail.com>
|
||||
*/
|
||||
class DataSigningException extends SigningException
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Ssl\Exception;
|
||||
|
||||
/**
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class KeyFormatException extends ParsingException
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Ssl\Exception;
|
||||
|
||||
/**
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class KeyGenerationException extends AcmeSslException
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Ssl\Exception;
|
||||
|
||||
/**
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class KeyPairGenerationException extends KeyGenerationException
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Ssl\Exception;
|
||||
|
||||
/**
|
||||
* @author Titouan Galopin <galopintitouan@gmail.com>
|
||||
*/
|
||||
class KeyParsingException extends ParsingException
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Ssl\Exception;
|
||||
|
||||
/**
|
||||
* @author Titouan Galopin <galopintitouan@gmail.com>
|
||||
*/
|
||||
class ParsingException extends AcmeSslException
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Ssl\Exception;
|
||||
|
||||
/**
|
||||
* @author Titouan Galopin <galopintitouan@gmail.com>
|
||||
*/
|
||||
class SigningException extends AcmeSslException
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Ssl\Generator;
|
||||
|
||||
use AcmePhp\Ssl\PrivateKey;
|
||||
|
||||
/**
|
||||
* Generate random RSA private key using OpenSSL.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class ChainPrivateKeyGenerator implements PrivateKeyGeneratorInterface
|
||||
{
|
||||
/** @var PrivateKeyGeneratorInterface[] */
|
||||
private $generators;
|
||||
|
||||
/**
|
||||
* @param PrivateKeyGeneratorInterface[] $generators
|
||||
*/
|
||||
public function __construct(iterable $generators)
|
||||
{
|
||||
$this->generators = $generators;
|
||||
}
|
||||
|
||||
public function generatePrivateKey(KeyOption $keyOption): PrivateKey
|
||||
{
|
||||
foreach ($this->generators as $generator) {
|
||||
if ($generator->supportsKeyOption($keyOption)) {
|
||||
return $generator->generatePrivateKey($keyOption);
|
||||
}
|
||||
}
|
||||
|
||||
throw new \LogicException(sprintf('Unable to find a generator for a key option of type %s', \get_class($keyOption)));
|
||||
}
|
||||
|
||||
public function supportsKeyOption(KeyOption $keyOption): bool
|
||||
{
|
||||
foreach ($this->generators as $generator) {
|
||||
if ($generator->supportsKeyOption($keyOption)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Ssl\Generator\DhKey;
|
||||
|
||||
use AcmePhp\Ssl\Generator\KeyOption;
|
||||
use AcmePhp\Ssl\Generator\OpensslPrivateKeyGeneratorTrait;
|
||||
use AcmePhp\Ssl\Generator\PrivateKeyGeneratorInterface;
|
||||
use AcmePhp\Ssl\PrivateKey;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* Generate random DH private key using OpenSSL.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class DhKeyGenerator implements PrivateKeyGeneratorInterface
|
||||
{
|
||||
use OpensslPrivateKeyGeneratorTrait;
|
||||
|
||||
public function generatePrivateKey(KeyOption $keyOption): PrivateKey
|
||||
{
|
||||
Assert::isInstanceOf($keyOption, DhKeyOption::class);
|
||||
|
||||
return $this->generatePrivateKeyFromOpensslOptions([
|
||||
'private_key_type' => OPENSSL_KEYTYPE_DH,
|
||||
'dh' => [
|
||||
'p' => $keyOption->getPrime(),
|
||||
'g' => $keyOption->getGenerator(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function supportsKeyOption(KeyOption $keyOption): bool
|
||||
{
|
||||
return $keyOption instanceof DhKeyOption;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Ssl\Generator\DhKey;
|
||||
|
||||
use AcmePhp\Ssl\Generator\KeyOption;
|
||||
|
||||
class DhKeyOption implements KeyOption
|
||||
{
|
||||
/** @var string */
|
||||
private $generator;
|
||||
|
||||
/** @var string */
|
||||
private $prime;
|
||||
|
||||
/**
|
||||
* @param string $prime Hexadecimal representation of the prime
|
||||
* @param string $generator Hexadecimal representation of the generator: ie. 02
|
||||
*
|
||||
* @see https://tools.ietf.org/html/rfc3526 how to choose a prime and generator numbers
|
||||
*/
|
||||
public function __construct(string $prime, string $generator = '02')
|
||||
{
|
||||
$this->generator = pack('H*', $generator);
|
||||
$this->prime = pack('H*', $prime);
|
||||
}
|
||||
|
||||
public function getGenerator(): string
|
||||
{
|
||||
return $this->generator;
|
||||
}
|
||||
|
||||
public function getPrime(): string
|
||||
{
|
||||
return $this->prime;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Ssl\Generator\DsaKey;
|
||||
|
||||
use AcmePhp\Ssl\Generator\KeyOption;
|
||||
use AcmePhp\Ssl\Generator\OpensslPrivateKeyGeneratorTrait;
|
||||
use AcmePhp\Ssl\Generator\PrivateKeyGeneratorInterface;
|
||||
use AcmePhp\Ssl\PrivateKey;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* Generate random DSA private key using OpenSSL.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class DsaKeyGenerator implements PrivateKeyGeneratorInterface
|
||||
{
|
||||
use OpensslPrivateKeyGeneratorTrait;
|
||||
|
||||
public function generatePrivateKey(KeyOption $keyOption): PrivateKey
|
||||
{
|
||||
Assert::isInstanceOf($keyOption, DsaKeyOption::class);
|
||||
|
||||
return $this->generatePrivateKeyFromOpensslOptions([
|
||||
'private_key_type' => OPENSSL_KEYTYPE_DSA,
|
||||
'private_key_bits' => $keyOption->getBits(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function supportsKeyOption(KeyOption $keyOption): bool
|
||||
{
|
||||
return $keyOption instanceof DsaKeyOption;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Ssl\Generator\DsaKey;
|
||||
|
||||
use AcmePhp\Ssl\Generator\KeyOption;
|
||||
|
||||
class DsaKeyOption implements KeyOption
|
||||
{
|
||||
/** @var int */
|
||||
private $bits;
|
||||
|
||||
public function __construct(int $bits = 2048)
|
||||
{
|
||||
$this->bits = $bits;
|
||||
}
|
||||
|
||||
public function getBits(): int
|
||||
{
|
||||
return $this->bits;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Ssl\Generator\EcKey;
|
||||
|
||||
use AcmePhp\Ssl\Generator\KeyOption;
|
||||
use AcmePhp\Ssl\Generator\OpensslPrivateKeyGeneratorTrait;
|
||||
use AcmePhp\Ssl\Generator\PrivateKeyGeneratorInterface;
|
||||
use AcmePhp\Ssl\PrivateKey;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* Generate random EC private key using OpenSSL.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class EcKeyGenerator implements PrivateKeyGeneratorInterface
|
||||
{
|
||||
use OpensslPrivateKeyGeneratorTrait;
|
||||
|
||||
public function generatePrivateKey(KeyOption $keyOption): PrivateKey
|
||||
{
|
||||
Assert::isInstanceOf($keyOption, EcKeyOption::class);
|
||||
|
||||
return $this->generatePrivateKeyFromOpensslOptions([
|
||||
'private_key_type' => OPENSSL_KEYTYPE_EC,
|
||||
'curve_name' => $keyOption->getCurveName(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function supportsKeyOption(KeyOption $keyOption): bool
|
||||
{
|
||||
return $keyOption instanceof EcKeyOption;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Ssl\Generator\EcKey;
|
||||
|
||||
use AcmePhp\Ssl\Generator\KeyOption;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
class EcKeyOption implements KeyOption
|
||||
{
|
||||
/** @var string */
|
||||
private $curveName;
|
||||
|
||||
public function __construct(string $curveName = 'secp384r1')
|
||||
{
|
||||
Assert::oneOf($curveName, openssl_get_curve_names(), 'The given curve %s is not supported. Available curves are: %s');
|
||||
|
||||
$this->curveName = $curveName;
|
||||
}
|
||||
|
||||
public function getCurveName(): string
|
||||
{
|
||||
return $this->curveName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Ssl\Generator;
|
||||
|
||||
interface KeyOption
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Ssl\Generator;
|
||||
|
||||
use AcmePhp\Ssl\Exception\KeyGenerationException;
|
||||
use AcmePhp\Ssl\Exception\KeyPairGenerationException;
|
||||
use AcmePhp\Ssl\Generator\DhKey\DhKeyGenerator;
|
||||
use AcmePhp\Ssl\Generator\DsaKey\DsaKeyGenerator;
|
||||
use AcmePhp\Ssl\Generator\EcKey\EcKeyGenerator;
|
||||
use AcmePhp\Ssl\Generator\RsaKey\RsaKeyGenerator;
|
||||
use AcmePhp\Ssl\Generator\RsaKey\RsaKeyOption;
|
||||
use AcmePhp\Ssl\KeyPair;
|
||||
|
||||
/**
|
||||
* Generate random KeyPair using OpenSSL.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class KeyPairGenerator
|
||||
{
|
||||
private $generator;
|
||||
|
||||
public function __construct(PrivateKeyGeneratorInterface $generator = null)
|
||||
{
|
||||
$this->generator = $generator ?: new ChainPrivateKeyGenerator(
|
||||
[
|
||||
new RsaKeyGenerator(),
|
||||
new EcKeyGenerator(),
|
||||
new DhKeyGenerator(),
|
||||
new DsaKeyGenerator(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param KeyOption|null $keyOption configuration of the key to generate
|
||||
*
|
||||
* @throws KeyPairGenerationException when OpenSSL failed to generate keys
|
||||
*/
|
||||
public function generateKeyPair(KeyOption $keyOption = null): KeyPair
|
||||
{
|
||||
if (null === $keyOption) {
|
||||
$keyOption = new RsaKeyOption();
|
||||
}
|
||||
|
||||
try {
|
||||
$privateKey = $this->generator->generatePrivateKey($keyOption);
|
||||
} catch (KeyGenerationException $e) {
|
||||
throw new KeyPairGenerationException('Fail to generate a KeyPair with the given options', 0, $e);
|
||||
}
|
||||
|
||||
return new KeyPair(
|
||||
$privateKey->getPublicKey(),
|
||||
$privateKey
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Ssl\Generator;
|
||||
|
||||
use AcmePhp\Ssl\Exception\KeyGenerationException;
|
||||
use AcmePhp\Ssl\Exception\KeyPairGenerationException;
|
||||
use AcmePhp\Ssl\PrivateKey;
|
||||
|
||||
trait OpensslPrivateKeyGeneratorTrait
|
||||
{
|
||||
private function generatePrivateKeyFromOpensslOptions(array $opensslOptions): PrivateKey
|
||||
{
|
||||
$resource = openssl_pkey_new($opensslOptions);
|
||||
|
||||
if (!$resource) {
|
||||
throw new KeyGenerationException(sprintf('OpenSSL key creation failed during generation with error: %s', openssl_error_string()));
|
||||
}
|
||||
if (!openssl_pkey_export($resource, $privateKey)) {
|
||||
throw new KeyPairGenerationException(sprintf('OpenSSL key export failed during generation with error: %s', openssl_error_string()));
|
||||
}
|
||||
|
||||
// PHP 8 automatically frees the key instance and deprecates the function
|
||||
if (\PHP_VERSION_ID < 80000) {
|
||||
openssl_free_key($resource);
|
||||
}
|
||||
|
||||
return new PrivateKey($privateKey);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user