Skip to content

Commit 2bf9900

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

File tree

12 files changed

+200
-82
lines changed

12 files changed

+200
-82
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,8 @@ The following additional configuration options are available in `AuthTokenValida
302302
- `withOcspRequestTimeout(Duration ocspRequestTimeout)` – sets both the connection and response timeout of user certificate revocation check OCSP requests. Default is 5 seconds.
303303
- `withDisallowedCertificatePolicies(ASN1ObjectIdentifier... policies)` – adds the given policies to the list of disallowed user certificate policies. In order for the user certificate to be considered valid, it must not contain any policies present in this list. Contains the Estonian Mobile-ID policies by default as it must not be possible to authenticate with a Mobile-ID certificate when an eID smart card is expected.
304304
- `withNonceDisabledOcspUrls(URI... urls)` – adds the given URLs to the list of OCSP responder access location URLs for which the nonce protocol extension will be disabled. Some OCSP responders don't support the nonce extension.
305+
- `withAllowedOcspResponseTimeSkew(Duration allowedTimeSkew)` – sets the allowed time skew for OCSP response's `thisUpdate` and `nextUpdate` times to allow discrepancies between the system clock and the OCSP responder's clock or revocation updates that are not published in real time. The default allowed time skew 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.
306+
- `withMaxOcspResponseThisUpdateAge(Duration maxThisUpdateAge)` – sets the maximum age for the OCSP response's `thisUpdate` time before it is considered too old to rely on. The default maximum age is 2 minutes.
305307
306308
Extended configuration example:
307309
@@ -312,6 +314,8 @@ AuthTokenValidator validator = new AuthTokenValidatorBuilder()
312314
.withoutUserCertificateRevocationCheckWithOcsp()
313315
.withDisallowedCertificatePolicies(new ASN1ObjectIdentifier("1.2.3"))
314316
.withNonceDisabledOcspUrls(URI.create("http://aia.example.org/cert"))
317+
.withAllowedOcspResponseTimeSkew(Duration.ofMinutes(10))
318+
.withMaxOcspResponseThisUpdateAge(Duration.ofMinutes(5))
315319
.build();
316320
```
317321

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: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,9 @@
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;
3838

