From 636feda43e8cd687aa3132640ccd75d7d79dc3fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guido=20Gro=CC=88o=CC=88n?= Date: Mon, 8 Aug 2022 12:58:34 +0300 Subject: [PATCH] WE2-711 Implemented OCSP request and response. Added unit tests. --- .gitattributes | 5 + .gitignore | 6 + README.md | 155 +++++++++- composer.json | 28 ++ phpunit.xml.dist | 18 ++ src/Ocsp.php | 109 +++++++ src/OcspBasicResponse.php | 131 +++++++++ src/OcspRequest.php | 88 ++++++ src/OcspResponse.php | 146 +++++++++ src/certificate/CertificateLoader.php | 122 ++++++++ src/exceptions/OcspCertificateException.php | 38 +++ src/exceptions/OcspException.php | 45 +++ .../OcspResponseDecodeException.php | 38 +++ src/exceptions/OcspVerifyFailedException.php | 38 +++ src/maps/OcspBasicResponseMap.php | 148 ++++++++++ src/maps/OcspRequestMap.php | 111 +++++++ src/maps/OcspResponseMap.php | 60 ++++ src/util/AsnUtil.php | 61 ++++ tests/OcspRequestTest.php | 107 +++++++ tests/OcspResponseTest.php | 278 ++++++++++++++++++ tests/OcspTest.php | 62 ++++ tests/_resources/invalid.crt | 21 ++ tests/_resources/ocsp_response.der | Bin 0 -> 1579 bytes tests/_resources/ocsp_response_revoked.der | Bin 0 -> 1601 bytes tests/_resources/ocsp_response_unknown.der | Bin 0 -> 1833 bytes .../ocsp_response_with_2_responses.der | Bin 0 -> 267 bytes tests/_resources/revoked.crt | 31 ++ tests/_resources/revoked.issuer.crt | 27 ++ tests/certificate/CertificateLoaderTest.php | 101 +++++++ 29 files changed, 1973 insertions(+), 1 deletion(-) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 composer.json create mode 100644 phpunit.xml.dist create mode 100644 src/Ocsp.php create mode 100644 src/OcspBasicResponse.php create mode 100644 src/OcspRequest.php create mode 100644 src/OcspResponse.php create mode 100644 src/certificate/CertificateLoader.php create mode 100644 src/exceptions/OcspCertificateException.php create mode 100644 src/exceptions/OcspException.php create mode 100644 src/exceptions/OcspResponseDecodeException.php create mode 100644 src/exceptions/OcspVerifyFailedException.php create mode 100644 src/maps/OcspBasicResponseMap.php create mode 100644 src/maps/OcspRequestMap.php create mode 100644 src/maps/OcspResponseMap.php create mode 100644 src/util/AsnUtil.php create mode 100644 tests/OcspRequestTest.php create mode 100644 tests/OcspResponseTest.php create mode 100644 tests/OcspTest.php create mode 100644 tests/_resources/invalid.crt create mode 100644 tests/_resources/ocsp_response.der create mode 100644 tests/_resources/ocsp_response_revoked.der create mode 100644 tests/_resources/ocsp_response_unknown.der create mode 100644 tests/_resources/ocsp_response_with_2_responses.der create mode 100644 tests/_resources/revoked.crt create mode 100644 tests/_resources/revoked.issuer.crt create mode 100644 tests/certificate/CertificateLoaderTest.php diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..01963ff --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +# Ignore all test and documentation with "export-ignore". +/.gitattributes export-ignore +/.gitignore export-ignore +/phpunit.xml.dist export-ignore +/tests export-ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ddfe609 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +composer.lock +.phpunit.result.cache +vendor +build +.DS_Store +phpunit.xml \ No newline at end of file diff --git a/README.md b/README.md index 8e82ece..15b3e8f 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,155 @@ # ocsp-php -OCSP library for PHP + +ocsp-php is a library for PHP for checking if certificates are revoked, by using Online Certificate Status Protocol (OCSP). + +This library does not include any HTTP client, you can use cURL for example. + +# Quickstart + +Complete the steps below to include the library in your project. + +A PHP web application that uses Composer to manage packages is needed for running this quickstart. + +## Add the library to your project + +Add the following lines to composer.json to include the ocsp-php library in your project: + +```json +"repositories": [ +{ + "type": "vcs", + "url": "https://github.com/web-eid/ocsp-php" +} +], +"require": { + "web_eid/ocsp_php": "dev-main" +}, +``` + +## Loading the certificates + +By using **CertificateLoader**, you can load certificates from file or string. + +```php +// Loading certificate from file +$certificate = (new CertificateLoader)->fromFile('/path/to/cert.crt')->getCert(); + +// Loading certificate from string +$certificate = (new CertificateLoader)->fromString('-----BEGIN CERTIFICATE-----MIIEAzCCA...-----END CERTIFICATE-----')->getCert(); +``` + +## Getting the issuer certificate from certificate + +The certificate usually contains a URL where you can find certificate of the certificate issuer. + +You can use this code to extract this URL from the certificate. + +```php +$certLoader = (new CertificateLoader)->fromFile('/path/to/cert.crt'); +$issuerCertificateUrl = $certLoader->getIssuerCertificateUrl(); +``` + +`$issuerCertificateUrl` will contain the URL where the issuer certificate can be downloaded. When it is an empty string, that means the issuer certificate URL is not included in the SSL certificate. + +## Getting the OCSP responder URL + +To check if a SSL Certificate is valid, you need to know the OCSP URL, that is provided by the authority that issued the certificate. This URL can be called to check if the certificate has been revoked. + +This URL may be included in the SSL Certificate itself. + +You can use this code to extract the OCSP responder URL from the SSL Certificate. + +```php +$certLoader = (new CertificateLoader)->fromFile('/path/to/cert.crt'); +$ocspResponderUrl = $certLoader->getOcspResponderUrl(); +``` +When it is an empty string, that means the OCSP responder URL is not included in the SSL Certificate. + +## Checking the revocation status of an SSL Certificate + +Once you have the SSL Certificate, the issuer certificate, and the OCSP responder URL, you can check whether the SSL certificate has been revoked or is still valid. + +```php +$subjectCert = (new CertificateLoader)->fromFile('/path/to/subject.crt')->getCert(); +$issuerCert = (new CertificateLoader)->fromFile('/path/to/issuer.crt')->getCert(); + +// Create the certificateId +$certificateId = (new Ocsp)->generateCertificateId($subjectCert, $issuerCert); + +// Build request body +$requestBody = new OcspRequest(); +$requestBody->addCertificateId($certificateId); + +// Add nonce extension when the nonce feature is enabled, +// otherwise skip this line +$requestBody->addNonceExtension(random_bytes(32)); + +// Send request to OCSP responder URL +$curl = curl_init(); +curl_setopt($curl, CURLOPT_URL, $ocspResponderUrl); +curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); +curl_setopt($curl, CURLOPT_POST, true); +curl_setopt($curl, CURLOPT_HTTPHEADER, ['Content-Type: ' .Ocsp::OCSP_REQUEST_MEDIATYPE]); +curl_setopt($curl, CURLOPT_POSTFIELDS, $requestBody->getEncodeDer()); +$result = curl_exec($curl); +$info = curl_getinfo($curl); +if ($info["http_code"] !== 200) { + throw new RuntimeException("HTTP status is not 200"); +} + +// Check the response content type +if ($info["content_type"] != Ocsp::OCSP_RESPONSE_MEDIATYPE) { + throw new RuntimeException("Content-Type header of the response is wrong"); +} + +// Decode the raw response from the OCSP Responder +$response = new OcspResponse($result); + +// Verify response +// It will verify that OCSP response contains only one response, +// validates certificateId and signature +$response->verify($certificateId); + +// Validate nonce when the nonce feature is enabled, +$basicResponse = $response->getBasicResponse(); +if ($requestBody->getNonceExtension() != $basicResponse->getNonceExtension()) { + throw new RuntimeException("OCSP request nonce and response nonce do not match"); +} + +``` +`$response` contains instance of the `web_eid\ocsp_php\OcspResponse` class: + +* `$response->isRevoked() === false` when the certificate is not revoked +* `$response->isRevoked() === true` when the certificate is revoked (to get revoke reason, call `$response->getRevokeReason()`) +* when `$response->isRevoked()` returns null, then the certificate revoke status is unknown + +To get more detailed information from response, you can use: + +```php +// Read response status +$response->getStatus(); +$basicResponse = $response->getBasicResponse(); +``` + +Following methods can be called with `$basicResponse`: + +* `$basicResponse->getResponses()` - returns array of the responses +* `$basicResponse->getCertificates()` - returns array of X.509 certificates (phpseclib3\File\X509) +* `$basicResponse->getSignature()` - returns signature +* `$basicResponse->getProducedAt()` - returns DateTime object +* `$basicResponse->getThisUpdate()` - returns DateTime object +* `$basicResponse->getNextUpdate()` - returns DateTime object (is `null` when `nextUpdate` field does not exist) +* `$basicResponse->getSignatureAlgorithm()` - returns signature algorithm as string (throws exception, when signature algorithm is not implemented) +* `$basicResponse->getNonceExtension()` - returns nonce (when value is `null` then nonce extension does not exist in response) + +# Exceptions + +All exceptions are handled by the `web_eid\ocsp_php\exceptions\OcspException` class. To catch these errors, you can enclose your code within try/catch statements: + +```php +try { + // code +} catch (OcspException $e) { + // exception handler +} +``` diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..0fdb18b --- /dev/null +++ b/composer.json @@ -0,0 +1,28 @@ +{ + "name": "web_eid/ocsp_php", + "description": "OCSP library for PHP", + "license": "MIT", + "type": "library", + "authors": [ + { + "name": "Guido Gröön", + "role" : "developer" + } + ], + "require-dev": { + "phpunit/phpunit": "^9.5" + }, + "autoload": { + "psr-4": { + "web_eid\\ocsp_php\\": ["src"] + } + }, + "autoload-dev": { + "psr-4": { + "web_eid\\ocsp_php\\": ["tests"] + } + }, + "require": { + "phpseclib/phpseclib": "3.0.14" + } +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..3dad96c --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,18 @@ + + + + + src/ + + + + + + + + + + tests + + + \ No newline at end of file diff --git a/src/Ocsp.php b/src/Ocsp.php new file mode 100644 index 0000000..6b5120c --- /dev/null +++ b/src/Ocsp.php @@ -0,0 +1,109 @@ + [], + "issuerNameHash" => "", + "issuerKeyHash" => "", + "serialNumber" => [], + ]; + + if (!isset($certificate->getCurrentCert()['tbsCertificate']['serialNumber'])) { + // Serial number of subject certificate does not exist + throw new OcspCertificateException("Serial number of subject certificate does not exist"); + } + + $certificateId["serialNumber"] = clone $certificate->getCurrentCert()['tbsCertificate']['serialNumber']; + + // issuer name + if (!isset($issuerCertificate->getCurrentCert()['tbsCertificate']['subject'])) { + // Serial number of issuer certificate does not exist + throw new OcspCertificateException("Serial number of issuer certificate does not exist"); + } + + $issuer = $issuerCertificate->getCurrentCert()['tbsCertificate']['subject']; + $issuerEncoded = ASN1::encodeDER($issuer, Name::MAP); + $certificateId["issuerNameHash"] = hash("sha1", $issuerEncoded, true); + + // issuer public key + if (!isset($issuerCertificate->getCurrentCert()['tbsCertificate']['subjectPublicKeyInfo']['subjectPublicKey'])) { + // SubjectPublicKey of issuer certificate does not exist + throw new OcspCertificateException("SubjectPublicKey of issuer certificate does not exist"); + } + + $publicKey = $issuerCertificate->getCurrentCert()['tbsCertificate']['subjectPublicKeyInfo']['subjectPublicKey']; + $certificateId['issuerKeyHash'] = hash("sha1", AsnUtil::extractKeyData($publicKey), true); + + $certificateId['hashAlgorithm']['algorithm'] = Asn1::getOID('id-sha1'); + + return $certificateId; + } + + +} \ No newline at end of file diff --git a/src/OcspBasicResponse.php b/src/OcspBasicResponse.php new file mode 100644 index 0000000..63416b0 --- /dev/null +++ b/src/OcspBasicResponse.php @@ -0,0 +1,131 @@ +ocspBasicResponse = $ocspBasicResponse; + } + + public function getResponses(): array + { + return $this->ocspBasicResponse['tbsResponseData']['responses']; + } + + public function getCertificates(): array + { + $certificatesArr = []; + if (isset($this->ocspBasicResponse['certs'])) { + foreach($this->ocspBasicResponse['certs'] as $cert) { + $x509 = new X509(); + /* + We need to DER encode each responder certificate array as there exists some + more loading in X509->loadX509 method, which is not executed when loading just basic array. + For example without this the publicKey would not be in PEM format and X509->getPublicKey() + will throw error. It also maps out the extensions from BIT STRING + */ + $x509->loadX509(ASN1::encodeDER($cert, Certificate::MAP)); + $certificatesArr[] = $x509; + } + unset($x509); + } + + return $certificatesArr; + } + + public function getSignature(): string + { + $signature = $this->ocspBasicResponse['signature']; + return pack('c*', ...array_slice(unpack('c*', $signature), 1)); + } + + public function getProducedAt(): DateTime + { + return new DateTime($this->ocspBasicResponse['tbsResponseData']['producedAt']); + } + + public function getThisUpdate(): DateTime + { + return new DateTime($this->getResponses()[0]['thisUpdate']); + } + + public function getNextUpdate(): ?DateTime + { + if (isset($this->getResponses()[0]['nextUpdate'])) { + return new DateTime($this->getResponses()[0]['nextUpdate']); + } + return null; + } + + public function getSignatureAlgorithm(): string + { + $algorithm = strtolower($this->ocspBasicResponse['signatureAlgorithm']['algorithm']); + + if (false !== ($pos = strpos($algorithm, 'sha3-'))) { + return substr($algorithm, $pos, 8); + } + if (false !== ($pos = strpos($algorithm, 'sha'))) { + return substr($algorithm, $pos, 6); + } + + throw new OcspCertificateException("Signature algorithm " . $algorithm . " not implemented"); + } + + public function getNonceExtension(): ?string + { + $filter = array_filter( + $this->ocspBasicResponse['tbsResponseData']['responseExtensions'], + function ($extension) { + return AsnUtil::ID_PKIX_OCSP_NONCE == ASN1::getOID($extension['extnId']); + } + ); + + if (isset($filter[0]['extnValue'])) { + return $filter[0]['extnValue']; + } + + return null; + } + + public function getEncodedResponseData(): string + { + return ASN1::encodeDER($this->ocspBasicResponse['tbsResponseData'], OcspBasicResponseMap::MAP['children']['tbsResponseData']); + } + +} \ No newline at end of file diff --git a/src/OcspRequest.php b/src/OcspRequest.php new file mode 100644 index 0000000..1adc7ed --- /dev/null +++ b/src/OcspRequest.php @@ -0,0 +1,88 @@ +ocspRequest = [ + 'tbsRequest' => [ + 'version' => 'v1', + 'requestList' => [], + 'requestExtensions' => [], + ], + ]; + + } + + public function addCertificateId(array $certificateId): void + { + $request = [ + 'reqCert' => $certificateId + ]; + $this->ocspRequest['tbsRequest']['requestList'][] = $request; + } + + public function addNonceExtension(string $nonce): void + { + $nonceExtension = [ + 'extnId' => AsnUtil::ID_PKIX_OCSP_NONCE, + 'critical' => false, + 'extnValue' => $nonce, + ]; + $this->ocspRequest['tbsRequest']['requestExtensions'][] = $nonceExtension; + } + + public function getNonceExtension(): string + { + return current( + array_filter( + $this->ocspRequest['tbsRequest']['requestExtensions'], + function ($extension) { + return AsnUtil::ID_PKIX_OCSP_NONCE == $extension['extnId']; + } + ) + )['extnValue']; + } + + public function getEncodeDer(): string + { + return ASN1::encodeDER($this->ocspRequest, OcspRequestMap::MAP); + } + + +} \ No newline at end of file diff --git a/src/OcspResponse.php b/src/OcspResponse.php new file mode 100644 index 0000000..bf0be07 --- /dev/null +++ b/src/OcspResponse.php @@ -0,0 +1,146 @@ +ocspResponse = ASN1::asn1map( + $decoded[0], + OcspResponseMap::MAP, + array('response' => function ($encoded) { + return ASN1::asn1map(ASN1::decodeBER($encoded)[0], OcspBasicResponseMap::MAP); + }) + ); + + } + + public function getBasicResponse(): OcspBasicResponse + { + if (Ocsp::ID_PKIX_OCSP_BASIC_STRING != $this->ocspResponse['responseBytes']['responseType']) { + throw new UnexpectedValueException('responseType is not "id-pkix-ocsp-basic" but is ' . $this->ocspResponse['responseBytes']['responseType']); + } + + if (!$this->ocspResponse['responseBytes']['response']) { + throw new UnexpectedValueException('Could not decode OcspResponse->responseBytes->responseType'); + } + + return new OcspBasicResponse($this->ocspResponse['responseBytes']['response']); + } + + public function getStatus(): string + { + return $this->ocspResponse['responseStatus']; + } + + public function getRevokeReason(): string + { + return $this->revokeReason; + } + + public function isRevoked() + { + $basicResponse = $this->getBasicResponse(); + if (isset($basicResponse->getResponses()[0]['certStatus']['good'])) { + return false; + } + if (isset($basicResponse->getResponses()[0]['certStatus']['revoked'])) { + $revokedStatus = $basicResponse->getResponses()[0]['certStatus']['revoked']; + // Check revoke reason + if (isset($revokedStatus['revokedReason'])) { + $this->revokeReason = $revokedStatus['revokedReason']; + } + return true; + } + return null; + } + + private function validateResponseSignature(OcspBasicResponse $basicResponse, X509 $certificate) + { + // get public key from responder certificate in order to verify signature on response + $publicKey = $certificate->getPublicKey()->withHash($basicResponse->getSignatureAlgorithm()); + + // verify response data + $encodedTbsResponseData = $basicResponse->getEncodedResponseData(); + $signature = $basicResponse->getSignature(); + + if (!$publicKey->verify($encodedTbsResponseData, $signature)) { + throw new OcspVerifyFailedException("OCSP response signature is not valid"); + } + + } + + public function verify(array $requestCertificateId) + { + + $basicResponse = $this->getBasicResponse(); + + // Must be one response + if (count($basicResponse->getResponses()) != 1) { + throw new OcspVerifyFailedException("OCSP response must contain one response, received " . count($basicResponse->getResponses()) . " responses instead"); + } + + $certStatusResponse = $basicResponse->getResponses()[0]; + + // Translate algorithm name to OID for correct equality check + $certStatusResponse['certID']['hashAlgorithm']['algorithm'] = ASN1::getOID($certStatusResponse['certID']['hashAlgorithm']['algorithm']); + + if ($requestCertificateId != $certStatusResponse['certID']) { + throw new OcspVerifyFailedException("OCSP responded with certificate ID that differs from the requested ID"); + } + + // At least on cert must exist in responder + if (count($basicResponse->getCertificates()) < 1) { + throw new OcspVerifyFailedException("OCSP response must contain the responder certificate, but non was provided"); + } + + // Validate responder certificate signature + $responderCert = $basicResponse->getCertificates()[0]; + $this->validateResponseSignature($basicResponse, $responderCert); + + } + + + +} \ No newline at end of file diff --git a/src/certificate/CertificateLoader.php b/src/certificate/CertificateLoader.php new file mode 100644 index 0000000..3335ea3 --- /dev/null +++ b/src/certificate/CertificateLoader.php @@ -0,0 +1,122 @@ +loadX509($fileContent); + if (!$loaded) { + throw new OcspCertificateException("Certificate decoding from Base64 or parsing failed for " . $pathToFile); + } + $this->certificate = $certificate; + return $this; + } + + /** + * Loads the certificate from string and returns the certificate + * + * @param string certString - certificate as string + * @throws OcspCertificateException when the certificate decoding or parse fails + */ + public function fromString(string $certString) + { + $certificate = new X509(); + $loaded = $certificate->loadX509($certString); + if (!$loaded) { + throw new OcspCertificateException("Certificate decoding from Base64 or parsing failed"); + } + $this->certificate = $certificate; + return $this; + } + + public function getIssuerCertificateUrl(): string + { + if (!$this->certificate) { + throw new OcspCertificateException("Certificate not loaded"); + } + + $url = ""; + $opts = $this->certificate->getExtension("id-pe-authorityInfoAccess"); + foreach ($opts as $opt) { + if ($opt["accessMethod"] == "id-ad-caIssuers") { + $url = $opt["accessLocation"]["uniformResourceIdentifier"]; + break; + } + } + return $url; + } + + public function getOcspResponderUrl(): string + { + if (!$this->certificate) { + throw new OcspCertificateException("Certificate not loaded"); + } + + $url = ""; + $opts = $this->certificate->getExtension("id-pe-authorityInfoAccess"); + foreach ($opts as $opt) { + if ($opt["accessMethod"] == "id-ad-ocsp") { + $url = $opt["accessLocation"]["uniformResourceIdentifier"]; + break; + } + } + return $url; + } + + public function getCert(): X509 + { + if (!$this->certificate) { + throw new OcspCertificateException("Certificate not loaded"); + } + return $this->certificate; + } + + +} \ No newline at end of file diff --git a/src/exceptions/OcspCertificateException.php b/src/exceptions/OcspCertificateException.php new file mode 100644 index 0000000..ccf485f --- /dev/null +++ b/src/exceptions/OcspCertificateException.php @@ -0,0 +1,38 @@ + ASN1::TYPE_SEQUENCE, + 'children' => [ + 'tbsResponseData' => [ + 'type' => ASN1::TYPE_SEQUENCE, + 'children' => [ + 'version' => [ + 'type' => ASN1::TYPE_INTEGER, + 'constant' => 0, + 'optional' => true, + 'explicit' => true, + 'mapping' => ['v1'], + 'default' => 'v1', + ], + 'responderID' => [ + 'type' => ASN1::TYPE_CHOICE, + 'children' => [ + 'byName' => [ + 'constant' => 1, + 'explicit' => true, ] + Name::MAP, + 'byKey' => [ + 'constant' => 2, + 'explicit' => true, + 'type' => ASN1::TYPE_OCTET_STRING, + ], + ], + ], + 'producedAt' => ['type' => ASN1::TYPE_GENERALIZED_TIME], + 'responses' => [ + 'type' => ASN1::TYPE_SEQUENCE, + 'min' => 0, + 'max' => -1, + 'children' => [ + 'type' => ASN1::TYPE_SEQUENCE, + 'children' => [ + 'certID' => [ + 'type' => ASN1::TYPE_SEQUENCE, + 'children' => [ + 'hashAlgorithm' => AlgorithmIdentifier::MAP, + 'issuerNameHash' => ['type' => ASN1::TYPE_OCTET_STRING], + 'issuerKeyHash' => ['type' => ASN1::TYPE_OCTET_STRING], + 'serialNumber' => CertificateSerialNumber::MAP, + ], + ], + 'certStatus' => [ + 'type' => ASN1::TYPE_CHOICE, + 'children' => [ + 'good' => [ + 'constant' => 0, + 'implicit' => true, + 'type' => ASN1::TYPE_NULL, + ], + 'revoked' => [ + 'constant' => 1, + 'implicit' => true, + 'type' => ASN1::TYPE_SEQUENCE, + 'children' => [ + 'revokedTime' => [ + 'type' => ASN1::TYPE_GENERALIZED_TIME, + ], + 'revokedReason' => [ + 'constant' => 0, + 'explicit' => true, + 'optional' => true, + ] + CRLReason::MAP, + ], + ], + 'unknown' => [ + 'constant' => 2, + 'implicit' => true, + 'type' => ASN1::TYPE_NULL, + ], + ], + ], + 'thisUpdate' => ['type' => ASN1::TYPE_GENERALIZED_TIME], + 'nextUpdate' => [ + 'type' => ASN1::TYPE_GENERALIZED_TIME, + 'constant' => 0, + 'explicit' => true, + 'optional' => true, + ], + 'singleExtensions' => [ + 'constant' => 1, + 'explicit' => true, + 'optional' => true, + ] + Extensions::MAP, + ], + ], + ], + 'responseExtensions' => [ + 'constant' => 1, + 'explicit' => true, + 'optional' => true, + ] + Extensions::MAP, + ], + ], + 'signatureAlgorithm' => AlgorithmIdentifier::MAP, + 'signature' => ['type' => ASN1::TYPE_BIT_STRING], + 'certs' => [ + 'constant' => 0, + 'explicit' => true, + 'optional' => true, + 'type' => ASN1::TYPE_SEQUENCE, + 'min' => 0, + 'max' => -1, + 'children' => Certificate::MAP, + ], + ], + ]; +} \ No newline at end of file diff --git a/src/maps/OcspRequestMap.php b/src/maps/OcspRequestMap.php new file mode 100644 index 0000000..4e9a9c7 --- /dev/null +++ b/src/maps/OcspRequestMap.php @@ -0,0 +1,111 @@ + ASN1::TYPE_SEQUENCE, + 'children' => [ + 'tbsRequest' => [ + 'type' => ASN1::TYPE_SEQUENCE, + 'children' => [ + 'version' => [ + 'constant' => 0, + 'explicit' => true, + 'optional' => true, + 'mapping' => [0 => 'v1'], + 'default' => 'v1', + 'type' => ASN1::TYPE_INTEGER, + ], + 'requestList' => [ + 'type' => ASN1::TYPE_SEQUENCE, + 'min' => 0, + 'max' => -1, + 'children' => [ + 'type' => ASN1::TYPE_SEQUENCE, + 'children' => [ + 'reqCert' => [ + 'type' => ASN1::TYPE_SEQUENCE, + 'children' => [ + 'hashAlgorithm' => AlgorithmIdentifier::MAP, + 'issuerNameHash' => ['type' => ASN1::TYPE_OCTET_STRING], + 'issuerKeyHash' => ['type' => ASN1::TYPE_OCTET_STRING], + 'serialNumber' => CertificateSerialNumber::MAP, + ], + ], + 'singleRequestExtensions' => [ + 'constant' => 0, + 'explicit' => true, + 'optional' => true, + ] + Extensions::MAP, + ], + ], + ], + 'requestExtensions' => [ + 'constant' => 2, + 'explicit' => true, + 'optional' => true, + ] + Extensions::MAP, + 'requestorName' => [ + 'constant' => 1, + 'optional' => true, + 'explicit' => true, + ] + GeneralName::MAP, + ], + ], + 'optionalSignature' => [ + 'constant' => 0, + 'explicit' => true, + 'optional' => true, + 'type' => ASN1::TYPE_SEQUENCE, + 'children' => [ + 'signatureAlgorithm' => AlgorithmIdentifier::MAP, + 'signature' => ['type' => ASN1::TYPE_BIT_STRING], + 'certs' => [ + 'constant' => 0, + 'explicit' => true, + 'optional' => true, + 'type' => ASN1::TYPE_SEQUENCE, + 'min' => 0, + 'max' => -1, + 'children' => Certificate::MAP, + ], + ], + ], + ], + ]; + +} \ No newline at end of file diff --git a/src/maps/OcspResponseMap.php b/src/maps/OcspResponseMap.php new file mode 100644 index 0000000..39a6206 --- /dev/null +++ b/src/maps/OcspResponseMap.php @@ -0,0 +1,60 @@ + ASN1::TYPE_SEQUENCE, + 'children' => [ + 'responseStatus' => [ + 'type' => ASN1::TYPE_ENUMERATED, + 'mapping' => [ + 0 => 'successful', + 1 => 'malformedRequest', + 2 => 'internalError', + 3 => 'tryLater', + 5 => 'sigRequired', + 6 => 'unauthorized', + ], + ], + 'responseBytes' => [ + 'constant' => 0, + 'explicit' => true, + 'optional' => true, + 'type' => ASN1::TYPE_SEQUENCE, + 'children' => array( + 'responseType' => array('type' => ASN1::TYPE_OBJECT_IDENTIFIER), + 'response' => array('type' => ASN1::TYPE_OCTET_STRING), + ), + ], + ], + ]; + +} \ No newline at end of file diff --git a/src/util/AsnUtil.php b/src/util/AsnUtil.php new file mode 100644 index 0000000..8878271 --- /dev/null +++ b/src/util/AsnUtil.php @@ -0,0 +1,61 @@ + self::ID_PKIX_OCSP_NONCE, + 'id-sha1' => '1.3.14.3.2.26', + 'sha256WithRSAEncryption' => '1.2.840.113549.1.1.11', + 'qcStatements(3)' => '1.3.6.1.5.5.7.1.3', + 'street' => '2.5.4.9', + 'id-pkix-ocsp-basic' => '1.3.6.1.5.5.7.48.1.1', + 'id-pkix-ocsp' => '1.3.6.1.5.5.7.48.1', + 'secp384r1' => '1.3.132.0.34', + 'id-pkix-ocsp-archive-cutoff' => '1.3.6.1.5.5.7.48.1.6', + 'id-pkix-ocsp-nocheck' => '1.3.6.1.5.5.7.48.1.5', + ]); + } + + public static function extractKeyData(string $publicKey): string + { + $extractedBER = ASN1::extractBER($publicKey); + $decodedBER = ASN1::decodeBER($extractedBER); + $subjectPublicKey = ASN1::asn1map($decodedBER[0], SubjectPublicKeyInfo::MAP)['subjectPublicKey']; + // Remove first byte + return pack('c*', ...array_slice(unpack('c*', $subjectPublicKey), 1)); + } + +} \ No newline at end of file diff --git a/tests/OcspRequestTest.php b/tests/OcspRequestTest.php new file mode 100644 index 0000000..2a1667e --- /dev/null +++ b/tests/OcspRequestTest.php @@ -0,0 +1,107 @@ + [ + 'version' => 'v1', + 'requestList' => [], + 'requestExtensions' => [], + ], + ]; + } + + private function getNonce(): array + { + return [ + 'extnId' => AsnUtil::ID_PKIX_OCSP_NONCE, + 'critical' => false, + 'extnValue' => "nonce", + ]; + } + + private function getExpectedRequestWithCertID(): array + { + $result = $this->getRequest(); + $result['tbsRequest']['requestList'][] = [ + 'reqCert' => [1] + ]; + return $result; + } + + private function getExpectedWithNonce(): array + { + $result = $this->getRequest(); + $result['tbsRequest']['requestExtensions'][] = $this->getNonce(); + return $result; + } + + public function testWhenAddCertificateIdSuccess(): void + { + $request = new OcspRequest(); + $request->addCertificateId([1]); + + $reflection = new ReflectionClass(get_class($request)); + $property = $reflection->getProperty('ocspRequest'); + $property->setAccessible(true); + + $this->assertEquals($this->getExpectedRequestWithCertID(), $property->getValue($request)); + + } + + public function testWhenAddNonceExtensionSuccess(): void + { + $request = new OcspRequest(); + $request->addNonceExtension("nonce"); + + $reflection = new ReflectionClass(get_class($request)); + $property = $reflection->getProperty('ocspRequest'); + $property->setAccessible(true); + + $this->assertEquals($this->getExpectedWithNonce(), $property->getValue($request)); + + } + + public function testWhenGetNonceExtensionSuccess(): void + { + $request = new OcspRequest(); + $request->addNonceExtension("nonce"); + + $this->assertEquals("nonce", $request->getNonceExtension()); + + } + +} \ No newline at end of file diff --git a/tests/OcspResponseTest.php b/tests/OcspResponseTest.php new file mode 100644 index 0000000..640c4fa --- /dev/null +++ b/tests/OcspResponseTest.php @@ -0,0 +1,278 @@ +expectException(OcspResponseDecodeException::class); + $this->expectExceptionMessage("Could not decode OCSP response"); + new OcspResponse("1"); + } + + public function testWhenCertificateNotRevoked(): void + { + $response = new OcspResponse(self::getOcspResponseBytesFromResources()); + $basicResponse = $response->getBasicResponse(); + + $mockCertificateID = $basicResponse->getResponses()[0]['certID']; + $mockCertificateID['hashAlgorithm']['algorithm'] = ASN1::getOID('id-sha1'); + + $response->verify($mockCertificateID); + + $this->assertFalse($response->isRevoked()); + $this->assertEquals("successful", $response->getStatus()); + $this->assertEquals("2021-09-17 18:25:24", $basicResponse->getProducedAt()->format("Y-m-d H:i:s")); + $this->assertEquals("2021-09-17 18:25:24", $basicResponse->getThisUpdate()->format("Y-m-d H:i:s")); + $this->assertNull($basicResponse->getNextUpdate()); + $this->assertEquals([71, 255, 175, 201, 24, 17, 119, 14], array_values(unpack('C*', $basicResponse->getNonceExtension()))); + + } + + public function testWhenCertificateIsRevoked(): void + { + $response = new OcspResponse(self::getOcspResponseBytesFromResources("ocsp_response_revoked.der")); + $this->assertTrue($response->isRevoked()); + $this->assertEquals("unspecified", $response->getRevokeReason()); + } + + public function testWhenCertificateRevokeStatusIsUnknown(): void + { + $response = new OcspResponse(self::getOcspResponseBytesFromResources("ocsp_response_unknown.der")); + $this->assertNull($response->isRevoked()); + } + + public function testWhenTwoResponsesThenThrows(): void + { + $response = new OcspResponse(self::getOcspResponseBytesFromResources("ocsp_response_with_2_responses.der")); + $basicResponse = $response->getBasicResponse(); + + $mockCertificateID = $basicResponse->getResponses()[0]['certID']; + $mockCertificateID['hashAlgorithm']['algorithm'] = ASN1::getOID('id-sha1'); + + $this->expectException(OcspVerifyFailedException::class); + $this->expectExceptionMessage("OCSP response must contain one response, received 2 responses instead"); + + $response->verify($mockCertificateID); + } + + public function testWhenCertificateIdsDoNotMatchThenThrows(): void + { + $response = new OcspResponse(self::getOcspResponseBytesFromResources()); + $basicResponse = $response->getBasicResponse(); + + $mockCertificateID = $basicResponse->getResponses()[0]['certID']; + $mockCertificateID['issuerNameHash'] = "1234"; + $mockCertificateID['hashAlgorithm']['algorithm'] = ASN1::getOID('id-sha1'); + + $this->expectException(OcspVerifyFailedException::class); + $this->expectExceptionMessage("OCSP responded with certificate ID that differs from the requested ID"); + + $response->verify($mockCertificateID); + } + + public function testWhenResponseTypeNotBasicResponseThrows(): void + { + + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessage('responseType is not "id-pkix-ocsp-basic" but is responseType'); + + $response = new OcspResponse(self::getOcspResponseBytesFromResources()); + + $reflection = new ReflectionClass(get_class($response)); + $property = $reflection->getProperty('ocspResponse'); + $property->setAccessible(true); + $mockResponse = $property->getValue($response); + $mockResponse['responseBytes']['responseType'] = "responseType"; + $property->setValue($response, $mockResponse); + + $response->getBasicResponse(); + + } + + public function testWhenMissingResponseThrows(): void + { + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessage('Could not decode OcspResponse->responseBytes->responseType'); + + $response = new OcspResponse(self::getOcspResponseBytesFromResources()); + + $reflection = new ReflectionClass(get_class($response)); + $property = $reflection->getProperty('ocspResponse'); + $property->setAccessible(true); + $mockResponse = $property->getValue($response); + $mockResponse['responseBytes']['response'] = null; + $property->setValue($response, $mockResponse); + + $response->getBasicResponse(); + } + + public function testWhenNoCertificatesInResponseThrows(): void + { + $this->expectException(OcspVerifyFailedException::class); + $this->expectExceptionMessage('OCSP response must contain the responder certificate, but non was provided'); + + $response = new OcspResponse(self::getOcspResponseBytesFromResources()); + + $basicResponse = $response->getBasicResponse(); + $mockCertificateID = $basicResponse->getResponses()[0]['certID']; + $mockCertificateID['hashAlgorithm']['algorithm'] = ASN1::getOID('id-sha1'); + + $reflection = new ReflectionClass(get_class($response)); + $property = $reflection->getProperty('ocspResponse'); + $property->setAccessible(true); + $mockResponse = $property->getValue($response); + $mockResponse['responseBytes']['response']['certs'] = []; + + $property->setValue($response, $mockResponse); + + $response->verify($mockCertificateID); + } + + public function testWhenResponseSignatureIsNotValidThrows(): void + { + $this->expectException(OcspVerifyFailedException::class); + $this->expectExceptionMessage('OCSP response signature is not valid'); + + $response = new OcspResponse(self::getOcspResponseBytesFromResources()); + + $basicResponse = $response->getBasicResponse(); + $mockCertificateID = $basicResponse->getResponses()[0]['certID']; + $mockCertificateID['hashAlgorithm']['algorithm'] = ASN1::getOID('id-sha1'); + + $reflection = new ReflectionClass(get_class($response)); + $property = $reflection->getProperty('ocspResponse'); + $property->setAccessible(true); + $mockResponse = $property->getValue($response); + $mockResponse['responseBytes']['response']['signature'] = "somesignature"; + + $property->setValue($response, $mockResponse); + + $response->verify($mockCertificateID); + } + + public function testWhenSignatureAlgorithmIsSha3(): void + { + $response = new OcspResponse(self::getOcspResponseBytesFromResources()); + + $reflection = new ReflectionClass(get_class($response)); + $property = $reflection->getProperty('ocspResponse'); + $property->setAccessible(true); + $mockResponse = $property->getValue($response); + $mockResponse['responseBytes']['response']['signatureAlgorithm']['algorithm'] = "NNNsha3-256NNN"; + $property->setValue($response, $mockResponse); + + $basicResponse = $response->getBasicResponse(); + + $this->assertEquals("sha3-256", $basicResponse->getSignatureAlgorithm()); + + } + + public function testWhenSignatureAlgorithmIsNotSupportedThenThrows(): void + { + + $this->expectException(OcspCertificateException::class); + $this->expectExceptionMessage('Signature algorithm somealgo not implemented'); + + $response = new OcspResponse(self::getOcspResponseBytesFromResources()); + + $reflection = new ReflectionClass(get_class($response)); + $property = $reflection->getProperty('ocspResponse'); + $property->setAccessible(true); + $mockResponse = $property->getValue($response); + $mockResponse['responseBytes']['response']['signatureAlgorithm']['algorithm'] = "someAlgo"; + $property->setValue($response, $mockResponse); + + $basicResponse = $response->getBasicResponse(); + $basicResponse->getSignatureAlgorithm(); + + } + + public function testWhenNextUpdateInResponse(): void + { + + $response = new OcspResponse(self::getOcspResponseBytesFromResources()); + + $reflection = new ReflectionClass(get_class($response)); + $property = $reflection->getProperty('ocspResponse'); + $property->setAccessible(true); + $mockResponse = $property->getValue($response); + $mockResponse['responseBytes']['response']['tbsResponseData']['responses'][0]['nextUpdate'] = 'Fri, 17 Sep 2021 18:25:24 +0000'; + $property->setValue($response, $mockResponse); + + $basicResponse = $response->getBasicResponse(); + + $this->assertEquals(new DateTime("Fri, 17 Sep 2021 18:25:24 +0000"), $basicResponse->getNextUpdate()); + + + } + + public function testWhenNonceExtensionDoesNotExistNullShouldReturned(): void + { + + $response = new OcspResponse(self::getOcspResponseBytesFromResources()); + + $reflection = new ReflectionClass(get_class($response)); + $property = $reflection->getProperty('ocspResponse'); + $property->setAccessible(true); + $mockResponse = $property->getValue($response); + $mockResponse['responseBytes']['response']['tbsResponseData']['responseExtensions'][0]['extnId'] = "id-pkix-ocsp-nonce1"; + $property->setValue($response, $mockResponse); + + $basicResponse = $response->getBasicResponse(); + + $this->assertNull($basicResponse->getNonceExtension()); + + + } + +} \ No newline at end of file diff --git a/tests/OcspTest.php b/tests/OcspTest.php new file mode 100644 index 0000000..0e3309e --- /dev/null +++ b/tests/OcspTest.php @@ -0,0 +1,62 @@ +generateCertificateId((new CertificateLoader)->fromFile(__DIR__.'/_resources/revoked.crt')->getCert(), (new CertificateLoader)->fromFile(__DIR__.'/_resources/revoked.issuer.crt')->getCert()); + + $this->assertEquals("1.3.14.3.2.26", $result['hashAlgorithm']['algorithm']); + $this->assertEquals([126, 230, 106, 231, 114, 154, 179, 252, 248, 162, 32, 100, 108, 22, 161, 45, 96, 113, 8, 93], array_values(unpack('C*', $result['issuerNameHash']))); + $this->assertEquals([168, 74, 106, 99, 4, 125, 221, 186, 230, 209, 57, 183, 166, 69, 101, 239, 243, 168, 236, 161], array_values(unpack('C*', $result['issuerKeyHash']))); + } + + public function testWhenMissingSerialNumberInSubjectCertificateThrow(): void + { + + $this->expectException(OcspCertificateException::class); + $this->expectExceptionMessage("Serial number of subject certificate does not exist"); + + $subject = new X509(); + $subject->setDNProp('id-at-organizationName', 'no serialnumber cert'); + + $issuer = new X509(); + $issuer->setDN($subject->getDN()); + + (new Ocsp)->generateCertificateId($subject, $issuer); + + } + +} \ No newline at end of file diff --git a/tests/_resources/invalid.crt b/tests/_resources/invalid.crt new file mode 100644 index 0000000..f0150e8 --- /dev/null +++ b/tests/_resources/invalid.crt @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDfDCCAi+ertIBAgIEMTIzNDBCBgkqhkiG9w0BAQowNaANMAsGCWCGSAFlAwQV +AaEaMBgGCSqGSIb3DQEBCDALBglghkgBZQMEAgGiAwIBIKMDAgEBMBAxDjAMBgNV +BAoMBXZhbGlkMB4XDTIyMDcwNTA4NDgyMloXDTIzMDgwNTA4NDgyMlowEDEOMAwG +A1UECgwFdmFsaWQwggFXMEIGCSqGSIb3DQEBCjA1oA0wCwYJYIZIAWUDBAIBoRow +GAYJKoZIhvcNAQEIMAsGCWCGSAFlAwQCAaIDAgEgowMCAQEDggEPADCCAQoCggEB +ANPSaz/kyl+bnhFu68YzE6+D3K0bjs8XUn/ByQabqr3TmUcaMoko+FHb99r8MnlD +6mLHegdooIh3vzUY09y+NZfqPbV/ELDjmYCbnObyZDem6Qgz3oOMTzsS4Y+h4VDY +Ie7PCeGJYaDvXVrObEKk6iU3zuPz0UXHkmx+D2smBme/1N4q6XdJ++YlTCiQE/ik +pHYeuQxQiZ0nGB3ZcZ4749zpkymge5khh7hFF/FFy+mBW1QWck3eUwYPaiQW/x5U ++4qqCNeYpOlc0Db27L3KCpXNKvexy9ASgBPl3P2UX23KvaSQEoVXWUGYgna5cq8N +Dn5cyHvnkuwOkPLIVf4kPhECAwEAAaM/MD0wCwYDVR0PBAQDAgEGMA8GA1UdEwEB +/wQFMAMBAf8wHQYDVR0OBBYEFC+3punyNRL3eZuWEGaa05ms9z43MEIGCSqGSIb3 +DQEBCjA1oA0wCwYJYIZIAWUDBAIBoRowGAYJKoZIhvcNAQEIMAsGCWCGSAFlAwQC +AaIDAgEgowMCAQEDggEBALGQMatVnyNNOAgeHcRLHkgrvDNWEh7oJkdCLQwHVV/G +fhH0ErFdfA20OhQITGtng8d8i9HlHcR7W7zwYoj9xp3gLvtEJvQqh4tcJcHOFIpU +9+CC1DdusHuGvnWIzMnVXINP3Zr05ZRkzbvhF8bJPJZ8Gd818VazDvkvf8qQj123 +6TfWJgGZxxYscx2Y8jKWMEq1FoF4IorqYdcpc8Fk1/41IO9cfe5++XgwFnVvOPl9 +nFdv9/Lqqp5iJusdUWH45XPQp8StPwJoYKxAQzdzKVNRdArMNvB4B28psAgdJZyX +rYA0Vmb6MXUvpfWYlQpoxdeuD3JrbOtz1Fsj1pU8niw= +-----END CERTIFICATE----- \ No newline at end of file diff --git a/tests/_resources/ocsp_response.der b/tests/_resources/ocsp_response.der new file mode 100644 index 0000000000000000000000000000000000000000..9bf94116a227ca66fc8a779caef5cb3f0807199b GIT binary patch literal 1579 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 zfromFile(__DIR__.'/../_resources/revoked.crt'); + + $issuerCertificateUrl = $loader->getIssuerCertificateUrl(); + $ocspResponderUrl = $loader->getOcspResponderUrl(); + + $this->assertEquals("318601422914101149693420017798940712227677", $loader->getCert()->getCurrentCert()['tbsCertificate']['serialNumber']); + $this->assertEquals("http://cert.int-x3.letsencrypt.org/", $issuerCertificateUrl); + $this->assertEquals("http://ocsp.int-x3.letsencrypt.org", $ocspResponderUrl); + } + + public function testWhenCertificateLoaderFromStringSuccess(): void + { + $certData = file_get_contents(__DIR__.'/../_resources/revoked.crt'); + $certificate = (new CertificateLoader)->fromString($certData)->getCert(); + $this->assertEquals("318601422914101149693420017798940712227677", $certificate->getCurrentCert()['tbsCertificate']['serialNumber']); + } + + public function testWhenCertificateFileDoNotExistThrows(): void + { + $this->expectException(OcspCertificateException::class); + $this->expectExceptionMessage('Certificate file not found: '.__DIR__.'/../_resources/somecert.crt'); + + (new CertificateLoader)->fromFile(__DIR__.'/../_resources/somecert.crt'); + + } + + public function testWhenCertificateIsInvalidThrows(): void + { + $this->expectException(OcspCertificateException::class); + $this->expectExceptionMessage('Certificate decoding from Base64 or parsing failed for '.__DIR__.'/../_resources/invalid.crt'); + + (new CertificateLoader)->fromFile(__DIR__.'/../_resources/invalid.crt'); + } + + public function testWhenCertificateStringIsNotValidThrows(): void + { + $this->expectException(OcspCertificateException::class); + $this->expectExceptionMessage('Certificate decoding from Base64 or parsing failed'); + + (new CertificateLoader)->fromString("certsource"); + } + + public function testWhenCertificateIsNotLoadedOnIssuerCertificateUrlThrows(): void + { + $this->expectException(OcspCertificateException::class); + $this->expectExceptionMessage('Certificate not loaded'); + + (new CertificateLoader)->getIssuerCertificateUrl(); + } + + public function testWhenCertificateIsNotLoadedOnOcspResponderUrlThrows(): void + { + $this->expectException(OcspCertificateException::class); + $this->expectExceptionMessage('Certificate not loaded'); + + (new CertificateLoader)->getOcspResponderUrl(); + } + + public function testWhenCertificateIsNotLoadedOnGetCertThrows(): void + { + $this->expectException(OcspCertificateException::class); + $this->expectExceptionMessage('Certificate not loaded'); + + (new CertificateLoader)->getCert(); + } + +} \ No newline at end of file