Skip to content

Commit 5f15f62

Browse files
Guido Gröönmrts
authored andcommitted
WE2-687 Replaced OCSP dependency
WE2-687 Implemented SubjectCertificateNotRevokedValidator and added unit tests WE2-687 Updated OcspClient, OcspRequestBuilder, OcspResponseValidator and OcspServiceProvider WE2-687 Added OcspResponseValidator unit tests
1 parent ecb24dd commit 5f15f62

18 files changed

+470
-104
lines changed

.gitattributes

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Ignore all test and documentation with "export-ignore".
2+
/.gitattributes export-ignore
3+
/.gitignore export-ignore
4+
/phpunit.xml.dist export-ignore
5+
/tests export-ignore

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,6 @@ vendor
22
composer.lock
33
.DS_Store
44
.phpunit.result.cache
5-
web-eid-authtoken-validation-php.log
5+
web-eid-authtoken-validation-php.log
6+
build
7+
phpunit.xml

composer.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,15 @@
2828
"web_eid\\web_eid_authtoken_validation_php\\": ["tests"]
2929
}
3030
},
31+
"repositories": [
32+
{
33+
"type": "vcs",
34+
"url": "https://github.com/web-eid/ocsp-php.git"
35+
}
36+
],
3137
"require": {
3238
"lyquidity/requester": "^1.0",
33-
"phpseclib/phpseclib": "3.0.14"
39+
"phpseclib/phpseclib": "3.0.14",
40+
"web_eid/ocsp_php": "dev-main"
3441
}
3542
}

phpunit.xml renamed to phpunit.xml.dist

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@
44
<include>
55
<directory suffix=".php">src/</directory>
66
</include>
7+
<report>
8+
<clover outputFile="build/logs/clover.xml" />
9+
<html outputDirectory="build/coverage" />
10+
<text outputFile="build/coverage.txt" />
11+
</report>
712
</coverage>
813
<testsuites>
914
<testsuite name="Web-eID PHP Test Suite">

src/util/DateAndTime.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,14 @@ public static function requirePositiveDuration(int $duration, string $fieldName)
6767
}
6868
}
6969

70+
public static function toUtcString(?DateTime $date): string
71+
{
72+
if (is_null($date)) {
73+
return 'null';
74+
}
75+
return ((clone $date)->setTimezone(new DateTimeZone('UTC')))->format('Y-m-d H:i:s e');
76+
}
77+
7078
}
7179

7280
final class DefaultClock

src/validator/certvalidators/SubjectCertificateNotRevokedValidator.php

Lines changed: 87 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,19 @@
2424

2525
namespace web_eid\web_eid_authtoken_validation_php\validator\certvalidators;
2626

27-
use lyquidity\OCSP\CertificateInfo;
28-
use lyquidity\OCSP\CertificateLoader;
29-
use lyquidity\OCSP\Response;
3027
use phpseclib3\File\X509;
3128
use web_eid\web_eid_authtoken_validation_php\util\Log;
3229
use web_eid\web_eid_authtoken_validation_php\validator\ocsp\OcspClient;
3330
use web_eid\web_eid_authtoken_validation_php\validator\ocsp\OcspServiceProvider;
3431
use web_eid\web_eid_authtoken_validation_php\validator\ocsp\OcspRequestBuilder;
3532
use web_eid\web_eid_authtoken_validation_php\validator\ocsp\OcspResponseValidator;
3633
use Throwable;
34+
use web_eid\ocsp_php\Ocsp;
35+
use web_eid\ocsp_php\OcspBasicResponse;
36+
use web_eid\ocsp_php\OcspRequest;
37+
use web_eid\ocsp_php\OcspResponse;
3738
use web_eid\web_eid_authtoken_validation_php\exceptions\UserCertificateOCSPCheckFailedException;
39+
use web_eid\web_eid_authtoken_validation_php\validator\ocsp\service\OcspService;
3840

