save
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
<?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\Exception\DataSigningException;
|
||||
use AcmePhp\Ssl\PrivateKey;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* Provide tools to sign data using a private key.
|
||||
*
|
||||
* @author Titouan Galopin <galopintitouan@gmail.com>
|
||||
*/
|
||||
class DataSigner
|
||||
{
|
||||
public const FORMAT_DER = 'DER';
|
||||
public const FORMAT_ECDSA = 'ECDSA';
|
||||
|
||||
/**
|
||||
* Generate a signature of the given data using a private key and an algorithm.
|
||||
*
|
||||
* @param string $data Data to sign
|
||||
* @param PrivateKey $privateKey Key used to sign
|
||||
* @param int $algorithm Signature algorithm defined by constants OPENSSL_ALGO_*
|
||||
* @param string $format Format of the output
|
||||
*/
|
||||
public function signData(string $data, PrivateKey $privateKey, int $algorithm = OPENSSL_ALGO_SHA256, string $format = self::FORMAT_DER): string
|
||||
{
|
||||
Assert::oneOf($format, [self::FORMAT_ECDSA, self::FORMAT_DER], 'The format %s to sign request does not exists. Available format: %s');
|
||||
|
||||
$resource = $privateKey->getResource();
|
||||
if (!openssl_sign($data, $signature, $resource, $algorithm)) {
|
||||
throw new DataSigningException(sprintf('OpenSSL data signing failed 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);
|
||||
}
|
||||
|
||||
switch ($format) {
|
||||
case self::FORMAT_DER:
|
||||
return $signature;
|
||||
case self::FORMAT_ECDSA:
|
||||
switch ($algorithm) {
|
||||
case OPENSSL_ALGO_SHA256:
|
||||
return $this->DERtoECDSA($signature, 64);
|
||||
case OPENSSL_ALGO_SHA384:
|
||||
return $this->DERtoECDSA($signature, 96);
|
||||
case OPENSSL_ALGO_SHA512:
|
||||
return $this->DERtoECDSA($signature, 132);
|
||||
}
|
||||
throw new DataSigningException('Unable to generate a ECDSA signature with the given algorithm');
|
||||
default:
|
||||
throw new DataSigningException('The given format does exists');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a DER signature into ECDSA.
|
||||
*
|
||||
* The code is a copy/paste from another lib (web-token/jwt-core) which is not compatible with php <= 7.0
|
||||
*
|
||||
* @see https://github.com/web-token/jwt-core/blob/master/Util/ECSignature.php
|
||||
*/
|
||||
private function DERtoECDSA($der, $partLength): string
|
||||
{
|
||||
$hex = unpack('H*', $der)[1];
|
||||
if ('30' !== mb_substr($hex, 0, 2, '8bit')) { // SEQUENCE
|
||||
throw new DataSigningException('Invalid signature provided');
|
||||
}
|
||||
if ('81' === mb_substr($hex, 2, 2, '8bit')) { // LENGTH > 128
|
||||
$hex = mb_substr($hex, 6, null, '8bit');
|
||||
} else {
|
||||
$hex = mb_substr($hex, 4, null, '8bit');
|
||||
}
|
||||
if ('02' !== mb_substr($hex, 0, 2, '8bit')) { // INTEGER
|
||||
throw new DataSigningException('Invalid signature provided');
|
||||
}
|
||||
|
||||
$Rl = hexdec(mb_substr($hex, 2, 2, '8bit'));
|
||||
$R = $this->retrievePositiveInteger(mb_substr($hex, 4, $Rl * 2, '8bit'));
|
||||
$R = str_pad($R, $partLength, '0', STR_PAD_LEFT);
|
||||
|
||||
$hex = mb_substr($hex, 4 + $Rl * 2, null, '8bit');
|
||||
if ('02' !== mb_substr($hex, 0, 2, '8bit')) { // INTEGER
|
||||
throw new DataSigningException('Invalid signature provided');
|
||||
}
|
||||
$Sl = hexdec(mb_substr($hex, 2, 2, '8bit'));
|
||||
$S = $this->retrievePositiveInteger(mb_substr($hex, 4, $Sl * 2, '8bit'));
|
||||
$S = str_pad($S, $partLength, '0', STR_PAD_LEFT);
|
||||
|
||||
return pack('H*', $R.$S);
|
||||
}
|
||||
|
||||
/**
|
||||
* The code is a copy/paste from another lib (web-token/jwt-core) which is not compatible with php <= 7.0.
|
||||
*
|
||||
* @see https://github.com/web-token/jwt-core/blob/master/Util/ECSignature.php
|
||||
*/
|
||||
private function retrievePositiveInteger($data)
|
||||
{
|
||||
while ('00' === mb_substr($data, 0, 2, '8bit') && mb_substr($data, 2, 2, '8bit') > '7f') {
|
||||
$data = mb_substr($data, 2, null, '8bit');
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user