Skip to content

Commit 0ff0a73

Browse files
committed
Refactor OcspResponseValidator.validateCertificateStatusUpdateTime(), make OCSP response time validation parameters configurable
WE2-868 Signed-off-by: Mart Somermaa <[email protected]>
1 parent b938031 commit 0ff0a73

File tree

10 files changed

+164
-68
lines changed

10 files changed

+164
-68
lines changed

src/main/java/eu/webeid/security/certificate/CertificateValidator.java

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@
2323
package eu.webeid.security.certificate;
2424

2525
import eu.webeid.security.exceptions.CertificateExpiredException;
26+
import eu.webeid.security.exceptions.CertificateNotTrustedException;
2627
import eu.webeid.security.exceptions.CertificateNotYetValidException;
2728
import eu.webeid.security.exceptions.JceException;
28-
import eu.webeid.security.exceptions.CertificateNotTrustedException;
2929

3030
import java.security.GeneralSecurityException;
3131
import java.security.InvalidAlgorithmParameterException;
@@ -40,7 +40,6 @@
4040
import java.security.cert.X509CertSelector;
4141
import java.security.cert.X509Certificate;
4242
import java.util.Collection;
43-
import java.util.Collections;
4443
import java.util.Date;
4544
import java.util.Set;
4645
import java.util.stream.Collectors;
@@ -91,9 +90,8 @@ public static X509Certificate validateIsSignedByTrustedCA(X509Certificate certif
9190
}
9291

9392
public static Set<TrustAnchor> buildTrustAnchorsFromCertificates(Collection<X509Certificate> certificates) {
94-
return Collections.unmodifiableSet(certificates.stream()
95-
.map(cert -> new TrustAnchor(cert, null))
96-
.collect(Collectors.toSet()));
93+
return certificates.stream()
94+
.map(cert -> new TrustAnchor(cert, null)).collect(Collectors.toUnmodifiableSet());
9795
}
9896

