/* dCache - http://www.dcache.org/
*
* Copyright (C) 2015 Deutsches Elektronen-Synchrotron
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.dcache.ssl;
import com.google.common.base.Supplier;
import com.google.common.base.Throwables;
import eu.emi.security.authn.x509.CrlCheckingMode;
import eu.emi.security.authn.x509.NamespaceCheckingMode;
import eu.emi.security.authn.x509.OCSPCheckingMode;
import eu.emi.security.authn.x509.OCSPParametes;
import eu.emi.security.authn.x509.ProxySupport;
import eu.emi.security.authn.x509.RevocationParameters;
import eu.emi.security.authn.x509.StoreUpdateListener;
import eu.emi.security.authn.x509.ValidationError;
import eu.emi.security.authn.x509.ValidationErrorCategory;
import eu.emi.security.authn.x509.ValidationErrorListener;
import eu.emi.security.authn.x509.X509CertChainValidator;
import eu.emi.security.authn.x509.X509Credential;
import eu.emi.security.authn.x509.helpers.ssl.SSLTrustManager;
import eu.emi.security.authn.x509.impl.OpensslCertChainValidator;
import eu.emi.security.authn.x509.impl.PEMCredential;
import eu.emi.security.authn.x509.impl.ValidatorParams;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.security.GeneralSecurityException;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import java.util.EnumSet;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import org.dcache.util.CachingCertificateValidator;
import static eu.emi.security.authn.x509.ValidationErrorCategory.*;
import static org.dcache.util.Callables.memoizeFromFiles;
import static org.dcache.util.Callables.memoizeWithExpiration;
/**
* SslContextFactory based on the CANL library. Uses the builder pattern to
* create immutable instances.
*/
public class CanlContextFactory implements SslContextFactory
{
private static final Logger LOGGER = LoggerFactory.getLogger(CanlContextFactory.class);
private static final EnumSet<ValidationErrorCategory> VALIDATION_ERRORS_TO_LOG =
EnumSet.of(NAMESPACE, X509_BASIC, X509_CHAIN, NAME_CONSTRAINT, CRL, OCSP);
private final SecureRandom secureRandom = new SecureRandom();
private final TrustManager[] trustManagers;
private static final AutoCloseable NOOP = new AutoCloseable()
{
@Override
public void close() throws Exception
{
}
};
protected CanlContextFactory(TrustManager... trustManagers)
{
this.trustManagers = trustManagers;
}
public static CanlContextFactory createDefault()
{
return new Builder().build();
}
public static Builder custom()
{
return new Builder();
}
public TrustManager[] getTrustManagers()
{
return trustManagers;
}
@Override
public SSLContext getContext(X509Credential credential)
throws GeneralSecurityException
{
KeyManager[] keyManagers = { credential.getKeyManager() };
SSLContext context = SSLContext.getInstance("TLS");
context.init(keyManagers, trustManagers, secureRandom);
return context;
}
public static class Builder
{
private Path certificateAuthorityPath = FileSystems.getDefault().getPath("/etc/grid-security/certificates");
private NamespaceCheckingMode namespaceMode = NamespaceCheckingMode.EUGRIDPMA_GLOBUS;
private CrlCheckingMode crlCheckingMode = CrlCheckingMode.IF_VALID;
private OCSPCheckingMode ocspCheckingMode = OCSPCheckingMode.IF_AVAILABLE;
private long certificateAuthorityUpdateInterval = 600000;
private boolean lazyMode = true;
private Path keyPath = FileSystems.getDefault().getPath("/etc/grid-security/hostkey.pem");
private Path certificatePath = FileSystems.getDefault().getPath("/etc/grid-security/hostcert.pem");
private long credentialUpdateInterval = 1;
private TimeUnit credentialUpdateIntervalUnit = TimeUnit.MINUTES;
private Supplier<AutoCloseable> loggingContextSupplier = new Supplier<AutoCloseable>()
{
@Override
public AutoCloseable get()
{
return NOOP;
}
};
private long validationCacheLifetime = 300000;
private Builder()
{
}
public Builder withCertificateAuthorityPath(Path certificateAuthorityPath)
{
this.certificateAuthorityPath = certificateAuthorityPath;
return this;
}
public Builder withCertificateAuthorityPath(String certificateAuthorityPath)
{
return withCertificateAuthorityPath(FileSystems.getDefault().getPath(certificateAuthorityPath));
}
public Builder withCertificateAuthorityUpdateInterval(long interval)
{
this.certificateAuthorityUpdateInterval = interval;
return this;
}
public Builder withCertificateAuthorityUpdateInterval(long interval, TimeUnit unit)
{
this.certificateAuthorityUpdateInterval = unit.toMillis(interval);
return this;
}
public Builder withCrlCheckingMode(CrlCheckingMode crlCheckingMode)
{
this.crlCheckingMode = crlCheckingMode;
return this;
}
public Builder withOcspCheckingMode(OCSPCheckingMode ocspCheckingMode)
{
this.ocspCheckingMode = ocspCheckingMode;
return this;
}
public Builder withNamespaceMode(NamespaceCheckingMode namespaceMode)
{
this.namespaceMode = namespaceMode;
return this;
}
public Builder withLazy(boolean lazyMode)
{
this.lazyMode = lazyMode;
return this;
}
public Builder withKeyPath(Path keyPath)
{
this.keyPath = keyPath;
return this;
}
public Builder withCertificatePath(Path certificatePath)
{
this.certificatePath = certificatePath;
return this;
}
public Builder withCredentialUpdateInterval(long duration, TimeUnit unit)
{
this.credentialUpdateInterval = duration;
this.credentialUpdateIntervalUnit = unit;
return this;
}
public Builder withLoggingContext(Supplier<AutoCloseable> contextSupplier)
{
this.loggingContextSupplier = contextSupplier;
return this;
}
public Builder withValidationCacheLifetime(long millis)
{
this.validationCacheLifetime = millis;
return this;
}
public Builder withValidationCacheLifetime(long duration, TimeUnit unit)
{
this.validationCacheLifetime = unit.toMillis(duration);
return this;
}
public CanlContextFactory build()
{
OCSPParametes ocspParameters = new OCSPParametes(ocspCheckingMode);
ValidatorParams validatorParams =
new ValidatorParams(new RevocationParameters(crlCheckingMode, ocspParameters),
ProxySupport.ALLOW);
X509CertChainValidator v =
new CachingCertificateValidator(
new OpensslCertChainValidator(certificateAuthorityPath.toString(), true, namespaceMode,
certificateAuthorityUpdateInterval,
validatorParams, lazyMode),
validationCacheLifetime);
v.addUpdateListener(new StoreUpdateListener()
{
@Override
public void loadingNotification(String location, String type, Severity level, Exception cause)
{
try (AutoCloseable ignored = loggingContextSupplier.get()) {
switch (level) {
case ERROR:
if (cause != null) {
LOGGER.error("Error loading {} from {}: {}", type, location, cause.getMessage());
} else {
LOGGER.error("Error loading {} from {}.", type, location);
}
break;
case WARNING:
if (cause != null) {
LOGGER.warn("Problem loading {} from {}: {}", type, location, cause.getMessage());
} else {
LOGGER.warn("Problem loading {} from {}.", type, location);
}
break;
case NOTIFICATION:
LOGGER.debug("Reloaded {} from {}.", type, location);
break;
}
} catch (Exception e) {
Throwables.propagate(e);
}
}
});
v.addValidationListener(new ValidationErrorListener()
{
@Override
public boolean onValidationError(ValidationError error)
{
if (VALIDATION_ERRORS_TO_LOG.contains(error.getErrorCategory())) {
X509Certificate[] chain = error.getChain();
String subject = (chain != null && chain.length > 0) ? chain[0].getSubjectX500Principal().getName() : "";
LOGGER.warn("The peer's certificate with DN {} was rejected: {}", subject, error);
}
return false;
}
});
return new CanlContextFactory(new SSLTrustManager(v));
}
public Callable<SSLContext> buildWithCaching()
{
final CanlContextFactory factory = build();
Callable<SSLContext> newContext =
new Callable<SSLContext>()
{
@Override
public SSLContext call() throws Exception
{
return factory.getContext(
new PEMCredential(keyPath.toString(), certificatePath.toString(), null));
}
};
return memoizeWithExpiration(memoizeFromFiles(newContext, keyPath, certificatePath),
credentialUpdateInterval, credentialUpdateIntervalUnit);
}
}
}