Java tutorial
/* * Licensed under the EUPL, Version 1.1 or as soon they will be approved by * the European Commission - subsequent versions of the EUPL (the "Licence"); * You may not use this work except in compliance with the Licence. You may * obtain a copy of the Licence at: * * http://www.osor.eu/eupl/european-union-public-licence-eupl-v.1.1 * * Unless required by applicable law or agreed to in writing, software * distributed under the Licence is distributed on an "AS IS" basis, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * Licence for the specific language governing permissions and limitations under * the Licence. */ package eu.eidas.auth.engine.core.impl; import eu.eidas.auth.commons.EIDASErrors; import eu.eidas.auth.commons.PropertiesLoader; import eu.eidas.auth.engine.SAMLEngineUtils; import eu.eidas.auth.engine.X500PrincipalUtil; import eu.eidas.auth.engine.core.SAMLEngineEncryptionI; import eu.eidas.auth.engine.core.SAMLExtensionFormat; import eu.eidas.auth.engine.metadata.MetadataProcessorI; import eu.eidas.encryption.SAMLAuthnResponseDecrypter; import eu.eidas.encryption.SAMLAuthnResponseEncrypter; import eu.eidas.encryption.exception.DecryptionException; import eu.eidas.encryption.exception.EncryptionException; import eu.eidas.engine.exceptions.SAMLEngineException; import eu.eidas.engine.exceptions.EIDASSAMLEngineException; import eu.eidas.samlengineconfig.BinaryParameter; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.bouncycastle.asn1.x500.X500Name; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.opensaml.saml2.core.EncryptedAssertion; import org.opensaml.saml2.core.Response; import org.opensaml.saml2.metadata.*; import org.opensaml.xml.encryption.EncryptedKey; import org.opensaml.xml.security.credential.Credential; import org.opensaml.xml.security.credential.UsageType; import org.opensaml.xml.security.x509.BasicX509Credential; import org.opensaml.xml.util.Base64; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.*; import java.security.*; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.util.*; import java.util.regex.Pattern; /** * Created by bodabel on 10/12/2014. */ public class EncryptionSW extends AbstractSAMLEngineModule implements SAMLEngineEncryptionI { /** * The logger. */ private static final Logger LOG = LoggerFactory.getLogger(EncryptionSW.class.getName()); /** * The Constant RESPONSE_TO_POINT_ISSUER. */ private static final String RESPONSE_TO_POINT_ISSUER = "responseToPointIssuer"; /** * The Constant RESPONSE_FROM_POINT_ISSUER. */ private static final String RESPONSE_FROM_POINT_ISSUER = "responseDecryptionIssuer"; /** * The Constant SERIAL_NUMBER. */ private static final String SERIAL_NUMBER = "serialNumber"; /** * The Constant KEYSTORE_TYPE. */ private static final String KEYSTORE_TYPE = "keystoreType"; /** * The Constant KEY_STORE_PASSWORD. */ private static final String KEY_STORE_PASS = "keyStorePassword"; public static final String RESPONSE_TO_POINT_SERIAL_NUMBER = "responseToPointSerialNumber"; public static final String ENCRYPTION_ACTIVATION = "encryptionActivation"; public static final String KEYSTORE_PATH = "keystorePath"; private static final String KEY_PASSWORD = "keyPassword"; /** * name of the parameter storing the JCA provider name */ private static final String PROVIDER_NAME = "jcaProviderName"; private static final String DEFAULT_PROVIDER_NAME_VALUE = BouncyCastleProvider.PROVIDER_NAME; /** * The encryption key store. */ private KeyStore encryptionKeyStore = null; /** * The SW encryption properties */ private SAMLAuthnResponseEncrypter samlAuthnResponseEncrypter; private SAMLAuthnResponseDecrypter samlAuthnResponseDecrypter; /** * allows to force a provider for the encryptiorn */ private String jcaProviderName = null; MetadataProcessorI metadataProcessor = null; public void setMetadataProcessor(MetadataProcessorI processor) { metadataProcessor = processor; } /** * Encryption configurations for the engine. * Specify to use encryption/decryption for the instances */ private Properties encryptionActivationProperties; @Override public void init(String fileConf) throws SAMLEngineException { //ENCRYPTION CONFIGURATION // try { this.loadProperties(fileConf); this.init(properties); } catch (SAMLEngineException e) { throw e; } catch (Exception e) { LOG.error("Error init method"); throw new SAMLEngineException(e); } } @Override public void init(Properties propsConf) throws SAMLEngineException { try { properties = propsConf; this.loadCryptServiceProvider(); this.loadKeystore(); this.initActivationConf(properties); //If algorithms should be parametrized from config it comes here somewhere provided for constructors... samlAuthnResponseEncrypter = new SAMLAuthnResponseEncrypter(); samlAuthnResponseDecrypter = new SAMLAuthnResponseDecrypter(); if (properties.containsKey(PROVIDER_NAME)) { setJcaProviderName(properties.getProperty(PROVIDER_NAME)); } else { setJcaProviderName(DEFAULT_PROVIDER_NAME_VALUE); } if (getJcaProviderName() != null) { samlAuthnResponseEncrypter.setJcaProviderName(getJcaProviderName()); samlAuthnResponseDecrypter.setJcaProviderName(getJcaProviderName()); } LOG.debug("Encryption loaded."); } catch (Exception e) { LOG.error("Error init method"); throw new SAMLEngineException(EIDASErrors.SAML_ENGINE_CONFIGURATION_ERROR.errorCode(), EIDASErrors.SAML_ENGINE_CONFIGURATION_ERROR.errorMessage(), e); } } private void initActivationConf(Properties props) throws SAMLEngineException { InputStream is = null; try { Object obj = props.get(ENCRYPTION_ACTIVATION); if (obj instanceof String) { String fileConf = (String) obj; LOG.debug("File containing encryption configuration: " + fileConf); encryptionActivationProperties = PropertiesLoader.loadPropertiesXMLFile(fileConf); } else if (obj instanceof BinaryParameter) { encryptionActivationProperties = new Properties(); is = new ByteArrayInputStream((byte[]) ((BinaryParameter) obj).getValue()); encryptionActivationProperties.loadFromXML(is); } } catch (FileNotFoundException fe) { LOG.error("ERROR : File not found! Encryption will not be activated {}", fe); } catch (Exception e) { LOG.error("ERROR : loading encryption activation configuration"); if (!(e.getCause() instanceof FileNotFoundException)) { throw new SAMLEngineException(e); } } finally { IOUtils.closeQuietly(is); } } @Override public Response encryptSAMLResponse(Response authResponse, String destinationCountryCode, String requestIssuer, String messageFormat) throws SAMLEngineException { BasicX509Credential credential = (BasicX509Credential) getMetadataEncryptionCredential(requestIssuer, messageFormat); if (credential == null && isEncryptionEnable(destinationCountryCode)) { LOG.debug("Encryption enable, proceeding..."); StringBuilder issuerKey = new StringBuilder(RESPONSE_TO_POINT_ISSUER).append(".") .append(destinationCountryCode); StringBuilder serialNumberKey = new StringBuilder("responseToPointSerialNumber").append(".") .append(destinationCountryCode); final String serialNumber = properties.getProperty(serialNumberKey.toString()); final String responseToPointIssuer = properties.getProperty(issuerKey.toString()); if (responseToPointIssuer != null && !responseToPointIssuer.isEmpty()) { try { String aliasCert; String alias = null; X509Certificate responsePointAliasCert = null; boolean find = false; for (final Enumeration<String> e = encryptionKeyStore.aliases(); e.hasMoreElements() && !find;) { aliasCert = e.nextElement(); responsePointAliasCert = (X509Certificate) encryptionKeyStore.getCertificate(aliasCert); final String serialNum = responsePointAliasCert.getSerialNumber().toString(16); X500Name issuerDN = new X500Name(responsePointAliasCert.getIssuerDN().getName()); X500Name issuerDNConf = new X500Name(responseToPointIssuer); if (serialNum.equalsIgnoreCase(serialNumber) && X500PrincipalUtil.principalEquals(issuerDN, issuerDNConf)) { alias = aliasCert; find = true; } } if (!find) { throw new SAMLEngineException(EIDASErrors.SAML_ENGINE_INVALID_CERTIFICATE.errorCode(), EIDASErrors.SAML_ENGINE_INVALID_CERTIFICATE.errorMessage()); } // Find configured certificate responsePointAliasCert = (X509Certificate) encryptionKeyStore.getCertificate(alias); checkCertificateValidityPeriod(responsePointAliasCert); checkCertificateIssuer(responsePointAliasCert); // Create basic credential and set the EntityCertificate credential = new BasicX509Credential(); credential.setEntityCertificate(responsePointAliasCert); } catch (KeyStoreException kse) { throw new SAMLEngineException(EIDASErrors.SAML_ENGINE_INVALID_KEYSTORE.errorCode(), EIDASErrors.SAML_ENGINE_INVALID_KEYSTORE.errorMessage(), kse); } catch (Exception e) { LOG.warn("Error encrypting SAML Response.", e.getMessage()); throw new SAMLEngineException(e); } finally { LOG.debug("Credential for encryption of SAML Response done for target: '" + responseToPointIssuer + "'"); } } else { LOG.error("Encryption of SAML Response NOT done, because no " + RESPONSE_TO_POINT_ISSUER + " " + "configured!"); } } if (isEncryptionEnable(destinationCountryCode)) { if (credential == null) { throw new SAMLEngineException(EIDASErrors.SAML_ENGINE_UNENCRYPTED_RESPONSE.errorCode(), EIDASErrors.SAML_ENGINE_UNENCRYPTED_RESPONSE.errorMessage()); } try { // Execute encryption return samlAuthnResponseEncrypter.encryptSAMLResponse(authResponse, credential); } catch (EncryptionException e) { LOG.info("ERROR : Error encrypting SAML Response.", e.getMessage()); throw new SAMLEngineException(e); } finally { LOG.debug("Encryption of SAML Response done for target: " + credential.getEntityCertificate().getIssuerDN()); } } return authResponse; } @Override public Response decryptSAMLResponse(Response authResponse, String fromCountryCode) throws SAMLEngineException { if (isModuleEncryptionEnable()) { LOG.debug("Decryption enable, proceeding..."); //Decryption is always made with private key. Only own certificate needed final String responseFromPointIssuer = properties.getProperty(RESPONSE_FROM_POINT_ISSUER); if (responseFromPointIssuer != null && !responseFromPointIssuer.isEmpty()) { try { // Aquire Private Key of current point as a SAMLResponse target // e.g.: the targeted ProxyService aquires its own PrivateKey from its own KeyStore // Aquire PublicKey of SAMLResponse Point // e.g.: SAMLAdapter aquires PublicKey of the targeted ProxyService from the SAMLAdapter's KeyStore String aliasCert; String alias = null; X509Certificate responsePointAliasCert = null; boolean find = false; //KEYINFO CERTIFICATE EncryptedAssertion encAssertion = authResponse.getEncryptedAssertions().get(0); EncryptedKey encryptedSymmetricKey = encAssertion.getEncryptedData().getKeyInfo() .getEncryptedKeys().get(0); org.opensaml.xml.signature.X509Certificate keyInfoX509Cert = encryptedSymmetricKey.getKeyInfo() .getX509Datas().get(0).getX509Certificates().get(0); final ByteArrayInputStream bis = new ByteArrayInputStream( Base64.decode(keyInfoX509Cert.getValue())); final CertificateFactory certFact = CertificateFactory.getInstance("X.509"); final X509Certificate keyInfoCert = (X509Certificate) certFact.generateCertificate(bis); //RESPOINSE POINT CERTIFICATE FIND for (final Enumeration<String> e = encryptionKeyStore.aliases(); e.hasMoreElements();) { aliasCert = e.nextElement(); responsePointAliasCert = (X509Certificate) encryptionKeyStore.getCertificate(aliasCert); //CHECK IF CERTIFICATES EQUAL if (Arrays.equals(keyInfoCert.getTBSCertificate(), responsePointAliasCert.getTBSCertificate())) { alias = aliasCert; find = true; break; } } if (!find) { throw new SAMLEngineException(EIDASErrors.SAML_ENGINE_INVALID_CERTIFICATE.errorCode(), EIDASErrors.SAML_ENGINE_INVALID_CERTIFICATE.errorMessage()); } String responseAlgorithm = encAssertion.getEncryptedData().getEncryptionMethod().getAlgorithm(); Set<String> allowedAlgorithms = getAllowedAlgorithms(); if (allowedAlgorithms == null || allowedAlgorithms.isEmpty() || !allowedAlgorithms.contains(responseAlgorithm)) { throw new SAMLEngineException(EIDASErrors.INVALID_ENCRYPTION_ALGORITHM.errorCode()); } //GET PRIVATE KEY by found alias final PrivateKey responsePointAliasPrivateKey = (PrivateKey) encryptionKeyStore.getKey(alias, properties.getProperty(KEY_PASSWORD).toCharArray()); BasicX509Credential credential = new BasicX509Credential(); credential.setPrivateKey(responsePointAliasPrivateKey); credential.setEntityCertificate(keyInfoCert); //metadata check: encryption certificate is retrieved and used during the encryption phase //decryption will fail if the public key credential exposed in the metadata (and used during encryption) //is not paired with the private key return samlAuthnResponseDecrypter.decryptSAMLResponse(authResponse, credential); } catch (KeyStoreException kse) { throw new SAMLEngineException(EIDASErrors.SAML_ENGINE_INVALID_KEYSTORE.errorCode(), EIDASErrors.SAML_ENGINE_INVALID_KEYSTORE.errorMessage(), kse); } catch (SAMLEngineException e) { throw e; } catch (UnrecoverableKeyException e) { LOG.info("ERROR : Error decrypting SAML Response.", e.getMessage()); throw new SAMLEngineException(e); } catch (NoSuchAlgorithmException e) { LOG.error("Error decrypting SAML Response.", e.getMessage()); throw new SAMLEngineException(e); } catch (CertificateException e) { LOG.error("Error decrypting SAML Response.", e.getMessage()); throw new SAMLEngineException(e); } catch (DecryptionException e) { LOG.error("Error decrypting SAML Response.", e.getMessage()); throw new SAMLEngineException(e); } finally { LOG.debug("Decryption of SAML Response done on: '" + responseFromPointIssuer + "'"); } } else { LOG.info("ERROR : Decryption of SAML Response NOT done, because no " + RESPONSE_FROM_POINT_ISSUER + " " + "configured!"); } } return authResponse; } private final void loadProperties(String fileConf) throws SAMLEngineException { LOG.debug("Loading Encryption Properties"); InputStream fileProperties = null; try { fileProperties = SignSW.class.getResourceAsStream("/" + fileConf); properties = new Properties(); properties.loadFromXML(fileProperties); fileProperties.close(); } catch (InvalidPropertiesFormatException e) { LOG.info("ERROR : Exception: invalid properties format.", e.getMessage()); throw new SAMLEngineException(e); } catch (IOException e) { LOG.info("ERROR : Exception: invalid file: " + fileConf, e.getMessage()); throw new SAMLEngineException(e); } finally { IOUtils.closeQuietly(fileProperties); } } /** * Load cryptographic service provider. * * @throws SAMLEngineException the SAML engine exception */ private final void loadCryptServiceProvider() throws SAMLEngineException { LOG.debug("Loading Encryption Cryptographic Service Provider"); try { // Dynamically register Bouncy Castle provider. boolean found = false; // Check if BouncyCastle is already registered as a provider final Provider[] providers = Security.getProviders(); for (int i = 0; i < providers.length; i++) { if (providers[i].getName().equals(BouncyCastleProvider.PROVIDER_NAME)) { found = true; } } // Register only if the provider has not been previously registered if (!found) { LOG.debug("SAMLCore: Register Bouncy Castle provider."); Security.insertProviderAt(new BouncyCastleProvider(), Security.getProviders().length); } else { LOG.debug("SAMLCore: Bouncy Castle provider already registered."); } } catch (Exception e) { LOG.error("ERROR : Error loading encryption CryptographicServiceProvider", e.getMessage()); throw new SAMLEngineException(EIDASErrors.SAML_ENGINE_LOAD_PROVIDER.errorCode(), EIDASErrors.SAML_ENGINE_LOAD_PROVIDER.errorMessage(), e); } } /** * Load encryption keystore. * * @throws SAMLEngineException the SAML engine exception */ private final void loadKeystore() throws EIDASSAMLEngineException { LOG.debug("Loading Encryption Keystore"); InputStream fis = null; try { encryptionKeyStore = KeyStore.getInstance(properties.getProperty(KEYSTORE_TYPE)); LOG.debug("Loading KeyInfo from keystore file " + properties.getProperty(KEYSTORE_PATH)); Object o = properties.get("keystorePath"); if (o instanceof String) { fis = new FileInputStream(o.toString()); } else if (o instanceof BinaryParameter) { fis = new ByteArrayInputStream((byte[]) ((BinaryParameter) o).getValue()); } encryptionKeyStore.load(fis, properties.getProperty(KEY_STORE_PASS).toCharArray()); } catch (Exception e) { throw new EIDASSAMLEngineException(EIDASErrors.SAML_ENGINE_INVALID_KEYSTORE.errorCode(), EIDASErrors.SAML_ENGINE_INVALID_KEYSTORE.errorMessage(), e); } finally { IOUtils.closeQuietly(fis); } } private boolean isEnable(String key) { boolean value = false; if (null != encryptionActivationProperties) { try { value = Boolean.parseBoolean(encryptionActivationProperties.getProperty(key)); } catch (Exception e) { LOG.info("ERROR : Error retrieving activation value. {}", e); } } LOG.debug("Is active for: " + key + " : " + value); return value; } public boolean isModuleEncryptionEnable() { //The application should be smart enough to detect //the encrypted resposes and then apply decryption //to it. //I leave this parameters just in case future decissions //change this behaviour return true; } public boolean isEncryptionEnable(String countryCode) { LOG.debug("Loading encryption configuration"); if (Boolean.parseBoolean(properties.getProperty(RESPONSE_ENCRYPTION_MANDATORY))) { return true; } if (StringUtils.isEmpty(countryCode)) { LOG.info("ERROR : Country code is empty!"); return false; } else { final String key = (new StringBuilder("EncryptTo.").append(countryCode)).toString(); return isEnable(key); } } private Properties properties = new Properties(); public void setProperty(String propName, String propValue) { if (propValue == null) { return; } properties.setProperty(propName, propValue); if (DATA_ENCRYPTION_ALGORITHM.equalsIgnoreCase(propName) && !propValue.isEmpty()) { samlAuthnResponseEncrypter.setDataEncAlgorithm(propValue); } } public Credential getEncryptionCredential() throws SAMLEngineException { Credential credential = null; try { final String serialNumber = properties.getProperty(SERIAL_NUMBER); final String issuer = properties.getProperty(RESPONSE_FROM_POINT_ISSUER); credential = SAMLEngineUtils.getEncryptionCredential(encryptionKeyStore, serialNumber, issuer); X509Certificate certificate = ((BasicX509Credential) credential).getEntityCertificate(); checkCertificateValidityPeriod(certificate); checkCertificateIssuer(certificate); } catch (NoSuchAlgorithmException e) { throw new SAMLEngineException( "A 'xmldsig#rsa-sha1' cryptographic algorithm is requested but is not available in the environment: " + e); } catch (KeyStoreException e) { throw new SAMLEngineException("Generic KeyStore exception:" + e); } catch (UnrecoverableKeyException e) { throw new SAMLEngineException("UnrecoverableKey exception:" + e); } return credential; } private EntityDescriptor getEntityFromMetadata(String metadataUrl) throws SAMLEngineException { if (metadataProcessor != null && metadataUrl != null && !metadataUrl.isEmpty()) { return metadataProcessor.getEntityDescriptor(metadataUrl); } return null; } private Credential getMetadataEncryptionCredential(RoleDescriptor rd) throws SAMLEngineException { for (KeyDescriptor kd : rd.getKeyDescriptors()) { if (kd.getUse() == UsageType.ENCRYPTION) { return SAMLEngineUtils.getKeyCredential(this, kd.getKeyInfo()); } } return null; } private Credential getMetadataEncryptionCredential(String metadataUrl, String messageFormat) throws SAMLEngineException { Credential credential = null; if (SAMLExtensionFormat.STORK1_FORMAT_NAME.equalsIgnoreCase(messageFormat)) { return null; } EntityDescriptor entity = getEntityFromMetadata(metadataUrl); if (entity == null || entity.getRoleDescriptors().isEmpty()) { LOG.info("METADATA EXCEPTION : cannot retrieve entity descriptor from url " + metadataUrl); } else { for (RoleDescriptor rd : entity.getRoleDescriptors()) { if (rd instanceof SPSSODescriptor) { credential = getMetadataEncryptionCredential(rd); if (credential != null) { break; } } } } return credential; } private static final String DEFAULT_ALLOWED_ALGORITHMS = "http://www.w3.org/2009/xmlenc11#aes128-gcm;http://www.w3.org/2009/xmlenc11#aes192-gcm;http://www.w3.org/2009/xmlenc11#aes256-gcm"; private Set<String> getAllowedAlgorithms() { Set<String> allowed = new HashSet<String>(); Pattern sepPattern = Pattern.compile(";"); String whitelist = properties.getProperty(ENCRYPTION_ALGORITHM_WHITELIST); if (whitelist == null || whitelist.isEmpty()) { whitelist = DEFAULT_ALLOWED_ALGORITHMS; } String[] wlAlgorithms = sepPattern.split(whitelist); if (wlAlgorithms != null && wlAlgorithms.length > 0) { for (String algo : wlAlgorithms) { algo = algo.trim(); if (!algo.isEmpty()) { allowed.add(algo); } } } return allowed.isEmpty() ? null : allowed; } public String getJcaProviderName() { return jcaProviderName; } public void setJcaProviderName(String jcaProviderName) { this.jcaProviderName = jcaProviderName; } }