3941
final class SubjectCertificateNotRevokedValidator implements SubjectCertificateValidator
4042
{
@@ -62,45 +64,100 @@ public function validate(X509 $subjectCertificate): void
6264
$this->logger->debug("Disabling OCSP nonce extension");
6365
}
6466

65-
$certificateLoader = new CertificateLoader();
67+
$certificateId = (new Ocsp())->generateCertificateId($subjectCertificate, $this->trustValidator->getSubjectCertificateIssuerCertificate());
68+
$request = (new OcspRequestBuilder())->withCertificateId($certificateId)->enableOcspNonce($ocspService->doesSupportNonce())->build();
6669

67-
// PEM encoded
68-
$certPem = $subjectCertificate->saveX509($subjectCertificate->getCurrentCert(), X509::FORMAT_PEM);
69-
$iss = $this->trustValidator->getSubjectCertificateIssuerCertificate();
70-
$issuerPem = $iss->saveX509($iss->getCurrentCert(), X509::FORMAT_PEM);
71-
72-
$certificate = $certificateLoader->fromString($certPem);
73-
$issuerCertificate = $certificateLoader->fromString($issuerPem);
74-
75-
// Extract the relevant data from the two certificates
76-
$certificateInfo = new CertificateInfo();
77-
$requestInfo = $certificateInfo->extractRequestInfo($certificate, $issuerCertificate);
78-
79-
$request = (new OcspRequestBuilder())
80-
->withCertificateId($requestInfo)
81-
->enableOcspNonce($ocspService->doesSupportNonce())
82-
->build();
83-
84-
8570
$this->logger->debug("Sending OCSP request");
8671

87-
// TODO
88-
// Replace faulty OCSP library
89-
return;
90-
91-
$response = $this->ocspClient->request($ocspService->getAccessLocation(), $request);
92-
$this->verifyOcspResponse($response);
93-
//$ocspResponderUrl = $certificateLoader->extractOcspResponderUrl($certificate);
72+
$response = $this->ocspClient->request($ocspService->getAccessLocation(), $request->getEncodeDer());
73+
74+
// When status is not successful
75+
if ($response->getStatus() != "successful") {
76+
throw new UserCertificateOCSPCheckFailedException("OCSP response status: " . $response->getStatus());
77+
}
78+
79+
$this->verifyOcspResponse($response, $ocspService, $certificateId);
80+
81+
if ($ocspService->doesSupportNonce()) {
82+
$this->checkNonce($request, $response->getBasicResponse());
83+
}
9484

9585
} catch (Throwable $e) {
9686
throw new UserCertificateOCSPCheckFailedException("Exception: " . $e->getMessage(), $e);
9787
}
9888

9989
}
10090