9997
public static CertStore buildCertStoreFromCertificates(Collection<X509Certificate> certificates) throws JceException {

src/main/java/eu/webeid/security/validator/AuthTokenValidationConfiguration.java

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,10 @@
3232
import java.security.cert.X509Certificate;
3333
import java.time.Duration;
3434
import java.util.Collection;
35-
import java.util.Collections;
3635
import java.util.HashSet;
3736
import java.util.Objects;
37+
import java.util.Set;
38+
import java.util.concurrent.TimeUnit;
3839

3940
import static eu.webeid.security.util.Collections.newHashSet;
4041
import static eu.webeid.security.util.DateAndTime.requirePositiveDuration;
@@ -48,6 +49,8 @@ public final class AuthTokenValidationConfiguration {
4849
private Collection<X509Certificate> trustedCACertificates = new HashSet<>();
4950
private boolean isUserCertificateRevocationCheckWithOcspEnabled = true;
5051
private Duration ocspRequestTimeout = Duration.ofSeconds(5);
52+
private long allowedOcspResponseTimeSkewMillis = TimeUnit.MINUTES.toMillis(15);
53+
private long maxOcspResponseThisUpdateAgeMillis = TimeUnit.MINUTES.toMillis(2);
5154
private DesignatedOcspServiceConfiguration designatedOcspServiceConfiguration;
5255
// Don't allow Estonian Mobile-ID policy by default.
5356
private Collection<ASN1ObjectIdentifier> disallowedSubjectCertificatePolicies = newHashSet(
@@ -63,12 +66,14 @@ public final class AuthTokenValidationConfiguration {
6366

6467
private AuthTokenValidationConfiguration(AuthTokenValidationConfiguration other) {
6568
this.siteOrigin = other.siteOrigin;
66-
this.trustedCACertificates = Collections.unmodifiableSet(new HashSet<>(other.trustedCACertificates));
69+
this.trustedCACertificates = Set.copyOf(other.trustedCACertificates);
6770
this.isUserCertificateRevocationCheckWithOcspEnabled = other.isUserCertificateRevocationCheckWithOcspEnabled;
6871
this.ocspRequestTimeout = other.ocspRequestTimeout;
72+
this.allowedOcspResponseTimeSkewMillis = other.allowedOcspResponseTimeSkewMillis;
73+
this.maxOcspResponseThisUpdateAgeMillis = other.maxOcspResponseThisUpdateAgeMillis;
6974
this.designatedOcspServiceConfiguration = other.designatedOcspServiceConfiguration;
70-
this.disallowedSubjectCertificatePolicies = Collections.unmodifiableSet(new HashSet<>(other.disallowedSubjectCertificatePolicies));
71-
this.nonceDisabledOcspUrls = Collections.unmodifiableSet(new HashSet<>(other.nonceDisabledOcspUrls));
75+
this.disallowedSubjectCertificatePolicies = Set.copyOf(other.disallowedSubjectCertificatePolicies);
76+
this.nonceDisabledOcspUrls = Set.copyOf(other.nonceDisabledOcspUrls);
7277
}
7378

7479
void setSiteOrigin(URI siteOrigin) {
@@ -99,6 +104,22 @@ void setOcspRequestTimeout(Duration ocspRequestTimeout) {
99104
this.ocspRequestTimeout = ocspRequestTimeout;
100105
}
101106

107+
public long getAllowedOcspResponseTimeSkewMillis() {
108+
return allowedOcspResponseTimeSkewMillis;
109+
}
110+
111+
public void setAllowedOcspResponseTimeSkewMillis(long allowedOcspResponseTimeSkewMillis) {
112+
this.allowedOcspResponseTimeSkewMillis = allowedOcspResponseTimeSkewMillis;
113+
}
114+
115+
public long getMaxOcspResponseThisUpdateAgeMillis() {
116+
return maxOcspResponseThisUpdateAgeMillis;
117+
}
118+
119+
public void setMaxOcspResponseThisUpdateAgeMillis(long maxOcspResponseThisUpdateAgeMillis) {
120+
this.maxOcspResponseThisUpdateAgeMillis = maxOcspResponseThisUpdateAgeMillis;
121+
}
122+
102123
public DesignatedOcspServiceConfiguration getDesignatedOcspServiceConfiguration() {
103124
return designatedOcspServiceConfiguration;
104125
}
@@ -128,6 +149,12 @@ void validate() {
128149
throw new IllegalArgumentException("At least one trusted certificate authority must be provided");
129150
}
130151
requirePositiveDuration(ocspRequestTimeout, "OCSP request timeout");
152+
if (allowedOcspResponseTimeSkewMillis <= 0) {
153+
throw new IllegalArgumentException("Allowed OCSP response time-skew must be greater than zero");
154+
}
155+
if (maxOcspResponseThisUpdateAgeMillis <= 0) {
156+
throw new IllegalArgumentException("Max OCSP response thisUpdate age must be greater than zero");
157+
}
131158
}
132159

133160
AuthTokenValidationConfiguration copy() {
@@ -156,5 +183,4 @@ public static void validateIsOriginURL(URI uri) throws IllegalArgumentException
156183
throw new IllegalArgumentException("An URI syntax exception occurred");
157184
}
158185
}
159-
160186
}

src/main/java/eu/webeid/security/validator/AuthTokenValidatorBuilder.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,38 @@ public AuthTokenValidatorBuilder withOcspRequestTimeout(Duration ocspRequestTime
127127
return this;
128128
}
129129

130+
/**
131+
* Sets the allowed time skew in milliseconds for OCSP response's thisUpdate and nextUpdate times.
132+
* This value is used to tolerate slight discrepancies between the system clock and the OCSP responder's clock.
133+
* <p>
134+
* This is an optional configuration parameter, the default is 15 minutes.
135+
*
136+
* @param allowedTimeSkewMillis the allowed time skew in milliseconds
137+
* @return the builder instance for method chaining.
138+
*/
139+
// TODO: use Duration.
140+
public AuthTokenValidatorBuilder withAllowedOcspResponseTimeSkewMilliseconds(long allowedTimeSkewMillis) {
141+
configuration.setAllowedOcspResponseTimeSkewMillis(allowedTimeSkewMillis);
142+
LOG.debug("Allowed OCSP response time skew set to {} milliseconds", allowedTimeSkewMillis);
143+
return this;
144+
}
145+
146+
/**
147+
* Sets the maximum age in milliseconds of the OCSP response's thisUpdate time before it is considered too old.
148+
* <p>
149+
* This is an optional configuration parameter, the default is 2 minutes.
150+
*
151+
* @param maxThisUpdateAgeMillis the maximum age of the OCSP response's thisUpdate time in milliseconds
152+
* @return the builder instance for method chaining.
153+
*/
154+
// TODO: use Duration.
155+
public AuthTokenValidatorBuilder withMaxOcspResponseThisUpdateAgeMilliseconds(long maxThisUpdateAgeMillis) {
156+
configuration.setMaxOcspResponseThisUpdateAgeMillis(maxThisUpdateAgeMillis);
157+
LOG.debug("Maximum OCSP response thisUpdate age set to {} milliseconds", maxThisUpdateAgeMillis);
158+
return this;
159+
}
160+
161+
130162
/**
131163
* Adds the given URLs to the list of OCSP URLs for which the nonce protocol extension will be disabled.
132164
* The OCSP URL is extracted from the user certificate and some OCSP services don't support the nonce extension.

src/main/java/eu/webeid/security/validator/AuthTokenValidatorImpl.java

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@
2727
import eu.webeid.security.authtoken.WebEidAuthToken;
2828
import eu.webeid.security.certificate.CertificateLoader;
2929
import eu.webeid.security.certificate.CertificateValidator;
30-
import eu.webeid.security.exceptions.JceException;
31-
import eu.webeid.security.exceptions.AuthTokenParseException;
3230
import eu.webeid.security.exceptions.AuthTokenException;
31+
import eu.webeid.security.exceptions.AuthTokenParseException;
32+
import eu.webeid.security.exceptions.JceException;
3333
import eu.webeid.security.validator.certvalidators.SubjectCertificateExpiryValidator;
3434
import eu.webeid.security.validator.certvalidators.SubjectCertificateNotRevokedValidator;
3535
import eu.webeid.security.validator.certvalidators.SubjectCertificatePolicyValidator;
@@ -64,10 +64,8 @@ final class AuthTokenValidatorImpl implements AuthTokenValidator {
6464
private final SubjectCertificateValidatorBatch simpleSubjectCertificateValidators;
6565
private final Set<TrustAnchor> trustedCACertificateAnchors;
6666
private final CertStore trustedCACertificateCertStore;
67-
// OcspClient uses OkHttp internally.
68-
// OkHttp performs best when a single OkHttpClient instance is created and reused for all HTTP calls.
69-
// This is because each client holds its own connection pool and thread pools.
70-
// Reusing connections and threads reduces latency and saves memory.
67+
// OcspClient uses built-in HttpClient internally by default.
68+
// A single HttpClient instance is reused for all HTTP calls to utilize connection and thread pools.
7169
private OcspClient ocspClient;
7270
private OcspServiceProvider ocspServiceProvider;
7371
private final AuthTokenSignatureValidator authTokenSignatureValidator;
@@ -186,7 +184,11 @@ private SubjectCertificateValidatorBatch getCertTrustValidators() {
186184
return SubjectCertificateValidatorBatch.createFrom(
187185
certTrustedValidator::validateCertificateTrusted
188186
).addOptional(configuration.isUserCertificateRevocationCheckWithOcspEnabled(),
189-
new SubjectCertificateNotRevokedValidator(certTrustedValidator, ocspClient, ocspServiceProvider)::validateCertificateNotRevoked
187+
new SubjectCertificateNotRevokedValidator(certTrustedValidator,
188+
ocspClient, ocspServiceProvider,
189+
configuration.getAllowedOcspResponseTimeSkewMillis(),
190+
configuration.getMaxOcspResponseThisUpdateAgeMillis()
191+
)::validateCertificateNotRevoked
190192
);
191193
}
192194

src/main/java/eu/webeid/security/validator/certvalidators/SubjectCertificateNotRevokedValidator.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,17 +63,23 @@ public final class SubjectCertificateNotRevokedValidator {
6363
private final SubjectCertificateTrustedValidator trustValidator;
6464
private final OcspClient ocspClient;
6565
private final OcspServiceProvider ocspServiceProvider;
66+
private final long allowedOcspResponseTimeSkewMillis;
67+
private final long maxOcspResponseThisUpdateAgeMillis;
6668

6769
static {
6870
Security.addProvider(new BouncyCastleProvider());
6971
}
7072

7173
public SubjectCertificateNotRevokedValidator(SubjectCertificateTrustedValidator trustValidator,
7274
OcspClient ocspClient,
73-
OcspServiceProvider ocspServiceProvider) {
75+
OcspServiceProvider ocspServiceProvider,
76+
long allowedOcspResponseTimeSkewMillis,
77+
long maxOcspResponseThisUpdateAgeMillis) {
7478
this.trustValidator = trustValidator;
7579
this.ocspClient = ocspClient;
7680
this.ocspServiceProvider = ocspServiceProvider;
81+
this.allowedOcspResponseTimeSkewMillis = allowedOcspResponseTimeSkewMillis;
82+
this.maxOcspResponseThisUpdateAgeMillis = maxOcspResponseThisUpdateAgeMillis;
7783
}
7884

7985
/**
@@ -166,7 +172,7 @@ private void verifyOcspResponse(BasicOCSPResp basicResponse, OcspService ocspSer
166172
// be available about the status of the certificate (nextUpdate) is
167173
// greater than the current time.
168174

169-
OcspResponseValidator.validateCertificateStatusUpdateTime(certStatusResponse);
175+
OcspResponseValidator.validateCertificateStatusUpdateTime(certStatusResponse, allowedOcspResponseTimeSkewMillis, maxOcspResponseThisUpdateAgeMillis);
170176

171177
// Now we can accept the signed response as valid and validate the certificate status.
172178
OcspResponseValidator.validateSubjectCertificateStatus(certStatusResponse);

src/main/java/eu/webeid/security/validator/ocsp/OcspResponseValidator.java

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@
4444
import java.util.Date;
4545
import java.util.Objects;
4646
import java.util.TimeZone;
47-
import java.util.concurrent.TimeUnit;
4847

4948
public final class OcspResponseValidator {
5049

@@ -54,8 +53,7 @@ public final class OcspResponseValidator {
5453
* https://oidref.com/1.3.6.1.5.5.7.3.9
5554
*/
5655
private static final String OID_OCSP_SIGNING = "1.3.6.1.5.5.7.3.9";
57-
58-
static final long ALLOWED_TIME_SKEW_MILLIS = TimeUnit.MINUTES.toMillis(15);
56+
private static final String ERROR_PREFIX = "Certificate status update time check failed: ";
5957

6058
public static void validateHasSigningExtension(X509Certificate certificate) throws OCSPCertificateException {
6159
Objects.requireNonNull(certificate, "certificate");
@@ -78,7 +76,7 @@ public static void validateResponseSignature(BasicOCSPResp basicResponse, X509Ce
7876
}
7977
}
8078

81-
public static void validateCertificateStatusUpdateTime(SingleResp certStatusResponse) throws UserCertificateOCSPCheckFailedException {
79+
public static void validateCertificateStatusUpdateTime(SingleResp certStatusResponse, long allowedTimeSkewMillis, long maxThisupdateAgeMillis) throws UserCertificateOCSPCheckFailedException {
8280
// From RFC 2560, https://www.ietf.org/rfc/rfc2560.txt:
8381
// 4.2.2. Notes on OCSP Responses
8482
// 4.2.2.1. Time
@@ -89,17 +87,33 @@ public static void validateCertificateStatusUpdateTime(SingleResp certStatusResp
8987
// If nextUpdate is not set, the responder is indicating that newer
9088
// revocation information is available all the time.
9189
final Date now = DateAndTime.DefaultClock.getInstance().now();
92-
final Date notAllowedBefore = new Date(now.getTime() - ALLOWED_TIME_SKEW_MILLIS);
93-
final Date notAllowedAfter = new Date(now.getTime() + ALLOWED_TIME_SKEW_MILLIS);
90+
final Date earliestAcceptableTimeSkew = new Date(now.getTime() - allowedTimeSkewMillis);
91+
final Date latestAcceptableTimeSkew = new Date(now.getTime() + allowedTimeSkewMillis);
92+
final Date earliestAcceptableThisUpdateTime = new Date(now.getTime() - maxThisupdateAgeMillis);
93+
9494
final Date thisUpdate = certStatusResponse.getThisUpdate();
95-
final Date nextUpdate = certStatusResponse.getNextUpdate() != null ? certStatusResponse.getNextUpdate() : thisUpdate;
96-
if (notAllowedAfter.before(thisUpdate) ||
97-
notAllowedBefore.after(nextUpdate)) {
98-
throw new UserCertificateOCSPCheckFailedException("Certificate status update time check failed: " +
99-
"notAllowedBefore: " + toUtcString(notAllowedBefore) +
100-
", notAllowedAfter: " + toUtcString(notAllowedAfter) +
101-
", thisUpdate: " + toUtcString(thisUpdate) +
102-
", nextUpdate: " + toUtcString(certStatusResponse.getNextUpdate()));
95+
if (thisUpdate.after(latestAcceptableTimeSkew)) {
96+
throw new UserCertificateOCSPCheckFailedException(ERROR_PREFIX +
97+
"thisUpdate '" + toUtcString(thisUpdate) + "' is too far in the future, " +
98+
"latest allowed: '" + toUtcString(latestAcceptableTimeSkew) + "'");
99+
}
100+
if (thisUpdate.before(earliestAcceptableThisUpdateTime)) {
101+
throw new UserCertificateOCSPCheckFailedException(ERROR_PREFIX +
102+
"thisUpdate '" + toUtcString(thisUpdate) + "' is too old, " +
103+
"earliest allowed: '" + toUtcString(earliestAcceptableThisUpdateTime) + "'");
104+
}
105+
106+
final Date nextUpdate = certStatusResponse.getNextUpdate();
107+
if (nextUpdate == null) {
108+
return;
109+
}
110+
if (nextUpdate.before(earliestAcceptableTimeSkew)) {
111+
throw new UserCertificateOCSPCheckFailedException(ERROR_PREFIX +
112+
"nextUpdate '" + toUtcString(nextUpdate) + "' is in the past");
113+
}
114+
if (nextUpdate.before(thisUpdate)) {
115+
throw new UserCertificateOCSPCheckFailedException(ERROR_PREFIX +
116+
"nextUpdate '" + toUtcString(nextUpdate) + "' is before thisUpdate '" + toUtcString(thisUpdate) + "'");
103117
}
104118
}
105119

src/test/java/eu/webeid/security/validator/AuthTokenValidatorBuilderTest.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,11 @@
2929

3030
import static org.assertj.core.api.Assertions.assertThatThrownBy;
3131

32-
class AuthTokenValidatorBuilderTest {
32+
public class AuthTokenValidatorBuilderTest {
33+
34+
// AuthTokenValidationConfiguration has a package-private constructor, but some tests need access to it outside its package.
35+
// Provide a public accessor to it for these tests.
36+
public static final AuthTokenValidationConfiguration CONFIGURATION = new AuthTokenValidationConfiguration();
3337

3438
final AuthTokenValidatorBuilder builder = new AuthTokenValidatorBuilder();
3539

0 commit comments

Comments
 (0)