/*
* Copyright (c) Members of the EGEE Collaboration. 2006-2010.
* See http://www.eu-egee.org/partners/ for details on the copyright holders.
*
* Licensed 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 org.glite.authz.pep.pip.provider;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Vector;
import javax.security.auth.x500.X500Principal;
import org.glite.authz.common.config.ConfigurationException;
import org.glite.authz.common.model.Attribute;
import org.glite.authz.common.model.Request;
import org.glite.authz.common.model.Subject;
import org.glite.authz.common.util.Strings;
import org.glite.authz.pep.pip.PIPProcessingException;
import org.glite.security.util.CertUtil;
import org.glite.security.util.FileCertReader;
import org.glite.voms.PKIStore;
import org.glite.voms.PKIUtils;
import org.glite.voms.PKIVerifier;
import org.glite.voms.VOMSAttribute;
import org.glite.voms.VOMSValidator;
import org.glite.voms.ac.ACValidator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** Base class for PIPs which work with X.509 certificates. */
public abstract class AbstractX509PIP extends AbstractPolicyInformationPoint {
/** Class logger. */
private Logger log= LoggerFactory.getLogger(AbstractX509PIP.class);
/**
* Reads a set of certificates in to a chain of {@link X509Certificate}
* objects.
*/
private FileCertReader certReader;
/**
* Whether the given cert chain must contain a proxy certificate in order to
* be valid.
*/
private boolean requireProxyCertificate;
/** Whether to perform PKIX validation on the incoming certificate. */
private boolean performPKIXValidation;
/** Whether VOMS AC support is currently enabled. */
private boolean vomsSupportEnabled;
/**
* PKIStore containing trust material used to validate the subject's end
* entity certificate
*/
private PKIStore eeTrustMaterial;
/**
* Verifier used to validate an X.509 certificate chain which may, or may
* not, include AC certs.
*/
private PKIVerifier certVerifier;
/**
* The constructor for this PIP. This constructor enables support for the
* VOMS attribute certificates.
*
* @param pipID
* ID of this PIP
* @param requireProxy
* whether a subject's certificate chain must require a proxy in
* order to be valid
* @param eeTrustMaterial
* trust material used to validate the subject's end entity
* certificate
* @param acTrustMaterial
* trust material used to validate the subject's attribute
* certificate certificate, may be null of AC support is not
* desired
*
* @throws ConfigurationException
* thrown if the configuration of the PIP fails
*/
public AbstractX509PIP(String pipID, boolean requireProxy,
PKIStore eeTrustMaterial, PKIStore acTrustMaterial)
throws ConfigurationException {
super(pipID);
requireProxyCertificate= requireProxy;
if (eeTrustMaterial == null) {
throw new ConfigurationException("Policy information point trust material may not be null");
}
if (acTrustMaterial == null) {
vomsSupportEnabled= false;
}
else {
vomsSupportEnabled= true;
}
try {
certReader= new FileCertReader();
this.eeTrustMaterial= eeTrustMaterial;
certVerifier= new PKIVerifier(acTrustMaterial, eeTrustMaterial);
} catch (Exception e) {
throw new ConfigurationException("Unable to create X509 trust manager: "
+ e.getMessage());
}
}
/**
* Gets whether VOMS support is enabled.
*
* @return whether VOMS support is enabled
*/
public boolean isVOMSSupportEnabled() {
return vomsSupportEnabled;
}
/**
* Gets whether the PKIX validation is performed against the processed cert
* chain.
*
* @return whether the PKIX validation is performed against the processed
* cert chain
*/
public boolean isPKIXValidationEnabled() {
return performPKIXValidation;
}
/**
* @return whether a proxy certificate is mandatory.
*/
public boolean isProxyCertificateRequired() {
return requireProxyCertificate;
}
/**
* Sets whether the PKIX validation is performed against the processed cert
* chain.
*
* @param perform
* whether the PKIX validation is performed against the processed
* cert chain
*/
public void performPKIXValidation(boolean perform) {
performPKIXValidation= perform;
}
/**
* Gets the PKI certificate verifier.
*
* @return PKI certificate verifier
*/
public PKIVerifier getCertVerifier() {
return certVerifier;
}
protected PKIStore getTrustMaterial() {
return eeTrustMaterial;
}
/** {@inheritDoc} */
public boolean populateRequest(Request request)
throws PIPProcessingException {
if (!appliesToRequest(request)) {
return false;
}
X509Certificate[] certChain;
X509Certificate endEntityCert;
Collection<Attribute> certAttributes;
for (Subject subject : request.getSubjects()) {
certChain= extractCertificateChain(subject);
if (certChain == null) {
continue;
}
// sort the cert chain starting from proxy/end-entity cert
certChain= sortCertificateChain(certChain);
// bug fix: complete cert chain up to trust anchor
certChain= completeCertificateChain(certChain);
if (log.isDebugEnabled()) {
int i= 0;
for (X509Certificate cert : certChain) {
log.debug("certChain[{}]: {}",
i++,
cert.getSubjectX500Principal().getName(X500Principal.RFC2253));
}
}
endEntityCert= certChain[CertUtil.findClientCert(certChain)];
String endEntitySubjectDN= endEntityCert.getSubjectX500Principal().getName(X500Principal.RFC2253);
if (performPKIXValidation && !certVerifier.verify(certChain)) {
String errorMsg= "Certificate with subject DN "
+ endEntitySubjectDN + " failed PKIX validation";
log.error(errorMsg);
throw new PIPProcessingException(errorMsg);
}
log.debug("Extracting subject attributes from certificate with subject DN {}",
endEntitySubjectDN);
certAttributes= processCertChain(endEntityCert, certChain);
if (certAttributes != null) {
log.debug("Extracted subject attributes {} from certificate with subject DN {}",
certAttributes,
endEntitySubjectDN);
updateSubjectCertificateAttributes(subject, certAttributes);
return true;
}
}
String errMsg= "Subject did not contain the required certificate chain in attribute: "
+ getCertificateAttributeId()
+ " datatype: "
+ getCertificateAttributeDatatype();
log.error(errMsg);
throw new PIPProcessingException(errMsg);
}
/**
* Update the Subject certificate attributes (subject-id, subject-issuer,
* ...) with the attributes given as parameter. If the subject already
* contains the attributes, their respective values will be overwritten.
*
* @param subject
* the subject to update
* @param certAttributes
* the certificate attributes
*/
private void updateSubjectCertificateAttributes(Subject subject,
Collection<Attribute> certAttributes) {
for (Attribute certAttribute : certAttributes) {
boolean alreadyExists= false;
String certAttributeId= certAttribute.getId();
String certAttributeDataType= certAttribute.getDataType();
for (Attribute subjectAttribute : subject.getAttributes()) {
if (subjectAttribute.getId().equals(certAttributeId)
&& subjectAttribute.getDataType().equals(certAttributeDataType)) {
alreadyExists= true;
log.debug("Subject {} already contains values, replace them with {}",
subjectAttribute,
certAttribute);
subjectAttribute.getValues().clear();
subjectAttribute.getValues().addAll(certAttribute.getValues());
}
}
if (!alreadyExists) {
log.debug("Add {} to Subject", certAttribute);
subject.getAttributes().add(certAttribute);
}
}
}
/**
* Checks whether this PIP applies to this request.
*
* @param request
* the incoming request to be checked
*
* @return true if this PIP applies to the request, false if not
*/
protected abstract boolean appliesToRequest(Request request);
/**
* Gets the certificate chain from the subject's attribute id and datatype
*
* @param subject
* subject from which to extract the certificate chain
*
* @return the extracted certificate chain or <code>null</code> if the
* subject did not contain a chain of X.509 version 3 certificates
*
* @throws PIPProcessingException
* thrown if the subject contained more than one certificate
* chain or if the chain was not properly PEM encoded
*
* @see #getCertificateAttributeId()
* @see #getCertificateAttributeDatatype()
*/
protected X509Certificate[] extractCertificateChain(Subject subject)
throws PIPProcessingException {
String pemCertChain= null;
for (Attribute attribute : subject.getAttributes()) {
// check attribute Id and datatype
if (Strings.safeEquals(attribute.getId(),
getCertificateAttributeId())
&& Strings.safeEquals(attribute.getDataType(),
getCertificateAttributeDatatype())) {
if (pemCertChain != null || attribute.getValues().size() < 1) {
String errorMsg= "Subject contains more than one X509 certificate chain.";
log.error(errorMsg);
throw new PIPProcessingException(errorMsg);
}
if (attribute.getValues().size() == 1) {
pemCertChain= Strings.safeTrimOrNullString((String) attribute.getValues().iterator().next());
}
}
}
if (pemCertChain == null) {
return null;
}
BufferedInputStream bis= new BufferedInputStream(new ByteArrayInputStream(pemCertChain.getBytes()));
Vector<X509Certificate> chainVector;
try {
chainVector= certReader.readCertChain(bis);
} catch (IOException e) {
log.error("Unable to parse subject cert chain", e);
throw new PIPProcessingException("Unable to parse subject cert chain",
e);
} finally {
try {
bis.close();
} catch (IOException e) {
log.error("Unable to close cert chain inputstream", e);
}
}
X509Certificate[] certChain= chainVector.toArray(new X509Certificate[] {});
boolean proxyPresent= false;
for (X509Certificate cert : certChain) {
if (cert.getVersion() < 3) {
log.warn("Subject certificate {} is not a version 3, or greater, certificate, certificate chain ignored",
cert.getSubjectX500Principal().getName(X500Principal.RFC2253));
return null;
}
if (isProxyCertificateRequired() && PKIUtils.isProxy(cert)) {
proxyPresent= true;
}
}
if (isProxyCertificateRequired() && !proxyPresent) {
log.warn("Proxy is required, but none found");
return null;
}
return certChain;
}
/**
* Gets the ID of the Subject attribute which is expected to carry the
* user's certificate.
*
* @return ID of the Subject attribute which is expected to carry the user's
* certificate
*/
protected abstract String getCertificateAttributeId();
/**
* Gets the datatype of the Subject attribute which is expected to carry the
* user's certificate.
*
* @return datatype of the Subject attribute which is expected to carry the
* user's certifica
*/
protected abstract String getCertificateAttributeDatatype();
/**
* Processes one certificate chain and extract the information for the
* subjects in the request.
*
* @param endEntityCertificate
* end entity certificate for the subject currently being
* processed
* @param certChain
* the certificate chain containing the end entity certificate
* from which information will be extracted
*
* @return the attribute extracted from the certificate chain
*
* @throws PIPProcessingException
* thrown if there is a problem reading the information from the
* certificate chain
*/
protected abstract Collection<Attribute> processCertChain(
X509Certificate endEntityCertificate, X509Certificate[] certChain)
throws PIPProcessingException;
/**
* Validates any VOMS attribute certificates within the cert chain and
* extract the attributes from within.
*
* @param certChain
* cert chain which may contain VOMS attribute certificates
*
* @return the attributes extracted from the VOMS certificate or null if
* there were no valid attribute certificates
*
* @throws PIPProcessingException
* thrown if there is more than one valid attribute certificate
* within the certificate chain
*/
@SuppressWarnings("unchecked")
protected VOMSAttribute extractAttributeCertificate(
X509Certificate[] certChain) throws PIPProcessingException {
VOMSValidator vomsValidator= new VOMSValidator(certChain,
new ACValidator(getCertVerifier()));
vomsValidator.validate();
List<VOMSAttribute> attributeCertificates= vomsValidator.getVOMSAttributes();
if (attributeCertificates == null || attributeCertificates.isEmpty()) {
return null;
}
if (attributeCertificates.size() > 1) {
String errorMsg= "End entity certificate for subject"
+ certChain[0].getSubjectX500Principal().getName(X500Principal.RFC2253)
+ " contains more than one attribute certificate";
log.error(errorMsg);
throw new PIPProcessingException(errorMsg);
}
return attributeCertificates.get(0);
}
/**
* Sort the certificate chain by issuer.
*
* @param certChain
* the certificate chain to sort
* @return the sorted certificate chain
* @throws PIPProcessingException
* if an error occurs
*/
protected X509Certificate[] sortCertificateChain(X509Certificate[] certChain)
throws PIPProcessingException {
if (certChain.length == 0)
return new X509Certificate[0];
List<X509Certificate> certificates= Arrays.asList(certChain);
Map<X500Principal, X509Certificate> certsMapBySubject= new HashMap<X500Principal, X509Certificate>();
// in this map root CA cert is not stored (as it has the same Issuer as
// its direct child)
Map<X500Principal, X509Certificate> certsMapByIssuer= new HashMap<X500Principal, X509Certificate>();
for (X509Certificate c : certificates) {
certsMapBySubject.put(c.getSubjectX500Principal(), c);
if (!c.getIssuerX500Principal().equals(c.getSubjectX500Principal()))
certsMapByIssuer.put(c.getIssuerX500Principal(), c);
}
// let's start from the random one (the 1st on the received list)
List<X509Certificate> certsList= new LinkedList<X509Certificate>();
X509Certificate current= certsMapBySubject.remove(certificates.get(0).getSubjectX500Principal());
if (!current.getIssuerX500Principal().equals(current.getSubjectX500Principal())) {
certsMapByIssuer.remove(current.getIssuerX500Principal());
}
certsList.add(current);
// build path from current to root
while (true) {
X509Certificate parent= certsMapBySubject.remove(current.getIssuerX500Principal());
if (parent != null) {
certsMapByIssuer.remove(parent.getIssuerX500Principal());
certsList.add(parent);
current= parent;
}
else
break;
}
// build path from the first on the list down to the user's certificate
current= certsList.get(0);
while (true) {
X509Certificate child= certsMapByIssuer.remove(current.getSubjectX500Principal());
if (child != null) {
certsList.add(0, child);
current= child;
}
else
break;
}
if (certsMapByIssuer.size() > 0) {
throw new PIPProcessingException("The certificate chain is inconsistent");
}
return certsList.toArray(new X509Certificate[] {});
}
/**
* Tries to complete the certificate chain up to a trust anchor from the
* eeTrustMaterial store. The cert chain MUST be sorted!!!
*
* When PKIX validation is enabled (checked with
* {@link #isPKIXValidationEnabled()} method), the method will throw a
* {@link PIPProcessingException} if the chain can not be completed.
* Otherwise, only a warning is logged.
*
* @param certChain
* the sorted certificate chain to complete
* @return the completed cert chain
* @throws PIPProcessingException
* if PKIX validation is enabled and an error occurs while
* building the complete cert chain
*/
@SuppressWarnings("unchecked")
protected X509Certificate[] completeCertificateChain(
X509Certificate[] certChain) throws PIPProcessingException {
if (certChain == null) {
return null;
}
if (certChain.length < 1) {
return certChain;
}
// get the trust anchors store
Hashtable<String, Vector<X509Certificate>> certificates= eeTrustMaterial.getCAs();
Vector<X509Certificate> certChainVector= new Vector<X509Certificate>();
// first cert is proxy or end-entity cert
X509Certificate currentCert= certChain[0];
certChainVector.add(currentCert);
// check the original cert chain
log.debug("Checking original certChain.length= {}", certChain.length);
for (int i= 1; i < certChain.length; i++) {
if (PKIUtils.checkIssued(certChain[i], certChain[i - 1])) {
if (log.isDebugEnabled()) {
log.debug("checkIssued: YES: {} issued {}",
certChain[i].getSubjectX500Principal().getName(X500Principal.RFC2253),
certChain[i - 1].getSubjectX500Principal().getName(X500Principal.RFC2253));
}
currentCert= certChain[i];
certChainVector.add(currentCert);
}
}
log.debug("is trust anchor? {}",
currentCert.getSubjectX500Principal().getName(X500Principal.RFC2253));
// check that currentCert is self signed and in ca store (trusted
// anchor).
if (PKIUtils.selfIssued(currentCert)) {
String hash= PKIUtils.getHash(currentCert);
Vector<X509Certificate> trustAnchors= certificates.get(hash);
if (trustAnchors == null || trustAnchors.indexOf(currentCert) == -1) {
String errorMessage= "Certificate "
+ currentCert.getSubjectX500Principal().getName(X500Principal.RFC2253)
+ " is self signed, but not a trust anchor";
if (isPKIXValidationEnabled()) {
log.error("PKIX validation failed: " + errorMessage);
throw new PIPProcessingException(errorMessage);
}
else {
log.warn(errorMessage);
}
}
else {
log.debug("YES: {} is a valid trust anchor",
currentCert.getSubjectX500Principal().getName(X500Principal.RFC2253));
}
}
else {
log.debug("NO: searching trust anchor for {}",
currentCert.getSubjectX500Principal().getName(X500Principal.RFC2253));
// and complete the certification path.
do {
// find trusted issuer
String hash= PKIUtils.getHash(currentCert.getIssuerX500Principal());
Vector<X509Certificate> issuers= certificates.get(hash);
if (log.isTraceEnabled()) {
log.trace("Issuers({}): {}", hash, issuers);
}
if (issuers != null) {
for (X509Certificate issuer : issuers) {
if (PKIUtils.checkIssued(issuer, currentCert)) {
if (log.isDebugEnabled()) {
log.debug("checkIssued: YES: {} issued {}",
issuer.getSubjectX500Principal().getName(X500Principal.RFC2253),
currentCert.getSubjectX500Principal().getName(X500Principal.RFC2253));
}
currentCert= issuer;
certChainVector.add(currentCert);
if (log.isDebugEnabled()) {
log.debug("currentCert: {}",
currentCert.getSubjectX500Principal().getName(X500Principal.RFC2253));
}
break;
}
}
}
else {
String errorMessage= "No trust anchor found for certificate "
+ currentCert.getSubjectX500Principal().getName(X500Principal.RFC2253);
if (isPKIXValidationEnabled()) {
log.error("PKIX validation failed: " + errorMessage);
throw new PIPProcessingException(errorMessage);
}
else {
log.warn(errorMessage);
// exit while loop
break;
}
}
} while (!PKIUtils.selfIssued(currentCert));
} // else
if (log.isTraceEnabled()) {
int i= 0;
for (X509Certificate cert : certChainVector) {
log.trace("completed chain[{}]: {}",
i++,
cert.getSubjectX500Principal().getName(X500Principal.RFC2253));
}
}
return certChainVector.toArray(new X509Certificate[certChainVector.size()]);
}
}