3939
import static eu.webeid.security.util.Collections.newHashSet;
4040
import static eu.webeid.security.util.DateAndTime.requirePositiveDuration;
@@ -48,6 +48,8 @@ public final class AuthTokenValidationConfiguration {
4848
private Collection<X509Certificate> trustedCACertificates = new HashSet<>();
4949
private boolean isUserCertificateRevocationCheckWithOcspEnabled = true;
5050
private Duration ocspRequestTimeout = Duration.ofSeconds(5);
51+
private Duration allowedOcspResponseTimeSkew = Duration.ofMinutes(15);
52+
private Duration maxOcspResponseThisUpdateAge = Duration.ofMinutes(2);
5153
private DesignatedOcspServiceConfiguration designatedOcspServiceConfiguration;
5254
// Don't allow Estonian Mobile-ID policy by default.
5355
private Collection<ASN1ObjectIdentifier> disallowedSubjectCertificatePolicies = newHashSet(
@@ -63,12 +65,14 @@ public final class AuthTokenValidationConfiguration {
6365

6466
private AuthTokenValidationConfiguration(AuthTokenValidationConfiguration other) {
6567
this.siteOrigin = other.siteOrigin;
66-
this.trustedCACertificates = Collections.unmodifiableSet(new HashSet<>(other.trustedCACertificates));
68+
this.trustedCACertificates = Set.copyOf(other.trustedCACertificates);
6769
this.isUserCertificateRevocationCheckWithOcspEnabled = other.isUserCertificateRevocationCheckWithOcspEnabled;
6870
this.ocspRequestTimeout = other.ocspRequestTimeout;
71+
this.allowedOcspResponseTimeSkew = other.allowedOcspResponseTimeSkew;
72+
this.maxOcspResponseThisUpdateAge = other.maxOcspResponseThisUpdateAge;
6973
this.designatedOcspServiceConfiguration = other.designatedOcspServiceConfiguration;
70-
this.disallowedSubjectCertificatePolicies = Collections.unmodifiableSet(new HashSet<>(other.disallowedSubjectCertificatePolicies));
71-
this.nonceDisabledOcspUrls = Collections.unmodifiableSet(new HashSet<>(other.nonceDisabledOcspUrls));
74+
this.disallowedSubjectCertificatePolicies = Set.copyOf(other.disallowedSubjectCertificatePolicies);
75+
this.nonceDisabledOcspUrls = Set.copyOf(other.nonceDisabledOcspUrls);
7276
}
7377

7478
void setSiteOrigin(URI siteOrigin) {
@@ -99,6 +103,22 @@ void setOcspRequestTimeout(Duration ocspRequestTimeout) {
99103
this.ocspRequestTimeout = ocspRequestTimeout;
100104
}
101105

106+
public Duration getAllowedOcspResponseTimeSkew() {
107+
return allowedOcspResponseTimeSkew;
108+
}
109+
110+
public void setAllowedOcspResponseTimeSkew(Duration allowedOcspResponseTimeSkew) {
111+
this.allowedOcspResponseTimeSkew = allowedOcspResponseTimeSkew;
112+
}
113+
114+
public Duration getMaxOcspResponseThisUpdateAge() {
115+
return maxOcspResponseThisUpdateAge;
116+
}
117+
118+
public void setMaxOcspResponseThisUpdateAge(Duration maxOcspResponseThisUpdateAge) {
119+
this.maxOcspResponseThisUpdateAge = maxOcspResponseThisUpdateAge;
120+
}
121+
102122
public DesignatedOcspServiceConfiguration getDesignatedOcspServiceConfiguration() {
103123
return designatedOcspServiceConfiguration;
104124
}
@@ -128,6 +148,8 @@ void validate() {
128148
throw new IllegalArgumentException("At least one trusted certificate authority must be provided");
129149
}
130150
requirePositiveDuration(ocspRequestTimeout, "OCSP request timeout");
151+
requirePositiveDuration(allowedOcspResponseTimeSkew, "Allowed OCSP response time-skew");
152+
requirePositiveDuration(maxOcspResponseThisUpdateAge, "Max OCSP response thisUpdate age");
131153
}
132154

133155
AuthTokenValidationConfiguration copy() {

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 for OCSP response's thisUpdate and nextUpdate times.
132+
* This parameter is used to allow discrepancies between the system clock and the OCSP responder's clock,
133+
* which may occur due to clock drift, network delays or revocation updates that are not published in real time.
134+
* <p>
135+
* This is an optional configuration parameter, the default is 15 minutes.
136+
* The relatively long default is specifically chosen to account for one particular OCSP responder that used
137+
* CRLs for authoritative revocation info, these CRLs were updated every 15 minutes.
138+
*
139+
* @param allowedTimeSkew the allowed time skew
140+
* @return the builder instance for method chaining.
141+
*/
142+
public AuthTokenValidatorBuilder withAllowedOcspResponseTimeSkew(Duration allowedTimeSkew) {
143+
configuration.setAllowedOcspResponseTimeSkew(allowedTimeSkew);
144+
LOG.debug("Allowed OCSP response time skew set to {}", allowedTimeSkew);
145+
return this;
146+
}
147+
148+
/**
149+
* Sets the maximum age of the OCSP response's thisUpdate time before it is considered too old.
150+
* <p>
151+
* This is an optional configuration parameter, the default is 2 minutes.
152+
*
153+
* @param maxThisUpdateAge the maximum age of the OCSP response's thisUpdate time
154+
* @return the builder instance for method chaining.
155+
*/
156+
public AuthTokenValidatorBuilder withMaxOcspResponseThisUpdateAge(Duration maxThisUpdateAge) {
157+
configuration.setMaxOcspResponseThisUpdateAge(maxThisUpdateAge);
158+
LOG.debug("Maximum OCSP response thisUpdate age set to {}", maxThisUpdateAge);
159+
return this;
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.getAllowedOcspResponseTimeSkew(),
190+
configuration.getMaxOcspResponseThisUpdateAge()
191+
)::validateCertificateNotRevoked
190192
);
191193
}
192194

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
import java.security.cert.CertificateEncodingException;
5353
import java.security.cert.CertificateException;
5454
import java.security.cert.X509Certificate;
55+
import java.time.Duration;
5556
import java.util.Date;
5657
import java.util.Objects;
5758

@@ -63,17 +64,23 @@ public final class SubjectCertificateNotRevokedValidator {
6364
private final SubjectCertificateTrustedValidator trustValidator;
6465
private final OcspClient ocspClient;
6566
private final OcspServiceProvider ocspServiceProvider;
67+
private final Duration allowedOcspResponseTimeSkew;
68+
private final Duration maxOcspResponseThisUpdateAge;
6669

6770
static {
6871
Security.addProvider(new BouncyCastleProvider());
6972
}
7073

7174
public SubjectCertificateNotRevokedValidator(SubjectCertificateTrustedValidator trustValidator,
7275
OcspClient ocspClient,
73-
OcspServiceProvider ocspServiceProvider) {
76+
OcspServiceProvider ocspServiceProvider,
77+
Duration allowedOcspResponseTimeSkew,
78+
Duration maxOcspResponseThisUpdateAge) {
7479
this.trustValidator = trustValidator;
7580
this.ocspClient = ocspClient;
7681
this.ocspServiceProvider = ocspServiceProvider;
82+
this.allowedOcspResponseTimeSkew = allowedOcspResponseTimeSkew;
83+
this.maxOcspResponseThisUpdateAge = maxOcspResponseThisUpdateAge;
7784
}
7885

7986
/**
@@ -166,7 +173,7 @@ private void verifyOcspResponse(BasicOCSPResp basicResponse, OcspService ocspSer
166173
// be available about the status of the certificate (nextUpdate) is
167174
// greater than the current time.
168175

169-
OcspResponseValidator.validateCertificateStatusUpdateTime(certStatusResponse);
176+
OcspResponseValidator.validateCertificateStatusUpdateTime(certStatusResponse, allowedOcspResponseTimeSkew, maxOcspResponseThisUpdateAge);
170177

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

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

Lines changed: 32 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,9 @@
4040
import java.security.cert.CertificateException;
4141
import java.security.cert.CertificateParsingException;
4242
import java.security.cert.X509Certificate;
43-
import java.text.SimpleDateFormat;
44-
import java.util.Date;
43+
import java.time.Duration;
44+
import java.time.Instant;
4545
import java.util.Objects;
46-
import java.util.TimeZone;
47-
import java.util.concurrent.TimeUnit;
4846

4947
public final class OcspResponseValidator {
5048

@@ -54,8 +52,7 @@ public final class OcspResponseValidator {
5452
* https://oidref.com/1.3.6.1.5.5.7.3.9
5553
*/
5654
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);
55+
private static final String ERROR_PREFIX = "Certificate status update time check failed: ";
5956

6057
public static void validateHasSigningExtension(X509Certificate certificate) throws OCSPCertificateException {
6158
Objects.requireNonNull(certificate, "certificate");
@@ -78,7 +75,7 @@ public static void validateResponseSignature(BasicOCSPResp basicResponse, X509Ce
7875
}
7976
}
8077

81-
public static void validateCertificateStatusUpdateTime(SingleResp certStatusResponse) throws UserCertificateOCSPCheckFailedException {
78+
public static void validateCertificateStatusUpdateTime(SingleResp certStatusResponse, Duration allowedTimeSkew, Duration maxThisupdateAge) throws UserCertificateOCSPCheckFailedException {
8279
// From RFC 2560, https://www.ietf.org/rfc/rfc2560.txt:
8380
// 4.2.2. Notes on OCSP Responses
8481
// 4.2.2.1. Time
@@ -88,18 +85,34 @@ public static void validateCertificateStatusUpdateTime(SingleResp certStatusResp
8885
// SHOULD be considered unreliable.
8986
// If nextUpdate is not set, the responder is indicating that newer
9087
// revocation information is available all the time.
91-
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);
94-
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()));
88+
final Instant now = DateAndTime.DefaultClock.getInstance().now().toInstant();
89+
final Instant earliestAcceptableTimeSkew = now.minus(allowedTimeSkew);
90+
final Instant latestAcceptableTimeSkew = now.plus(allowedTimeSkew);
91+
final Instant minimumValidThisUpdateTime = now.minus(maxThisupdateAge);
92+
93+
final Instant thisUpdate = certStatusResponse.getThisUpdate().toInstant();
94+
if (thisUpdate.isAfter(latestAcceptableTimeSkew)) {
95+
throw new UserCertificateOCSPCheckFailedException(ERROR_PREFIX +
96+
"thisUpdate '" + thisUpdate + "' is too far in the future, " +
97+
"latest allowed: '" + latestAcceptableTimeSkew + "'");
98+
}
99+
if (thisUpdate.isBefore(minimumValidThisUpdateTime)) {
100+
throw new UserCertificateOCSPCheckFailedException(ERROR_PREFIX +
101+
"thisUpdate '" + thisUpdate + "' is too old, " +
102+
"minimum time allowed: '" + minimumValidThisUpdateTime + "'");
103+
}
104+
105+
if (certStatusResponse.getNextUpdate() == null) {
106+
return;
107+
}
108+
final Instant nextUpdate = certStatusResponse.getNextUpdate().toInstant();
109+
if (nextUpdate.isBefore(earliestAcceptableTimeSkew)) {
110+
throw new UserCertificateOCSPCheckFailedException(ERROR_PREFIX +
111+
"nextUpdate '" + nextUpdate + "' is in the past");
112+
}
113+
if (nextUpdate.isBefore(thisUpdate)) {
114+
throw new UserCertificateOCSPCheckFailedException(ERROR_PREFIX +
115+
"nextUpdate '" + nextUpdate + "' is before thisUpdate '" + thisUpdate + "'");
103116
}
104117
}
105118

@@ -120,15 +133,6 @@ public static void validateSubjectCertificateStatus(SingleResp certStatusRespons
120133
}
121134
}
122135

123-
static String toUtcString(Date date) {
124-
if (date == null) {
125-
return String.valueOf((Object) null);
126-
}
127-
final SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z");
128-
dateFormatter.setTimeZone(TimeZone.getTimeZone("UTC"));
129-
return dateFormatter.format(date);
130-
}
131-
132136
private OcspResponseValidator() {
133137
throw new IllegalStateException("Utility class");
134138
}

src/test/java/eu/webeid/security/testutil/AuthTokenValidators.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,10 @@ public static AuthTokenValidator getAuthTokenValidatorWithDisallowedESTEIDPolicy
8989
.build();
9090
}
9191

92+
public static AuthTokenValidatorBuilder getDefaultAuthTokenValidatorBuilder() throws CertificateException, IOException {
93+
return getAuthTokenValidatorBuilder(TOKEN_ORIGIN_URL, getCACertificates());
94+
}
95+
9296
private static AuthTokenValidatorBuilder getAuthTokenValidatorBuilder(String uri, X509Certificate[] certificates) {
9397
return new AuthTokenValidatorBuilder()
9498
.withSiteOrigin(URI.create(uri))

0 commit comments

Comments
 (0)