/*
* Copyright 2018 A-SIT Plus GmbH
* AT-specific eIDAS Connector has been developed in a cooperation between EGIZ,
* A-SIT Plus GmbH, A-SIT, and Graz University of Technology.
*
* Licensed under the EUPL, Version 1.2 or - as soon they will be approved by
* the European Commission - subsequent versions of the EUPL (the "License");
* You may not use this work except in compliance with the License.
* You may obtain a copy of the License at:
* https://joinup.ec.europa.eu/news/understanding-eupl-v12
*
* 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.
*
* This product combines work with different licenses. See the "NOTICE" text
* file for details on the various modules and licenses.
* The "NOTICE" text file is part of the distribution. Any derivative works
* that you distribute must include a readable copy of the "NOTICE" text file.
*/
package at.gv.egiz.eaaf.modules.pvp2.impl.validation.metadata;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.MessageDigest;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Nonnull;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.xml.security.keys.KeyInfo;
import org.apache.xml.security.keys.keyresolver.KeyResolverException;
import org.opensaml.saml.common.SignableSAMLObject;
import org.opensaml.saml.saml2.metadata.EntitiesDescriptor;
import org.opensaml.saml.saml2.metadata.EntityDescriptor;
import org.opensaml.saml.security.impl.SAMLSignatureProfileValidator;
import org.opensaml.security.x509.BasicX509Credential;
import org.opensaml.xmlsec.signature.Signature;
import org.opensaml.xmlsec.signature.impl.SignatureImpl;
import org.opensaml.xmlsec.signature.support.SignatureException;
import org.opensaml.xmlsec.signature.support.SignatureValidator;
import at.gv.egiz.eaaf.core.exceptions.EaafConfigurationException;
import at.gv.egiz.eaaf.core.exceptions.EaafException;
import at.gv.egiz.eaaf.core.impl.credential.EaafKeyStoreUtils;
import at.gv.egiz.eaaf.modules.pvp2.exception.Pvp2MetadataException;
import at.gv.egiz.eaaf.modules.pvp2.exception.SamlMetadataSignatureException;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class SimpleMetadataSignatureVerificationFilter extends AbstractMetadataSignatureFilter {
private final String metadataUrl;
private final KeyStore trustedCredential;
private static final String ERROR_07 = "internal.pvp.07";
private static final String ERROR_12 = "internal.pvp.12";
private static final String ERROR_MSG_ENTITIESDESC = "EntitiesDescritors are NOT supported";
private static final String ERROR_MSG_SIGNOTVALID = "Signature not valid or no trusted certificate found";
/**
* SAML2 metadata-signature verification-filter that uses a simple {@link List}
* of trusted {@link BasicX509Credential} as truststore.
*
* This filter only validates {@link EntityDescriptor} elements.
* SAML2 metadata with {@link EntitiesDescriptor} are not supported.
*
*
* @param keyStore TrustStore that contains trusted X509 certificates
* @param metadataUrl Metadata URL for logging purposes
*/
public SimpleMetadataSignatureVerificationFilter(@Nonnull KeyStore keyStore,
@Nonnull String metadataUrl) {
this.metadataUrl = metadataUrl;
this.trustedCredential = keyStore;
}
@Override
protected void verify(EntityDescriptor desc) throws Pvp2MetadataException {
try {
internalVerify(desc);
} catch (final EaafException e) {
log.info("Metadata verification FAILED for: {} Reason: {}", metadataUrl, e.getMessage());
throw new Pvp2MetadataException(ERROR_07,
new Object[] { metadataUrl, e.getMessage() }, e);
}
}
@Override
protected void verify(EntitiesDescriptor desc) throws Pvp2MetadataException {
throw new Pvp2MetadataException(ERROR_07,
new Object[] { metadataUrl, ERROR_MSG_ENTITIESDESC });
}
@Override
protected void verify(EntityDescriptor entity, EntitiesDescriptor desc) throws Pvp2MetadataException {
throw new Pvp2MetadataException(ERROR_07,
new Object[] { metadataUrl, ERROR_MSG_ENTITIESDESC });
}
private void internalVerify(SignableSAMLObject signedElement)
throws EaafException {
// check if signature exists
if (signedElement.getSignature() == null) {
throw new Pvp2MetadataException(ERROR_12,
new Object[] { metadataUrl });
}
// perform general signature validation
try {
final SAMLSignatureProfileValidator sigValidator = new SAMLSignatureProfileValidator();
sigValidator.validate(signedElement.getSignature());
} catch (final SignatureException e) {
log.error("Failed to validate Signature", e);
throw new Pvp2MetadataException(ERROR_07,
new Object[] { metadataUrl, e.getMessage() }, e);
}
// perform cryptographic signature verification
if (!performCryptographicCheck(signedElement.getSignature())) {
log.info("PVP2 metadata: " + metadataUrl + " are NOT trusted!");
throw new SamlMetadataSignatureException(metadataUrl, ERROR_MSG_SIGNOTVALID);
}
}
private boolean performCryptographicCheck(Signature signature) throws EaafConfigurationException {
// extract trusted signer certificate from signature
BasicX509Credential x509FromSig = extractTrustedX509FromSignature(signature);
if (x509FromSig != null) {
try {
SignatureValidator.validate(signature, x509FromSig);
return true;
} catch (final SignatureException e) {
log.debug("Failed to verfiy Signature with cert: {} Reason: {}",
x509FromSig.getEntityCertificate().getSubjectDN(), e.getMessage());
}
} else {
for (final BasicX509Credential cred : getTrustedCertificates()) {
log.trace("Validating signature with credential: {} ... ",
cred.getEntityCertificate().getSubjectDN());
try {
SignatureValidator.validate(signature, cred);
return true;
} catch (final SignatureException e) {
log.debug("Failed to verfiy Signature with cert: {} Reason: {}",
cred.getEntityCertificate().getSubjectDN(), e.getMessage());
}
}
}
return false;
}
private BasicX509Credential extractTrustedX509FromSignature(Signature signature) {
try {
KeyInfo keyInfo = ((SignatureImpl) signature).getXMLSignature().getKeyInfo();
byte[] encSigCert = keyInfo.getX509Certificate() != null
? keyInfo.getX509Certificate().getEncoded()
: ArrayUtils.EMPTY_BYTE_ARRAY;
return getTrustedCertificates().stream()
.filter(el -> {
try {
return MessageDigest.isEqual(el.getEntityCertificate().getEncoded(), encSigCert);
} catch (CertificateEncodingException e) {
log.warn("Can not match certificates.", e);
return false;
}
})
.findFirst()
.orElse(null);
} catch (KeyResolverException | EaafConfigurationException | CertificateEncodingException e) {
log.info("Can not extract X509 certificate from XML signature. Reason: {}", e.getMessage());
}
return null;
}
private List getTrustedCertificates() throws EaafConfigurationException {
try {
final List certs =
EaafKeyStoreUtils.readCertsFromKeyStore(trustedCredential);
if (certs.isEmpty()) {
log.warn("No trusted metadata-signing certificates in configuration");
throw new EaafConfigurationException("module.eidasauth.02",
new Object[] { "No trusted metadata-signing certificates" });
}
final List result = new ArrayList<>();
for (final X509Certificate cert : certs) {
result.add(new BasicX509Credential(cert));
}
return result;
} catch (final KeyStoreException e) {
throw new EaafConfigurationException("module.eidasauth.01",
new Object[] { "Trusted metadata-signing certificates", e.getMessage() }, e);
}
}
}