From 2e783f34bd0e304438a0d754afa391a17c5cd295 Mon Sep 17 00:00:00 2001 From: Mart Somermaa Date: Fri, 12 Dec 2025 17:37:47 +0200 Subject: [PATCH] Use plaform OCSP implementation by default, move custom OCSP implementation to eu.webeid.ocsp and make it optional WE2-1030 Signed-off-by: Mart Somermaa --- .github/workflows/coverity-analysis.yml | 2 +- .github/workflows/maven-build.yml | 2 +- .github/workflows/maven-deploy.yml | 2 +- pom.xml | 4 +- ...aultOcspCertificateRevocationChecker.java} | 125 ++++++++------ .../ocsp => ocsp/client}/OcspClient.java | 2 +- .../ocsp => ocsp/client}/OcspClientImpl.java | 10 +- .../exceptions/OCSPCertificateException.java | 4 +- ...erCertificateOCSPCheckFailedException.java | 23 ++- .../UserCertificateRevokedException.java | 19 ++- .../protocol}/DigestCalculatorImpl.java | 2 +- .../protocol}/OcspRequestBuilder.java | 2 +- .../protocol}/OcspResponseValidator.java | 39 ++--- .../ocsp => ocsp/protocol}/OcspUrl.java | 2 +- .../ocsp/service/AiaOcspService.java | 21 ++- .../service/AiaOcspServiceConfiguration.java | 2 +- .../ocsp/service/DesignatedOcspService.java | 4 +- .../DesignatedOcspServiceConfiguration.java | 6 +- .../ocsp/service/OcspService.java | 2 +- .../service}/OcspServiceProvider.java | 7 +- .../security/authtoken/WebEidAuthToken.java | 45 +---- .../certificate/CertificateValidator.java | 161 +++++++++++++++++- .../eu/webeid/security/util/DateAndTime.java | 3 +- .../AuthTokenValidationConfiguration.java | 117 +++++++------ .../validator/AuthTokenValidator.java | 2 +- .../validator/AuthTokenValidatorBuilder.java | 113 +++--------- .../validator/AuthTokenValidatorImpl.java | 81 ++++----- .../security/validator/ValidationInfo.java | 15 ++ .../SubjectCertificateTrustedValidator.java | 78 --------- .../SubjectCertificateValidatorBatch.java | 7 - .../OcspCertificateRevocationChecker.java | 34 ++++ .../revocationcheck/RevocationInfo.java | 34 ++++ .../revocationcheck/RevocationMode.java | 5 + ...OcspCertificateRevocationCheckerTest.java} | 150 +++++++++------- .../client}/OcspClientOverrideTest.java | 29 +++- .../protocol}/OcspResponseValidatorTest.java | 24 +-- .../ocsp => ocsp/protocol}/OcspUrlTest.java | 4 +- .../service}/OcspServiceMaker.java | 7 +- .../service}/OcspServiceProviderTest.java | 32 +++- .../testutil/AuthTokenValidators.java | 24 +-- .../validator/AuthTokenCertificateTest.java | 13 +- .../validator/AuthTokenSignatureTest.java | 2 +- .../AuthTokenSignatureValidatorTest.java | 8 +- .../AuthTokenValidatorBuilderTest.java | 26 +-- 44 files changed, 706 insertions(+), 588 deletions(-) rename src/main/java/eu/webeid/{security/validator/certvalidators/SubjectCertificateNotRevokedValidator.java => ocsp/DefaultOcspCertificateRevocationChecker.java} (66%) rename src/main/java/eu/webeid/{security/validator/ocsp => ocsp/client}/OcspClient.java (96%) rename src/main/java/eu/webeid/{security/validator/ocsp => ocsp/client}/OcspClientImpl.java (90%) rename src/main/java/eu/webeid/{security => ocsp}/exceptions/OCSPCertificateException.java (93%) rename src/main/java/eu/webeid/{security => ocsp}/exceptions/UserCertificateOCSPCheckFailedException.java (76%) rename src/main/java/eu/webeid/{security => ocsp}/exceptions/UserCertificateRevokedException.java (74%) rename src/main/java/eu/webeid/{security/validator/ocsp => ocsp/protocol}/DigestCalculatorImpl.java (98%) rename src/main/java/eu/webeid/{security/validator/ocsp => ocsp/protocol}/OcspRequestBuilder.java (98%) rename src/main/java/eu/webeid/{security/validator/ocsp => ocsp/protocol}/OcspResponseValidator.java (83%) rename src/main/java/eu/webeid/{security/validator/ocsp => ocsp/protocol}/OcspUrl.java (98%) rename src/main/java/eu/webeid/{security/validator => }/ocsp/service/AiaOcspService.java (84%) rename src/main/java/eu/webeid/{security/validator => }/ocsp/service/AiaOcspServiceConfiguration.java (97%) rename src/main/java/eu/webeid/{security/validator => }/ocsp/service/DesignatedOcspService.java (96%) rename src/main/java/eu/webeid/{security/validator => }/ocsp/service/DesignatedOcspServiceConfiguration.java (95%) rename src/main/java/eu/webeid/{security/validator => }/ocsp/service/OcspService.java (96%) rename src/main/java/eu/webeid/{security/validator/ocsp => ocsp/service}/OcspServiceProvider.java (87%) create mode 100644 src/main/java/eu/webeid/security/validator/ValidationInfo.java delete mode 100644 src/main/java/eu/webeid/security/validator/certvalidators/SubjectCertificateTrustedValidator.java create mode 100644 src/main/java/eu/webeid/security/validator/revocationcheck/OcspCertificateRevocationChecker.java create mode 100644 src/main/java/eu/webeid/security/validator/revocationcheck/RevocationInfo.java create mode 100644 src/main/java/eu/webeid/security/validator/revocationcheck/RevocationMode.java rename src/test/java/eu/webeid/{security/validator/certvalidators/SubjectCertificateNotRevokedValidatorTest.java => ocsp/DefaultOcspCertificateRevocationCheckerTest.java} (71%) rename src/test/java/eu/webeid/{security/validator/ocsp => ocsp/client}/OcspClientOverrideTest.java (66%) rename src/test/java/eu/webeid/{security/validator/ocsp => ocsp/protocol}/OcspResponseValidatorTest.java (87%) rename src/test/java/eu/webeid/{security/validator/ocsp => ocsp/protocol}/OcspUrlTest.java (96%) rename src/test/java/eu/webeid/{security/testutil => ocsp/service}/OcspServiceMaker.java (93%) rename src/test/java/eu/webeid/{security/validator/ocsp => ocsp/service}/OcspServiceProviderTest.java (72%) diff --git a/.github/workflows/coverity-analysis.yml b/.github/workflows/coverity-analysis.yml index f8035d46..e3d5d574 100644 --- a/.github/workflows/coverity-analysis.yml +++ b/.github/workflows/coverity-analysis.yml @@ -20,7 +20,7 @@ jobs: - uses: actions/setup-java@v4 with: distribution: zulu - java-version: 11 + java-version: 17 - name: Cache Maven packages uses: actions/cache@v4 diff --git a/.github/workflows/maven-build.yml b/.github/workflows/maven-build.yml index 6528af68..d745a84b 100644 --- a/.github/workflows/maven-build.yml +++ b/.github/workflows/maven-build.yml @@ -20,7 +20,7 @@ jobs: - uses: actions/setup-java@v4 with: distribution: zulu - java-version: 11 + java-version: 17 - name: Cache Maven packages uses: actions/cache@v4 diff --git a/.github/workflows/maven-deploy.yml b/.github/workflows/maven-deploy.yml index 29a316f5..b4a9104d 100644 --- a/.github/workflows/maven-deploy.yml +++ b/.github/workflows/maven-deploy.yml @@ -14,7 +14,7 @@ jobs: - uses: actions/setup-java@v4 with: distribution: zulu - java-version: 11 + java-version: 17 - name: Cache Maven packages uses: actions/cache@v4 diff --git a/pom.xml b/pom.xml index 2816da7c..c5906759 100644 --- a/pom.xml +++ b/pom.xml @@ -5,13 +5,13 @@ 4.0.0 authtoken-validation eu.webeid.security - 3.2.0 + 4.0.0-SNAPSHOT jar authtoken-validation Web eID authentication token validation library for Java - 11 + 17 0.12.6 1.81 2.19.1 diff --git a/src/main/java/eu/webeid/security/validator/certvalidators/SubjectCertificateNotRevokedValidator.java b/src/main/java/eu/webeid/ocsp/DefaultOcspCertificateRevocationChecker.java similarity index 66% rename from src/main/java/eu/webeid/security/validator/certvalidators/SubjectCertificateNotRevokedValidator.java rename to src/main/java/eu/webeid/ocsp/DefaultOcspCertificateRevocationChecker.java index c460d405..9b2cc436 100644 --- a/src/main/java/eu/webeid/security/validator/certvalidators/SubjectCertificateNotRevokedValidator.java +++ b/src/main/java/eu/webeid/ocsp/DefaultOcspCertificateRevocationChecker.java @@ -20,17 +20,19 @@ * SOFTWARE. */ -package eu.webeid.security.validator.certvalidators; +package eu.webeid.ocsp; +import eu.webeid.ocsp.client.OcspClient; +import eu.webeid.ocsp.protocol.DigestCalculatorImpl; +import eu.webeid.ocsp.protocol.OcspRequestBuilder; +import eu.webeid.ocsp.protocol.OcspResponseValidator; import eu.webeid.security.exceptions.AuthTokenException; -import eu.webeid.security.exceptions.UserCertificateOCSPCheckFailedException; +import eu.webeid.ocsp.exceptions.UserCertificateOCSPCheckFailedException; import eu.webeid.security.util.DateAndTime; -import eu.webeid.security.validator.ocsp.DigestCalculatorImpl; -import eu.webeid.security.validator.ocsp.OcspClient; -import eu.webeid.security.validator.ocsp.OcspRequestBuilder; -import eu.webeid.security.validator.ocsp.OcspResponseValidator; -import eu.webeid.security.validator.ocsp.OcspServiceProvider; -import eu.webeid.security.validator.ocsp.service.OcspService; +import eu.webeid.ocsp.service.OcspServiceProvider; +import eu.webeid.ocsp.service.OcspService; +import eu.webeid.security.validator.revocationcheck.OcspCertificateRevocationChecker; +import eu.webeid.security.validator.revocationcheck.RevocationInfo; import org.bouncycastle.asn1.ocsp.OCSPObjectIdentifiers; import org.bouncycastle.asn1.ocsp.OCSPResponseStatus; import org.bouncycastle.asn1.x509.Extension; @@ -49,52 +51,64 @@ import java.io.IOException; import java.math.BigInteger; +import java.net.URI; import java.security.Security; import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.time.Duration; import java.util.Date; -import java.util.Objects; +import java.util.List; +import java.util.Map; -public final class SubjectCertificateNotRevokedValidator { +import static eu.webeid.security.util.DateAndTime.requirePositiveDuration; +import static java.util.Objects.requireNonNull; - private static final Logger LOG = LoggerFactory.getLogger(SubjectCertificateNotRevokedValidator.class); +public final class DefaultOcspCertificateRevocationChecker implements OcspCertificateRevocationChecker { + + public static final Duration DEFAULT_TIME_SKEW = Duration.ofMinutes(15); + public static final Duration DEFAULT_THIS_UPDATE_AGE = Duration.ofMinutes(2); + + private static final Logger LOG = LoggerFactory.getLogger(DefaultOcspCertificateRevocationChecker.class); - private final SubjectCertificateTrustedValidator trustValidator; private final OcspClient ocspClient; private final OcspServiceProvider ocspServiceProvider; private final Duration allowedOcspResponseTimeSkew; private final Duration maxOcspResponseThisUpdateAge; static { - Security.addProvider(new BouncyCastleProvider()); + if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) { + Security.addProvider(new BouncyCastleProvider()); + } } - public SubjectCertificateNotRevokedValidator(SubjectCertificateTrustedValidator trustValidator, - OcspClient ocspClient, - OcspServiceProvider ocspServiceProvider, - Duration allowedOcspResponseTimeSkew, - Duration maxOcspResponseThisUpdateAge) { - this.trustValidator = trustValidator; - this.ocspClient = ocspClient; - this.ocspServiceProvider = ocspServiceProvider; - this.allowedOcspResponseTimeSkew = allowedOcspResponseTimeSkew; - this.maxOcspResponseThisUpdateAge = maxOcspResponseThisUpdateAge; + public DefaultOcspCertificateRevocationChecker(OcspClient ocspClient, + OcspServiceProvider ocspServiceProvider, + Duration allowedOcspResponseTimeSkew, + Duration maxOcspResponseThisUpdateAge) { + this.ocspClient = requireNonNull(ocspClient, "ocspClient"); + this.ocspServiceProvider = requireNonNull(ocspServiceProvider, "ocspServiceProvider"); + this.allowedOcspResponseTimeSkew = requirePositiveDuration(allowedOcspResponseTimeSkew, "allowedOcspResponseTimeSkew"); + this.maxOcspResponseThisUpdateAge = requirePositiveDuration(maxOcspResponseThisUpdateAge, "maxOcspResponseThisUpdateAge"); } /** - * Validates that the user certificate from the authentication token is not revoked with OCSP. + * Validates with OCSP that the user certificate from the authentication token is not revoked. * * @param subjectCertificate user certificate to be validated * @throws AuthTokenException when user certificate is revoked or revocation check fails. */ - public void validateCertificateNotRevoked(X509Certificate subjectCertificate) throws AuthTokenException { + @Override + public List validateCertificateNotRevoked(X509Certificate subjectCertificate, X509Certificate issuerCertificate) throws AuthTokenException { + requireNonNull(subjectCertificate, "subjectCertificate"); + requireNonNull(issuerCertificate, "issuerCertificate"); + + URI ocspResponderUri = null; try { OcspService ocspService = ocspServiceProvider.getService(subjectCertificate); + ocspResponderUri = requireNonNull(ocspService.getAccessLocation(), "ocspResponderUri"); - final CertificateID certificateId = getCertificateId(subjectCertificate, - Objects.requireNonNull(trustValidator.getSubjectCertificateIssuerCertificate())); + final CertificateID certificateId = getCertificateId(subjectCertificate, issuerCertificate); final OCSPReq request = new OcspRequestBuilder() .withCertificateId(certificateId) @@ -106,21 +120,27 @@ public void validateCertificateNotRevoked(X509Certificate subjectCertificate) th } LOG.debug("Sending OCSP request"); - final OCSPResp response = Objects.requireNonNull(ocspClient.request(ocspService.getAccessLocation(), request)); + final OCSPResp response = requireNonNull(ocspClient.request(ocspResponderUri, request), "OCSPResp"); if (response.getStatus() != OCSPResponseStatus.SUCCESSFUL) { - throw new UserCertificateOCSPCheckFailedException("Response status: " + ocspStatusToString(response.getStatus())); + throw new UserCertificateOCSPCheckFailedException("Response status: " + ocspStatusToString(response.getStatus()), ocspResponderUri); } final BasicOCSPResp basicResponse = (BasicOCSPResp) response.getResponseObject(); if (basicResponse == null) { - throw new UserCertificateOCSPCheckFailedException("Missing Basic OCSP Response"); + throw new UserCertificateOCSPCheckFailedException("Missing Basic OCSP Response", ocspResponderUri); } + LOG.debug("OCSP response received successfully"); + verifyOcspResponse(basicResponse, ocspService, certificateId); if (ocspService.doesSupportNonce()) { - checkNonce(request, basicResponse); + checkNonce(request, basicResponse, ocspResponderUri); } + LOG.debug("OCSP response verified successfully"); + + return List.of(new RevocationInfo(ocspResponderUri, Map.of(RevocationInfo.KEY_OCSP_RESPONSE, response))); + } catch (OCSPException | CertificateException | OperatorCreationException | IOException e) { - throw new UserCertificateOCSPCheckFailedException(e); + throw new UserCertificateOCSPCheckFailedException(e, ocspResponderUri); } } @@ -137,11 +157,12 @@ private void verifyOcspResponse(BasicOCSPResp basicResponse, OcspService ocspSer // As we sent the request for only a single certificate, we expect only a single response. if (basicResponse.getResponses().length != 1) { throw new UserCertificateOCSPCheckFailedException("OCSP response must contain one response, " - + "received " + basicResponse.getResponses().length + " responses instead"); + + "received " + basicResponse.getResponses().length + " responses instead", ocspService.getAccessLocation()); } final SingleResp certStatusResponse = basicResponse.getResponses()[0]; if (!requestCertificateId.equals(certStatusResponse.getCertID())) { - throw new UserCertificateOCSPCheckFailedException("OCSP responded with certificate ID that differs from the requested ID"); + throw new UserCertificateOCSPCheckFailedException("OCSP responded with certificate ID that differs from the requested ID", + ocspService.getAccessLocation()); } // 2. The signature on the response is valid. @@ -151,11 +172,11 @@ private void verifyOcspResponse(BasicOCSPResp basicResponse, OcspService ocspSer // is standard practice. if (basicResponse.getCerts().length < 1) { throw new UserCertificateOCSPCheckFailedException("OCSP response must contain the responder certificate, " - + "but none was provided"); + + "but none was provided", ocspService.getAccessLocation()); } // The first certificate is the responder certificate, other certificates, if given, are the certificate's chain. final X509CertificateHolder responderCert = basicResponse.getCerts()[0]; - OcspResponseValidator.validateResponseSignature(basicResponse, responderCert); + OcspResponseValidator.validateResponseSignature(basicResponse, responderCert, ocspService.getAccessLocation()); // 3. The identity of the signer matches the intended recipient of the // request. @@ -174,23 +195,23 @@ 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); + OcspResponseValidator.validateCertificateStatusUpdateTime(certStatusResponse, allowedOcspResponseTimeSkew, maxOcspResponseThisUpdateAge, ocspService.getAccessLocation()); // Now we can accept the signed response as valid and validate the certificate status. - OcspResponseValidator.validateSubjectCertificateStatus(certStatusResponse); + OcspResponseValidator.validateSubjectCertificateStatus(certStatusResponse, ocspService.getAccessLocation()); LOG.debug("OCSP check result is GOOD"); } - private static void checkNonce(OCSPReq request, BasicOCSPResp response) throws UserCertificateOCSPCheckFailedException { + private 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) { throw new UserCertificateOCSPCheckFailedException("OCSP request or response nonce extension missing, " + - "possible replay attack"); + "possible replay attack", ocspResponderUri); } if (!requestNonce.equals(responseNonce)) { throw new UserCertificateOCSPCheckFailedException("OCSP request and response nonces differ, " + - "possible replay attack"); + "possible replay attack", ocspResponderUri); } } @@ -202,20 +223,14 @@ private static CertificateID getCertificateId(X509Certificate subjectCertificate } private static String ocspStatusToString(int status) { - switch (status) { - case OCSPResp.MALFORMED_REQUEST: - return "malformed request"; - case OCSPResp.INTERNAL_ERROR: - return "internal error"; - case OCSPResp.TRY_LATER: - return "service unavailable"; - case OCSPResp.SIG_REQUIRED: - return "request signature missing"; - case OCSPResp.UNAUTHORIZED: - return "unauthorized"; - default: - return "unknown"; - } + return switch (status) { + case OCSPResp.MALFORMED_REQUEST -> "malformed request"; + case OCSPResp.INTERNAL_ERROR -> "internal error"; + case OCSPResp.TRY_LATER -> "service unavailable"; + case OCSPResp.SIG_REQUIRED -> "request signature missing"; + case OCSPResp.UNAUTHORIZED -> "unauthorized"; + default -> "unknown"; + }; } } diff --git a/src/main/java/eu/webeid/security/validator/ocsp/OcspClient.java b/src/main/java/eu/webeid/ocsp/client/OcspClient.java similarity index 96% rename from src/main/java/eu/webeid/security/validator/ocsp/OcspClient.java rename to src/main/java/eu/webeid/ocsp/client/OcspClient.java index 7f2e9477..b0b83412 100644 --- a/src/main/java/eu/webeid/security/validator/ocsp/OcspClient.java +++ b/src/main/java/eu/webeid/ocsp/client/OcspClient.java @@ -20,7 +20,7 @@ * SOFTWARE. */ -package eu.webeid.security.validator.ocsp; +package eu.webeid.ocsp.client; import org.bouncycastle.cert.ocsp.OCSPReq; import org.bouncycastle.cert.ocsp.OCSPResp; diff --git a/src/main/java/eu/webeid/security/validator/ocsp/OcspClientImpl.java b/src/main/java/eu/webeid/ocsp/client/OcspClientImpl.java similarity index 90% rename from src/main/java/eu/webeid/security/validator/ocsp/OcspClientImpl.java rename to src/main/java/eu/webeid/ocsp/client/OcspClientImpl.java index f0a0c50a..2134d04c 100644 --- a/src/main/java/eu/webeid/security/validator/ocsp/OcspClientImpl.java +++ b/src/main/java/eu/webeid/ocsp/client/OcspClientImpl.java @@ -20,7 +20,7 @@ * SOFTWARE. */ -package eu.webeid.security.validator.ocsp; +package eu.webeid.ocsp.client; import org.bouncycastle.cert.ocsp.OCSPReq; import org.bouncycastle.cert.ocsp.OCSPResp; @@ -34,6 +34,9 @@ import java.net.http.HttpResponse; import java.time.Duration; +import static eu.webeid.security.util.DateAndTime.requirePositiveDuration; +import static java.util.Objects.requireNonNull; + public class OcspClientImpl implements OcspClient { private static final Logger LOG = LoggerFactory.getLogger(OcspClientImpl.class); @@ -45,6 +48,7 @@ public class OcspClientImpl implements OcspClient { private final Duration ocspRequestTimeout; public static OcspClient build(Duration ocspRequestTimeout) { + requirePositiveDuration(ocspRequestTimeout, "ocspRequestTimeout"); return new OcspClientImpl( HttpClient.newBuilder() .connectTimeout(ocspRequestTimeout) @@ -91,8 +95,8 @@ public OCSPResp request(URI uri, OCSPReq ocspReq) throws IOException { } public OcspClientImpl(HttpClient httpClient, Duration ocspRequestTimeout) { - this.httpClient = httpClient; - this.ocspRequestTimeout = ocspRequestTimeout; + this.httpClient = requireNonNull(httpClient, "httpClient"); + this.ocspRequestTimeout = requirePositiveDuration(ocspRequestTimeout, "ocspRequestTimeout"); } } diff --git a/src/main/java/eu/webeid/security/exceptions/OCSPCertificateException.java b/src/main/java/eu/webeid/ocsp/exceptions/OCSPCertificateException.java similarity index 93% rename from src/main/java/eu/webeid/security/exceptions/OCSPCertificateException.java rename to src/main/java/eu/webeid/ocsp/exceptions/OCSPCertificateException.java index 397cf23f..983a4da9 100644 --- a/src/main/java/eu/webeid/security/exceptions/OCSPCertificateException.java +++ b/src/main/java/eu/webeid/ocsp/exceptions/OCSPCertificateException.java @@ -20,7 +20,9 @@ * SOFTWARE. */ -package eu.webeid.security.exceptions; +package eu.webeid.ocsp.exceptions; + +import eu.webeid.security.exceptions.AuthTokenException; public class OCSPCertificateException extends AuthTokenException { diff --git a/src/main/java/eu/webeid/security/exceptions/UserCertificateOCSPCheckFailedException.java b/src/main/java/eu/webeid/ocsp/exceptions/UserCertificateOCSPCheckFailedException.java similarity index 76% rename from src/main/java/eu/webeid/security/exceptions/UserCertificateOCSPCheckFailedException.java rename to src/main/java/eu/webeid/ocsp/exceptions/UserCertificateOCSPCheckFailedException.java index 5ca68dc5..1bafdadf 100644 --- a/src/main/java/eu/webeid/security/exceptions/UserCertificateOCSPCheckFailedException.java +++ b/src/main/java/eu/webeid/ocsp/exceptions/UserCertificateOCSPCheckFailedException.java @@ -20,17 +20,34 @@ * SOFTWARE. */ -package eu.webeid.security.exceptions; +package eu.webeid.ocsp.exceptions; + +import eu.webeid.security.exceptions.AuthTokenException; + +import java.net.URI; /** * Thrown when user certificate revocation check with OCSP fails. */ public class UserCertificateOCSPCheckFailedException extends AuthTokenException { - public UserCertificateOCSPCheckFailedException(Throwable cause) { + + private final URI ocspResponderUri; + + public UserCertificateOCSPCheckFailedException(Throwable cause, URI ocspResponderUri) { super("User certificate revocation check has failed", cause); + this.ocspResponderUri = ocspResponderUri; } - public UserCertificateOCSPCheckFailedException(String message) { + public UserCertificateOCSPCheckFailedException(String message, URI ocspResponderUri) { super("User certificate revocation check has failed: " + message); + this.ocspResponderUri = ocspResponderUri; + } + + public UserCertificateOCSPCheckFailedException(String message) { + this(message, null); + } + + public URI getOcspResponderUri() { + return ocspResponderUri; } } diff --git a/src/main/java/eu/webeid/security/exceptions/UserCertificateRevokedException.java b/src/main/java/eu/webeid/ocsp/exceptions/UserCertificateRevokedException.java similarity index 74% rename from src/main/java/eu/webeid/security/exceptions/UserCertificateRevokedException.java rename to src/main/java/eu/webeid/ocsp/exceptions/UserCertificateRevokedException.java index 83eb53ab..14ee6d5a 100644 --- a/src/main/java/eu/webeid/security/exceptions/UserCertificateRevokedException.java +++ b/src/main/java/eu/webeid/ocsp/exceptions/UserCertificateRevokedException.java @@ -20,17 +20,30 @@ * SOFTWARE. */ -package eu.webeid.security.exceptions; +package eu.webeid.ocsp.exceptions; + +import eu.webeid.security.exceptions.AuthTokenException; + +import java.net.URI; /** * Thrown when the user certificate has been revoked. */ public class UserCertificateRevokedException extends AuthTokenException { - public UserCertificateRevokedException() { + + private final URI ocspResponderUri; + + public UserCertificateRevokedException(URI ocspResponderUri) { super("User certificate has been revoked"); + this.ocspResponderUri = ocspResponderUri; } - public UserCertificateRevokedException(String msg) { + public UserCertificateRevokedException(String msg, URI ocspResponderUri) { super("User certificate has been revoked: " + msg); + this.ocspResponderUri = ocspResponderUri; + } + + public URI getOcspResponderUri() { + return ocspResponderUri; } } diff --git a/src/main/java/eu/webeid/security/validator/ocsp/DigestCalculatorImpl.java b/src/main/java/eu/webeid/ocsp/protocol/DigestCalculatorImpl.java similarity index 98% rename from src/main/java/eu/webeid/security/validator/ocsp/DigestCalculatorImpl.java rename to src/main/java/eu/webeid/ocsp/protocol/DigestCalculatorImpl.java index 35f9fcb7..90a1c810 100644 --- a/src/main/java/eu/webeid/security/validator/ocsp/DigestCalculatorImpl.java +++ b/src/main/java/eu/webeid/ocsp/protocol/DigestCalculatorImpl.java @@ -20,7 +20,7 @@ * SOFTWARE. */ -package eu.webeid.security.validator.ocsp; +package eu.webeid.ocsp.protocol; import org.bouncycastle.asn1.nist.NISTObjectIdentifiers; import org.bouncycastle.asn1.oiw.OIWObjectIdentifiers; diff --git a/src/main/java/eu/webeid/security/validator/ocsp/OcspRequestBuilder.java b/src/main/java/eu/webeid/ocsp/protocol/OcspRequestBuilder.java similarity index 98% rename from src/main/java/eu/webeid/security/validator/ocsp/OcspRequestBuilder.java rename to src/main/java/eu/webeid/ocsp/protocol/OcspRequestBuilder.java index 27ad87f1..75f43ff0 100644 --- a/src/main/java/eu/webeid/security/validator/ocsp/OcspRequestBuilder.java +++ b/src/main/java/eu/webeid/ocsp/protocol/OcspRequestBuilder.java @@ -20,7 +20,7 @@ * SOFTWARE. */ -package eu.webeid.security.validator.ocsp; +package eu.webeid.ocsp.protocol; import org.bouncycastle.asn1.DEROctetString; import org.bouncycastle.asn1.ocsp.OCSPObjectIdentifiers; diff --git a/src/main/java/eu/webeid/security/validator/ocsp/OcspResponseValidator.java b/src/main/java/eu/webeid/ocsp/protocol/OcspResponseValidator.java similarity index 83% rename from src/main/java/eu/webeid/security/validator/ocsp/OcspResponseValidator.java rename to src/main/java/eu/webeid/ocsp/protocol/OcspResponseValidator.java index 0dc4fda5..6c7d69fa 100644 --- a/src/main/java/eu/webeid/security/validator/ocsp/OcspResponseValidator.java +++ b/src/main/java/eu/webeid/ocsp/protocol/OcspResponseValidator.java @@ -20,11 +20,11 @@ * SOFTWARE. */ -package eu.webeid.security.validator.ocsp; +package eu.webeid.ocsp.protocol; -import eu.webeid.security.exceptions.OCSPCertificateException; -import eu.webeid.security.exceptions.UserCertificateOCSPCheckFailedException; -import eu.webeid.security.exceptions.UserCertificateRevokedException; +import eu.webeid.ocsp.exceptions.OCSPCertificateException; +import eu.webeid.ocsp.exceptions.UserCertificateOCSPCheckFailedException; +import eu.webeid.ocsp.exceptions.UserCertificateRevokedException; import eu.webeid.security.util.DateAndTime; import org.bouncycastle.cert.X509CertificateHolder; import org.bouncycastle.cert.ocsp.BasicOCSPResp; @@ -33,10 +33,12 @@ import org.bouncycastle.cert.ocsp.RevokedStatus; import org.bouncycastle.cert.ocsp.SingleResp; import org.bouncycastle.cert.ocsp.UnknownStatus; +import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.operator.ContentVerifierProvider; import org.bouncycastle.operator.OperatorCreationException; import org.bouncycastle.operator.jcajce.JcaContentVerifierProviderBuilder; +import java.net.URI; import java.security.cert.CertificateException; import java.security.cert.CertificateParsingException; import java.security.cert.X509Certificate; @@ -66,16 +68,16 @@ public static void validateHasSigningExtension(X509Certificate certificate) thro } } - public static void validateResponseSignature(BasicOCSPResp basicResponse, X509CertificateHolder responderCert) throws CertificateException, OperatorCreationException, OCSPException, UserCertificateOCSPCheckFailedException { + public static void validateResponseSignature(BasicOCSPResp basicResponse, X509CertificateHolder responderCert, URI ocspResponderUri) throws CertificateException, OperatorCreationException, OCSPException, UserCertificateOCSPCheckFailedException { final ContentVerifierProvider verifierProvider = new JcaContentVerifierProviderBuilder() - .setProvider("BC") + .setProvider(BouncyCastleProvider.PROVIDER_NAME) .build(responderCert); if (!basicResponse.isSignatureValid(verifierProvider)) { - throw new UserCertificateOCSPCheckFailedException("OCSP response signature is invalid"); + throw new UserCertificateOCSPCheckFailedException("OCSP response signature is invalid", ocspResponderUri); } } - public static void validateCertificateStatusUpdateTime(SingleResp certStatusResponse, Duration allowedTimeSkew, Duration maxThisupdateAge) throws UserCertificateOCSPCheckFailedException { + public static void validateCertificateStatusUpdateTime(SingleResp certStatusResponse, Duration allowedTimeSkew, Duration maxThisupdateAge, URI ocspResponderUri) throws UserCertificateOCSPCheckFailedException { // From RFC 2560, https://www.ietf.org/rfc/rfc2560.txt: // 4.2.2. Notes on OCSP Responses // 4.2.2.1. Time @@ -94,12 +96,12 @@ public static void validateCertificateStatusUpdateTime(SingleResp certStatusResp if (thisUpdate.isAfter(latestAcceptableTimeSkew)) { throw new UserCertificateOCSPCheckFailedException(ERROR_PREFIX + "thisUpdate '" + thisUpdate + "' is too far in the future, " + - "latest allowed: '" + latestAcceptableTimeSkew + "'"); + "latest allowed: '" + latestAcceptableTimeSkew + "'", ocspResponderUri); } if (thisUpdate.isBefore(minimumValidThisUpdateTime)) { throw new UserCertificateOCSPCheckFailedException(ERROR_PREFIX + "thisUpdate '" + thisUpdate + "' is too old, " + - "minimum time allowed: '" + minimumValidThisUpdateTime + "'"); + "minimum time allowed: '" + minimumValidThisUpdateTime + "'", ocspResponderUri); } if (certStatusResponse.getNextUpdate() == null) { @@ -108,28 +110,27 @@ public static void validateCertificateStatusUpdateTime(SingleResp certStatusResp final Instant nextUpdate = certStatusResponse.getNextUpdate().toInstant(); if (nextUpdate.isBefore(earliestAcceptableTimeSkew)) { throw new UserCertificateOCSPCheckFailedException(ERROR_PREFIX + - "nextUpdate '" + nextUpdate + "' is in the past"); + "nextUpdate '" + nextUpdate + "' is in the past", ocspResponderUri); } if (nextUpdate.isBefore(thisUpdate)) { throw new UserCertificateOCSPCheckFailedException(ERROR_PREFIX + - "nextUpdate '" + nextUpdate + "' is before thisUpdate '" + thisUpdate + "'"); + "nextUpdate '" + nextUpdate + "' is before thisUpdate '" + thisUpdate + "'", ocspResponderUri); } } - public static void validateSubjectCertificateStatus(SingleResp certStatusResponse) throws UserCertificateRevokedException { + public static void validateSubjectCertificateStatus(SingleResp certStatusResponse, URI ocspResponderUri) throws UserCertificateRevokedException { final CertificateStatus status = certStatusResponse.getCertStatus(); if (status == null) { return; } - if (status instanceof RevokedStatus) { - RevokedStatus revokedStatus = (RevokedStatus) status; + if (status instanceof RevokedStatus revokedStatus) { throw (revokedStatus.hasRevocationReason() ? - new UserCertificateRevokedException("Revocation reason: " + revokedStatus.getRevocationReason()) : - new UserCertificateRevokedException()); + new UserCertificateRevokedException("Revocation reason: " + revokedStatus.getRevocationReason(), ocspResponderUri) : + new UserCertificateRevokedException(ocspResponderUri)); } else if (status instanceof UnknownStatus) { - throw new UserCertificateRevokedException("Unknown status"); + throw new UserCertificateRevokedException("Unknown status", ocspResponderUri); } else { - throw new UserCertificateRevokedException("Status is neither good, revoked nor unknown"); + throw new UserCertificateRevokedException("Status is neither good, revoked nor unknown", ocspResponderUri); } } diff --git a/src/main/java/eu/webeid/security/validator/ocsp/OcspUrl.java b/src/main/java/eu/webeid/ocsp/protocol/OcspUrl.java similarity index 98% rename from src/main/java/eu/webeid/security/validator/ocsp/OcspUrl.java rename to src/main/java/eu/webeid/ocsp/protocol/OcspUrl.java index 96894080..f307d767 100644 --- a/src/main/java/eu/webeid/security/validator/ocsp/OcspUrl.java +++ b/src/main/java/eu/webeid/ocsp/protocol/OcspUrl.java @@ -20,7 +20,7 @@ * SOFTWARE. */ -package eu.webeid.security.validator.ocsp; +package eu.webeid.ocsp.protocol; import org.bouncycastle.asn1.ASN1String; import org.bouncycastle.asn1.x509.AccessDescription; diff --git a/src/main/java/eu/webeid/security/validator/ocsp/service/AiaOcspService.java b/src/main/java/eu/webeid/ocsp/service/AiaOcspService.java similarity index 84% rename from src/main/java/eu/webeid/security/validator/ocsp/service/AiaOcspService.java rename to src/main/java/eu/webeid/ocsp/service/AiaOcspService.java index e04823c3..29b25079 100644 --- a/src/main/java/eu/webeid/security/validator/ocsp/service/AiaOcspService.java +++ b/src/main/java/eu/webeid/ocsp/service/AiaOcspService.java @@ -20,13 +20,14 @@ * SOFTWARE. */ -package eu.webeid.security.validator.ocsp.service; +package eu.webeid.ocsp.service; import eu.webeid.security.certificate.CertificateValidator; import eu.webeid.security.exceptions.AuthTokenException; -import eu.webeid.security.exceptions.OCSPCertificateException; -import eu.webeid.security.exceptions.UserCertificateOCSPCheckFailedException; -import eu.webeid.security.validator.ocsp.OcspResponseValidator; +import eu.webeid.ocsp.exceptions.OCSPCertificateException; +import eu.webeid.ocsp.exceptions.UserCertificateOCSPCheckFailedException; +import eu.webeid.ocsp.protocol.OcspResponseValidator; +import eu.webeid.security.validator.revocationcheck.RevocationMode; import org.bouncycastle.cert.X509CertificateHolder; import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; @@ -39,7 +40,7 @@ import java.util.Objects; import java.util.Set; -import static eu.webeid.security.validator.ocsp.OcspUrl.getOcspUri; +import static eu.webeid.ocsp.protocol.OcspUrl.getOcspUri; /** * An OCSP service that uses the responders from the Certificates' Authority Information Access (AIA) extension. @@ -77,7 +78,15 @@ public void validateResponderCertificate(X509CertificateHolder cert, Date now) t CertificateValidator.certificateIsValidOnDate(certificate, now, "AIA OCSP responder"); // Trusted certificates' validity has been already verified in validateCertificateExpiry(). OcspResponseValidator.validateHasSigningExtension(certificate); - CertificateValidator.validateIsSignedByTrustedCA(certificate, trustedCACertificateAnchors, trustedCACertificateCertStore, now); + CertificateValidator.validateCertificateTrustAndRevocation( + certificate, + trustedCACertificateAnchors, + trustedCACertificateCertStore, + now, + RevocationMode.DISABLED, + null, + null + ); } catch (CertificateException e) { throw new OCSPCertificateException("Invalid responder certificate", e); } diff --git a/src/main/java/eu/webeid/security/validator/ocsp/service/AiaOcspServiceConfiguration.java b/src/main/java/eu/webeid/ocsp/service/AiaOcspServiceConfiguration.java similarity index 97% rename from src/main/java/eu/webeid/security/validator/ocsp/service/AiaOcspServiceConfiguration.java rename to src/main/java/eu/webeid/ocsp/service/AiaOcspServiceConfiguration.java index 8781e3a5..1a97f5d5 100644 --- a/src/main/java/eu/webeid/security/validator/ocsp/service/AiaOcspServiceConfiguration.java +++ b/src/main/java/eu/webeid/ocsp/service/AiaOcspServiceConfiguration.java @@ -20,7 +20,7 @@ * SOFTWARE. */ -package eu.webeid.security.validator.ocsp.service; +package eu.webeid.ocsp.service; import java.net.URI; import java.security.cert.CertStore; diff --git a/src/main/java/eu/webeid/security/validator/ocsp/service/DesignatedOcspService.java b/src/main/java/eu/webeid/ocsp/service/DesignatedOcspService.java similarity index 96% rename from src/main/java/eu/webeid/security/validator/ocsp/service/DesignatedOcspService.java rename to src/main/java/eu/webeid/ocsp/service/DesignatedOcspService.java index bafba269..9dda6a03 100644 --- a/src/main/java/eu/webeid/security/validator/ocsp/service/DesignatedOcspService.java +++ b/src/main/java/eu/webeid/ocsp/service/DesignatedOcspService.java @@ -20,11 +20,11 @@ * SOFTWARE. */ -package eu.webeid.security.validator.ocsp.service; +package eu.webeid.ocsp.service; import org.bouncycastle.cert.X509CertificateHolder; import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; -import eu.webeid.security.exceptions.OCSPCertificateException; +import eu.webeid.ocsp.exceptions.OCSPCertificateException; import eu.webeid.security.exceptions.AuthTokenException; import java.net.URI; diff --git a/src/main/java/eu/webeid/security/validator/ocsp/service/DesignatedOcspServiceConfiguration.java b/src/main/java/eu/webeid/ocsp/service/DesignatedOcspServiceConfiguration.java similarity index 95% rename from src/main/java/eu/webeid/security/validator/ocsp/service/DesignatedOcspServiceConfiguration.java rename to src/main/java/eu/webeid/ocsp/service/DesignatedOcspServiceConfiguration.java index 0bc03193..fa3f5178 100644 --- a/src/main/java/eu/webeid/security/validator/ocsp/service/DesignatedOcspServiceConfiguration.java +++ b/src/main/java/eu/webeid/ocsp/service/DesignatedOcspServiceConfiguration.java @@ -20,12 +20,12 @@ * SOFTWARE. */ -package eu.webeid.security.validator.ocsp.service; +package eu.webeid.ocsp.service; -import eu.webeid.security.validator.ocsp.OcspResponseValidator; +import eu.webeid.ocsp.protocol.OcspResponseValidator; import org.bouncycastle.asn1.x500.X500Name; import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder; -import eu.webeid.security.exceptions.OCSPCertificateException; +import eu.webeid.ocsp.exceptions.OCSPCertificateException; import java.net.URI; import java.security.cert.CertificateEncodingException; diff --git a/src/main/java/eu/webeid/security/validator/ocsp/service/OcspService.java b/src/main/java/eu/webeid/ocsp/service/OcspService.java similarity index 96% rename from src/main/java/eu/webeid/security/validator/ocsp/service/OcspService.java rename to src/main/java/eu/webeid/ocsp/service/OcspService.java index 97bbdf2c..8d346e37 100644 --- a/src/main/java/eu/webeid/security/validator/ocsp/service/OcspService.java +++ b/src/main/java/eu/webeid/ocsp/service/OcspService.java @@ -20,7 +20,7 @@ * SOFTWARE. */ -package eu.webeid.security.validator.ocsp.service; +package eu.webeid.ocsp.service; import org.bouncycastle.cert.X509CertificateHolder; import eu.webeid.security.exceptions.AuthTokenException; diff --git a/src/main/java/eu/webeid/security/validator/ocsp/OcspServiceProvider.java b/src/main/java/eu/webeid/ocsp/service/OcspServiceProvider.java similarity index 87% rename from src/main/java/eu/webeid/security/validator/ocsp/OcspServiceProvider.java rename to src/main/java/eu/webeid/ocsp/service/OcspServiceProvider.java index 5f83c1d9..56deb1e6 100644 --- a/src/main/java/eu/webeid/security/validator/ocsp/OcspServiceProvider.java +++ b/src/main/java/eu/webeid/ocsp/service/OcspServiceProvider.java @@ -20,14 +20,9 @@ * SOFTWARE. */ -package eu.webeid.security.validator.ocsp; +package eu.webeid.ocsp.service; import eu.webeid.security.exceptions.AuthTokenException; -import eu.webeid.security.validator.ocsp.service.AiaOcspService; -import eu.webeid.security.validator.ocsp.service.AiaOcspServiceConfiguration; -import eu.webeid.security.validator.ocsp.service.DesignatedOcspService; -import eu.webeid.security.validator.ocsp.service.DesignatedOcspServiceConfiguration; -import eu.webeid.security.validator.ocsp.service.OcspService; import java.security.cert.CertificateEncodingException; import java.security.cert.X509Certificate; diff --git a/src/main/java/eu/webeid/security/authtoken/WebEidAuthToken.java b/src/main/java/eu/webeid/security/authtoken/WebEidAuthToken.java index 77d80bc4..cfb09178 100644 --- a/src/main/java/eu/webeid/security/authtoken/WebEidAuthToken.java +++ b/src/main/java/eu/webeid/security/authtoken/WebEidAuthToken.java @@ -25,43 +25,10 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; @JsonIgnoreProperties(ignoreUnknown = true) -public class WebEidAuthToken { - - private String unverifiedCertificate; - private String signature; - private String algorithm; - private String format; - - public String getUnverifiedCertificate() { - return unverifiedCertificate; - } - - public void setUnverifiedCertificate(String unverifiedCertificate) { - this.unverifiedCertificate = unverifiedCertificate; - } - - public String getSignature() { - return signature; - } - - public void setSignature(String signature) { - this.signature = signature; - } - - public String getAlgorithm() { - return algorithm; - } - - public void setAlgorithm(String algorithm) { - this.algorithm = algorithm; - } - - public String getFormat() { - return format; - } - - public void setFormat(String format) { - this.format = format; - } - +public record WebEidAuthToken( + String unverifiedCertificate, + String signature, + String algorithm, + String format +) { } diff --git a/src/main/java/eu/webeid/security/certificate/CertificateValidator.java b/src/main/java/eu/webeid/security/certificate/CertificateValidator.java index 8a6701b3..68bd0ebf 100644 --- a/src/main/java/eu/webeid/security/certificate/CertificateValidator.java +++ b/src/main/java/eu/webeid/security/certificate/CertificateValidator.java @@ -22,10 +22,14 @@ package eu.webeid.security.certificate; +import eu.webeid.security.exceptions.AuthTokenException; import eu.webeid.security.exceptions.CertificateExpiredException; import eu.webeid.security.exceptions.CertificateNotTrustedException; import eu.webeid.security.exceptions.CertificateNotYetValidException; import eu.webeid.security.exceptions.JceException; +import eu.webeid.security.validator.revocationcheck.OcspCertificateRevocationChecker; +import eu.webeid.security.validator.revocationcheck.RevocationInfo; +import eu.webeid.security.validator.revocationcheck.RevocationMode; import java.security.GeneralSecurityException; import java.security.InvalidAlgorithmParameterException; @@ -36,14 +40,19 @@ import java.security.cert.CollectionCertStoreParameters; import java.security.cert.PKIXBuilderParameters; import java.security.cert.PKIXCertPathBuilderResult; +import java.security.cert.PKIXRevocationChecker; import java.security.cert.TrustAnchor; import java.security.cert.X509CertSelector; import java.security.cert.X509Certificate; import java.util.Collection; import java.util.Date; +import java.util.EnumSet; +import java.util.List; import java.util.Set; import java.util.stream.Collectors; +import static java.util.Objects.requireNonNull; + public final class CertificateValidator { public static void certificateIsValidOnDate(X509Certificate cert, Date date, String subject) throws CertificateNotYetValidException, CertificateExpiredException { @@ -56,10 +65,65 @@ public static void certificateIsValidOnDate(X509Certificate cert, Date date, Str } } - public static X509Certificate validateIsSignedByTrustedCA(X509Certificate certificate, - Set trustedCACertificateAnchors, - CertStore trustedCACertificateCertStore, - Date now) throws CertificateNotTrustedException, JceException, CertificateNotYetValidException, CertificateExpiredException { + /** + * Validates that the provided {@code certificate} is trusted and performs certificate revocation checking + * depending on {@code revocationMode}. + *

+ * Trust validation is performed by building a certification path from {@code certificate} to one of the + * configured {@code trustedCACertificateAnchors} using the supplied {@code trustedCACertificateCertStore}. + * The effective validation time is {@code now}. In addition, the trust anchor certificate's validity period + * is explicitly validated. + *

+ * Revocation behavior is controlled by {@code revocationMode}: + *

    + *
  • {@link RevocationMode#DISABLED} - no revocation checking is performed. Both + * {@code ocspCertificateRevocationChecker} and {@code customPkixRevocationChecker} must be {@code null}.
  • + *
  • {@link RevocationMode#CUSTOM_OCSP} - revocation is checked by the provided + * {@code ocspCertificateRevocationChecker}. Platform (provider default) revocation checking is disabled. + * {@code customPkixRevocationChecker} must be {@code null}.
  • + *
  • {@link RevocationMode#CUSTOM_PKIX} - revocation is checked by the provided + * {@code customPkixRevocationChecker} installed as the (only) PKIX cert path checker. Provider default + * revocation checking is disabled. {@code ocspCertificateRevocationChecker} must be {@code null}.
  • + *
  • {@link RevocationMode#PLATFORM_OCSP} - revocation is checked using the platform PKIX revocation checker + * configured to enforce OCSP checking for the subject certificate with no fallback to CRLs + * ({@link PKIXRevocationChecker.Option#ONLY_END_ENTITY} and {@link PKIXRevocationChecker.Option#NO_FALLBACK}). + * Provider default revocation checking is disabled. Both custom checker parameters must be {@code null}.
  • + *
+ * + * @param certificate the subject certificate to validate + * @param trustedCACertificateAnchors trust anchors used for PKIX path building (Web eID typically configures issuing intermediates) + * @param trustedCACertificateCertStore certificate store containing trusted CA/intermediate certificates used during path building + * @param now validation time used for certificate validity and PKIX path building + * @param revocationMode revocation checking mode + * @param ocspCertificateRevocationChecker custom OCSP revocation checker (required only for {@code CUSTOM_OCSP}) + * @param customPkixRevocationChecker custom PKIX revocation checker (required only for {@code CUSTOM_PKIX}) + * @return a list of {@link RevocationInfo} objects; the list is non-null and may be empty. + * It is populated for {@link RevocationMode#CUSTOM_OCSP}, and may be populated for + * {@link RevocationMode#CUSTOM_PKIX} when the provided {@code customPkixRevocationChecker} + * has an explicit OCSP responder URI configured; otherwise it is empty. + *

+ * @throws NullPointerException if any required parameter is {@code null} + * @throws IllegalArgumentException if the supplied checker parameters are inconsistent with {@code revocationMode} + * @throws CertificateNotYetValidException if the subject or trust anchor certificate is not yet valid at {@code now} + * @throws CertificateExpiredException if the subject or trust anchor certificate is expired at {@code now} + * @throws CertificateNotTrustedException if no valid certification path can be built to the configured trust anchors + * @throws JceException if the underlying JCA/JCE implementation fails unexpectedly + * @throws AuthTokenException if a custom revocation checker fails or reports the certificate as revoked + */ + public static List validateCertificateTrustAndRevocation(X509Certificate certificate, + Set trustedCACertificateAnchors, + CertStore trustedCACertificateCertStore, + Date now, + RevocationMode revocationMode, + OcspCertificateRevocationChecker ocspCertificateRevocationChecker, + PKIXRevocationChecker customPkixRevocationChecker) throws AuthTokenException { + + requireNonNull(certificate, "certificate"); + requireNonNull(trustedCACertificateAnchors, "trustedCACertificateAnchors"); + requireNonNull(trustedCACertificateCertStore, "trustedCACertificateCertStore"); + requireNonNull(now, "now"); + requireNonNull(revocationMode, "revocationMode"); + certificateIsValidOnDate(certificate, now, "User"); final X509CertSelector selector = new X509CertSelector(); @@ -67,21 +131,86 @@ public static X509Certificate validateIsSignedByTrustedCA(X509Certificate certif try { final PKIXBuilderParameters pkixBuilderParameters = new PKIXBuilderParameters(trustedCACertificateAnchors, selector); - // Certificate revocation check is intentionally disabled as we do the OCSP check with SubjectCertificateNotRevokedValidator ourselves. - pkixBuilderParameters.setRevocationEnabled(false); pkixBuilderParameters.setDate(now); pkixBuilderParameters.addCertStore(trustedCACertificateCertStore); // See the comment in buildCertStoreFromCertificates() below why we use the default JCE provider. final CertPathBuilder certPathBuilder = CertPathBuilder.getInstance(CertPathBuilder.getDefaultType()); + + List revocationInfoList = List.of(); + + switch (revocationMode) { + case DISABLED -> { + if (customPkixRevocationChecker != null || ocspCertificateRevocationChecker != null) { + throw new IllegalArgumentException("customPkixRevocationChecker and ocspCertificateRevocationChecker must be null when revocationMode is DISABLED"); + } + pkixBuilderParameters.setRevocationEnabled(false); + } + + case CUSTOM_OCSP -> { + if (customPkixRevocationChecker != null) { + throw new IllegalArgumentException("customPkixRevocationChecker must be null when revocationMode is CUSTOM_OCSP"); + } + if (ocspCertificateRevocationChecker == null) { + throw new IllegalArgumentException("ocspCertificateRevocationChecker must be provided when revocationMode is CUSTOM_OCSP"); + } + // OcspCertificateRevocationChecker performs revocation checking, disable platform's revocation checks. + pkixBuilderParameters.setRevocationEnabled(false); + } + + case CUSTOM_PKIX -> { + if (ocspCertificateRevocationChecker != null) { + throw new IllegalArgumentException("ocspCertificateRevocationChecker must be null when revocationMode is CUSTOM_PKIX"); + } + if (customPkixRevocationChecker == null) { + throw new IllegalArgumentException("customPkixRevocationChecker must be provided when revocationMode is CUSTOM_PKIX"); + } + // Setting RevocationEnabled is not required for the checker to run, but disable + // the provider default revocation just in case to avoid surprises across providers. + pkixBuilderParameters.setRevocationEnabled(false); + pkixBuilderParameters.setCertPathCheckers(List.of(customPkixRevocationChecker)); + + if (customPkixRevocationChecker.getOcspResponder() != null) { + revocationInfoList = List.of(new RevocationInfo(customPkixRevocationChecker.getOcspResponder(), null)); + } + } + + case PLATFORM_OCSP -> { + if (customPkixRevocationChecker != null || ocspCertificateRevocationChecker != null) { + throw new IllegalArgumentException("customPkixRevocationChecker and ocspCertificateRevocationChecker must be null when revocationMode is PLATFORM_OCSP"); + } + + final PKIXRevocationChecker checker = buildOcspEnforcedPkixRevocationChecker(certPathBuilder); + // See the comment in CUSTOM_PKIX case above. + pkixBuilderParameters.setRevocationEnabled(false); + pkixBuilderParameters.setCertPathCheckers(List.of(checker)); + } + + default -> throw new IllegalStateException("Unhandled revocationMode: " + revocationMode); + } + final PKIXCertPathBuilderResult result = (PKIXCertPathBuilderResult) certPathBuilder.build(pkixBuilderParameters); final X509Certificate trustedCACert = result.getTrustAnchor().getTrustedCert(); + if (trustedCACert == null) { + throw new IllegalStateException("TrustAnchor.getTrustedCert() returned null, it must contain a trusted certificate"); + } - // Verify that the trusted CA cert is presently valid before returning the result. + // PKIX path building does not validate trust anchor validity period, do it ourselves. certificateIsValidOnDate(trustedCACert, now, "Trusted CA"); - return trustedCACert; + if (revocationMode == RevocationMode.CUSTOM_OCSP) { + if (!certificate.getIssuerX500Principal().equals(trustedCACert.getSubjectX500Principal())) { + throw new IllegalStateException( + "Trust anchor is not the issuer of the subject certificate, check your configured certificate authorities. " + + "Subject issuer=" + certificate.getIssuerX500Principal() + + ", trust anchor subject=" + trustedCACert.getSubjectX500Principal() + ); + } + revocationInfoList = ocspCertificateRevocationChecker.validateCertificateNotRevoked(certificate, trustedCACert); + } + + return revocationInfoList; } catch (InvalidAlgorithmParameterException | NoSuchAlgorithmException e) { throw new JceException(e); @@ -96,7 +225,7 @@ public static Set buildTrustAnchorsFromCertificates(Collection certificates) throws JceException { - // We use the default JCE provider as there is no reason to use Bouncy Castle, moreover BC requires + // Use the default JCE provider as there is no reason to use Bouncy Castle, moreover BC requires // the validated certificate to be in the certificate store which breaks the clean immutable usage of // trustedCACertificateCertStore in SubjectCertificateTrustedValidator. try { @@ -106,6 +235,20 @@ public static CertStore buildCertStoreFromCertificates(Collection trustedCACertificates = new HashSet<>(); - private boolean isUserCertificateRevocationCheckWithOcspEnabled = true; - private Duration ocspRequestTimeout = Duration.ofSeconds(5); - private Duration allowedOcspResponseTimeSkew = Duration.ofMinutes(15); - private Duration maxOcspResponseThisUpdateAge = Duration.ofMinutes(2); - private DesignatedOcspServiceConfiguration designatedOcspServiceConfiguration; // Don't allow Estonian Mobile-ID policy by default. private Collection disallowedSubjectCertificatePolicies = newHashSet( SubjectCertificatePolicies.ESTEID_SK_2015_MOBILE_ID_POLICY_V1, @@ -58,7 +53,10 @@ public final class AuthTokenValidationConfiguration { SubjectCertificatePolicies.ESTEID_SK_2015_MOBILE_ID_POLICY_V3, SubjectCertificatePolicies.ESTEID_SK_2015_MOBILE_ID_POLICY ); - private Collection nonceDisabledOcspUrls = new HashSet<>(); + private boolean isUserCertificateRevocationCheckEnabled = true; + private OcspCertificateRevocationChecker ocspCertificateRevocationChecker; + private PKIXRevocationChecker pkixRevocationChecker; + private RevocationMode revocationMode = RevocationMode.PLATFORM_OCSP; AuthTokenValidationConfiguration() { } @@ -66,13 +64,11 @@ public final class AuthTokenValidationConfiguration { private AuthTokenValidationConfiguration(AuthTokenValidationConfiguration other) { this.siteOrigin = other.siteOrigin; this.trustedCACertificates = Set.copyOf(other.trustedCACertificates); - this.isUserCertificateRevocationCheckWithOcspEnabled = other.isUserCertificateRevocationCheckWithOcspEnabled; - this.ocspRequestTimeout = other.ocspRequestTimeout; - this.allowedOcspResponseTimeSkew = other.allowedOcspResponseTimeSkew; - this.maxOcspResponseThisUpdateAge = other.maxOcspResponseThisUpdateAge; - this.designatedOcspServiceConfiguration = other.designatedOcspServiceConfiguration; this.disallowedSubjectCertificatePolicies = Set.copyOf(other.disallowedSubjectCertificatePolicies); - this.nonceDisabledOcspUrls = Set.copyOf(other.nonceDisabledOcspUrls); + this.isUserCertificateRevocationCheckEnabled = other.isUserCertificateRevocationCheckEnabled; + this.ocspCertificateRevocationChecker = other.ocspCertificateRevocationChecker; + this.pkixRevocationChecker = other.pkixRevocationChecker; + this.revocationMode = other.revocationMode; } void setSiteOrigin(URI siteOrigin) { @@ -87,52 +83,36 @@ Collection getTrustedCACertificates() { return trustedCACertificates; } - boolean isUserCertificateRevocationCheckWithOcspEnabled() { - return isUserCertificateRevocationCheckWithOcspEnabled; - } - - void setUserCertificateRevocationCheckWithOcspDisabled() { - isUserCertificateRevocationCheckWithOcspEnabled = false; - } - - public Duration getOcspRequestTimeout() { - return ocspRequestTimeout; - } - - void setOcspRequestTimeout(Duration ocspRequestTimeout) { - this.ocspRequestTimeout = ocspRequestTimeout; - } - - public Duration getAllowedOcspResponseTimeSkew() { - return allowedOcspResponseTimeSkew; + public Collection getDisallowedSubjectCertificatePolicies() { + return disallowedSubjectCertificatePolicies; } - public void setAllowedOcspResponseTimeSkew(Duration allowedOcspResponseTimeSkew) { - this.allowedOcspResponseTimeSkew = allowedOcspResponseTimeSkew; + boolean isUserCertificateRevocationCheckEnabled() { + return isUserCertificateRevocationCheckEnabled; } - public Duration getMaxOcspResponseThisUpdateAge() { - return maxOcspResponseThisUpdateAge; + void setUserCertificateRevocationCheckDisabled() { + isUserCertificateRevocationCheckEnabled = false; } - public void setMaxOcspResponseThisUpdateAge(Duration maxOcspResponseThisUpdateAge) { - this.maxOcspResponseThisUpdateAge = maxOcspResponseThisUpdateAge; + public void setOcspCertificateRevocationChecker(OcspCertificateRevocationChecker ocspCertificateRevocationChecker) { + this.ocspCertificateRevocationChecker = ocspCertificateRevocationChecker; } - public DesignatedOcspServiceConfiguration getDesignatedOcspServiceConfiguration() { - return designatedOcspServiceConfiguration; + public OcspCertificateRevocationChecker getOcspCertificateRevocationChecker() { + return ocspCertificateRevocationChecker; } - public void setDesignatedOcspServiceConfiguration(DesignatedOcspServiceConfiguration designatedOcspServiceConfiguration) { - this.designatedOcspServiceConfiguration = designatedOcspServiceConfiguration; + public void setPkixRevocationChecker(PKIXRevocationChecker pkixRevocationChecker) { + this.pkixRevocationChecker = pkixRevocationChecker; } - public Collection getDisallowedSubjectCertificatePolicies() { - return disallowedSubjectCertificatePolicies; + public PKIXRevocationChecker getPkixRevocationChecker() { + return pkixRevocationChecker; } - public Collection getNonceDisabledOcspUrls() { - return nonceDisabledOcspUrls; + public RevocationMode getRevocationMode() { + return revocationMode; } /** @@ -147,9 +127,7 @@ void validate() { if (trustedCACertificates.isEmpty()) { throw new IllegalArgumentException("At least one trusted certificate authority must be provided"); } - requirePositiveDuration(ocspRequestTimeout, "OCSP request timeout"); - requirePositiveDuration(allowedOcspResponseTimeSkew, "Allowed OCSP response time-skew"); - requirePositiveDuration(maxOcspResponseThisUpdateAge, "Max OCSP response thisUpdate age"); + validateRevocationConfiguration(); } AuthTokenValidationConfiguration copy() { @@ -179,4 +157,43 @@ public static void validateIsOriginURL(URI uri) throws IllegalArgumentException } } + /** + * Validates that the revocation check configuration is consistent and derives the {@link RevocationMode} from it. + *

+ * Configuration is inconsistent if revocation checking is disabled but a checker is configured or if both + * checkers are configured simultaneously. + * + * @throws IllegalArgumentException if configuration is inconsistent + */ + private void validateRevocationConfiguration() { + final boolean hasOcspChecker = ocspCertificateRevocationChecker != null; + final boolean hasPkixChecker = pkixRevocationChecker != null; + + if (!isUserCertificateRevocationCheckEnabled) { + // If revocation check is disabled, no checker is allowed. + if (hasOcspChecker || hasPkixChecker) { + throw new IllegalArgumentException( + "User certificate revocation check is disabled, but a revocation checker was configured. " + + "Do not combine withoutUserCertificateRevocationCheck() with withOcspCertificateRevocationChecker(...) " + + "or withPKIXRevocationChecker(...)." + ); + } + revocationMode = RevocationMode.DISABLED; + } else { + // Revocation check enabled, at most one checker allowed, if no checker provided, use default PKIX revocation checker in OCSP mode. + if (hasOcspChecker && hasPkixChecker) { + throw new IllegalArgumentException( + "Only one of OcspCertificateRevocationChecker or PKIXRevocationChecker may be configured. " + + "Do not combine withOcspCertificateRevocationChecker(...) with withPKIXRevocationChecker(...)." + ); + } + if (hasOcspChecker) { + revocationMode = RevocationMode.CUSTOM_OCSP; + } else if (hasPkixChecker) { + revocationMode = RevocationMode.CUSTOM_PKIX; + } else { + revocationMode = RevocationMode.PLATFORM_OCSP; + } + } + } } diff --git a/src/main/java/eu/webeid/security/validator/AuthTokenValidator.java b/src/main/java/eu/webeid/security/validator/AuthTokenValidator.java index 3476ea41..8cb95a8b 100644 --- a/src/main/java/eu/webeid/security/validator/AuthTokenValidator.java +++ b/src/main/java/eu/webeid/security/validator/AuthTokenValidator.java @@ -57,6 +57,6 @@ public interface AuthTokenValidator { * @return validated subject certificate * @throws AuthTokenException when validation fails */ - X509Certificate validate(WebEidAuthToken authToken, String currentChallengeNonce) throws AuthTokenException; + ValidationInfo validate(WebEidAuthToken authToken, String currentChallengeNonce) throws AuthTokenException; } diff --git a/src/main/java/eu/webeid/security/validator/AuthTokenValidatorBuilder.java b/src/main/java/eu/webeid/security/validator/AuthTokenValidatorBuilder.java index 9122ee67..6c99649e 100644 --- a/src/main/java/eu/webeid/security/validator/AuthTokenValidatorBuilder.java +++ b/src/main/java/eu/webeid/security/validator/AuthTokenValidatorBuilder.java @@ -23,16 +23,14 @@ package eu.webeid.security.validator; import eu.webeid.security.exceptions.JceException; -import eu.webeid.security.validator.ocsp.OcspClient; -import eu.webeid.security.validator.ocsp.OcspClientImpl; -import eu.webeid.security.validator.ocsp.service.DesignatedOcspServiceConfiguration; +import eu.webeid.security.validator.revocationcheck.OcspCertificateRevocationChecker; import org.bouncycastle.asn1.ASN1ObjectIdentifier; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.net.URI; +import java.security.cert.PKIXRevocationChecker; import java.security.cert.X509Certificate; -import java.time.Duration; import java.util.Collections; import java.util.stream.Collectors; @@ -44,7 +42,6 @@ public class AuthTokenValidatorBuilder { private static final Logger LOG = LoggerFactory.getLogger(AuthTokenValidatorBuilder.class); private final AuthTokenValidationConfiguration configuration = new AuthTokenValidationConfiguration(); - private OcspClient ocspClient; /** * Sets the expected site origin, i.e. the domain that the application is running on. @@ -98,105 +95,50 @@ public AuthTokenValidatorBuilder withDisallowedCertificatePolicies(ASN1ObjectIde } /** - * Turns off user certificate revocation check with OCSP. + * Turns off user certificate revocation check (with OCSP and/or CRL). *

- * Turning off user certificate revocation check with OCSP is dangerous and should be - * used only in exceptional circumstances. + * Turning off user certificate revocation check is dangerous and should be used only in + * exceptional circumstances. * By default, the revocation check is turned on. * * @return the builder instance for method chaining. */ - public AuthTokenValidatorBuilder withoutUserCertificateRevocationCheckWithOcsp() { - configuration.setUserCertificateRevocationCheckWithOcspDisabled(); - LOG.warn("User certificate revocation check with OCSP is disabled, " + + public AuthTokenValidatorBuilder withoutUserCertificateRevocationCheck() { + configuration.setUserCertificateRevocationCheckDisabled(); + LOG.warn("User certificate revocation check is disabled, " + "you should turn off the revocation check only in exceptional circumstances"); return this; } /** - * Sets both the connection and response timeout of user certificate revocation check OCSP requests. + * Configures a custom OCSP revocation checker for validating user certificate revocation status. *

- * This is an optional configuration parameter, the default is 5 seconds. + * When set, the platform (provider default) revocation mechanism is disabled and revocation checking + * delegated to the given {@link OcspCertificateRevocationChecker}. This option is mutually exclusive with + * {@link #withPKIXRevocationChecker(PKIXRevocationChecker)} and {@link #withoutUserCertificateRevocationCheck()}. * - * @param ocspRequestTimeout the duration of OCSP request connection and response timeout - * @return the builder instance for method chaining. - */ - public AuthTokenValidatorBuilder withOcspRequestTimeout(Duration ocspRequestTimeout) { - configuration.setOcspRequestTimeout(ocspRequestTimeout); - LOG.debug("OCSP request timeout set to {}", ocspRequestTimeout); - return this; - } - - /** - * Sets the allowed time skew for OCSP response's thisUpdate and nextUpdate times. - * This parameter is used to allow discrepancies between the system clock and the OCSP responder's clock, - * which may occur due to clock drift, network delays or revocation updates that are not published in real time. - *

- * This is an optional configuration parameter, the default is 15 minutes. - * The relatively long default is specifically chosen to account for one particular OCSP responder that used - * CRLs for authoritative revocation info, these CRLs were updated every 15 minutes. - * - * @param allowedTimeSkew the allowed time skew - * @return the builder instance for method chaining. - */ - public AuthTokenValidatorBuilder withAllowedOcspResponseTimeSkew(Duration allowedTimeSkew) { - configuration.setAllowedOcspResponseTimeSkew(allowedTimeSkew); - LOG.debug("Allowed OCSP response time skew set to {}", allowedTimeSkew); - return this; - } - - /** - * Sets the maximum age of the OCSP response's thisUpdate time before it is considered too old. - *

- * This is an optional configuration parameter, the default is 2 minutes. - * - * @param maxThisUpdateAge the maximum age of the OCSP response's thisUpdate time - * @return the builder instance for method chaining. - */ - public AuthTokenValidatorBuilder withMaxOcspResponseThisUpdateAge(Duration maxThisUpdateAge) { - configuration.setMaxOcspResponseThisUpdateAge(maxThisUpdateAge); - LOG.debug("Maximum OCSP response thisUpdate age set to {}", maxThisUpdateAge); - return this; - } - - /** - * Adds the given URLs to the list of OCSP URLs for which the nonce protocol extension will be disabled. - * The OCSP URL is extracted from the user certificate and some OCSP services don't support the nonce extension. - * - * @param urls OCSP URLs for which the nonce protocol extension will be disabled - * @return the builder instance for method chaining - */ - public AuthTokenValidatorBuilder withNonceDisabledOcspUrls(URI... urls) { - Collections.addAll(configuration.getNonceDisabledOcspUrls(), urls); - LOG.debug("OCSP URLs for which the nonce protocol extension is disabled set to {}", configuration.getNonceDisabledOcspUrls()); - return this; - } - - /** - * Activates the provided designated OCSP service for user certificate revocation check with OCSP. - * The designated service is only used for checking the status of the certificates whose issuers are - * supported by the service, falling back to the default OCSP service access location from - * the certificate's AIA extension if not. - * - * @param serviceConfiguration configuration of the designated OCSP service + * @param customChecker custom OCSP revocation checker implementation * @return the builder instance for method chaining */ - public AuthTokenValidatorBuilder withDesignatedOcspServiceConfiguration(DesignatedOcspServiceConfiguration serviceConfiguration) { - configuration.setDesignatedOcspServiceConfiguration(serviceConfiguration); - LOG.debug("Using designated OCSP service configuration"); + public AuthTokenValidatorBuilder withOcspCertificateRevocationChecker(OcspCertificateRevocationChecker customChecker) { + configuration.setOcspCertificateRevocationChecker(customChecker); return this; } /** - * Uses the provided OCSP client instance during user certificate revocation check with OCSP. - * The provided client instance must be thread-safe. + * Configures a custom {@link PKIXRevocationChecker} to be used as the revocation mechanism during user certificate + * validation with platform PKIX. + *

+ * When set, this checker replaces the platform (provider default) {@link PKIXRevocationChecker}. This option is + * mutually exclusive with {@link #withOcspCertificateRevocationChecker(OcspCertificateRevocationChecker)} + * and {@link #withoutUserCertificateRevocationCheck()}. * - * @param ocspClient OCSP client instance + * @param customChecker custom PKIX revocation checker * @return the builder instance for method chaining + * @throws NullPointerException if {@code customChecker} is null */ - public AuthTokenValidatorBuilder withOcspClient(OcspClient ocspClient) { - this.ocspClient = ocspClient; - LOG.debug("Using the OCSP client provided by API consumer"); + public AuthTokenValidatorBuilder withPKIXRevocationChecker(PKIXRevocationChecker customChecker) { + configuration.setPkixRevocationChecker(customChecker); return this; } @@ -211,10 +153,7 @@ public AuthTokenValidatorBuilder withOcspClient(OcspClient ocspClient) { */ public AuthTokenValidator build() throws NullPointerException, IllegalArgumentException, JceException { configuration.validate(); - if (configuration.isUserCertificateRevocationCheckWithOcspEnabled() && ocspClient == null) { - ocspClient = OcspClientImpl.build(configuration.getOcspRequestTimeout()); - } - return new AuthTokenValidatorImpl(configuration, ocspClient); + return new AuthTokenValidatorImpl(configuration); } } diff --git a/src/main/java/eu/webeid/security/validator/AuthTokenValidatorImpl.java b/src/main/java/eu/webeid/security/validator/AuthTokenValidatorImpl.java index 14cf3e78..9aae53f2 100644 --- a/src/main/java/eu/webeid/security/validator/AuthTokenValidatorImpl.java +++ b/src/main/java/eu/webeid/security/validator/AuthTokenValidatorImpl.java @@ -30,14 +30,12 @@ import eu.webeid.security.exceptions.AuthTokenException; import eu.webeid.security.exceptions.AuthTokenParseException; import eu.webeid.security.exceptions.JceException; -import eu.webeid.security.validator.certvalidators.SubjectCertificateNotRevokedValidator; +import eu.webeid.security.util.DateAndTime; import eu.webeid.security.validator.certvalidators.SubjectCertificatePolicyValidator; import eu.webeid.security.validator.certvalidators.SubjectCertificatePurposeValidator; -import eu.webeid.security.validator.certvalidators.SubjectCertificateTrustedValidator; import eu.webeid.security.validator.certvalidators.SubjectCertificateValidatorBatch; -import eu.webeid.security.validator.ocsp.OcspClient; -import eu.webeid.security.validator.ocsp.OcspServiceProvider; -import eu.webeid.security.validator.ocsp.service.AiaOcspServiceConfiguration; +import eu.webeid.ocsp.service.OcspServiceProvider; +import eu.webeid.security.validator.revocationcheck.RevocationInfo; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -45,7 +43,8 @@ import java.security.cert.CertStore; import java.security.cert.TrustAnchor; import java.security.cert.X509Certificate; -import java.util.Objects; +import java.util.Date; +import java.util.List; import java.util.Set; /** @@ -65,15 +64,13 @@ final class AuthTokenValidatorImpl implements AuthTokenValidator { private final CertStore trustedCACertificateCertStore; // OcspClient uses built-in HttpClient internally by default. // A single HttpClient instance is reused for all HTTP calls to utilize connection and thread pools. - private OcspClient ocspClient; private OcspServiceProvider ocspServiceProvider; private final AuthTokenSignatureValidator authTokenSignatureValidator; /** * @param configuration configuration parameters for the token validator - * @param ocspClient client for communicating with the OCSP service */ - AuthTokenValidatorImpl(AuthTokenValidationConfiguration configuration, OcspClient ocspClient) throws JceException { + AuthTokenValidatorImpl(AuthTokenValidationConfiguration configuration) throws JceException { // Copy the configuration object to make AuthTokenValidatorImpl immutable and thread-safe. this.configuration = configuration.copy(); @@ -86,16 +83,6 @@ final class AuthTokenValidatorImpl implements AuthTokenValidator { new SubjectCertificatePolicyValidator(configuration.getDisallowedSubjectCertificatePolicies())::validateCertificatePolicies ); - if (configuration.isUserCertificateRevocationCheckWithOcspEnabled()) { - // The OCSP client may be provided by the API consumer. - this.ocspClient = Objects.requireNonNull(ocspClient, "OCSP client must not be null when OCSP check is enabled"); - ocspServiceProvider = new OcspServiceProvider( - configuration.getDesignatedOcspServiceConfiguration(), - new AiaOcspServiceConfiguration(configuration.getNonceDisabledOcspUrls(), - trustedCACertificateAnchors, - trustedCACertificateCertStore)); - } - authTokenSignatureValidator = new AuthTokenSignatureValidator(configuration.getSiteOrigin()); } @@ -113,7 +100,7 @@ public WebEidAuthToken parse(String authToken) throws AuthTokenException { } @Override - public X509Certificate validate(WebEidAuthToken authToken, String currentChallengeNonce) throws AuthTokenException { + public ValidationInfo validate(WebEidAuthToken authToken, String currentChallengeNonce) throws AuthTokenException { try { LOG.info("Starting token validation"); return validateToken(authToken, currentChallengeNonce); @@ -145,49 +132,41 @@ private WebEidAuthToken parseToken(String authToken) throws AuthTokenParseExcept } } - private X509Certificate validateToken(WebEidAuthToken token, String currentChallengeNonce) throws AuthTokenException { - if (token.getFormat() == null || !token.getFormat().startsWith(CURRENT_TOKEN_FORMAT_VERSION)) { + private ValidationInfo validateToken(WebEidAuthToken token, String currentChallengeNonce) throws AuthTokenException { + if (token.format() == null || !token.format().startsWith(CURRENT_TOKEN_FORMAT_VERSION)) { throw new AuthTokenParseException("Only token format version '" + CURRENT_TOKEN_FORMAT_VERSION + "' is currently supported"); } - if (token.getUnverifiedCertificate() == null || token.getUnverifiedCertificate().isEmpty()) { + if (token.unverifiedCertificate() == null || token.unverifiedCertificate().isEmpty()) { throw new AuthTokenParseException("'unverifiedCertificate' field is missing, null or empty"); } - final X509Certificate subjectCertificate = CertificateLoader.decodeCertificateFromBase64(token.getUnverifiedCertificate()); + final X509Certificate subjectCertificate = CertificateLoader.decodeCertificateFromBase64(token.unverifiedCertificate()); simpleSubjectCertificateValidators.executeFor(subjectCertificate); - getCertTrustValidators().executeFor(subjectCertificate); + + // Use the clock instance so that the date can be mocked in tests. + final Date now = DateAndTime.DefaultClock.getInstance().now(); + + final List revocationInfoList = CertificateValidator.validateCertificateTrustAndRevocation( + subjectCertificate, + trustedCACertificateAnchors, + trustedCACertificateCertStore, + now, + configuration.getRevocationMode(), + configuration.getOcspCertificateRevocationChecker(), + configuration.getPkixRevocationChecker() + ); + LOG.debug("Subject certificate is valid and signed by a trusted CA"); // It is guaranteed that if the signature verification succeeds, then the origin and challenge // have been implicitly and correctly verified without the need to implement any additional checks. - authTokenSignatureValidator.validate(token.getAlgorithm(), - token.getSignature(), + authTokenSignatureValidator.validate(token.algorithm(), + token.signature(), subjectCertificate.getPublicKey(), - currentChallengeNonce); - - return subjectCertificate; - } - - /** - * Creates the certificate trust validators batch. - * As SubjectCertificateTrustedValidator has mutable state that SubjectCertificateNotRevokedValidator depends on, - * they cannot be reused/cached in an instance variable in a multi-threaded environment. Hence, they are - * re-created for each validation run for thread safety. - * - * @return certificate trust validator batch - */ - private SubjectCertificateValidatorBatch getCertTrustValidators() { - final SubjectCertificateTrustedValidator certTrustedValidator = - new SubjectCertificateTrustedValidator(trustedCACertificateAnchors, trustedCACertificateCertStore); - return SubjectCertificateValidatorBatch.createFrom( - certTrustedValidator::validateCertificateTrusted - ).addOptional(configuration.isUserCertificateRevocationCheckWithOcspEnabled(), - new SubjectCertificateNotRevokedValidator(certTrustedValidator, - ocspClient, ocspServiceProvider, - configuration.getAllowedOcspResponseTimeSkew(), - configuration.getMaxOcspResponseThisUpdateAge() - )::validateCertificateNotRevoked + currentChallengeNonce ); + + return new ValidationInfo(subjectCertificate, revocationInfoList); } } diff --git a/src/main/java/eu/webeid/security/validator/ValidationInfo.java b/src/main/java/eu/webeid/security/validator/ValidationInfo.java new file mode 100644 index 00000000..11a61561 --- /dev/null +++ b/src/main/java/eu/webeid/security/validator/ValidationInfo.java @@ -0,0 +1,15 @@ +package eu.webeid.security.validator; + +import eu.webeid.security.validator.revocationcheck.RevocationInfo; + +import java.security.cert.X509Certificate; +import java.util.List; + +import static java.util.Objects.requireNonNull; + +public record ValidationInfo(X509Certificate subjectCertificate, List revocationInfoList) { + public ValidationInfo { + requireNonNull(subjectCertificate, "subjectCertificate"); + requireNonNull(revocationInfoList, "revocationInfoList"); + } +} diff --git a/src/main/java/eu/webeid/security/validator/certvalidators/SubjectCertificateTrustedValidator.java b/src/main/java/eu/webeid/security/validator/certvalidators/SubjectCertificateTrustedValidator.java deleted file mode 100644 index a706ffc4..00000000 --- a/src/main/java/eu/webeid/security/validator/certvalidators/SubjectCertificateTrustedValidator.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * 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.security.validator.certvalidators; - -import eu.webeid.security.certificate.CertificateValidator; -import eu.webeid.security.exceptions.AuthTokenException; -import eu.webeid.security.exceptions.CertificateExpiredException; -import eu.webeid.security.exceptions.CertificateNotTrustedException; -import eu.webeid.security.exceptions.CertificateNotYetValidException; -import eu.webeid.security.util.DateAndTime; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.security.cert.CertStore; -import java.security.cert.TrustAnchor; -import java.security.cert.X509Certificate; -import java.util.Date; -import java.util.Set; - -public final class SubjectCertificateTrustedValidator { - - private static final Logger LOG = LoggerFactory.getLogger(SubjectCertificateTrustedValidator.class); - - private final Set trustedCACertificateAnchors; - private final CertStore trustedCACertificateCertStore; - private X509Certificate subjectCertificateIssuerCertificate; - - public SubjectCertificateTrustedValidator(Set trustedCACertificateAnchors, CertStore trustedCACertificateCertStore) { - this.trustedCACertificateAnchors = trustedCACertificateAnchors; - this.trustedCACertificateCertStore = trustedCACertificateCertStore; - } - - /** - * Checks that the user certificate from the authentication token is valid and signed by - * a trusted certificate authority. Also checks the validity of the user certificate's - * trusted CA certificate. - * - * @param subjectCertificate user certificate to be validated - * @throws CertificateNotTrustedException when user certificate is not signed by a trusted CA - * @throws CertificateNotYetValidException when a CA certificate in the chain or the user certificate is not yet valid - * @throws CertificateExpiredException when a CA certificate in the chain or the user certificate is expired - */ - public void validateCertificateTrusted(X509Certificate subjectCertificate) throws AuthTokenException { - // Use the clock instance so that the date can be mocked in tests. - final Date now = DateAndTime.DefaultClock.getInstance().now(); - subjectCertificateIssuerCertificate = CertificateValidator.validateIsSignedByTrustedCA( - subjectCertificate, - trustedCACertificateAnchors, - trustedCACertificateCertStore, - now - ); - LOG.debug("Subject certificate is valid and signed by a trusted CA"); - } - - public X509Certificate getSubjectCertificateIssuerCertificate() { - return subjectCertificateIssuerCertificate; - } -} diff --git a/src/main/java/eu/webeid/security/validator/certvalidators/SubjectCertificateValidatorBatch.java b/src/main/java/eu/webeid/security/validator/certvalidators/SubjectCertificateValidatorBatch.java index 264135e0..64f4765f 100644 --- a/src/main/java/eu/webeid/security/validator/certvalidators/SubjectCertificateValidatorBatch.java +++ b/src/main/java/eu/webeid/security/validator/certvalidators/SubjectCertificateValidatorBatch.java @@ -45,13 +45,6 @@ public void executeFor(X509Certificate subjectCertificate) throws AuthTokenExcep } } - public SubjectCertificateValidatorBatch addOptional(boolean condition, SubjectCertificateValidator optionalValidator) { - if (condition) { - validatorList.add(optionalValidator); - } - return this; - } - private SubjectCertificateValidatorBatch(List validatorList) { this.validatorList = validatorList; } diff --git a/src/main/java/eu/webeid/security/validator/revocationcheck/OcspCertificateRevocationChecker.java b/src/main/java/eu/webeid/security/validator/revocationcheck/OcspCertificateRevocationChecker.java new file mode 100644 index 00000000..bb0421cf --- /dev/null +++ b/src/main/java/eu/webeid/security/validator/revocationcheck/OcspCertificateRevocationChecker.java @@ -0,0 +1,34 @@ +/* + * 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.security.validator.revocationcheck; + +import eu.webeid.security.exceptions.AuthTokenException; + +import java.security.cert.X509Certificate; +import java.util.List; + +public interface OcspCertificateRevocationChecker { + + List validateCertificateNotRevoked(X509Certificate subjectCertificate, + X509Certificate issuerCertificate) throws AuthTokenException; + +} \ No newline at end of file diff --git a/src/main/java/eu/webeid/security/validator/revocationcheck/RevocationInfo.java b/src/main/java/eu/webeid/security/validator/revocationcheck/RevocationInfo.java new file mode 100644 index 00000000..3147ddd0 --- /dev/null +++ b/src/main/java/eu/webeid/security/validator/revocationcheck/RevocationInfo.java @@ -0,0 +1,34 @@ +/* + * 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.security.validator.revocationcheck; + +import java.net.URI; +import java.util.Map; + +import static java.util.Objects.requireNonNull; + +public record RevocationInfo(URI ocspResponderUri, Map ocspResponseAttributes) { + + public static final String KEY_OCSP_RESPONSE = "OCSP_RESPONSE"; + public static final String KEY_OCSP_ERROR = "OCSP_ERROR"; + +} \ No newline at end of file diff --git a/src/main/java/eu/webeid/security/validator/revocationcheck/RevocationMode.java b/src/main/java/eu/webeid/security/validator/revocationcheck/RevocationMode.java new file mode 100644 index 00000000..e9236873 --- /dev/null +++ b/src/main/java/eu/webeid/security/validator/revocationcheck/RevocationMode.java @@ -0,0 +1,5 @@ +package eu.webeid.security.validator.revocationcheck; + +public enum RevocationMode { + PLATFORM_OCSP, CUSTOM_OCSP, CUSTOM_PKIX, DISABLED +} diff --git a/src/test/java/eu/webeid/security/validator/certvalidators/SubjectCertificateNotRevokedValidatorTest.java b/src/test/java/eu/webeid/ocsp/DefaultOcspCertificateRevocationCheckerTest.java similarity index 71% rename from src/test/java/eu/webeid/security/validator/certvalidators/SubjectCertificateNotRevokedValidatorTest.java rename to src/test/java/eu/webeid/ocsp/DefaultOcspCertificateRevocationCheckerTest.java index 771c3018..bdfcd8d9 100644 --- a/src/test/java/eu/webeid/security/validator/certvalidators/SubjectCertificateNotRevokedValidatorTest.java +++ b/src/test/java/eu/webeid/ocsp/DefaultOcspCertificateRevocationCheckerTest.java @@ -20,17 +20,20 @@ * SOFTWARE. */ -package eu.webeid.security.validator.certvalidators; +package eu.webeid.ocsp; import eu.webeid.security.exceptions.CertificateExpiredException; import eu.webeid.security.exceptions.CertificateNotTrustedException; import eu.webeid.security.exceptions.JceException; -import eu.webeid.security.exceptions.UserCertificateOCSPCheckFailedException; -import eu.webeid.security.exceptions.UserCertificateRevokedException; +import eu.webeid.ocsp.exceptions.UserCertificateOCSPCheckFailedException; +import eu.webeid.ocsp.exceptions.UserCertificateRevokedException; +import eu.webeid.security.testutil.AbstractTestWithValidator; +import eu.webeid.security.testutil.AuthTokenValidators; import eu.webeid.security.util.DateAndTime; -import eu.webeid.security.validator.ocsp.OcspClient; -import eu.webeid.security.validator.ocsp.OcspClientImpl; -import eu.webeid.security.validator.ocsp.OcspServiceProvider; +import eu.webeid.ocsp.client.OcspClient; +import eu.webeid.ocsp.client.OcspClientImpl; +import eu.webeid.ocsp.service.OcspServiceProvider; +import eu.webeid.security.validator.AuthTokenValidator; import org.bouncycastle.asn1.ocsp.OCSPResponseStatus; import org.bouncycastle.cert.ocsp.OCSPException; import org.bouncycastle.cert.ocsp.OCSPResp; @@ -40,7 +43,6 @@ import java.io.IOException; import java.io.InputStream; -import java.lang.reflect.Field; import java.net.ConnectException; import java.net.URI; import java.net.URISyntaxException; @@ -56,33 +58,39 @@ import static eu.webeid.security.testutil.Certificates.getJaakKristjanEsteid2018Cert; import static eu.webeid.security.testutil.Certificates.getTestEsteid2018CA; import static eu.webeid.security.testutil.DateMocker.mockDate; -import static eu.webeid.security.testutil.OcspServiceMaker.getAiaOcspServiceProvider; -import static eu.webeid.security.testutil.OcspServiceMaker.getDesignatedOcspServiceProvider; -import static eu.webeid.security.validator.AuthTokenValidatorBuilderTest.CONFIGURATION; +import static eu.webeid.ocsp.service.OcspServiceMaker.getAiaOcspServiceProvider; +import static eu.webeid.ocsp.service.OcspServiceMaker.getDesignatedOcspServiceProvider; import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.when; -class SubjectCertificateNotRevokedValidatorTest { +class DefaultOcspCertificateRevocationCheckerTest extends AbstractTestWithValidator { private final OcspClient ocspClient = OcspClientImpl.build(Duration.ofSeconds(5)); - private SubjectCertificateTrustedValidator trustedValidator; private X509Certificate estEid2018Cert; + private X509Certificate testEsteid2018CA; @BeforeEach void setUp() throws Exception { - trustedValidator = new SubjectCertificateTrustedValidator(null, null); - setSubjectCertificateIssuerCertificate(trustedValidator); estEid2018Cert = getJaakKristjanEsteid2018Cert(); + testEsteid2018CA = getTestEsteid2018CA(); + } + + @Test + void whenValidDefaultConfiguration_thenSucceeds() throws Exception { + final AuthTokenValidator validator = getAuthTokenValidatorWithDefaultOcspCertificateRevocationChecker(); + assertThatCode(() -> validator.validate(validAuthToken, VALID_CHALLENGE_NONCE)) + .doesNotThrowAnyException(); } @Test void whenValidAiaOcspResponderConfiguration_thenSucceeds() throws Exception { - final SubjectCertificateNotRevokedValidator validator = getSubjectCertificateNotRevokedValidator(ocspClient, getAiaOcspServiceProvider()); + final DefaultOcspCertificateRevocationChecker validator = getOcspCertificateRevocationChecker(ocspClient, getAiaOcspServiceProvider()); assertThatCode(() -> - validator.validateCertificateNotRevoked(estEid2018Cert)) + validator.validateCertificateNotRevoked(estEid2018Cert, testEsteid2018CA)) .doesNotThrowAnyException(); } @@ -90,9 +98,9 @@ void whenValidAiaOcspResponderConfiguration_thenSucceeds() throws Exception { @Disabled("As new designated test OCSP responder certificates are issued more frequently now, it is no longer feasible to keep the certificates up to date") void whenValidDesignatedOcspResponderConfiguration_thenSucceeds() throws Exception { final OcspServiceProvider ocspServiceProvider = getDesignatedOcspServiceProvider(); - final SubjectCertificateNotRevokedValidator validator = getSubjectCertificateNotRevokedValidator(ocspServiceProvider); + final DefaultOcspCertificateRevocationChecker validator = getOcspCertificateRevocationChecker(ocspServiceProvider); assertThatCode(() -> - validator.validateCertificateNotRevoked(estEid2018Cert)) + validator.validateCertificateNotRevoked(estEid2018Cert, testEsteid2018CA)) .doesNotThrowAnyException(); } @@ -100,18 +108,18 @@ void whenValidDesignatedOcspResponderConfiguration_thenSucceeds() throws Excepti @Disabled("As new designated test OCSP responder certificates are issued more frequently now, it is no longer feasible to keep the certificates up to date") void whenValidOcspNonceDisabledConfiguration_thenSucceeds() throws Exception { final OcspServiceProvider ocspServiceProvider = getDesignatedOcspServiceProvider(false); - final SubjectCertificateNotRevokedValidator validator = getSubjectCertificateNotRevokedValidator(ocspServiceProvider); + final DefaultOcspCertificateRevocationChecker validator = getOcspCertificateRevocationChecker(ocspServiceProvider); assertThatCode(() -> - validator.validateCertificateNotRevoked(estEid2018Cert)) + validator.validateCertificateNotRevoked(estEid2018Cert, testEsteid2018CA)) .doesNotThrowAnyException(); } @Test void whenOcspUrlIsInvalid_thenThrows() throws Exception { final OcspServiceProvider ocspServiceProvider = getDesignatedOcspServiceProvider("http://invalid.invalid"); - final SubjectCertificateNotRevokedValidator validator = getSubjectCertificateNotRevokedValidator(ocspServiceProvider); + final DefaultOcspCertificateRevocationChecker validator = getOcspCertificateRevocationChecker(ocspServiceProvider); assertThatCode(() -> - validator.validateCertificateNotRevoked(estEid2018Cert)) + validator.validateCertificateNotRevoked(estEid2018Cert, testEsteid2018CA)) .isInstanceOf(UserCertificateOCSPCheckFailedException.class) .cause() .isInstanceOf(ConnectException.class); @@ -120,9 +128,9 @@ void whenOcspUrlIsInvalid_thenThrows() throws Exception { @Test void whenOcspRequestFails_thenThrows() throws Exception { final OcspServiceProvider ocspServiceProvider = getDesignatedOcspServiceProvider("http://demo.sk.ee/ocsps"); - final SubjectCertificateNotRevokedValidator validator = getSubjectCertificateNotRevokedValidator(ocspServiceProvider); + final DefaultOcspCertificateRevocationChecker validator = getOcspCertificateRevocationChecker(ocspServiceProvider); assertThatCode(() -> - validator.validateCertificateNotRevoked(estEid2018Cert)) + validator.validateCertificateNotRevoked(estEid2018Cert, testEsteid2018CA)) .isInstanceOf(UserCertificateOCSPCheckFailedException.class) .cause() .isInstanceOf(IOException.class) @@ -131,11 +139,11 @@ void whenOcspRequestFails_thenThrows() throws Exception { @Test void whenOcspRequestHasInvalidBody_thenThrows() throws Exception { - final SubjectCertificateNotRevokedValidator validator = getSubjectCertificateNotRevokedValidatorWithAiaOcsp( + final DefaultOcspCertificateRevocationChecker validator = getOcspCertificateRevocationCheckerWithAiaOcsp( getMockedResponse("invalid".getBytes()) ); assertThatCode(() -> - validator.validateCertificateNotRevoked(estEid2018Cert)) + validator.validateCertificateNotRevoked(estEid2018Cert, testEsteid2018CA)) .isInstanceOf(UserCertificateOCSPCheckFailedException.class) .cause() .isInstanceOf(IOException.class) @@ -144,44 +152,44 @@ void whenOcspRequestHasInvalidBody_thenThrows() throws Exception { @Test void whenOcspResponseIsNotSuccessful_thenThrows() throws Exception { - final SubjectCertificateNotRevokedValidator validator = getSubjectCertificateNotRevokedValidatorWithAiaOcsp( + final DefaultOcspCertificateRevocationChecker validator = getOcspCertificateRevocationCheckerWithAiaOcsp( getMockedResponse(buildOcspResponseBodyWithInternalErrorStatus()) ); assertThatExceptionOfType(UserCertificateOCSPCheckFailedException.class) .isThrownBy(() -> - validator.validateCertificateNotRevoked(estEid2018Cert)) + validator.validateCertificateNotRevoked(estEid2018Cert, testEsteid2018CA)) .withMessage("User certificate revocation check has failed: Response status: internal error"); } @Test void whenOcspResponseHasInvalidCertificateId_thenThrows() throws Exception { - final SubjectCertificateNotRevokedValidator validator = getSubjectCertificateNotRevokedValidatorWithAiaOcsp( + final DefaultOcspCertificateRevocationChecker validator = getOcspCertificateRevocationCheckerWithAiaOcsp( getMockedResponse(buildOcspResponseBodyWithInvalidCertificateId()) ); assertThatExceptionOfType(UserCertificateOCSPCheckFailedException.class) .isThrownBy(() -> - validator.validateCertificateNotRevoked(estEid2018Cert)) + validator.validateCertificateNotRevoked(estEid2018Cert, testEsteid2018CA)) .withMessage("User certificate revocation check has failed: OCSP responded with certificate ID that differs from the requested ID"); } @Test void whenOcspResponseHasInvalidSignature_thenThrows() throws Exception { - final SubjectCertificateNotRevokedValidator validator = getSubjectCertificateNotRevokedValidatorWithAiaOcsp( + final DefaultOcspCertificateRevocationChecker validator = getOcspCertificateRevocationCheckerWithAiaOcsp( getMockedResponse(buildOcspResponseBodyWithInvalidSignature()) ); assertThatExceptionOfType(UserCertificateOCSPCheckFailedException.class) .isThrownBy(() -> - validator.validateCertificateNotRevoked(estEid2018Cert)) + validator.validateCertificateNotRevoked(estEid2018Cert, testEsteid2018CA)) .withMessage("User certificate revocation check has failed: OCSP response signature is invalid"); } @Test void whenOcspResponseHasInvalidResponderCert_thenThrows() throws Exception { - final SubjectCertificateNotRevokedValidator validator = getSubjectCertificateNotRevokedValidatorWithAiaOcsp( + final DefaultOcspCertificateRevocationChecker validator = getOcspCertificateRevocationCheckerWithAiaOcsp( getMockedResponse(buildOcspResponseBodyWithInvalidResponderCert()) ); assertThatCode(() -> - validator.validateCertificateNotRevoked(estEid2018Cert)) + validator.validateCertificateNotRevoked(estEid2018Cert, testEsteid2018CA)) .isInstanceOf(UserCertificateOCSPCheckFailedException.class) .cause() .isInstanceOf(OCSPException.class) @@ -190,11 +198,11 @@ void whenOcspResponseHasInvalidResponderCert_thenThrows() throws Exception { @Test void whenOcspResponseHasInvalidTag_thenThrows() throws Exception { - final SubjectCertificateNotRevokedValidator validator = getSubjectCertificateNotRevokedValidatorWithAiaOcsp( + final DefaultOcspCertificateRevocationChecker validator = getOcspCertificateRevocationCheckerWithAiaOcsp( getMockedResponse(buildOcspResponseBodyWithInvalidTag()) ); assertThatCode(() -> - validator.validateCertificateNotRevoked(estEid2018Cert)) + validator.validateCertificateNotRevoked(estEid2018Cert, testEsteid2018CA)) .isInstanceOf(UserCertificateOCSPCheckFailedException.class) .cause() .isInstanceOf(OCSPException.class) @@ -203,36 +211,36 @@ void whenOcspResponseHasInvalidTag_thenThrows() throws Exception { @Test void whenOcspResponseHas2CertResponses_thenThrows() throws Exception { - final SubjectCertificateNotRevokedValidator validator = getSubjectCertificateNotRevokedValidatorWithAiaOcsp( + final DefaultOcspCertificateRevocationChecker validator = getOcspCertificateRevocationCheckerWithAiaOcsp( getMockedResponse(getOcspResponseBytesFromResources("ocsp_response_with_2_responses.der")) ); assertThatExceptionOfType(UserCertificateOCSPCheckFailedException.class) .isThrownBy(() -> - validator.validateCertificateNotRevoked(estEid2018Cert)) + validator.validateCertificateNotRevoked(estEid2018Cert, testEsteid2018CA)) .withMessage("User certificate revocation check has failed: OCSP response must contain one response, received 2 responses instead"); } @Disabled("It is difficult to make Python and Java CertId equal, needs more work") void whenOcspResponseHas2ResponderCerts_thenThrows() throws Exception { - final SubjectCertificateNotRevokedValidator validator = getSubjectCertificateNotRevokedValidatorWithAiaOcsp( + final DefaultOcspCertificateRevocationChecker validator = getOcspCertificateRevocationCheckerWithAiaOcsp( getMockedResponse(getOcspResponseBytesFromResources("ocsp_response_with_2_responder_certs.der")) ); assertThatExceptionOfType(UserCertificateOCSPCheckFailedException.class) .isThrownBy(() -> - validator.validateCertificateNotRevoked(estEid2018Cert)) + validator.validateCertificateNotRevoked(estEid2018Cert, testEsteid2018CA)) .withMessage("User certificate revocation check has failed: OCSP response must contain one responder certificate, received 2 certificates instead"); } @Test void whenOcspResponseRevoked_thenThrows() throws Exception { - final SubjectCertificateNotRevokedValidator validator = getSubjectCertificateNotRevokedValidatorWithAiaOcsp( + final DefaultOcspCertificateRevocationChecker validator = getOcspCertificateRevocationCheckerWithAiaOcsp( getMockedResponse(getOcspResponseBytesFromResources("ocsp_response_revoked.der")) ); try (var mockedClock = mockStatic(DateAndTime.DefaultClock.class)) { mockDate("2021-09-18", mockedClock); assertThatExceptionOfType(UserCertificateRevokedException.class) .isThrownBy(() -> - validator.validateCertificateNotRevoked(estEid2018Cert)) + validator.validateCertificateNotRevoked(estEid2018Cert, testEsteid2018CA)) .withMessage("User certificate has been revoked: Revocation reason: 0"); } } @@ -241,55 +249,79 @@ void whenOcspResponseRevoked_thenThrows() throws Exception { void whenOcspResponseUnknown_thenThrows() throws Exception { final OcspServiceProvider ocspServiceProvider = getDesignatedOcspServiceProvider("https://web-eid-test.free.beeceptor.com"); final HttpResponse response = getMockedResponse(getOcspResponseBytesFromResources("ocsp_response_unknown.der")); - final SubjectCertificateNotRevokedValidator validator = getSubjectCertificateNotRevokedValidator(getMockClient(response), ocspServiceProvider); + final DefaultOcspCertificateRevocationChecker validator = getOcspCertificateRevocationChecker(getMockClient(response), ocspServiceProvider); try (var mockedClock = mockStatic(DateAndTime.DefaultClock.class)) { mockDate("2021-09-18T00:16:25", mockedClock); assertThatExceptionOfType(UserCertificateRevokedException.class) .isThrownBy(() -> - validator.validateCertificateNotRevoked(estEid2018Cert)) + validator.validateCertificateNotRevoked(estEid2018Cert, testEsteid2018CA)) .withMessage("User certificate has been revoked: Unknown status"); } } @Test void whenOcspResponseCACertNotTrusted_thenThrows() throws Exception { - final SubjectCertificateNotRevokedValidator validator = getSubjectCertificateNotRevokedValidatorWithAiaOcsp( + final DefaultOcspCertificateRevocationChecker validator = getOcspCertificateRevocationCheckerWithAiaOcsp( getMockedResponse(getOcspResponseBytesFromResources("ocsp_response_unknown.der")) ); try (var mockedClock = mockStatic(DateAndTime.DefaultClock.class)) { mockDate("2021-09-18T00:16:25", mockedClock); assertThatExceptionOfType(CertificateNotTrustedException.class) .isThrownBy(() -> - validator.validateCertificateNotRevoked(estEid2018Cert)) + validator.validateCertificateNotRevoked(estEid2018Cert, testEsteid2018CA)) .withMessage("Certificate EMAILADDRESS=pki@sk.ee, CN=TEST of SK OCSP RESPONDER 2020, OU=OCSP, O=AS Sertifitseerimiskeskus, C=EE is not trusted"); } } @Test void whenOcspResponseCACertExpired_thenThrows() throws Exception { - final SubjectCertificateNotRevokedValidator validator = getSubjectCertificateNotRevokedValidatorWithAiaOcsp( + final DefaultOcspCertificateRevocationChecker validator = getOcspCertificateRevocationCheckerWithAiaOcsp( getMockedResponse(getOcspResponseBytesFromResources("ocsp_response_unknown.der")) ); assertThatExceptionOfType(CertificateExpiredException.class) .isThrownBy(() -> - validator.validateCertificateNotRevoked(estEid2018Cert)) + validator.validateCertificateNotRevoked(estEid2018Cert, testEsteid2018CA)) .withMessage("AIA OCSP responder certificate has expired"); } @Test void whenNonceDiffers_thenThrows() throws Exception { - final SubjectCertificateNotRevokedValidator validator = getSubjectCertificateNotRevokedValidatorWithAiaOcsp( + final DefaultOcspCertificateRevocationChecker validator = getOcspCertificateRevocationCheckerWithAiaOcsp( getMockedResponse(getOcspResponseBytesFromResources()) ); try (var mockedClock = mockStatic(DateAndTime.DefaultClock.class)) { mockDate("2021-09-17T18:25:24", mockedClock); assertThatExceptionOfType(UserCertificateOCSPCheckFailedException.class) .isThrownBy(() -> - validator.validateCertificateNotRevoked(estEid2018Cert)) + validator.validateCertificateNotRevoked(estEid2018Cert, testEsteid2018CA)) .withMessage("User certificate revocation check has failed: OCSP request and response nonces differ, possible replay attack"); } } + @Test + void whenInvalidOcspResponseTimeSkew_thenThrows() { + assertThatThrownBy(() -> getOcspCertificateRevocationCheckerWithTimeSkewAndUpdateAge(Duration.ofMinutes(-1), Duration.ofMinutes(1))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageStartingWith("allowedOcspResponseTimeSkew must be greater than zero"); + } + + @Test + void whenInvalidMaxOcspResponseThisUpdateAge_thenThrows() { + assertThatThrownBy(() -> getOcspCertificateRevocationCheckerWithTimeSkewAndUpdateAge(Duration.ofMinutes(1), Duration.ZERO)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageStartingWith("maxOcspResponseThisUpdateAge must be greater than zero"); + } + + private static AuthTokenValidator getAuthTokenValidatorWithDefaultOcspCertificateRevocationChecker() throws CertificateException, JceException, IOException { + return AuthTokenValidators.getDefaultAuthTokenValidatorBuilder() + .withOcspCertificateRevocationChecker(new DefaultOcspCertificateRevocationChecker( + OcspClientImpl.build(Duration.ofSeconds(5)), + getAiaOcspServiceProvider(), + DefaultOcspCertificateRevocationChecker.DEFAULT_TIME_SKEW, + DefaultOcspCertificateRevocationChecker.DEFAULT_THIS_UPDATE_AGE + )).build(); + } + private static byte[] buildOcspResponseBodyWithInternalErrorStatus() throws IOException { final byte[] ocspResponseBytes = getOcspResponseBytesFromResources(); final int STATUS_OFFSET = 6; @@ -338,22 +370,20 @@ private static byte[] getOcspResponseBytesFromResources(String resource) throws } } - private SubjectCertificateNotRevokedValidator getSubjectCertificateNotRevokedValidatorWithAiaOcsp(HttpResponse response) throws JceException { - return getSubjectCertificateNotRevokedValidator(getMockClient(response), getAiaOcspServiceProvider()); + private DefaultOcspCertificateRevocationChecker getOcspCertificateRevocationCheckerWithAiaOcsp(HttpResponse response) throws JceException { + return getOcspCertificateRevocationChecker(getMockClient(response), getAiaOcspServiceProvider()); } - private SubjectCertificateNotRevokedValidator getSubjectCertificateNotRevokedValidator(OcspServiceProvider ocspServiceProvider) { - return getSubjectCertificateNotRevokedValidator(ocspClient, ocspServiceProvider); + private DefaultOcspCertificateRevocationChecker getOcspCertificateRevocationChecker(OcspServiceProvider ocspServiceProvider) { + return getOcspCertificateRevocationChecker(ocspClient, ocspServiceProvider); } - private SubjectCertificateNotRevokedValidator getSubjectCertificateNotRevokedValidator(OcspClient client, OcspServiceProvider ocspServiceProvider) { - return new SubjectCertificateNotRevokedValidator(trustedValidator, client, ocspServiceProvider, CONFIGURATION.getAllowedOcspResponseTimeSkew(), CONFIGURATION.getMaxOcspResponseThisUpdateAge()); + private DefaultOcspCertificateRevocationChecker getOcspCertificateRevocationChecker(OcspClient client, OcspServiceProvider ocspServiceProvider) { + return new DefaultOcspCertificateRevocationChecker(client, ocspServiceProvider, DefaultOcspCertificateRevocationChecker.DEFAULT_TIME_SKEW, DefaultOcspCertificateRevocationChecker.DEFAULT_THIS_UPDATE_AGE); } - private static void setSubjectCertificateIssuerCertificate(SubjectCertificateTrustedValidator trustedValidator) throws NoSuchFieldException, IllegalAccessException, CertificateException, IOException { - final Field field = trustedValidator.getClass().getDeclaredField("subjectCertificateIssuerCertificate"); - field.setAccessible(true); - field.set(trustedValidator, getTestEsteid2018CA()); + private void getOcspCertificateRevocationCheckerWithTimeSkewAndUpdateAge(Duration timeSkew, Duration updateAge) throws JceException { + new DefaultOcspCertificateRevocationChecker(ocspClient, getAiaOcspServiceProvider(), timeSkew, updateAge); } private HttpResponse getMockedResponse(byte[] bodyContent) throws URISyntaxException { diff --git a/src/test/java/eu/webeid/security/validator/ocsp/OcspClientOverrideTest.java b/src/test/java/eu/webeid/ocsp/client/OcspClientOverrideTest.java similarity index 66% rename from src/test/java/eu/webeid/security/validator/ocsp/OcspClientOverrideTest.java rename to src/test/java/eu/webeid/ocsp/client/OcspClientOverrideTest.java index 4cd1d951..529b7c34 100644 --- a/src/test/java/eu/webeid/security/validator/ocsp/OcspClientOverrideTest.java +++ b/src/test/java/eu/webeid/ocsp/client/OcspClientOverrideTest.java @@ -20,8 +20,9 @@ * SOFTWARE. */ -package eu.webeid.security.validator.ocsp; +package eu.webeid.ocsp.client; +import eu.webeid.ocsp.DefaultOcspCertificateRevocationChecker; import eu.webeid.security.exceptions.JceException; import eu.webeid.security.testutil.AbstractTestWithValidator; import eu.webeid.security.testutil.AuthTokenValidators; @@ -37,6 +38,7 @@ import java.security.cert.CertificateException; import java.time.Duration; +import static eu.webeid.ocsp.service.OcspServiceMaker.getAiaOcspServiceProvider; import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -44,22 +46,41 @@ class OcspClientOverrideTest extends AbstractTestWithValidator { @Test void whenOcspClientIsOverridden_thenItIsUsed() throws JceException, CertificateException, IOException { - final AuthTokenValidator validator = AuthTokenValidators.getAuthTokenValidatorWithOverriddenOcspClient(new OcpClientThatThrows()); + final AuthTokenValidator validator = getAuthTokenValidatorWithOverriddenOcspClient(new OcpClientThatThrows()); assertThatThrownBy(() -> validator.validate(validAuthToken, VALID_CHALLENGE_NONCE)) .cause() .isInstanceOf(OcpClientThatThrowsException.class); } @Test - @Disabled("Demonstrates how to configure the built-in HttpClient instance for OcspClientImpl") + void whenInvalidOcspRequestTimeout_thenThrows() throws Exception { + assertThatThrownBy(() -> OcspClientImpl.build(Duration.ofMinutes(-1))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageStartingWith("ocspRequestTimeout must be greater than zero"); + } + + /** + * Demonstrates how to configure the built-in HttpClient instance for OcspClientImpl. + */ + @Test void whenOcspClientIsConfiguredWithCustomHttpClient_thenOcspCallSucceeds() throws JceException, CertificateException, IOException { - final AuthTokenValidator validator = AuthTokenValidators.getAuthTokenValidatorWithOverriddenOcspClient( + final AuthTokenValidator validator = getAuthTokenValidatorWithOverriddenOcspClient( new OcspClientImpl(HttpClient.newBuilder().build(), Duration.ofSeconds(5)) ); assertThatCode(() -> validator.validate(validAuthToken, VALID_CHALLENGE_NONCE)) .doesNotThrowAnyException(); } + private static AuthTokenValidator getAuthTokenValidatorWithOverriddenOcspClient(OcspClient ocspClient) throws CertificateException, JceException, IOException { + return AuthTokenValidators.getDefaultAuthTokenValidatorBuilder() + .withOcspCertificateRevocationChecker(new DefaultOcspCertificateRevocationChecker( + ocspClient, + getAiaOcspServiceProvider(), + DefaultOcspCertificateRevocationChecker.DEFAULT_TIME_SKEW, + DefaultOcspCertificateRevocationChecker.DEFAULT_THIS_UPDATE_AGE + )).build(); + } + private static class OcpClientThatThrows implements OcspClient { @Override public OCSPResp request(URI url, OCSPReq request) throws IOException { diff --git a/src/test/java/eu/webeid/security/validator/ocsp/OcspResponseValidatorTest.java b/src/test/java/eu/webeid/ocsp/protocol/OcspResponseValidatorTest.java similarity index 87% rename from src/test/java/eu/webeid/security/validator/ocsp/OcspResponseValidatorTest.java rename to src/test/java/eu/webeid/ocsp/protocol/OcspResponseValidatorTest.java index a8e80ddf..2952f583 100644 --- a/src/test/java/eu/webeid/security/validator/ocsp/OcspResponseValidatorTest.java +++ b/src/test/java/eu/webeid/ocsp/protocol/OcspResponseValidatorTest.java @@ -20,19 +20,20 @@ * SOFTWARE. */ -package eu.webeid.security.validator.ocsp; +package eu.webeid.ocsp.protocol; -import eu.webeid.security.exceptions.UserCertificateOCSPCheckFailedException; +import eu.webeid.ocsp.DefaultOcspCertificateRevocationChecker; +import eu.webeid.ocsp.exceptions.UserCertificateOCSPCheckFailedException; import org.bouncycastle.cert.ocsp.SingleResp; import org.junit.jupiter.api.Test; +import java.net.URI; import java.time.Duration; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.Date; -import static eu.webeid.security.validator.AuthTokenValidatorBuilderTest.CONFIGURATION; -import static eu.webeid.security.validator.ocsp.OcspResponseValidator.validateCertificateStatusUpdateTime; +import static eu.webeid.ocsp.protocol.OcspResponseValidator.validateCertificateStatusUpdateTime; import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.mockito.Mockito.mock; @@ -40,8 +41,9 @@ class OcspResponseValidatorTest { - private static final Duration TIME_SKEW = CONFIGURATION.getAllowedOcspResponseTimeSkew(); - private static final Duration THIS_UPDATE_AGE = CONFIGURATION.getMaxOcspResponseThisUpdateAge(); + private static final Duration TIME_SKEW = DefaultOcspCertificateRevocationChecker.DEFAULT_TIME_SKEW; + private static final Duration THIS_UPDATE_AGE = DefaultOcspCertificateRevocationChecker.DEFAULT_THIS_UPDATE_AGE; + private static final URI OCSP_URL = URI.create("https://example.org"); @Test void whenThisAndNextUpdateWithinSkew_thenValidationSucceeds() { @@ -51,7 +53,7 @@ void whenThisAndNextUpdateWithinSkew_thenValidationSucceeds() { var nextUpdateWithinAgeLimit = Date.from(now.minus(THIS_UPDATE_AGE.minusSeconds(2))); when(mockResponse.getThisUpdate()).thenReturn(thisUpdateWithinAgeLimit); when(mockResponse.getNextUpdate()).thenReturn(nextUpdateWithinAgeLimit); - assertThatCode(() -> validateCertificateStatusUpdateTime(mockResponse, TIME_SKEW, THIS_UPDATE_AGE)) + assertThatCode(() -> validateCertificateStatusUpdateTime(mockResponse, TIME_SKEW, THIS_UPDATE_AGE, OCSP_URL)) .doesNotThrowAnyException(); } @@ -65,7 +67,7 @@ void whenNextUpdateBeforeThisUpdate_thenThrows() { when(mockResponse.getNextUpdate()).thenReturn(beforeThisUpdate); assertThatExceptionOfType(UserCertificateOCSPCheckFailedException.class) .isThrownBy(() -> - validateCertificateStatusUpdateTime(mockResponse, TIME_SKEW, THIS_UPDATE_AGE)) + validateCertificateStatusUpdateTime(mockResponse, TIME_SKEW, THIS_UPDATE_AGE, OCSP_URL)) .withMessageStartingWith("User certificate revocation check has failed: " + "Certificate status update time check failed: " + "nextUpdate '" + beforeThisUpdate.toInstant() + "' is before thisUpdate '" + thisUpdateWithinAgeLimit.toInstant() + "'"); @@ -79,7 +81,7 @@ void whenThisUpdateHalfHourBeforeNow_thenThrows() { when(mockResponse.getThisUpdate()).thenReturn(halfHourBeforeNow); assertThatExceptionOfType(UserCertificateOCSPCheckFailedException.class) .isThrownBy(() -> - validateCertificateStatusUpdateTime(mockResponse, TIME_SKEW, THIS_UPDATE_AGE)) + validateCertificateStatusUpdateTime(mockResponse, TIME_SKEW, THIS_UPDATE_AGE, OCSP_URL)) .withMessageStartingWith("User certificate revocation check has failed: " + "Certificate status update time check failed: " + "thisUpdate '" + halfHourBeforeNow.toInstant() + "' is too old, minimum time allowed: "); @@ -93,7 +95,7 @@ void whenThisUpdateHalfHourAfterNow_thenThrows() { when(mockResponse.getThisUpdate()).thenReturn(halfHourAfterNow); assertThatExceptionOfType(UserCertificateOCSPCheckFailedException.class) .isThrownBy(() -> - validateCertificateStatusUpdateTime(mockResponse, TIME_SKEW, THIS_UPDATE_AGE)) + validateCertificateStatusUpdateTime(mockResponse, TIME_SKEW, THIS_UPDATE_AGE, OCSP_URL)) .withMessageStartingWith("User certificate revocation check has failed: " + "Certificate status update time check failed: " + "thisUpdate '" + halfHourAfterNow.toInstant() + "' is too far in the future, latest allowed: "); @@ -109,7 +111,7 @@ void whenNextUpdateHalfHourBeforeNow_thenThrows() { when(mockResponse.getNextUpdate()).thenReturn(halfHourBeforeNow); assertThatExceptionOfType(UserCertificateOCSPCheckFailedException.class) .isThrownBy(() -> - validateCertificateStatusUpdateTime(mockResponse, TIME_SKEW, THIS_UPDATE_AGE)) + validateCertificateStatusUpdateTime(mockResponse, TIME_SKEW, THIS_UPDATE_AGE, OCSP_URL)) .withMessage("User certificate revocation check has failed: " + "Certificate status update time check failed: " + "nextUpdate '" + halfHourBeforeNow.toInstant() + "' is in the past"); diff --git a/src/test/java/eu/webeid/security/validator/ocsp/OcspUrlTest.java b/src/test/java/eu/webeid/ocsp/protocol/OcspUrlTest.java similarity index 96% rename from src/test/java/eu/webeid/security/validator/ocsp/OcspUrlTest.java rename to src/test/java/eu/webeid/ocsp/protocol/OcspUrlTest.java index 95b5759e..247c18a4 100644 --- a/src/test/java/eu/webeid/security/validator/ocsp/OcspUrlTest.java +++ b/src/test/java/eu/webeid/ocsp/protocol/OcspUrlTest.java @@ -20,7 +20,7 @@ * SOFTWARE. */ -package eu.webeid.security.validator.ocsp; +package eu.webeid.ocsp.protocol; import org.junit.jupiter.api.Test; @@ -30,7 +30,7 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import static eu.webeid.security.validator.ocsp.OcspUrl.getOcspUri; +import static eu.webeid.ocsp.protocol.OcspUrl.getOcspUri; class OcspUrlTest { diff --git a/src/test/java/eu/webeid/security/testutil/OcspServiceMaker.java b/src/test/java/eu/webeid/ocsp/service/OcspServiceMaker.java similarity index 93% rename from src/test/java/eu/webeid/security/testutil/OcspServiceMaker.java rename to src/test/java/eu/webeid/ocsp/service/OcspServiceMaker.java index 8f559c4b..9a99ca30 100644 --- a/src/test/java/eu/webeid/security/testutil/OcspServiceMaker.java +++ b/src/test/java/eu/webeid/ocsp/service/OcspServiceMaker.java @@ -20,14 +20,11 @@ * SOFTWARE. */ -package eu.webeid.security.testutil; +package eu.webeid.ocsp.service; import eu.webeid.security.certificate.CertificateValidator; import eu.webeid.security.exceptions.JceException; -import eu.webeid.security.exceptions.OCSPCertificateException; -import eu.webeid.security.validator.ocsp.OcspServiceProvider; -import eu.webeid.security.validator.ocsp.service.AiaOcspServiceConfiguration; -import eu.webeid.security.validator.ocsp.service.DesignatedOcspServiceConfiguration; +import eu.webeid.ocsp.exceptions.OCSPCertificateException; import java.io.IOException; import java.net.URI; diff --git a/src/test/java/eu/webeid/security/validator/ocsp/OcspServiceProviderTest.java b/src/test/java/eu/webeid/ocsp/service/OcspServiceProviderTest.java similarity index 72% rename from src/test/java/eu/webeid/security/validator/ocsp/OcspServiceProviderTest.java rename to src/test/java/eu/webeid/ocsp/service/OcspServiceProviderTest.java index 00337fd9..123f996c 100644 --- a/src/test/java/eu/webeid/security/validator/ocsp/OcspServiceProviderTest.java +++ b/src/test/java/eu/webeid/ocsp/service/OcspServiceProviderTest.java @@ -20,20 +20,25 @@ * SOFTWARE. */ -package eu.webeid.security.validator.ocsp; +package eu.webeid.ocsp.service; import org.bouncycastle.cert.X509CertificateHolder; import org.junit.jupiter.api.Test; -import eu.webeid.security.exceptions.OCSPCertificateException; -import eu.webeid.security.validator.ocsp.service.OcspService; +import eu.webeid.ocsp.exceptions.OCSPCertificateException; import java.net.URI; import java.util.Date; -import static org.assertj.core.api.Assertions.*; -import static eu.webeid.security.testutil.Certificates.*; -import static eu.webeid.security.testutil.OcspServiceMaker.getAiaOcspServiceProvider; -import static eu.webeid.security.testutil.OcspServiceMaker.getDesignatedOcspServiceProvider; +import static eu.webeid.ocsp.service.OcspServiceMaker.getAiaOcspServiceProvider; +import static eu.webeid.ocsp.service.OcspServiceMaker.getDesignatedOcspServiceProvider; +import static eu.webeid.security.testutil.Certificates.getJaakKristjanEsteid2018Cert; +import static eu.webeid.security.testutil.Certificates.getMariliisEsteid2015Cert; +import static eu.webeid.security.testutil.Certificates.getTestEsteid2015CA; +import static eu.webeid.security.testutil.Certificates.getTestEsteid2018CA; +import static eu.webeid.security.testutil.Certificates.getTestSkOcspResponder2020; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; class OcspServiceProviderTest { @@ -83,3 +88,16 @@ void whenAiaOcspServiceConfigurationDoesNotHaveResponderCertTrustedCA_thenThrows } } + +// Old disabled example AuthTokenValidator test with designated OCSP check. +// +// @Test +// @Disabled("A new designated test OCSP responder certificate was issued whose validity period no longer overlaps with the revoked certificate") +// void whenCertificateIsRevoked_thenOcspCheckWithDesignatedOcspServiceFails() throws Exception { +// mockDate("2020-01-01", mockedClock); +// final AuthTokenValidator validatorWithOcspCheck = AuthTokenValidators.getAuthTokenValidatorWithDesignatedOcspCheck(); +// final WebEidAuthToken token = replaceTokenField(AUTH_TOKEN, "X5C", REVOKED_CERT); +// assertThatThrownBy(() -> validatorWithOcspCheck +// .validate(token, VALID_CHALLENGE_NONCE)) +// .isInstanceOf(UserCertificateRevokedException.class); +// } \ No newline at end of file diff --git a/src/test/java/eu/webeid/security/testutil/AuthTokenValidators.java b/src/test/java/eu/webeid/security/testutil/AuthTokenValidators.java index ec977e71..faa28060 100644 --- a/src/test/java/eu/webeid/security/testutil/AuthTokenValidators.java +++ b/src/test/java/eu/webeid/security/testutil/AuthTokenValidators.java @@ -24,19 +24,14 @@ import eu.webeid.security.certificate.CertificateLoader; import eu.webeid.security.exceptions.JceException; -import eu.webeid.security.exceptions.OCSPCertificateException; import eu.webeid.security.validator.AuthTokenValidator; import eu.webeid.security.validator.AuthTokenValidatorBuilder; -import eu.webeid.security.validator.ocsp.OcspClient; import org.bouncycastle.asn1.ASN1ObjectIdentifier; import java.io.IOException; import java.net.URI; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; -import java.time.Duration; - -import static eu.webeid.security.testutil.OcspServiceMaker.getDesignatedOcspServiceConfiguration; public final class AuthTokenValidators { @@ -53,16 +48,7 @@ public static AuthTokenValidator getAuthTokenValidator(String url) throws Certif public static AuthTokenValidator getAuthTokenValidator(String url, X509Certificate... certificates) throws JceException { return getAuthTokenValidatorBuilder(url, certificates) - // Assure that all builder methods are covered with tests. - .withOcspRequestTimeout(Duration.ofSeconds(1)) - .withNonceDisabledOcspUrls(URI.create("http://example.org")) - .withoutUserCertificateRevocationCheckWithOcsp() - .build(); - } - - public static AuthTokenValidator getAuthTokenValidatorWithOverriddenOcspClient(OcspClient ocspClient) throws CertificateException, JceException, IOException { - return getAuthTokenValidatorBuilder(TOKEN_ORIGIN_URL, getCACertificates()) - .withOcspClient(ocspClient) + .withoutUserCertificateRevocationCheck() .build(); } @@ -71,12 +57,6 @@ public static AuthTokenValidator getAuthTokenValidatorWithOcspCheck() throws Cer .build(); } - public static AuthTokenValidator getAuthTokenValidatorWithDesignatedOcspCheck() throws CertificateException, JceException, IOException, OCSPCertificateException { - return getAuthTokenValidatorBuilder(TOKEN_ORIGIN_URL, getCACertificates()) - .withDesignatedOcspServiceConfiguration(getDesignatedOcspServiceConfiguration()) - .build(); - } - public static AuthTokenValidator getAuthTokenValidatorWithWrongTrustedCA() throws CertificateException, JceException, IOException { return getAuthTokenValidator(TOKEN_ORIGIN_URL, CertificateLoader.loadCertificatesFromResources("ESTEID2018.cer")); @@ -90,7 +70,7 @@ public static AuthTokenValidator getAuthTokenValidatorWithJuly2024ExpiredUnrelat public static AuthTokenValidator getAuthTokenValidatorWithDisallowedESTEIDPolicy() throws CertificateException, JceException, IOException { return getAuthTokenValidatorBuilder(TOKEN_ORIGIN_URL, getCACertificates()) .withDisallowedCertificatePolicies(EST_IDEMIA_POLICY) - .withoutUserCertificateRevocationCheckWithOcsp() + .withoutUserCertificateRevocationCheck() .build(); } diff --git a/src/test/java/eu/webeid/security/validator/AuthTokenCertificateTest.java b/src/test/java/eu/webeid/security/validator/AuthTokenCertificateTest.java index 14ed0666..0ded31e7 100644 --- a/src/test/java/eu/webeid/security/validator/AuthTokenCertificateTest.java +++ b/src/test/java/eu/webeid/security/validator/AuthTokenCertificateTest.java @@ -32,7 +32,7 @@ import eu.webeid.security.exceptions.CertificateNotYetValidException; import eu.webeid.security.exceptions.UserCertificateDisallowedPolicyException; import eu.webeid.security.exceptions.UserCertificateMissingPurposeException; -import eu.webeid.security.exceptions.UserCertificateRevokedException; +import eu.webeid.ocsp.exceptions.UserCertificateRevokedException; import eu.webeid.security.exceptions.UserCertificateWrongPurposeException; import eu.webeid.security.testutil.AbstractTestWithValidator; import eu.webeid.security.testutil.AuthTokenValidators; @@ -281,17 +281,6 @@ void whenCertificateIsRevoked_thenOcspCheckFails() throws Exception { .isInstanceOf(UserCertificateRevokedException.class); } - @Test - @Disabled("A new designated test OCSP responder certificate was issued whose validity period no longer overlaps with the revoked certificate") - void whenCertificateIsRevoked_thenOcspCheckWithDesignatedOcspServiceFails() throws Exception { - mockDate("2020-01-01", mockedClock); - final AuthTokenValidator validatorWithOcspCheck = AuthTokenValidators.getAuthTokenValidatorWithDesignatedOcspCheck(); - final WebEidAuthToken token = replaceTokenField(AUTH_TOKEN, "X5C", REVOKED_CERT); - assertThatThrownBy(() -> validatorWithOcspCheck - .validate(token, VALID_CHALLENGE_NONCE)) - .isInstanceOf(UserCertificateRevokedException.class); - } - @Test void whenCertificateCaIsNotPartOfTrustChain_thenValidationFails() throws Exception { final AuthTokenValidator validatorWithWrongTrustedCA = AuthTokenValidators.getAuthTokenValidatorWithWrongTrustedCA(); diff --git a/src/test/java/eu/webeid/security/validator/AuthTokenSignatureTest.java b/src/test/java/eu/webeid/security/validator/AuthTokenSignatureTest.java index 3f596858..70b9c89e 100644 --- a/src/test/java/eu/webeid/security/validator/AuthTokenSignatureTest.java +++ b/src/test/java/eu/webeid/security/validator/AuthTokenSignatureTest.java @@ -48,7 +48,7 @@ class AuthTokenSignatureTest extends AbstractTestWithValidator { @Test void whenValidTokenAndNonce_thenValidationSucceeds() throws Exception { - final X509Certificate result = validator.validate(validAuthToken, VALID_CHALLENGE_NONCE); + final X509Certificate result = validator.validate(validAuthToken, VALID_CHALLENGE_NONCE).subjectCertificate(); assertThat(CertificateData.getSubjectCN(result).orElseThrow()) .isEqualTo("JÕEORG\\,JAAK-KRISTJAN\\,38001085718"); diff --git a/src/test/java/eu/webeid/security/validator/AuthTokenSignatureValidatorTest.java b/src/test/java/eu/webeid/security/validator/AuthTokenSignatureValidatorTest.java index fc7edd0c..35ca01b3 100644 --- a/src/test/java/eu/webeid/security/validator/AuthTokenSignatureValidatorTest.java +++ b/src/test/java/eu/webeid/security/validator/AuthTokenSignatureValidatorTest.java @@ -51,10 +51,10 @@ void whenValidES384Signature_thenSucceeds() throws Exception { new AuthTokenSignatureValidator(URI.create("https://ria.ee")); final WebEidAuthToken authToken = OBJECT_READER.readValue(VALID_AUTH_TOKEN); - final X509Certificate x509Certificate = CertificateLoader.decodeCertificateFromBase64(authToken.getUnverifiedCertificate()); + final X509Certificate x509Certificate = CertificateLoader.decodeCertificateFromBase64(authToken.unverifiedCertificate()); assertThatCode(() -> signatureValidator - .validate("ES384", authToken.getSignature(), x509Certificate.getPublicKey(), VALID_CHALLENGE_NONCE)) + .validate("ES384", authToken.signature(), x509Certificate.getPublicKey(), VALID_CHALLENGE_NONCE)) .doesNotThrowAnyException(); } @@ -64,10 +64,10 @@ void whenValidRS256Signature_thenSucceeds() throws Exception { new AuthTokenSignatureValidator(URI.create("https://ria.ee")); final WebEidAuthToken authToken = OBJECT_READER.readValue(VALID_RS256_AUTH_TOKEN); - final X509Certificate x509Certificate = CertificateLoader.decodeCertificateFromBase64(authToken.getUnverifiedCertificate()); + final X509Certificate x509Certificate = CertificateLoader.decodeCertificateFromBase64(authToken.unverifiedCertificate()); assertThatCode(() -> signatureValidator - .validate("RS256", authToken.getSignature(), x509Certificate.getPublicKey(), VALID_CHALLENGE_NONCE)) + .validate("RS256", authToken.signature(), x509Certificate.getPublicKey(), VALID_CHALLENGE_NONCE)) .doesNotThrowAnyException(); } diff --git a/src/test/java/eu/webeid/security/validator/AuthTokenValidatorBuilderTest.java b/src/test/java/eu/webeid/security/validator/AuthTokenValidatorBuilderTest.java index 8e58466f..46abda72 100644 --- a/src/test/java/eu/webeid/security/validator/AuthTokenValidatorBuilderTest.java +++ b/src/test/java/eu/webeid/security/validator/AuthTokenValidatorBuilderTest.java @@ -86,30 +86,6 @@ void testValidatorOriginNotValidSyntax() { .hasMessageStartingWith("An URI syntax exception occurred"); } - @Test - void testInvalidOcspResponseTimeSkew() throws Exception { - final AuthTokenValidatorBuilder builderWithInvalidOcspResponseTimeSkew = AuthTokenValidators.getDefaultAuthTokenValidatorBuilder() - .withAllowedOcspResponseTimeSkew(Duration.ofMinutes(-1)); - assertThatThrownBy(builderWithInvalidOcspResponseTimeSkew::build) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageStartingWith("Allowed OCSP response time-skew must be greater than zero"); - } - - @Test - void testInvalidMaxOcspResponseThisUpdateAge() throws Exception { - final AuthTokenValidatorBuilder builderWithInvalidOcspResponseTimeSkew = AuthTokenValidators.getDefaultAuthTokenValidatorBuilder() - .withMaxOcspResponseThisUpdateAge(Duration.ZERO); - assertThatThrownBy(builderWithInvalidOcspResponseTimeSkew::build) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageStartingWith("Max OCSP response thisUpdate age must be greater than zero"); - } + // TODO: add tests for revocation config - @Test - void testInvalidOcspRequestTimeout() throws Exception { - final AuthTokenValidatorBuilder builderWithInvalidOcspResponseTimeSkew = AuthTokenValidators.getDefaultAuthTokenValidatorBuilder() - .withOcspRequestTimeout(Duration.ofMinutes(-1)); - assertThatThrownBy(builderWithInvalidOcspResponseTimeSkew::build) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageStartingWith("OCSP request timeout must be greater than zero"); - } }