Skip to content
29 changes: 29 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<bouncycastle.version>1.81</bouncycastle.version>
<jackson.version>2.19.1</jackson.version>
<slf4j.version>2.0.17</slf4j.version>
<resilience4j.version>2.3.0</resilience4j.version>
<junit-jupiter.version>5.13.3</junit-jupiter.version>
<assertj.version>3.27.3</assertj.version>
<mockito.version>5.18.0</mockito.version>
Expand Down Expand Up @@ -65,6 +66,34 @@
<artifactId>bcpkix-jdk18on</artifactId>
<version>${bouncycastle.version}</version>
</dependency>
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-all</artifactId>
<version>${resilience4j.version}</version>
<exclusions>
<exclusion>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-bulkhead</artifactId>
</exclusion>
<exclusion>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-cache</artifactId>
</exclusion>
<exclusion>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-ratelimiter</artifactId>
</exclusion>
<exclusion>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-timelimiter</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-vavr</artifactId>
<version>${resilience4j.version}</version>
</dependency>

<dependency>
<groupId>org.junit.jupiter</groupId>
Expand Down
23 changes: 15 additions & 8 deletions src/main/java/eu/webeid/ocsp/OcspCertificateRevocationChecker.java
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
import static eu.webeid.security.util.DateAndTime.requirePositiveDuration;
import static java.util.Objects.requireNonNull;

