Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/coverity-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/maven-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/maven-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
33 changes: 31 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,18 @@
<modelVersion>4.0.0</modelVersion>
<artifactId>authtoken-validation</artifactId>
<groupId>eu.webeid.security</groupId>
<version>3.2.0</version>
<version>4.0.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>authtoken-validation</name>
<description>Web eID authentication token validation library for Java</description>

<properties>
<java.version>11</java.version>
<java.version>17</java.version>
<jjwt.version>0.12.6</jjwt.version>
<bouncycastle.version>1.81</bouncycastle.version>
<jackson.version>2.19.1</jackson.version>
<slf4j.version>2.0.17</slf4j.version>
<resilience4j.version>2.3.0</resilience4j.version>
<junit-jupiter.version>5.13.3</junit-jupiter.version>
<assertj.version>3.27.3</assertj.version>
<mockito.version>5.18.0</mockito.version>
Expand Down Expand Up @@ -65,6 +66,34 @@
<artifactId>bcpkix-jdk18on</artifactId>
<version>${bouncycastle.version}</version>
</dependency>
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-all</artifactId>
<version>${resilience4j.version}</version>
<exclusions>
<exclusion>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-bulkhead</artifactId>
</exclusion>
<exclusion>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-cache</artifactId>
</exclusion>
<exclusion>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-ratelimiter</artifactId>
</exclusion>
<exclusion>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-timelimiter</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-vavr</artifactId>
<version>${resilience4j.version}</version>
</dependency>

<dependency>
<groupId>org.junit.jupiter</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.CertificateRevocationChecker;
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;
Expand All @@ -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 class OcspCertificateRevocationChecker implements CertificateRevocationChecker {

public static final Duration DEFAULT_TIME_SKEW = Duration.ofMinutes(15);
public static final Duration DEFAULT_THIS_UPDATE_AGE = Duration.ofMinutes(2);

private static final Logger LOG = LoggerFactory.getLogger(OcspCertificateRevocationChecker.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 OcspCertificateRevocationChecker(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<RevocationInfo> 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)
Expand All @@ -106,25 +120,31 @@ 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);
}
verifyOcspResponse(basicResponse, ocspService, certificateId);
LOG.debug("OCSP response received successfully");

verifyOcspResponse(basicResponse, ocspService, certificateId, false, false);
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);
}
}

private void verifyOcspResponse(BasicOCSPResp basicResponse, OcspService ocspService, CertificateID requestCertificateId) throws AuthTokenException, OCSPException, CertificateException, OperatorCreationException {
protected void verifyOcspResponse(BasicOCSPResp basicResponse, OcspService ocspService, CertificateID requestCertificateId, boolean rejectUnknownOcspResponseStatus, boolean allowThisUpdateInPast) throws AuthTokenException, OCSPException, CertificateException, OperatorCreationException {
// The verification algorithm follows RFC 2560, https://www.ietf.org/rfc/rfc2560.txt.
//
// 3.2. Signed Response Acceptance Requirements
Expand All @@ -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.
Expand All @@ -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.
Expand All @@ -174,48 +195,49 @@ 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(), allowThisUpdateInPast);

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

private static void checkNonce(OCSPReq request, BasicOCSPResp response) throws UserCertificateOCSPCheckFailedException {
protected static void checkNonce(OCSPReq request, BasicOCSPResp response, URI ocspResponderUri) throws UserCertificateOCSPCheckFailedException {
final Extension requestNonce = request.getExtension(OCSPObjectIdentifiers.id_pkix_ocsp_nonce);
final Extension responseNonce = response.getExtension(OCSPObjectIdentifiers.id_pkix_ocsp_nonce);
if (requestNonce == null || responseNonce == null) {
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);
}
}

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

private static String ocspStatusToString(int status) {
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";
}
protected static String ocspStatusToString(int status) {
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";
};
}

protected OcspClient getOcspClient() {
return ocspClient;
}

protected OcspServiceProvider getOcspServiceProvider() {
return ocspServiceProvider;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,16 @@
* SOFTWARE.
*/

package eu.webeid.security.validator.ocsp;
package eu.webeid.ocsp.client;

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

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

public interface OcspClient {

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

}
Loading