save
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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\Ssl\Generator;
|
||||
|
||||
use AcmePhp\Ssl\Exception\KeyGenerationException;
|
||||
use AcmePhp\Ssl\PrivateKey;
|
||||
|
||||
/**
|
||||
* Generate random private key.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
interface PrivateKeyGeneratorInterface
|
||||
{
|
||||
/**
|
||||
* Generate a PrivateKey.
|
||||
*
|
||||
* @param KeyOption $keyOption configuration of the key to generate
|
||||
*
|
||||
* @throws KeyGenerationException when OpenSSL failed to generate keys
|
||||
*/
|
||||
public function generatePrivateKey(KeyOption $keyOption): PrivateKey;
|
||||
|
||||
/**
|
||||
* Returns whether the instance is able to generator a private key from the given option.
|
||||
*
|
||||
* @param KeyOption $keyOption configuration of the key to generate
|
||||
*/
|
||||
public function supportsKeyOption(KeyOption $keyOption): bool;
|
||||
}
|
||||
@@ -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\RsaKey;
|
||||
|
||||
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 RSA private key using OpenSSL.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class RsaKeyGenerator implements PrivateKeyGeneratorInterface
|
||||
{
|
||||
use OpensslPrivateKeyGeneratorTrait;
|
||||
|
||||
public function generatePrivateKey(KeyOption $keyOption): PrivateKey
|
||||
{
|
||||
Assert::isInstanceOf($keyOption, RsaKeyOption::class);
|
||||
|
||||
return $this->generatePrivateKeyFromOpensslOptions([
|
||||
'private_key_type' => OPENSSL_KEYTYPE_RSA,
|
||||
'private_key_bits' => $keyOption->getBits(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function supportsKeyOption(KeyOption $keyOption): bool
|
||||
{
|
||||
return $keyOption instanceof RsaKeyOption;
|
||||
}
|
||||
}
|
||||
@@ -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\RsaKey;
|
||||
|
||||
use AcmePhp\Ssl\Generator\KeyOption;
|
||||
|
||||
class RsaKeyOption implements KeyOption
|
||||
{
|
||||
/** @var int */
|
||||
private $bits;
|
||||
|
||||
public function __construct(int $bits = 4096)
|
||||
{
|
||||
$this->bits = $bits;
|
||||
}
|
||||
|
||||
public function getBits(): int
|
||||
{
|
||||
return $this->bits;
|
||||
}
|
||||
}
|
||||
Vendored
+53
@@ -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\Ssl;
|
||||
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* Represent a SSL key.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
abstract class Key
|
||||
{
|
||||
/** @var string */
|
||||
protected $keyPEM;
|
||||
|
||||
public function __construct(string $keyPEM)
|
||||
{
|
||||
Assert::stringNotEmpty($keyPEM, __CLASS__.'::$keyPEM should not be an empty string. Got %s');
|
||||
|
||||
$this->keyPEM = $keyPEM;
|
||||
}
|
||||
|
||||
public function getPEM(): string
|
||||
{
|
||||
return $this->keyPEM;
|
||||
}
|
||||
|
||||
public function getDER(): string
|
||||
{
|
||||
$lines = explode("\n", trim($this->keyPEM));
|
||||
unset($lines[\count($lines) - 1]);
|
||||
unset($lines[0]);
|
||||
$result = implode('', $lines);
|
||||
$result = base64_decode($result);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return resource|\OpenSSLAsymmetricKey
|
||||
*/
|
||||
abstract public function getResource();
|
||||
}
|
||||
Vendored
+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;
|
||||
|
||||
/**
|
||||
* Represent a SSL key-pair (public and private).
|
||||
*
|
||||
* @author Titouan Galopin <galopintitouan@gmail.com>
|
||||
*/
|
||||
class KeyPair
|
||||
{
|
||||
/** @var PublicKey */
|
||||
private $publicKey;
|
||||
|
||||
/** @var PrivateKey */
|
||||
private $privateKey;
|
||||
|
||||
public function __construct(PublicKey $publicKey, PrivateKey $privateKey)
|
||||
{
|
||||
$this->publicKey = $publicKey;
|
||||
$this->privateKey = $privateKey;
|
||||
}
|
||||
|
||||
public function getPublicKey(): PublicKey
|
||||
{
|
||||
return $this->publicKey;
|
||||
}
|
||||
|
||||
public function getPrivateKey(): PrivateKey
|
||||
{
|
||||
return $this->privateKey;
|
||||
}
|
||||
}
|
||||
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.
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
<?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 the content of a parsed certificate.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class ParsedCertificate
|
||||
{
|
||||
/** @var Certificate */
|
||||
private $source;
|
||||
|
||||
/** @var string */
|
||||
private $subject;
|
||||
|
||||
/** @var string */
|
||||
private $issuer;
|
||||
|
||||
/** @var bool */
|
||||
private $selfSigned;
|
||||
|
||||
/** @var \DateTime */
|
||||
private $validFrom;
|
||||
|
||||
/** @var \DateTime */
|
||||
private $validTo;
|
||||
|
||||
/** @var string */
|
||||
private $serialNumber;
|
||||
|
||||
/** @var array */
|
||||
private $subjectAlternativeNames;
|
||||
|
||||
/**
|
||||
* @param string $issuer
|
||||
* @param \DateTime $validFrom
|
||||
* @param \DateTime $validTo
|
||||
* @param string $serialNumber
|
||||
*/
|
||||
public function __construct(
|
||||
Certificate $source,
|
||||
string $subject,
|
||||
string $issuer = null,
|
||||
bool $selfSigned = true,
|
||||
\DateTime $validFrom = null,
|
||||
\DateTime $validTo = null,
|
||||
string $serialNumber = null,
|
||||
array $subjectAlternativeNames = []
|
||||
) {
|
||||
Assert::stringNotEmpty($subject, __CLASS__.'::$subject expected a non empty string. Got: %s');
|
||||
Assert::allStringNotEmpty(
|
||||
$subjectAlternativeNames,
|
||||
__CLASS__.'::$subjectAlternativeNames expected a array of non empty string. Got: %s'
|
||||
);
|
||||
|
||||
$this->source = $source;
|
||||
$this->subject = $subject;
|
||||
$this->issuer = $issuer;
|
||||
$this->selfSigned = $selfSigned;
|
||||
$this->validFrom = $validFrom;
|
||||
$this->validTo = $validTo;
|
||||
$this->serialNumber = $serialNumber;
|
||||
$this->subjectAlternativeNames = $subjectAlternativeNames;
|
||||
}
|
||||
|
||||
public function getSource(): Certificate
|
||||
{
|
||||
return $this->source;
|
||||
}
|
||||
|
||||
public function getSubject(): string
|
||||
{
|
||||
return $this->subject;
|
||||
}
|
||||
|
||||
public function getIssuer(): ?string
|
||||
{
|
||||
return $this->issuer;
|
||||
}
|
||||
|
||||
public function isSelfSigned(): bool
|
||||
{
|
||||
return $this->selfSigned;
|
||||
}
|
||||
|
||||
public function getValidFrom(): \DateTimeInterface
|
||||
{
|
||||
return $this->validFrom;
|
||||
}
|
||||
|
||||
public function getValidTo(): \DateTimeInterface
|
||||
{
|
||||
return $this->validTo;
|
||||
}
|
||||
|
||||
public function isExpired(): bool
|
||||
{
|
||||
return $this->validTo < (new \DateTime());
|
||||
}
|
||||
|
||||
public function getSerialNumber(): ?string
|
||||
{
|
||||
return $this->serialNumber;
|
||||
}
|
||||
|
||||
public function getSubjectAlternativeNames(): array
|
||||
{
|
||||
return $this->subjectAlternativeNames;
|
||||
}
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
<?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 the content of a parsed key.
|
||||
*
|
||||
* @see openssl_pkey_get_details
|
||||
*
|
||||
* @author Titouan Galopin <galopintitouan@gmail.com>
|
||||
*/
|
||||
class ParsedKey
|
||||
{
|
||||
/** @var Key */
|
||||
private $source;
|
||||
|
||||
/** @var string */
|
||||
private $key;
|
||||
|
||||
/** @var int */
|
||||
private $bits;
|
||||
|
||||
/** @var int */
|
||||
private $type;
|
||||
|
||||
/** @var array */
|
||||
private $details;
|
||||
|
||||
public function __construct(Key $source, string $key, int $bits, int $type, array $details = [])
|
||||
{
|
||||
Assert::stringNotEmpty($key, __CLASS__.'::$key expected a non empty string. Got: %s');
|
||||
Assert::oneOf(
|
||||
$type,
|
||||
[OPENSSL_KEYTYPE_RSA, OPENSSL_KEYTYPE_DSA, OPENSSL_KEYTYPE_DH, OPENSSL_KEYTYPE_EC],
|
||||
__CLASS__.'::$type expected one of: %2$s. Got: %s'
|
||||
);
|
||||
|
||||
$this->source = $source;
|
||||
$this->key = $key;
|
||||
$this->bits = $bits;
|
||||
$this->type = $type;
|
||||
$this->details = $details;
|
||||
}
|
||||
|
||||
public function getSource(): Key
|
||||
{
|
||||
return $this->source;
|
||||
}
|
||||
|
||||
public function getKey(): string
|
||||
{
|
||||
return $this->key;
|
||||
}
|
||||
|
||||
public function getBits(): int
|
||||
{
|
||||
return $this->bits;
|
||||
}
|
||||
|
||||
public function getType(): int
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function getDetails(): array
|
||||
{
|
||||
return $this->details;
|
||||
}
|
||||
|
||||
public function hasDetail(string $name): bool
|
||||
{
|
||||
return isset($this->details[$name]);
|
||||
}
|
||||
|
||||
public function getDetail(string $name)
|
||||
{
|
||||
Assert::oneOf($name, array_keys($this->details), 'ParsedKey::getDetail() expected one of: %2$s. Got: %s');
|
||||
|
||||
return $this->details[$name];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?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\Parser;
|
||||
|
||||
use AcmePhp\Ssl\Certificate;
|
||||
use AcmePhp\Ssl\Exception\CertificateParsingException;
|
||||
use AcmePhp\Ssl\ParsedCertificate;
|
||||
|
||||
/**
|
||||
* Parse certificate to extract metadata.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class CertificateParser
|
||||
{
|
||||
public function parse(Certificate $certificate): ParsedCertificate
|
||||
{
|
||||
$rawData = openssl_x509_parse($certificate->getPEM());
|
||||
|
||||
if (!\is_array($rawData)) {
|
||||
throw new CertificateParsingException(sprintf('Fail to parse certificate with error: %s', openssl_error_string()));
|
||||
}
|
||||
|
||||
if (!isset($rawData['subject']['CN'])) {
|
||||
throw new CertificateParsingException('Missing expected key "subject.cn" in certificate');
|
||||
}
|
||||
if (!isset($rawData['serialNumber'])) {
|
||||
throw new CertificateParsingException('Missing expected key "serialNumber" in certificate');
|
||||
}
|
||||
if (!isset($rawData['validFrom_time_t'])) {
|
||||
throw new CertificateParsingException('Missing expected key "validFrom_time_t" in certificate');
|
||||
}
|
||||
if (!isset($rawData['validTo_time_t'])) {
|
||||
throw new CertificateParsingException('Missing expected key "validTo_time_t" in certificate');
|
||||
}
|
||||
|
||||
$subjectAlternativeName = [];
|
||||
|
||||
if (isset($rawData['extensions']['subjectAltName'])) {
|
||||
$subjectAlternativeName = array_map(
|
||||
function ($item) {
|
||||
return explode(':', trim($item), 2)[1];
|
||||
},
|
||||
array_filter(
|
||||
explode(
|
||||
',',
|
||||
$rawData['extensions']['subjectAltName']
|
||||
),
|
||||
function ($item) {
|
||||
return false !== strpos($item, ':');
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return new ParsedCertificate(
|
||||
$certificate,
|
||||
$rawData['subject']['CN'],
|
||||
isset($rawData['issuer']['CN']) ? $rawData['issuer']['CN'] : null,
|
||||
$rawData['subject'] === $rawData['issuer'],
|
||||
new \DateTime('@'.$rawData['validFrom_time_t']),
|
||||
new \DateTime('@'.$rawData['validTo_time_t']),
|
||||
$rawData['serialNumber'],
|
||||
$subjectAlternativeName
|
||||
);
|
||||
}
|
||||
}
|
||||
+65
@@ -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\Ssl\Parser;
|
||||
|
||||
use AcmePhp\Ssl\Exception\KeyFormatException;
|
||||
use AcmePhp\Ssl\Exception\KeyParsingException;
|
||||
use AcmePhp\Ssl\Key;
|
||||
use AcmePhp\Ssl\ParsedKey;
|
||||
|
||||
/**
|
||||
* Parse keys to extract metadata.
|
||||
*
|
||||
* @author Titouan Galopin
|
||||
*/
|
||||
class KeyParser
|
||||
{
|
||||
public function parse(Key $key): ParsedKey
|
||||
{
|
||||
try {
|
||||
$resource = $key->getResource();
|
||||
} catch (KeyFormatException $e) {
|
||||
throw new KeyParsingException('Fail to load resource for key', 0, $e);
|
||||
}
|
||||
|
||||
$rawData = openssl_pkey_get_details($resource);
|
||||
|
||||
// PHP 8 automatically frees the key instance and deprecates the function
|
||||
if (\PHP_VERSION_ID < 80000) {
|
||||
openssl_free_key($resource);
|
||||
}
|
||||
|
||||
if (!\is_array($rawData)) {
|
||||
throw new KeyParsingException(sprintf('Fail to parse key with error: %s', openssl_error_string()));
|
||||
}
|
||||
|
||||
foreach (['type', 'key', 'bits'] as $requiredKey) {
|
||||
if (!isset($rawData[$requiredKey])) {
|
||||
throw new KeyParsingException(sprintf('Missing expected key "%s" in OpenSSL key', $requiredKey));
|
||||
}
|
||||
}
|
||||
|
||||
$details = [];
|
||||
|
||||
if (OPENSSL_KEYTYPE_RSA === $rawData['type']) {
|
||||
$details = $rawData['rsa'];
|
||||
} elseif (OPENSSL_KEYTYPE_DSA === $rawData['type']) {
|
||||
$details = $rawData['dsa'];
|
||||
} elseif (OPENSSL_KEYTYPE_DH === $rawData['type']) {
|
||||
$details = $rawData['dh'];
|
||||
} elseif (OPENSSL_KEYTYPE_EC === $rawData['type']) {
|
||||
$details = $rawData['ec'];
|
||||
}
|
||||
|
||||
return new ParsedKey($key, $rawData['key'], $rawData['bits'], $rawData['type'], $details);
|
||||
}
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
<?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\KeyFormatException;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* Represent a SSL Private key.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class PrivateKey extends Key
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getResource()
|
||||
{
|
||||
if (!$resource = openssl_pkey_get_private($this->keyPEM)) {
|
||||
throw new KeyFormatException(sprintf('Failed to convert key into resource: %s', openssl_error_string()));
|
||||
}
|
||||
|
||||
return $resource;
|
||||
}
|
||||
|
||||
public function getPublicKey(): PublicKey
|
||||
{
|
||||
$resource = $this->getResource();
|
||||
if (!$details = openssl_pkey_get_details($resource)) {
|
||||
throw new KeyFormatException(sprintf('Failed to extract public key: %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 PublicKey($details['key']);
|
||||
}
|
||||
|
||||
public static function fromDER(string $keyDER): self
|
||||
{
|
||||
Assert::stringNotEmpty($keyDER, __METHOD__.'::$keyDER should be a non-empty string. Got %s');
|
||||
|
||||
$der = base64_encode($keyDER);
|
||||
$lines = str_split($der, 65);
|
||||
array_unshift($lines, '-----BEGIN PRIVATE KEY-----');
|
||||
$lines[] = '-----END PRIVATE KEY-----';
|
||||
$lines[] = '';
|
||||
|
||||
return new self(implode("\n", $lines));
|
||||
}
|
||||
}
|
||||
+53
@@ -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\Ssl;
|
||||
|
||||
use AcmePhp\Ssl\Exception\KeyFormatException;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* Represent a SSL Public key.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class PublicKey extends Key
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getResource()
|
||||
{
|
||||
if (!$resource = openssl_pkey_get_public($this->keyPEM)) {
|
||||
throw new KeyFormatException(sprintf('Failed to convert key into resource: %s', openssl_error_string()));
|
||||
}
|
||||
|
||||
return $resource;
|
||||
}
|
||||
|
||||
public static function fromDER(string $keyDER): self
|
||||
{
|
||||
Assert::stringNotEmpty($keyDER, __METHOD__.'::$keyDER should be a non-empty string. Got %s');
|
||||
|
||||
$der = base64_encode($keyDER);
|
||||
$lines = str_split($der, 65);
|
||||
array_unshift($lines, '-----BEGIN PUBLIC KEY-----');
|
||||
$lines[] = '-----END PUBLIC KEY-----';
|
||||
$lines[] = '';
|
||||
|
||||
return new self(implode("\n", $lines));
|
||||
}
|
||||
|
||||
public function getHPKP(): string
|
||||
{
|
||||
return base64_encode(hash('sha256', $this->getDER(), true));
|
||||
}
|
||||
}
|
||||
Vendored
+36
@@ -0,0 +1,36 @@
|
||||
Acme PHP SSL 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 SSL is a PHP wrapper around OpenSSL extension providing SSL encoding,
|
||||
decoding, parsing and signing features.
|
||||
|
||||
It uses the recommended security settings and let you interact in a OOP
|
||||
manner with SSL entities (public/private keys, certificates, ...).
|
||||
|
||||
> If you want to chat with us or have questions, ping
|
||||
> @tgalopin or @jderusse on the [Symfony Slack](https://symfony.com/support)!
|
||||
|
||||
## Why use Acme PHP SSL?
|
||||
|
||||
Acme PHP SSL provides various useful tools solving different use-cases:
|
||||
- generate public and private keys (see the `Generator\KeyPairGenerator`) ;
|
||||
- sign data using a private key (see `Signer\DataSigner`) ;
|
||||
- parse certificates to extract informations about them (see `Parser\CertificateParser`) ;
|
||||
|
||||
There are many more possible use-cases, don't hesitate to dig a bit deeper in the
|
||||
documentation to find out if this library can solve your problem!
|
||||
|
||||
## Documentation
|
||||
|
||||
Read the official [Acme PHP SSL documentation](https://acmephp.github.io/acmephp/ssl/introduction.html).
|
||||
|
||||
## 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).
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "acmephp/ssl",
|
||||
"description": "PHP wrapper around OpenSSL extension providing SSL encoding, decoding, parsing and signing features",
|
||||
"type": "library",
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/acmephp/ssl",
|
||||
"keywords": [
|
||||
"certificate",
|
||||
"https",
|
||||
"acmephp",
|
||||
"ssl",
|
||||
"RSA",
|
||||
"ECDSA",
|
||||
"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"
|
||||
}
|
||||
],
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"AcmePhp\\Ssl\\": "."
|
||||
}
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.2.5",
|
||||
"ext-hash": "*",
|
||||
"ext-openssl": "*",
|
||||
"lib-openssl": ">=0.9.8",
|
||||
"webmozart/assert": "^1.0"
|
||||
},
|
||||
"config": {
|
||||
"sort-packages": true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user