101-
private function verifyOcspResponse(Response $response): void
91+
// Todo, check ocspService
92+
private function verifyOcspResponse(OcspResponse $response, OcspService $ocspService, array $requestCertificateId): void
10293
{
94+
$basicResponse = $response->getBasicResponse();
95+
96+
// The verification algorithm follows RFC 2560, https://www.ietf.org/rfc/rfc2560.txt.
97+
//
98+
// 3.2. Signed Response Acceptance Requirements
99+
// Prior to accepting a signed response for a particular certificate as
100+
// valid, OCSP clients SHALL confirm that:
101+
//
102+
// 1. The certificate identified in a received response corresponds to
103+
// the certificate that was identified in the corresponding request.
104+
105+
// As we sent the request for only a single certificate, we expect only a single response.
106+
if (count($basicResponse->getResponses()) != 1) {
107+
throw new UserCertificateOCSPCheckFailedException("OCSP response must contain one response, received " . count($basicResponse->getResponses()) . " responses instead");
108+
}
109+
110+
if ($requestCertificateId != $basicResponse->getCertID()) {
111+
throw new UserCertificateOCSPCheckFailedException("OCSP responded with certificate ID that differs from the requested ID");
112+
}
113+
114+
// 2. The signature on the response is valid.
115+
116+
// We assume that the responder includes its certificate in the certs field of the response
117+
// that helps us to verify it. According to RFC 2560 this field is optional, but including it
118+
// is standard practice.
119+
120+
if (count($basicResponse->getCertificates()) < 1) {
121+
throw new UserCertificateOCSPCheckFailedException("OCSP response must contain the responder certificate, but none was provided");
122+
}
123+
124+
// The first certificate is the responder certificate, other certificates, if given, are the certificate's chain.
125+
$responderCert = $basicResponse->getCertificates()[0];
126+
127+
OcspResponseValidator::validateResponseSignature($basicResponse, $responderCert);
128+
129+
// 3. The identity of the signer matches the intended recipient of the
130+
// request.
131+
//
132+
// 4. The signer is currently authorized to provide a response for the
133+
// certificate in question.
134+
135+
$producedAt = $basicResponse->getProducedAt();
136+
$ocspService->validateResponderCertificate($responderCert, $producedAt);
137+
138+
// 5. The time at which the status being indicated is known to be
139+
// correct (thisUpdate) is sufficiently recent.
140+
//
141+
// 6. When available, the time at or before which newer information will
142+
// be available about the status of the certificate (nextUpdate) is
143+
// greater than the current time.
144+
145+
OcspResponseValidator::validateCertificateStatusUpdateTime($basicResponse, $producedAt);
146+
147+
// Now we can accept the signed response as valid and validate the certificate status.
103148
OcspResponseValidator::validateSubjectCertificateStatus($response);
149+
$this->logger->debug("OCSP check result is GOOD");
150+
151+
}
152+
153+
private static function checkNonce(OcspRequest $request, OcspBasicResponse $basicResponse): void
154+
{
155+
$requestNonce = $request->getNonceExtension();
156+
$responseNonce = $basicResponse->getNonceExtension();
157+
158+
if ($requestNonce != $responseNonce) {
159+
throw new UserCertificateOCSPCheckFailedException("OCSP request and response nonces differ, possible replay attack");
160+
}
104161
}
105162

106163
}

src/validator/ocsp/OcspClient.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@
2525
namespace web_eid\web_eid_authtoken_validation_php\validator\ocsp;
2626

2727
use web_eid\web_eid_authtoken_validation_php\util\Uri;
28-
use lyquidity\OCSP\Response;
28+
use web_eid\ocsp_php\OcspResponse;
2929

3030
interface OcspClient
3131
{
32-
public function request(Uri $url, string $requestBody): Response;
32+
public function request(Uri $url, string $requestBody): OcspResponse;
3333
}

src/validator/ocsp/OcspClientImpl.php

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,51 +24,61 @@
2424

2525
namespace web_eid\web_eid_authtoken_validation_php\validator\ocsp;
2626

27-
use lyquidity\OCSP\Ocsp;
28-
use lyquidity\OCSP\Response;
2927
use web_eid\web_eid_authtoken_validation_php\exceptions\UserCertificateOCSPCheckFailedException;
28+
use web_eid\web_eid_authtoken_validation_php\util\Log;
3029
use web_eid\web_eid_authtoken_validation_php\util\Uri;
30+
use web_eid\ocsp_php\OcspResponse;
3131

