Sindbad~EG File Manager

Current Path : /opt/nginxhttpd_/src/Service/Ocsp/
Upload File :
Current File : //opt/nginxhttpd_/src/Service/Ocsp/OcspFetcher.php

<?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