/*
* Copyright (C) 2006-2016 DLR, Germany
*
* All rights reserved
*
* http://www.rcenvironment.de/
*/
package de.rcenvironment.core.authentication.internal;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateNotYetValidException;
import java.security.cert.X509CRL;
import java.security.cert.X509Certificate;
import java.util.Date;
import java.util.List;
import java.util.Properties;
import java.util.Random;
import java.util.Vector;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.naming.Context;
import javax.naming.NamingException;
import javax.naming.directory.InitialDirContext;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.globus.gsi.CertUtil;
import org.globus.gsi.OpenSSLKey;
import org.globus.gsi.bc.BouncyCastleOpenSSLKey;
import org.osgi.framework.BundleContext;
import de.rcenvironment.core.authentication.AuthenticationException;
import de.rcenvironment.core.authentication.AuthenticationService;
import de.rcenvironment.core.authentication.CertificateUser;
import de.rcenvironment.core.authentication.LDAPUser;
import de.rcenvironment.core.authentication.SingleUser;
import de.rcenvironment.core.authentication.User;
import de.rcenvironment.core.configuration.ConfigurationService;
import de.rcenvironment.core.utils.common.StringUtils;
import de.rcenvironment.core.utils.incubator.Assertions;
/**
* Implementation of <code>AuthenticationService</code> interface.
*
* @author Doreen Seider
* @author Tobias Menden
* @author Alice Zorn
*/
public class AuthenticationServiceImpl implements AuthenticationService {
/* For LDAP authentication */
/**
* Constant that holds the name of the environment property for specifying how referrals encountered by the service provider are to be
* processed. The value of the property is one of the following strings: "follow": follow referrals automatically "ignore": ignore
* referrals "throw": throw ReferralException when a referral is encountered.
*/
private static final String REFERRAL = "follow";
/** Context Factory class. */
private static final String CONTEXT_FACTORY_CLASS = "com.sun.jndi.ldap.LdapCtxFactory";
/** LDAP authentification method. */
private static final String LDAP_AUTH_METHOD = "simple";
/** LDAP protocol. */
private static final String LDAP_PROTOCOL = "ldap://";
private static final String ASSERTIONS_PARAMETER_NULL = "The parameter \"%s\" must not be null.";
private static final String DSA = "DSA";
private static final String RSA = "RSA";
private static final Log LOGGER = LogFactory.getLog(AuthenticationServiceImpl.class);
private static final String CERTIFICATE_COULD_NOT_BE_LOADED = "The given CA certificate (%s) could not be loaded.";
private static final String CRL_COULD_NOT_BE_LOADED = "The given certificate revocation list (CRL) (%s) could not be loaded.";
private AuthenticationConfiguration myConfiguration;
private ConfigurationService configurationService;
private String bundleSymbolicName;
protected void activate(BundleContext context) {
bundleSymbolicName = context.getBundle().getSymbolicName();
// note: disabled old configuration loading for 6.0.0 as it is not being used anyway
// myConfiguration = configurationService.getConfiguration(bundleSymbolicName, AuthenticationConfiguration.class);
// TODO using default values until reworked or removed
myConfiguration = new AuthenticationConfiguration();
}
protected void bindConfigurationService(ConfigurationService newConfigurationService) {
configurationService = newConfigurationService;
}
@Override
@Deprecated
// note: some unit tests are already ignored due to maintenance effort for required test infrastructure
public X509AuthenticationResult authenticate(X509Certificate certificate, OpenSSLKey encryptedKey, String password)
throws AuthenticationException {
Assertions.isDefined(certificate, StringUtils.format(ASSERTIONS_PARAMETER_NULL, "certificate"));
Assertions.isDefined(encryptedKey, StringUtils.format(ASSERTIONS_PARAMETER_NULL, "key"));
X509AuthenticationResult result = null;
try {
certificate.checkValidity();
} catch (CertificateNotYetValidException e) {
throw new AuthenticationException(e);
} catch (CertificateExpiredException e) {
throw new AuthenticationException(e);
}
if (password == null && isPasswordNeeded(encryptedKey)) {
result = X509AuthenticationResult.PASSWORD_REQUIRED;
}
if (result == null && !isPasswordCorrect(encryptedKey, password)) {
result = X509AuthenticationResult.PASSWORD_INCORRECT;
}
if (result == null && !isPrivateKeyBelongingToCertificate(certificate, encryptedKey)) {
result = X509AuthenticationResult.PRIVATE_KEY_NOT_BELONGS_TO_PUBLIC_KEY;
}
if (result == null && !isSignedByTrustedCA(certificate)) {
result = X509AuthenticationResult.NOT_SIGNED_BY_TRUSTED_CA;
}
if (result == null && isRevoced(certificate)) {
result = X509AuthenticationResult.CERTIFICATE_REVOKED;
}
if (result == null) {
result = X509AuthenticationResult.AUTHENTICATED;
}
return result;
}
@Override
public LDAPAuthenticationResult authenticate(String username, String password) {
if (password == null || password.trim().isEmpty()
|| username == null || username.trim().isEmpty()) {
return LDAPAuthenticationResult.PASSWORD__OR_USERNAME_INVALID;
}
String baseDn = myConfiguration.getLdapBaseDn();
String server = myConfiguration.getLdapServer();
String domain = myConfiguration.getLdapDomain();
try {
connect(server, baseDn, username + "@" + domain, password);
} catch (NamingException e) {
return LDAPAuthenticationResult.PASSWORD_OR_USERNAME_INCORRECT;
}
return LDAPAuthenticationResult.AUTHENTICATED;
}
@Override
public User createUser(X509Certificate certificate, int validityInDays) {
Assertions.isDefined(certificate, ASSERTIONS_PARAMETER_NULL);
return new CertificateUser(certificate, validityInDays);
}
@Override
public User createUser(String userIdLdap, int validityInDays) {
Assertions.isDefined(userIdLdap, ASSERTIONS_PARAMETER_NULL);
return new LDAPUser(userIdLdap, validityInDays, myConfiguration.getLdapDomain());
}
@Override
public X509Certificate loadCertificate(String file) throws AuthenticationException {
Assertions.isDefined(file, "The parameter 'file' (path to the certificate) must not be null.");
try {
return CertUtil.loadCertificate(file);
} catch (IOException e) {
throw new AuthenticationException(e);
} catch (GeneralSecurityException e) {
throw new AuthenticationException(e);
}
}
@Override
public OpenSSLKey loadKey(String file) throws AuthenticationException {
Assertions.isDefined(file, "The parameter 'file' (path to the key) must not be null.");
try {
return new BouncyCastleOpenSSLKey(file);
} catch (IOException e) {
throw new AuthenticationException(e);
} catch (GeneralSecurityException e) {
throw new AuthenticationException(e);
}
}
@Override
public User createUser(int validityInDays) {
return new SingleUser(validityInDays);
}
/**
*
* Tries to set up and bind the LDAP-Connection and sets dirContext to an initialized directory service. Reads properties from the file
* ldap.properties and sets the context.
*
* @param server the ldap server
* @param baseDn the ldap base dn
* @param dn the ldap dn
* @param password the ldap password
* @throws NamingException if the input was a wrong password or username
*
*/
private void connect(String server, String baseDn, String dn, String password) throws NamingException {
Properties env = new Properties();
env.setProperty(Context.INITIAL_CONTEXT_FACTORY, CONTEXT_FACTORY_CLASS);
env.setProperty(Context.PROVIDER_URL, LDAP_PROTOCOL + server);
env.setProperty(Context.SECURITY_AUTHENTICATION, LDAP_AUTH_METHOD);
env.setProperty(Context.SECURITY_PRINCIPAL, dn);
env.setProperty(Context.SECURITY_CREDENTIALS, password);
env.setProperty(Context.REFERRAL, REFERRAL);
// If a username or password does not exists a NamingException is thrown.
new InitialDirContext(env);
}
/**
* Checks if a password is required.
*
* @param encryptedKey The private key to decrypt.
* @return true if password is needed, else false.
*/
private boolean isPasswordNeeded(OpenSSLKey encryptedKey) {
return encryptedKey.isEncrypted();
}
/**
* Checks if the password to decrypt the private key is correct.
*
* @param encryptedKey The private key to decrypt.
* @param password The password for decrypting.
* @return true if password is correct, else false.
* @throws AuthenticationException if an error during decrypting occurs.
*/
private boolean isPasswordCorrect(OpenSSLKey encryptedKey, String password) throws AuthenticationException {
boolean correct = true;
// validate the password by decrypting the private key with the password
if (encryptedKey.isEncrypted()) {
Assertions.isDefined(password, "If the key is encrypted the password must not be null.");
try {
encryptedKey.decrypt(password);
} catch (BadPaddingException e) {
correct = false;
} catch (RuntimeException e) {
throw new AuthenticationException(e);
} catch (InvalidKeyException e) {
throw new AuthenticationException(e);
} catch (GeneralSecurityException e) {
throw new AuthenticationException(e);
}
}
return correct;
}
/**
* Checks if the given private key belongs to the given public key (certificate).
*
* @param certificate The public key (certificate).
* @param encryptedKey The private key.
* @return true if the private key belongs to the certificate, else false.
* @throws AuthenticationException if an exception during decrypting/encrypting occurs.
*/
private boolean isPrivateKeyBelongingToCertificate(X509Certificate certificate, OpenSSLKey encryptedKey)
throws AuthenticationException {
boolean belongs = true;
PrivateKey privateKey = encryptedKey.getPrivateKey();
PublicKey publicKey = certificate.getPublicKey();
Random random = new Random();
String original = Long.toString(Math.abs(random.nextLong()));
// encrypt (with the private key) and decrypt (with the public key) a
// random string
try {
Cipher cipher = Cipher.getInstance(privateKey.getAlgorithm());
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
byte[] encrypted = cipher.doFinal(original.getBytes());
cipher.init(Cipher.DECRYPT_MODE, publicKey);
byte[] decrypted = cipher.doFinal(encrypted);
if (!original.equals(new String(decrypted))) {
belongs = false;
}
} catch (BadPaddingException e) {
belongs = false;
} catch (NoSuchAlgorithmException e) {
throw new AuthenticationException(e);
} catch (NoSuchPaddingException e) {
throw new AuthenticationException(e);
} catch (InvalidKeyException e) {
throw new AuthenticationException(e);
} catch (IllegalBlockSizeException e) {
throw new AuthenticationException(e);
}
return belongs;
}
/**
* Checks if the certificate is signed by a trusted CA.
*
* @param certificate The certificate to validate.
* @return true if it is signed by a trusted CA, else false.
* @throws AuthenticationException if an an error occurs during verification.
*/
private boolean isSignedByTrustedCA(X509Certificate certificate) throws AuthenticationException {
boolean signed = false;
// add the given certificates to the context
for (X509Certificate caCertificate : getCertificateAuthorities()) {
try {
caCertificate.checkValidity();
} catch (CertificateNotYetValidException e) {
throw new AuthenticationException(e);
} catch (CertificateExpiredException e) {
throw new AuthenticationException(e);
}
// if the CA signed the certificate
if (certificate.getIssuerDN().equals(caCertificate.getSubjectDN())) {
// try to encrypt the signature of the certificate with the
// public key
// of the CA
String encryptionAlgorithm = null;
if (certificate.getSigAlgName().contains(RSA)) {
encryptionAlgorithm = RSA;
} else if (certificate.getSigAlgName().contains(DSA)) {
encryptionAlgorithm = DSA;
} else {
throw new AuthenticationException("The encryption algorithm of the certificates's signature is not supported.");
}
try {
Cipher cipher = Cipher.getInstance(encryptionAlgorithm);
cipher.init(Cipher.DECRYPT_MODE, caCertificate.getPublicKey());
cipher.doFinal(certificate.getSignature());
signed = true;
} catch (InvalidKeyException e) {
throw new AuthenticationException(e);
} catch (NoSuchAlgorithmException e) {
throw new AuthenticationException(e);
} catch (NoSuchPaddingException e) {
throw new AuthenticationException(e);
} catch (IllegalBlockSizeException e) {
throw new AuthenticationException(e);
} catch (BadPaddingException e) {
throw new AuthenticationException(e);
}
}
}
return signed;
}
/**
* Checks if the certificate is revoked from its CA.
*
* @param certificate The certificate to check.
* @return false if the certificate is not revoked, else true;
* @throws AuthenticationException if an error during checking for revocation occurs.
*/
private boolean isRevoced(X509Certificate certificate) throws AuthenticationException {
boolean revoked = false;
// read the given revocation lists
for (X509CRL revocationList : getCertificateRevocationLists()) {
Date now = new Date();
// is the CRL of the CA signing the certificate
if (revocationList.getIssuerDN().equals(certificate.getIssuerDN())) {
// is the CRL not expired
if (revocationList.getThisUpdate().before(now)
&& (revocationList.getNextUpdate() == null || revocationList.getNextUpdate().after(now))) {
if (revocationList.isRevoked(certificate)) {
revoked = true;
}
} else {
throw new AuthenticationException("The CRL of the CA is not valid (e.g., it is expired).");
}
}
}
return revoked;
}
/**
*
* Returns the certificate authority certificate paths as a comma separated string.
*
* @return comma separated string with CRL paths.
*/
private List<X509Certificate> getCertificateAuthorities() {
List<X509Certificate> certificates = new Vector<X509Certificate>();
String absPath = null;
for (String path : myConfiguration.getCaFiles()) {
try {
absPath = configurationService.resolveBundleConfigurationPath(bundleSymbolicName, path);
certificates.add(CertUtil.loadCertificate(absPath));
} catch (IOException e) {
LOGGER.error(StringUtils.format(CERTIFICATE_COULD_NOT_BE_LOADED, absPath));
} catch (GeneralSecurityException e) {
LOGGER.error(StringUtils.format(CERTIFICATE_COULD_NOT_BE_LOADED, absPath));
}
}
return certificates;
}
/**
*
* Returns the certificate revocation list paths as a comma separated string.
*
* @return comma separated string with CA paths.
*/
private List<X509CRL> getCertificateRevocationLists() {
List<X509CRL> certificateRevocationLists = new Vector<X509CRL>();
String absPath = null;
for (String path : myConfiguration.getCrlFiles()) {
try {
absPath = configurationService.resolveBundleConfigurationPath(bundleSymbolicName, path);
certificateRevocationLists.add(CertUtil.loadCrl(absPath));
} catch (IOException e) {
LOGGER.error(StringUtils.format(CRL_COULD_NOT_BE_LOADED, absPath));
} catch (GeneralSecurityException e) {
LOGGER.error(StringUtils.format(CRL_COULD_NOT_BE_LOADED, absPath));
}
}
return certificateRevocationLists;
}
}