3232
class OcspClientImpl implements OcspClient
3333
{
3434

3535
private const OCSP_REQUEST_TYPE = "application/ocsp-request";
3636
private const OCSP_RESPONSE_TYPE = "application/ocsp-response";
3737
private int $requestTimeout;
38+
private Log $logger;
3839

3940
public function __construct(int $ocspRequestTimeout)
4041
{
4142
$this->requestTimeout = $ocspRequestTimeout;
43+
$this->logger = Log::getLogger(self::class);
4244
}
4345

4446
public static function build(int $ocspRequestTimeout): OcspClient
4547
{
4648
return new OcspClientImpl($ocspRequestTimeout);
4749
}
4850

49-
public function request(Uri $uri, string $ocspReq): Response
51+
public function request(Uri $uri, string $encodedOcspRequest): OcspResponse
5052
{
5153
$curl = curl_init();
5254
curl_setopt($curl, CURLOPT_URL, $uri->getUrl());
5355
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
56+
curl_setopt($curl, CURLOPT_FAILONERROR, true);
5457
curl_setopt($curl, CURLOPT_POST, true);
5558
curl_setopt($curl, CURLOPT_HTTPHEADER, ["Content-Type: " . self::OCSP_REQUEST_TYPE]);
56-
curl_setopt($curl, CURLOPT_POSTFIELDS, $ocspReq);
59+
curl_setopt($curl, CURLOPT_POSTFIELDS, $encodedOcspRequest);
5760
curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, $this->requestTimeout);
5861
curl_setopt($curl, CURLOPT_TIMEOUT, $this->requestTimeout);
5962
$result = curl_exec($curl);
63+
64+
if (curl_errno($curl)) {
65+
throw new UserCertificateOCSPCheckFailedException(curl_error($curl));
66+
}
67+
6068
$info = curl_getinfo($curl);
6169
if ($info["http_code"] !== 200) {
6270
throw new UserCertificateOCSPCheckFailedException("OCSP request was not successful, response: " + $result);
6371
}
72+
73+
$response = new OcspResponse($result);
74+
75+
$responseJson = json_encode($response->getResponse(), JSON_INVALID_UTF8_IGNORE);
76+
$this->logger->debug("OCSP response: ", json_encode($responseJson));
6477

6578
if ($info["content_type"] !== self::OCSP_RESPONSE_TYPE) {
6679
throw new UserCertificateOCSPCheckFailedException("OCSP response content type is not ". self::OCSP_RESPONSE_TYPE);
6780
}
6881

69-
$ocsp = new Ocsp();
70-
// Decode the raw response from the OCSP Responder
71-
$response = $ocsp->decodeOcspResponseSingle($result);
7282
return $response;
7383
}
7484
}

src/validator/ocsp/OcspRequestBuilder.php

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,14 @@
2626

2727
namespace web_eid\web_eid_authtoken_validation_php\validator\ocsp;
2828

29-
use lyquidity\OCSP\CertificateInfo;
30-
use lyquidity\OCSP\Ocsp;
31-
use lyquidity\OCSP\Request;
29+
use web_eid\ocsp_php\OcspRequest;
3230

3331
final class OcspRequestBuilder
3432
{
3533

3634
private $secureRandom;
3735
private bool $ocspNonceEnabled = true;
38-
private $certificateId;
36+
private array $certificateId;
3937

4038
public function __construct()
4139
{
@@ -44,9 +42,9 @@ public function __construct()
4442
};
4543
}
4644

47-
public function withCertificateId(Request $certInfo): OcspRequestBuilder
45+
public function withCertificateId(array $certificateId): OcspRequestBuilder
4846
{
49-
$this->certificateId = $certInfo;
47+
$this->certificateId = $certificateId;
5048
return $this;
5149
}
5250

@@ -56,10 +54,17 @@ public function enableOcspNonce(bool $ocspNonceEnabled): OcspRequestBuilder
5654
return $this;
5755
}
5856

59-
public function build()
57+
public function build(): OcspRequest
6058
{
61-
$ocsp = new Ocsp();
62-
return $ocsp->buildOcspRequestBodySingle($this->certificateId);
59+
$ocspRequest = new OcspRequest();
60+
$ocspRequest->addCertificateId($this->certificateId);
61+
62+
if ($this->ocspNonceEnabled) {
63+
$nonceBytes = call_user_func($this->secureRandom, 8);
64+
$ocspRequest->addNonceExtension($nonceBytes);
65+
}
66+
67+
return $ocspRequest;
6368
}
6469

6570
private function generateSecureRandom(int $nounce_length): string {

0 commit comments

Comments
 (0)