public final class OcspCertificateRevocationChecker implements CertificateRevocationChecker {
public class OcspCertificateRevocationChecker implements CertificateRevocationChecker {

public static final Duration DEFAULT_TIME_SKEW = Duration.ofMinutes(15);
public static final Duration DEFAULT_THIS_UPDATE_AGE = Duration.ofMinutes(2);
Expand Down Expand Up @@ -131,7 +131,7 @@ public List<RevocationInfo> validateCertificateNotRevoked(X509Certificate subjec
}
LOG.debug("OCSP response received successfully");

verifyOcspResponse(basicResponse, ocspService, certificateId);
verifyOcspResponse(basicResponse, ocspService, certificateId, false, false);
if (ocspService.doesSupportNonce()) {
checkNonce(request, basicResponse, ocspResponderUri);
}
Expand All @@ -144,7 +144,7 @@ public List<RevocationInfo> validateCertificateNotRevoked(X509Certificate subjec
}
}

private void verifyOcspResponse(BasicOCSPResp basicResponse, OcspService ocspService, CertificateID requestCertificateId) throws AuthTokenException, OCSPException, CertificateException, OperatorCreationException {
protected void verifyOcspResponse(BasicOCSPResp basicResponse, OcspService ocspService, CertificateID requestCertificateId, boolean rejectUnknownOcspResponseStatus, boolean allowThisUpdateInPast) throws AuthTokenException, OCSPException, CertificateException, OperatorCreationException {
// The verification algorithm follows RFC 2560, https://www.ietf.org/rfc/rfc2560.txt.
//
// 3.2. Signed Response Acceptance Requirements
Expand Down Expand Up @@ -195,14 +195,14 @@ private void verifyOcspResponse(BasicOCSPResp basicResponse, OcspService ocspSer
// be available about the status of the certificate (nextUpdate) is
// greater than the current time.

OcspResponseValidator.validateCertificateStatusUpdateTime(certStatusResponse, allowedOcspResponseTimeSkew, maxOcspResponseThisUpdateAge, ocspService.getAccessLocation());
OcspResponseValidator.validateCertificateStatusUpdateTime(certStatusResponse, allowedOcspResponseTimeSkew, maxOcspResponseThisUpdateAge, ocspService.getAccessLocation(), allowThisUpdateInPast);

// Now we can accept the signed response as valid and validate the certificate status.
OcspResponseValidator.validateSubjectCertificateStatus(certStatusResponse, ocspService.getAccessLocation());
OcspResponseValidator.validateSubjectCertificateStatus(certStatusResponse, ocspService.getAccessLocation(), rejectUnknownOcspResponseStatus);
LOG.debug("OCSP check result is GOOD");
}

private static void checkNonce(OCSPReq request, BasicOCSPResp response, URI ocspResponderUri) throws UserCertificateOCSPCheckFailedException {
protected static void checkNonce(OCSPReq request, BasicOCSPResp response, URI ocspResponderUri) throws UserCertificateOCSPCheckFailedException {
final Extension requestNonce = request.getExtension(OCSPObjectIdentifiers.id_pkix_ocsp_nonce);
final Extension responseNonce = response.getExtension(OCSPObjectIdentifiers.id_pkix_ocsp_nonce);
if (requestNonce == null || responseNonce == null) {
Expand All @@ -215,14 +215,14 @@ private static void checkNonce(OCSPReq request, BasicOCSPResp response, URI ocsp
}
}

private static CertificateID getCertificateId(X509Certificate subjectCertificate, X509Certificate issuerCertificate) throws CertificateEncodingException, IOException, OCSPException {
protected static CertificateID getCertificateId(X509Certificate subjectCertificate, X509Certificate issuerCertificate) throws CertificateEncodingException, IOException, OCSPException {
final BigInteger serial = subjectCertificate.getSerialNumber();
final DigestCalculator digestCalculator = DigestCalculatorImpl.sha1();
return new CertificateID(digestCalculator,
new X509CertificateHolder(issuerCertificate.getEncoded()), serial);
}

private static String ocspStatusToString(int status) {
protected static String ocspStatusToString(int status) {
return switch (status) {
case OCSPResp.MALFORMED_REQUEST -> "malformed request";
case OCSPResp.INTERNAL_ERROR -> "internal error";
Expand All @@ -233,4 +233,11 @@ private static String ocspStatusToString(int status) {
};
}

protected OcspClient getOcspClient() {
return ocspClient;
}

protected OcspServiceProvider getOcspServiceProvider() {
return ocspServiceProvider;
}
}
4 changes: 2 additions & 2 deletions src/main/java/eu/webeid/ocsp/client/OcspClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,14 @@

package eu.webeid.ocsp.client;

import eu.webeid.ocsp.exceptions.OCSPClientException;
import org.bouncycastle.cert.ocsp.OCSPReq;
import org.bouncycastle.cert.ocsp.OCSPResp;

import java.io.IOException;
import java.net.URI;

public interface OcspClient {

OCSPResp request(URI url, OCSPReq request) throws IOException;
OCSPResp request(URI url, OCSPReq request) throws OCSPClientException;

}
30 changes: 23 additions & 7 deletions src/main/java/eu/webeid/ocsp/client/OcspClientImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

package eu.webeid.ocsp.client;

import eu.webeid.ocsp.exceptions.OCSPClientException;
import org.bouncycastle.cert.ocsp.OCSPReq;
import org.bouncycastle.cert.ocsp.OCSPResp;
import org.slf4j.Logger;
Expand Down Expand Up @@ -62,15 +63,21 @@ public static OcspClient build(Duration ocspRequestTimeout) {
* @param uri OCSP server URL
* @param ocspReq OCSP request
* @return OCSP response from the server
* @throws IOException if the request could not be executed due to cancellation, a connectivity problem or timeout,
* @throws OCSPClientException if the request could not be executed due to cancellation, a connectivity problem or timeout,
* or if the response status is not successful, or if response has wrong content type.
*/
@Override
public OCSPResp request(URI uri, OCSPReq ocspReq) throws IOException {
public OCSPResp request(URI uri, OCSPReq ocspReq) throws OCSPClientException {
byte[] encodedOcspReq;
try {
encodedOcspReq = ocspReq.getEncoded();
} catch (IOException e) {
throw new OCSPClientException(e);
}
final HttpRequest request = HttpRequest.newBuilder()
.uri(uri)
.header(CONTENT_TYPE, OCSP_REQUEST_TYPE)
.POST(HttpRequest.BodyPublishers.ofByteArray(ocspReq.getEncoded()))
.POST(HttpRequest.BodyPublishers.ofByteArray(encodedOcspReq))
.timeout(ocspRequestTimeout)
.build();

Expand All @@ -79,19 +86,28 @@ public OCSPResp request(URI uri, OCSPReq ocspReq) throws IOException {
response = httpClient.send(request, HttpResponse.BodyHandlers.ofByteArray());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IOException("Interrupted while sending OCSP request", e);
throw new OCSPClientException("Interrupted while sending OCSP request", e);
} catch (IOException e) {
throw new OCSPClientException(e);
}

if (response.statusCode() != 200) {
throw new IOException("OCSP request was not successful, response: " + response);
throw new OCSPClientException("OCSP request was not successful", response.body(), response.statusCode());
} else {
LOG.debug("OCSP response: {}", response);
}
final String contentType = response.headers().firstValue(CONTENT_TYPE).orElse("");
if (!contentType.startsWith(OCSP_RESPONSE_TYPE)) {
throw new IOException("OCSP response content type is not " + OCSP_RESPONSE_TYPE);
throw new OCSPClientException("OCSP response content type is not " + OCSP_RESPONSE_TYPE);
}

OCSPResp ocspResp;
try {
ocspResp = new OCSPResp(response.body());
} catch (IOException e) {
throw new OCSPClientException(e);
}
return new OCSPResp(response.body());
return ocspResp;
}

public OcspClientImpl(HttpClient httpClient, Duration ocspRequestTimeout) {
Expand Down
59 changes: 59 additions & 0 deletions src/main/java/eu/webeid/ocsp/exceptions/OCSPClientException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright (c) 2020-2025 Estonian Information System Authority
*
* 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.
*/

package eu.webeid.ocsp.exceptions;

public class OCSPClientException extends RuntimeException {

private byte[] responseBody;

private Integer statusCode;

public OCSPClientException() {
}

public OCSPClientException(String message) {
super(message);
}

public OCSPClientException(Throwable cause) {
super(cause);
}

public OCSPClientException(String message, Throwable cause) {
super(message, cause);
}

public OCSPClientException(String message, byte[] responseBody, int statusCode) {
super(message);
this.responseBody = responseBody;
this.statusCode = statusCode;
}

public byte[] getResponseBody() {
return responseBody;
}

public Integer getStatusCode() {
return statusCode;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@
*/
public class UserCertificateOCSPCheckFailedException extends AuthTokenException {

public UserCertificateOCSPCheckFailedException() {
super("User certificate revocation check has failed");
}

public UserCertificateOCSPCheckFailedException(Throwable cause, URI ocspResponderUri) {
super(withResponderUri("User certificate revocation check has failed", ocspResponderUri), cause);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@
*/
public class UserCertificateRevokedException extends AuthTokenException {

public UserCertificateRevokedException() {
super("User certificate has been revoked");
}

public UserCertificateRevokedException(URI ocspResponderUri) {
super(withResponderUri("User certificate has been revoked", ocspResponderUri));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright (c) 2020-2025 Estonian Information System Authority
*
* 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.
*/

package eu.webeid.ocsp.exceptions;

import eu.webeid.security.exceptions.AuthTokenException;

import java.net.URI;

import static eu.webeid.ocsp.exceptions.OcspResponderUriMessage.withResponderUri;

public class UserCertificateUnknownException extends AuthTokenException {

public UserCertificateUnknownException(String msg, URI ocspResponderUri) {
super(withResponderUri("User certificate status is unknown: " + msg, ocspResponderUri));
}
}
14 changes: 9 additions & 5 deletions src/main/java/eu/webeid/ocsp/protocol/OcspResponseValidator.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
import eu.webeid.ocsp.exceptions.OCSPCertificateException;
import eu.webeid.ocsp.exceptions.UserCertificateOCSPCheckFailedException;
import eu.webeid.ocsp.exceptions.UserCertificateRevokedException;
import eu.webeid.ocsp.exceptions.UserCertificateUnknownException;
import eu.webeid.security.exceptions.AuthTokenException;
import eu.webeid.security.util.DateAndTime;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.ocsp.BasicOCSPResp;
Expand Down Expand Up @@ -77,7 +79,7 @@ public static void validateResponseSignature(BasicOCSPResp basicResponse, X509Ce
}
}

public static void validateCertificateStatusUpdateTime(SingleResp certStatusResponse, Duration allowedTimeSkew, Duration maxThisupdateAge, URI ocspResponderUri) throws UserCertificateOCSPCheckFailedException {
public static void validateCertificateStatusUpdateTime(SingleResp certStatusResponse, Duration allowedTimeSkew, Duration maxThisupdateAge, URI ocspResponderUri, boolean allowThisUpdateInPast) throws UserCertificateOCSPCheckFailedException {
// From RFC 2560, https://www.ietf.org/rfc/rfc2560.txt:
// 4.2.2. Notes on OCSP Responses
// 4.2.2.1. Time
Expand All @@ -98,7 +100,7 @@ public static void validateCertificateStatusUpdateTime(SingleResp certStatusResp
"thisUpdate '" + thisUpdate + "' is too far in the future, " +
"latest allowed: '" + latestAcceptableTimeSkew + "'", ocspResponderUri);
}
if (thisUpdate.isBefore(minimumValidThisUpdateTime)) {
if (!allowThisUpdateInPast && thisUpdate.isBefore(minimumValidThisUpdateTime)) {
throw new UserCertificateOCSPCheckFailedException(ERROR_PREFIX +
"thisUpdate '" + thisUpdate + "' is too old, " +
"minimum time allowed: '" + minimumValidThisUpdateTime + "'", ocspResponderUri);
Expand All @@ -118,7 +120,7 @@ public static void validateCertificateStatusUpdateTime(SingleResp certStatusResp
}
}

public static void validateSubjectCertificateStatus(SingleResp certStatusResponse, URI ocspResponderUri) throws UserCertificateRevokedException {
public static void validateSubjectCertificateStatus(SingleResp certStatusResponse, URI ocspResponderUri, boolean rejectUnknownOcspResponseStatus) throws AuthTokenException {
final CertificateStatus status = certStatusResponse.getCertStatus();
if (status == null) {
return;
Expand All @@ -128,9 +130,11 @@ public static void validateSubjectCertificateStatus(SingleResp certStatusRespons
new UserCertificateRevokedException("Revocation reason: " + revokedStatus.getRevocationReason(), ocspResponderUri) :
new UserCertificateRevokedException(ocspResponderUri));
} else if (status instanceof UnknownStatus) {
throw new UserCertificateRevokedException("Unknown status", ocspResponderUri);
throw rejectUnknownOcspResponseStatus ? new UserCertificateUnknownException("Unknown status", ocspResponderUri)
: new UserCertificateRevokedException("Unknown status", ocspResponderUri);
} else {
throw new UserCertificateRevokedException("Status is neither good, revoked nor unknown", ocspResponderUri);
throw rejectUnknownOcspResponseStatus ? new UserCertificateUnknownException("Status is neither good, revoked nor unknown", ocspResponderUri)
: new UserCertificateRevokedException("Status is neither good, revoked nor unknown", ocspResponderUri);
}
}

Expand Down
Loading