Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,8 @@ The following additional configuration options are available in `AuthTokenValida
- `withOcspRequestTimeout(Duration ocspRequestTimeout)` – sets both the connection and response timeout of user certificate revocation check OCSP requests. Default is 5 seconds.
- `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.
- `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.
- `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.
- `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.

Extended configuration example:

Expand All @@ -312,6 +314,8 @@ AuthTokenValidator validator = new AuthTokenValidatorBuilder()
.withoutUserCertificateRevocationCheckWithOcsp()
.withDisallowedCertificatePolicies(new ASN1ObjectIdentifier("1.2.3"))
.withNonceDisabledOcspUrls(URI.create("http://aia.example.org/cert"))
.withAllowedOcspResponseTimeSkew(Duration.ofMinutes(10))
.withMaxOcspResponseThisUpdateAge(Duration.ofMinutes(5))
.build();
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@
package eu.webeid.security.certificate;

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.exceptions.CertificateNotTrustedException;

import java.security.GeneralSecurityException;
import java.security.InvalidAlgorithmParameterException;
Expand All @@ -40,7 +40,6 @@
import java.security.cert.X509CertSelector;
import java.security.cert.X509Certificate;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Set;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -91,9 +90,8 @@ public static X509Certificate validateIsSignedByTrustedCA(X509Certificate certif
}

public static Set<TrustAnchor> buildTrustAnchorsFromCertificates(Collection<X509Certificate> certificates) {
return Collections.unmodifiableSet(certificates.stream()
.map(cert -> new TrustAnchor(cert, null))
.collect(Collectors.toSet()));
return certificates.stream()
.map(cert -> new TrustAnchor(cert, null)).collect(Collectors.toUnmodifiableSet());
}

public static CertStore buildCertStoreFromCertificates(Collection<X509Certificate> certificates) throws JceException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@
import java.security.cert.X509Certificate;
import java.time.Duration;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;

import static eu.webeid.security.util.Collections.newHashSet;
import static eu.webeid.security.util.DateAndTime.requirePositiveDuration;
Expand All @@ -48,6 +48,8 @@ public final class AuthTokenValidationConfiguration {
private Collection<X509Certificate> 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<ASN1ObjectIdentifier> disallowedSubjectCertificatePolicies = newHashSet(
Expand All @@ -63,12 +65,14 @@ public final class AuthTokenValidationConfiguration {

private AuthTokenValidationConfiguration(AuthTokenValidationConfiguration other) {
this.siteOrigin = other.siteOrigin;
this.trustedCACertificates = Collections.unmodifiableSet(new HashSet<>(other.trustedCACertificates));
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 = Collections.unmodifiableSet(new HashSet<>(other.disallowedSubjectCertificatePolicies));
this.nonceDisabledOcspUrls = Collections.unmodifiableSet(new HashSet<>(other.nonceDisabledOcspUrls));
this.disallowedSubjectCertificatePolicies = Set.copyOf(other.disallowedSubjectCertificatePolicies);
this.nonceDisabledOcspUrls = Set.copyOf(other.nonceDisabledOcspUrls);
}

void setSiteOrigin(URI siteOrigin) {
Expand Down Expand Up @@ -99,6 +103,22 @@ void setOcspRequestTimeout(Duration ocspRequestTimeout) {
this.ocspRequestTimeout = ocspRequestTimeout;
}

public Duration getAllowedOcspResponseTimeSkew() {
return allowedOcspResponseTimeSkew;
}

public void setAllowedOcspResponseTimeSkew(Duration allowedOcspResponseTimeSkew) {
this.allowedOcspResponseTimeSkew = allowedOcspResponseTimeSkew;
}

public Duration getMaxOcspResponseThisUpdateAge() {
return maxOcspResponseThisUpdateAge;
}

public void setMaxOcspResponseThisUpdateAge(Duration maxOcspResponseThisUpdateAge) {
this.maxOcspResponseThisUpdateAge = maxOcspResponseThisUpdateAge;
}

public DesignatedOcspServiceConfiguration getDesignatedOcspServiceConfiguration() {
return designatedOcspServiceConfiguration;
}
Expand Down Expand Up @@ -128,6 +148,8 @@ void validate() {
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");
}

AuthTokenValidationConfiguration copy() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,38 @@ public AuthTokenValidatorBuilder withOcspRequestTimeout(Duration ocspRequestTime
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.
* <p>
* 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.
* <p>
* 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@
import eu.webeid.security.authtoken.WebEidAuthToken;
import eu.webeid.security.certificate.CertificateLoader;
import eu.webeid.security.certificate.CertificateValidator;
import eu.webeid.security.exceptions.JceException;
import eu.webeid.security.exceptions.AuthTokenParseException;
import eu.webeid.security.exceptions.AuthTokenException;
import eu.webeid.security.exceptions.AuthTokenParseException;
import eu.webeid.security.exceptions.JceException;
import eu.webeid.security.validator.certvalidators.SubjectCertificateExpiryValidator;
import eu.webeid.security.validator.certvalidators.SubjectCertificateNotRevokedValidator;
import eu.webeid.security.validator.certvalidators.SubjectCertificatePolicyValidator;
Expand Down Expand Up @@ -64,10 +64,8 @@ final class AuthTokenValidatorImpl implements AuthTokenValidator {
private final SubjectCertificateValidatorBatch simpleSubjectCertificateValidators;
private final Set<TrustAnchor> trustedCACertificateAnchors;
private final CertStore trustedCACertificateCertStore;
// OcspClient uses OkHttp internally.
// OkHttp performs best when a single OkHttpClient instance is created and reused for all HTTP calls.
// This is because each client holds its own connection pool and thread pools.
// Reusing connections and threads reduces latency and saves memory.
// 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;
Expand Down Expand Up @@ -186,7 +184,11 @@ private SubjectCertificateValidatorBatch getCertTrustValidators() {
return SubjectCertificateValidatorBatch.createFrom(
certTrustedValidator::validateCertificateTrusted
).addOptional(configuration.isUserCertificateRevocationCheckWithOcspEnabled(),
new SubjectCertificateNotRevokedValidator(certTrustedValidator, ocspClient, ocspServiceProvider)::validateCertificateNotRevoked
new SubjectCertificateNotRevokedValidator(certTrustedValidator,
ocspClient, ocspServiceProvider,
configuration.getAllowedOcspResponseTimeSkew(),
configuration.getMaxOcspResponseThisUpdateAge()
)::validateCertificateNotRevoked
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@

import eu.webeid.security.exceptions.AuthTokenException;
import eu.webeid.security.exceptions.UserCertificateOCSPCheckFailedException;
import eu.webeid.security.validator.ocsp.*;
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 org.bouncycastle.asn1.ocsp.OCSPObjectIdentifiers;
import org.bouncycastle.asn1.ocsp.OCSPResponseStatus;
Expand All @@ -41,39 +45,42 @@
import org.bouncycastle.operator.OperatorCreationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import eu.webeid.security.validator.ocsp.Digester;
import eu.webeid.security.validator.ocsp.OcspClient;
import eu.webeid.security.validator.ocsp.OcspRequestBuilder;
import eu.webeid.security.validator.ocsp.OcspServiceProvider;

import java.io.IOException;
import java.math.BigInteger;
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;

public final class SubjectCertificateNotRevokedValidator {

private static final Logger LOG = LoggerFactory.getLogger(SubjectCertificateNotRevokedValidator.class);
private static final DigestCalculator DIGEST_CALCULATOR = Digester.sha1();
private static final DigestCalculator DIGEST_CALCULATOR = DigestCalculatorImpl.sha1();

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());
}

public SubjectCertificateNotRevokedValidator(SubjectCertificateTrustedValidator trustValidator,
OcspClient ocspClient,
OcspServiceProvider ocspServiceProvider) {
OcspServiceProvider ocspServiceProvider,
Duration allowedOcspResponseTimeSkew,
Duration maxOcspResponseThisUpdateAge) {
this.trustValidator = trustValidator;
this.ocspClient = ocspClient;
this.ocspServiceProvider = ocspServiceProvider;
this.allowedOcspResponseTimeSkew = allowedOcspResponseTimeSkew;
this.maxOcspResponseThisUpdateAge = maxOcspResponseThisUpdateAge;
}

/**
Expand All @@ -86,10 +93,6 @@ public void validateCertificateNotRevoked(X509Certificate subjectCertificate) th
try {
OcspService ocspService = ocspServiceProvider.getService(subjectCertificate);

if (!ocspService.doesSupportNonce()) {
LOG.debug("Disabling OCSP nonce extension");
}

final CertificateID certificateId = getCertificateId(subjectCertificate,
Objects.requireNonNull(trustValidator.getSubjectCertificateIssuerCertificate()));

Expand All @@ -98,6 +101,10 @@ public void validateCertificateNotRevoked(X509Certificate subjectCertificate) th
.enableOcspNonce(ocspService.doesSupportNonce())
.build();

if (!ocspService.doesSupportNonce()) {
LOG.debug("Disabling OCSP nonce extension");
}

LOG.debug("Sending OCSP request");
final OCSPResp response = Objects.requireNonNull(ocspClient.request(ocspService.getAccessLocation(), request));
if (response.getStatus() != OCSPResponseStatus.SUCCESSFUL) {
Expand Down Expand Up @@ -166,7 +173,7 @@ 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, producedAt);
OcspResponseValidator.validateCertificateStatusUpdateTime(certStatusResponse, allowedOcspResponseTimeSkew, maxOcspResponseThisUpdateAge);

// Now we can accept the signed response as valid and validate the certificate status.
OcspResponseValidator.validateSubjectCertificateStatus(certStatusResponse);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,26 +20,9 @@
* SOFTWARE.
*/

/*
* Copyright 2017 The Netty Project
* Copyright 2020 The Web eID project
*
* The Netty Project and The Web eID Project license this file to you under the
* Apache License, version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/

package eu.webeid.security.validator.ocsp;

import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.nist.NISTObjectIdentifiers;
import org.bouncycastle.asn1.oiw.OIWObjectIdentifiers;
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
import org.bouncycastle.crypto.Digest;
Expand All @@ -52,33 +35,26 @@

/**
* BouncyCastle's OCSPReqBuilder needs a DigestCalculator but BC doesn't
* provide any public implementations of that interface. That's why we need to
* write our own. There's a default SHA-1 implementation and one for SHA-256.
* Which one to use will depend on the Certificate Authority (CA).
* provide any public implementations of it, hence this implementation.
*/
public final class Digester implements DigestCalculator {
public final class DigestCalculatorImpl implements DigestCalculator {

private static final AlgorithmIdentifier SHA1 = new AlgorithmIdentifier(OIWObjectIdentifiers.idSHA1);
private static final AlgorithmIdentifier SHA256 = new AlgorithmIdentifier(NISTObjectIdentifiers.id_sha256);

private final DigestOutputStream dos;
private final AlgorithmIdentifier algId;

public static DigestCalculator sha1() {
final Digest digest = new SHA1Digest();
final AlgorithmIdentifier algId = new AlgorithmIdentifier(OIWObjectIdentifiers.idSHA1);

return new Digester(digest, algId);
public static DigestCalculator sha1() {
return new DigestCalculatorImpl(new SHA1Digest(), SHA1);
}

public static DigestCalculator sha256() {
Digest digest = new SHA256Digest();

// The OID for SHA-256: http://www.oid-info.com/get/2.16.840.1.101.3.4.2.1
final ASN1ObjectIdentifier oid = new ASN1ObjectIdentifier("2.16.840.1.101.3.4.2.1").intern();
final AlgorithmIdentifier algId = new AlgorithmIdentifier(oid);

return new Digester(digest, algId);
return new DigestCalculatorImpl(new SHA256Digest(), SHA256);
}

private Digester(Digest digest, AlgorithmIdentifier algId) {
private DigestCalculatorImpl(Digest digest, AlgorithmIdentifier algId) {
this.dos = new DigestOutputStream(digest);
this.algId = algId;
}
Expand Down
Loading