Sindbad~EG File Manager
<?php
namespace App\Service\Ocsp;
use Ocsp\Asn1\Der\Decoder;
use Ocsp\Asn1\Element;
use Ocsp\Asn1\Tag;
use Ocsp\Asn1\UniversalTagID;
use Ocsp\CertificateLoader;
use Ocsp\Exception\Asn1DecodingException;
use Ocsp\Ocsp;
use Ocsp\Response;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpClient\RetryableHttpClient;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class OcspFetcher
{
/**
* @var HttpClientInterface
*/
private $httpClient;
/**
* @var array Cache with key = CA OCSP Request URL and value = certificate in PEM format. Avoid multiple HTTP calls.
*/
private static $caCache = [];
/**
* @var array OCSP cache with key = md5(pem_crt) and value = OCSP in DER format. Avoid re-requesting the OCSP for each domains alias with the same SSL certificate
*/
private static $ocspCache = [];
/**
* @var array PEM CRT cache from the PEM file. Key = path to PEM file. Value = extract PEM CRT text. /!\ Memory intensive.
*/
private static $pemCrtCache = [];
/**
* @var LoggerInterface
*/
private $logger;
public function __construct(HttpClientInterface $httpClient, LoggerInterface $logger){
$this->httpClient = $httpClient;
if(class_exists(' Symfony\Component\HttpClient\RetryableHttpClient')){
$this->httpClient = new RetryableHttpClient($httpClient);
}
$this->logger = $logger;
}
/**
* Extract just the certificate from a concatenated PEM file contains the certificate, ca and keys.
* @param string $pem
* @return string|null
*/
public function extractCertificateFromPemString(string $pem) : ?string {
$pem = openssl_x509_read($pem);
if($pem === false){
$this->logger->error("cant read/parse the file content with openssl_x509_read");
return null;
}
if(openssl_x509_export($pem, $crt) === false){
$this->logger->error("cant extract the CRT with openssl_x509_export");
return null;
}
return $crt;
}
/**
* Extract the PEM Certificate from a concatenated PEM file containing the private key, certificate and cabundle.
* @param string $pemPath
* @return string|null
*/
public function extractCertificateFromConcatenatedPemFile(string $pemPath) : ?string {
if((isset(self::$pemCrtCache[$pemPath]) && self::$pemCrtCache[$pemPath] === null) || !file_exists($pemPath)){
$this->logger->error("$pemPath : file does not exists.");
self::$pemCrtCache[$pemPath] = null;
return null;
}
$pem = file_get_contents($pemPath);
if($pem === false){
$this->logger->error("$pemPath : file is not readable.");
self::$pemCrtCache[$pemPath] = null;
return null;
}
$pem = $this->extractCertificateFromPemString($pem);
self::$pemCrtCache[$pemPath] = $pem;
return $pem;
}
/**
* Fetch the OCSP Response from the PEM Encoded certificate.
* @param string $pemCrt
* @return OcspResponse|null
*/
public function fetchOcsp(string $pemCrt) : ?OcspResponse {
$certificateLoader = new \Ocsp\CertificateLoader();
$md5 = md5($pemCrt);
if(isset(self::$ocspCache[$md5])){
return self::$ocspCache[$md5];
}
try {
$certificate = $certificateLoader->fromString($pemCrt);
} catch (Asn1DecodingException $e){
$this->logger->error("Asn1DecodingException : " . $e->getMessage());
self::$ocspCache[$md5] = null;
return null;
}
$certificateInfo = new \Ocsp\CertificateInfo();
$ocspResponderUrl = $certificateInfo->extractOcspResponderUrl($certificate);
if(empty($ocspResponderUrl)){
self::$ocspCache[$md5] = null;
return null;
}
$urlOfIssuerCertificate = $certificateInfo->extractIssuerCertificateUrl($certificate);
if(empty($urlOfIssuerCertificate)){
self::$ocspCache[$md5] = null;
return null;
}
$issuerCertificate = $this->getIssuerCertificate($urlOfIssuerCertificate);
if(!$issuerCertificate instanceof Element\Sequence){
self::$ocspCache[$md5] = null;
return null;
}
$requestInfo = $certificateInfo->extractRequestInfo($certificate, $issuerCertificate);
$ocsp = new \Ocsp\Ocsp();
$requestBody = $ocsp->buildOcspRequestBodySingle($requestInfo);
try {
$response = $this->httpClient->request('POST', $ocspResponderUrl, [
'body' => $requestBody,
'headers' => [
'Content-Type' => \Ocsp\Ocsp::OCSP_REQUEST_MEDIATYPE,
]
]);
$rawOcspResponse = $response->getContent();
$ocspResponse = $ocsp->decodeOcspResponseSingle($rawOcspResponse);
} catch (\Exception $e){
$this->logger->error("Cant retrieve or decode the OCSP response " . $e->getMessage());
self::$ocspCache[$md5] = null;
return null;
}
$nextUpdate = $this->extractNextUpdateFromOcspResponse($rawOcspResponse);
if(!$nextUpdate instanceof \DateTimeImmutable){
$this->logger->error("Cant retrive the nextUpdate field. Setting a default +1 day value.");
$nextUpdate = new \DateTimeImmutable('+1 day');
}
return new OcspResponse($rawOcspResponse, $ocspResponse->isRevoked() === false, $ocspResponse->getThisUpdate(), $nextUpdate);
}
public function validateOcsp(string $rawOcspResponse) : ?OcspResponse {
$ocsp = new Ocsp();
try {
$ocspResponse = $ocsp->decodeOcspResponseSingle($rawOcspResponse);
$nextUpdate = $this->extractNextUpdateFromOcspResponse($rawOcspResponse);
} catch (\Exception $e){
return null;
}
if(!$nextUpdate instanceof \DateTimeImmutable){
$this->logger->error("Cant retrive the nextUpdate field. Setting a default +1 hour value if valid.");
$nextUpdate = new \DateTimeImmutable('+1 hour');
if($ocspResponse->isRevoked() === false){
$nextUpdate = new \DateTimeImmutable('-10 hour');
}
}
return new OcspResponse($rawOcspResponse, $ocspResponse->isRevoked() === false, $ocspResponse->getThisUpdate(), $nextUpdate);
}
/**
* Extract the NextUpdate field from the raw binary OCSP Response (DER format)
* @param string $rawOcspResponse
* @return \DateTimeImmutable|null
*/
public function extractNextUpdateFromOcspResponse(string $rawOcspResponse) : ?\DateTimeImmutable {
$derDecoder = new Decoder();
try {
$r = $derDecoder->decodeElement($rawOcspResponse);
} catch (\Exception $e){
return null;
}
$responseBytes = $r->getFirstChildOfType(0, Element::CLASS_CONTEXTSPECIFIC, Tag::ENVIRONMENT_EXPLICIT);
if(!$responseBytes instanceof Element\Sequence){
return null;
}
$responseType = $responseBytes->getFirstChildOfType(UniversalTagID::OBJECT_IDENTIFIER);
$response = $responseBytes->getFirstChildOfType(UniversalTagID::OCTET_STRING);
if ($responseType !== null && $response !== null) {
switch ($responseType->getIdentifier()) {
case '1.3.6.1.5.5.7.48.1.1':
$r = $derDecoder->decodeElement($response->getValue());
if(!$r instanceof Element){
return null;
}
$r = $r->getFirstChildOfType(UniversalTagID::SEQUENCE);
}
}
if(!$r instanceof Element){
return null;
}
$r = $r->getFirstChildOfType(UniversalTagID::SEQUENCE);
if(!$r instanceof Element){
return null;
}
$r = $r->getFirstChildOfType(UniversalTagID::SEQUENCE);
if($r === null){
return null;
}
$r = $r->getElements();
if(!isset($r[3]) || !$r[3] instanceof Element\GeneralizedTime){
return null;
}
try {
return $r[3]->getValue();
} catch (\Exception $e){
return null;
}
}
/**
* Concert a certificate from the DER format (binary) to PEM (text)
* @param string $derData
* @param string $type
* @return string
*/
public function der2pem(string $derData, string $type = 'CERTIFICATE') : string {
$pem = chunk_split(base64_encode($derData), 64, "\n");
return "-----BEGIN $type-----\n" . $pem . "-----END $type-----\n";
}
/**
* Return the certificate from the $urlOfIssuerCertificate. Download it and parse it.
* @param string $urlOfIssuerCertificate
* @return Element\Sequence|null
*/
protected function getIssuerCertificate(string $urlOfIssuerCertificate) : ?Element\Sequence {
if(isset(self::$caCache[$urlOfIssuerCertificate])){
return self::$caCache[$urlOfIssuerCertificate];
}
try {
$certificateLoader = new CertificateLoader();
$response = $this->httpClient->request('GET', $urlOfIssuerCertificate);
$issuerCertificate = $certificateLoader->fromString($this->der2pem($response->getContent()));
self::$caCache[$urlOfIssuerCertificate] = $issuerCertificate;
} catch (\Exception $e){
$this->logger->error("Cant retrieve the CA Certificate, HTTP error. " . $e->getMessage());
self::$caCache[$urlOfIssuerCertificate] = null;
return null;
}
return $issuerCertificate;
}
}
Sindbad File Manager Version 1.0, Coded By Sindbad EG ~ The Terrorists