131 lines
4.2 KiB
PHP
131 lines
4.2 KiB
PHP
<?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\Signer;
|
|
|
|
use AcmePhp\Ssl\CertificateRequest;
|
|
use AcmePhp\Ssl\DistinguishedName;
|
|
use AcmePhp\Ssl\Exception\CSRSigningException;
|
|
|
|
/**
|
|
* Provide tools to sign certificate request.
|
|
*
|
|
* @author Jérémy Derussé <jeremy@derusse.com>
|
|
*/
|
|
class CertificateRequestSigner
|
|
{
|
|
/**
|
|
* Generate a CSR from the given distinguishedName and keyPair.
|
|
*/
|
|
public function signCertificateRequest(CertificateRequest $certificateRequest): string
|
|
{
|
|
$csrObject = $this->createCsrWithSANsObject($certificateRequest);
|
|
|
|
if (!$csrObject || !openssl_csr_export($csrObject, $csrExport)) {
|
|
throw new CSRSigningException(sprintf('OpenSSL CSR signing failed with error: %s', openssl_error_string()));
|
|
}
|
|
|
|
return $csrExport;
|
|
}
|
|
|
|
/**
|
|
* Generate a CSR object with SANs from the given distinguishedName and keyPair.
|
|
*/
|
|
protected function createCsrWithSANsObject(CertificateRequest $certificateRequest)
|
|
{
|
|
$sslConfigTemplate = <<<'EOL'
|
|
[ req ]
|
|
distinguished_name = req_distinguished_name
|
|
req_extensions = v3_req
|
|
[ req_distinguished_name ]
|
|
[ v3_req ]
|
|
basicConstraints = CA:FALSE
|
|
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
|
|
subjectAltName = @req_subject_alt_name
|
|
[ req_subject_alt_name ]
|
|
%s
|
|
EOL;
|
|
$sslConfigDomains = [];
|
|
|
|
$distinguishedName = $certificateRequest->getDistinguishedName();
|
|
$domains = array_merge(
|
|
[$distinguishedName->getCommonName()],
|
|
$distinguishedName->getSubjectAlternativeNames()
|
|
);
|
|
|
|
foreach (array_values($domains) as $index => $domain) {
|
|
$sslConfigDomains[] = 'DNS.'.($index + 1).' = '.$domain;
|
|
}
|
|
|
|
$sslConfigContent = sprintf($sslConfigTemplate, implode("\n", $sslConfigDomains));
|
|
$sslConfigFile = tempnam(sys_get_temp_dir(), 'acmephp_');
|
|
|
|
try {
|
|
file_put_contents($sslConfigFile, $sslConfigContent);
|
|
|
|
$resource = $certificateRequest->getKeyPair()->getPrivateKey()->getResource();
|
|
|
|
$csr = openssl_csr_new(
|
|
$this->getCSRPayload($distinguishedName),
|
|
$resource,
|
|
[
|
|
'digest_alg' => 'sha256',
|
|
'config' => $sslConfigFile,
|
|
]
|
|
);
|
|
|
|
// PHP 8 automatically frees the key instance and deprecates the function
|
|
if (\PHP_VERSION_ID < 80000) {
|
|
openssl_free_key($resource);
|
|
}
|
|
|
|
if (!$csr) {
|
|
throw new CSRSigningException(sprintf('OpenSSL CSR signing failed with error: %s', openssl_error_string()));
|
|
}
|
|
|
|
return $csr;
|
|
} finally {
|
|
unlink($sslConfigFile);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Retrieves a CSR payload from the given distinguished name.
|
|
*/
|
|
private function getCSRPayload(DistinguishedName $distinguishedName): array
|
|
{
|
|
$payload = [];
|
|
if (null !== $countryName = $distinguishedName->getCountryName()) {
|
|
$payload['countryName'] = $countryName;
|
|
}
|
|
if (null !== $stateOrProvinceName = $distinguishedName->getStateOrProvinceName()) {
|
|
$payload['stateOrProvinceName'] = $stateOrProvinceName;
|
|
}
|
|
if (null !== $localityName = $distinguishedName->getLocalityName()) {
|
|
$payload['localityName'] = $localityName;
|
|
}
|
|
if (null !== $OrganizationName = $distinguishedName->getOrganizationName()) {
|
|
$payload['organizationName'] = $OrganizationName;
|
|
}
|
|
if (null !== $organizationUnitName = $distinguishedName->getOrganizationalUnitName()) {
|
|
$payload['organizationalUnitName'] = $organizationUnitName;
|
|
}
|
|
if (null !== $commonName = $distinguishedName->getCommonName()) {
|
|
$payload['commonName'] = $commonName;
|
|
}
|
|
if (null !== $emailAddress = $distinguishedName->getEmailAddress()) {
|
|
$payload['emailAddress'] = $emailAddress;
|
|
}
|
|
|
|
return $payload;
|
|
}
|
|
}
|