/*
* 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.security.Security;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.List;
import java.util.Set;
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.Environment;
import org.glite.authz.common.model.Request;
import org.glite.authz.common.model.Subject;
import org.glite.authz.common.profile.CommonXACMLAuthorizationProfileConstants;
import org.glite.authz.common.profile.GLiteAuthorizationProfileConstants;
import org.glite.authz.common.util.Base64;
import org.glite.authz.common.util.Strings;
import org.glite.authz.pep.pip.PIPProcessingException;
import org.glite.voms.FQAN;
import org.glite.voms.PKIStore;
import org.glite.voms.PKIUtils;
import org.glite.voms.VOMSAttribute;
import org.glite.voms.VOMSValidator;
import org.glite.voms.ac.ACValidator;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The PIP applies to request which have a profile identifier
* {@value GLiteAuthorizationProfileConstants#ID_ATTRIBUTE_PROFILE_ID} defined
* in the request environment. By default accept all profile identifier values,
* but a list of accepted profile identifier values can be specified.
* <p>
* The PIP extracts information from a X.509, version 3, certificate. The
* certificate may include VOMS attribute certificates. All extract information
* is added to the subject(s) containing a valid certificate chain.
* <p>
* The PEM encoded end entity certificate, and its certificate chain, are
* expected to be bound to the subject attribute
* {@value Attribute#ID_SUB_KEY_INFO} with a datatype of
* {@value Attribute#DT_BASE64_BINARY}.
* <p>
* Only one end-entity certificate may be present in the chain.
* <p>
* If the end entity certificate contains a VOMS attribute certificate, and VOMS
* certificate validation is enabled, information from that attribute
* certificate will also be added to the subject. Only one VOMS attribute
* certificate may be present in the end-entity certificate.
*
* @see <a href="https://twiki.cnaf.infn.it/cgi-bin/twiki/view/VOMS">VOMS
* website</a>
*/
public class CommonXACMLAuthorizationProfilePIP extends AbstractX509PIP {
static {
/* add BouncyCastle security provider if not already done */
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
Security.addProvider(new BouncyCastleProvider());
}
}
/** List of accepted profile IDs, if <code>null</code> accept all profile Id */
private List<String> acceptedProfileIds_= null;
/** Class logger. */
private Logger log= LoggerFactory.getLogger(CommonXACMLAuthorizationProfilePIP.class);
/** Certificate factory */
private CertificateFactory cf_;
/**
* 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 <code>null</code> if AC
* support is not desired
* @param performPKIXValidation
* perform or not PKIX validation on the certificate
* @throws ConfigurationException
* thrown if the configuration of the PIP fails
*/
public CommonXACMLAuthorizationProfilePIP(String pipID,
boolean requireProxy, PKIStore eeTrustMaterial,
PKIStore acTrustMaterial, boolean performPKIXValidation)
throws ConfigurationException {
super(pipID, requireProxy, eeTrustMaterial, acTrustMaterial);
performPKIXValidation(performPKIXValidation);
try {
cf_= CertificateFactory.getInstance("X.509",
Security.getProvider(BouncyCastleProvider.PROVIDER_NAME));
} catch (CertificateException e) {
throw new ConfigurationException("Fail to get instance of the X.509 certificate factory",
e);
}
}
/**
* Constructor with a list of accepted profile IDs found in the request
* environment attribute
* {@value CommonXACMLAuthorizationProfileConstants#ID_ATTRIBUTE_PROFILE_ID}
*
* @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 <code>null</code> if AC
* support is not desired
* @param performPKIXValidation
* perform or not PKIX validation on the certificate
* @param acceptedProfileIds
* list of accepted profile IDs found in the request environment.
* If <code>null</code> accept every profile IDs, if empty accept
* none.
* @throws ConfigurationException
* thrown if the configuration of the PIP fails
*/
public CommonXACMLAuthorizationProfilePIP(String pipID,
boolean requireProxy, PKIStore eeTrustMaterial,
PKIStore acTrustMaterial, boolean performPKIXValidation,
String[] acceptedProfileIds) throws ConfigurationException {
this(pipID,
requireProxy,
eeTrustMaterial,
acTrustMaterial,
performPKIXValidation);
if (acceptedProfileIds == null) {
// accept all
log.debug("{}: accept all profile ID values", pipID);
acceptedProfileIds_= null;
}
else if (acceptedProfileIds.length == 0) {
// accept none
log.debug("{}: accept NO profile ID value", pipID);
acceptedProfileIds_= Collections.emptyList();
}
else {
log.debug("{}: accept profile ID values: ",
pipID,
Arrays.toString(acceptedProfileIds));
acceptedProfileIds_= new ArrayList<String>(Arrays.asList(acceptedProfileIds));
}
}
/**
* Checks that the incoming {@link Request} contains a profile identifier
* attribute in the environment.
*
* @param request
* the incoming request to be checked
*
* @return true if this PIP applies to the request, false if not
*/
protected boolean appliesToRequest(Request request) {
Environment env= request.getEnvironment();
if (env != null) {
for (Attribute attrib : env.getAttributes()) {
if (CommonXACMLAuthorizationProfileConstants.ID_ATTRIBUTE_PROFILE_ID.equals(attrib.getId())) {
if (acceptedProfileIds_ == null) {
// accept all profile IDs
log.trace("PIP '{}' accept all {} value",
getId(),
CommonXACMLAuthorizationProfileConstants.ID_ATTRIBUTE_PROFILE_ID);
return true;
}
else if (acceptedProfileIds_.isEmpty()) {
// accept none
log.warn("PIP '{}' don't accept any profile ID, specify 'acceptedProfileIDs = ...' in config.",
getId());
return false;
}
else {
// accept only listed one
for (String acceptedProfileId : acceptedProfileIds_) {
if (attrib.getValues().contains(acceptedProfileId)) {
log.trace("PIP '{}' accept {}",
getId(),
acceptedProfileId);
return true;
}
}
log.debug("PIP '{}' don't accept profile ID: {}",
getId(),
attrib.getValues());
return false;
}
}
}
}
log.debug("Skipping PIP '{}', request does not contain a profile identifier in environment",
getId());
return false;
}
/**
* {@inheritDoc}
*
* @return {@value CommonXACMLAuthorizationProfileConstants#ID_ATTRIBUTE_SUBJECT_KEY_INFO}
*/
protected String getCertificateAttributeId() {
return CommonXACMLAuthorizationProfileConstants.ID_ATTRIBUTE_SUBJECT_KEY_INFO;
}
/**
* {@inheritDoc}
*
* @return {@value CommonXACMLAuthorizationProfileConstants#DATATYPE_BASE64_BINARY}
*/
protected String getCertificateAttributeDatatype() {
return CommonXACMLAuthorizationProfileConstants.DATATYPE_BASE64_BINARY;
}
/**
* Processes one certificate chain and adds the information to 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 Collection<Attribute> processCertChain(
X509Certificate endEntityCertificate, X509Certificate[] certChain)
throws PIPProcessingException {
if (endEntityCertificate == null || certChain == null
|| certChain.length == 0) {
return null;
}
log.debug("Extracting end-entity certificate attributes");
Set<Attribute> subjectAttributes= new HashSet<Attribute>();
// get and set the subject DN attribute.
String endEntitySubjectDN= endEntityCertificate.getSubjectX500Principal().getName(X500Principal.RFC2253);
Attribute subjectIdAttribute= new Attribute(CommonXACMLAuthorizationProfileConstants.ID_ATTRIBUTE_SUBJECT_ID,
CommonXACMLAuthorizationProfileConstants.DATATYPE_X500_NAME);
subjectIdAttribute.getValues().add(endEntitySubjectDN);
log.debug("subject-id attribute: {}", subjectIdAttribute);
subjectAttributes.add(subjectIdAttribute);
// set the issuer DN attribute.
Attribute subjectIssuerAttribute= new Attribute(CommonXACMLAuthorizationProfileConstants.ID_ATTRIBUTE_SUBJECT_ISSUER,
CommonXACMLAuthorizationProfileConstants.DATATYPE_X500_NAME);
for (int i= 1; i < certChain.length; i++) {
subjectIssuerAttribute.getValues().add(certChain[i].getSubjectX500Principal().getName(X500Principal.RFC2253));
}
log.debug("subject-issuer attribute: {}", subjectIssuerAttribute);
subjectAttributes.add(subjectIssuerAttribute);
if (isVOMSSupportEnabled()) {
Collection<Attribute> vomsAttributes= processVOMS(certChain);
if (vomsAttributes != null) {
subjectAttributes.addAll(vomsAttributes);
}
}
return subjectAttributes;
}
/**
* Processes the VOMS attributes and extract VO related attributes for the
* subject object.
*
* @param certChain
* certificate chain containing the end entity certificate that
* contains the VOMS attribute certificate
*
* @return the attributes extracted from the VOMS attribute certificates or
* <code>null</code> if the cert chain doesn't contain any VOMS AC.
*/
@SuppressWarnings("unchecked")
protected Collection<Attribute> processVOMS(X509Certificate[] certChain) {
log.debug("Extracting VOMS ACs");
List<VOMSAttribute> vomsACs= extractVOMSAttributes(certChain);
if (vomsACs == null || vomsACs.isEmpty()) {
log.debug("No VOMS AC found in cert chain");
return null;
}
Set<Attribute> vomsSubjectAttributes= new HashSet<Attribute>();
// VO
Attribute voAttribute= new Attribute();
voAttribute.setId(CommonXACMLAuthorizationProfileConstants.ID_ATTRIBUTE_VIRTUAL_ORGANIZATION);
voAttribute.setDataType(CommonXACMLAuthorizationProfileConstants.DATATYPE_STRING);
// groups
Attribute primaryGroupAttribute= new Attribute(CommonXACMLAuthorizationProfileConstants.ID_ATTRIBUTE_PRIMARY_GROUP,
CommonXACMLAuthorizationProfileConstants.DATATYPE_STRING);
Attribute primaryRoleAttribute= null;
Attribute groupAttribute= new Attribute(CommonXACMLAuthorizationProfileConstants.ID_ATTRIBUTE_GROUP,
CommonXACMLAuthorizationProfileConstants.DATATYPE_STRING);
// issuer -> HASHTABLE(groupName, roleAttribute);
Hashtable<String, Attribute> issuerRoleAttributeHT= new Hashtable<String, Attribute>();
boolean primaryGroupRole= true;
for (VOMSAttribute vomsAC : vomsACs) {
// VO name
String voName= vomsAC.getVO();
voAttribute.getValues().add(voName);
// extract groups and roles from AC -> FQANs
List<FQAN> fqans= vomsAC.getListOfFQAN();
for (FQAN fqan : fqans) {
// group name
String groupName= fqan.getGroup();
groupAttribute.getValues().add(groupName);
// role name, issuer is group name
String roleName= fqan.getRole();
if (!isNullorNULL(roleName)) {
Attribute roleAttribute= issuerRoleAttributeHT.get(groupName);
if (roleAttribute == null) {
// group didn't issue any role yet, create and add to HT
roleAttribute= new Attribute(CommonXACMLAuthorizationProfileConstants.ID_ATTRIBUTE_ROLE,
CommonXACMLAuthorizationProfileConstants.DATATYPE_STRING,
groupName);
issuerRoleAttributeHT.put(groupName, roleAttribute);
}
roleAttribute.getValues().add(roleName);
}
if (primaryGroupRole) {
primaryGroupAttribute.getValues().add(groupName);
if (!isNullorNULL(roleName)) {
primaryRoleAttribute= new Attribute(CommonXACMLAuthorizationProfileConstants.ID_ATTRIBUTE_PRIMARY_ROLE,
CommonXACMLAuthorizationProfileConstants.DATATYPE_STRING,
groupName);
primaryRoleAttribute.getValues().add(roleName);
}
primaryGroupRole= false;
}
}
}
log.debug("VO attribute: {}", voAttribute);
vomsSubjectAttributes.add(voAttribute);
log.debug("Primary group attribute: {}", primaryGroupAttribute);
vomsSubjectAttributes.add(primaryGroupAttribute);
log.debug("Group attribute: {}", groupAttribute);
vomsSubjectAttributes.add(groupAttribute);
if (primaryRoleAttribute != null) {
log.debug("Primary role attribute: {}", primaryRoleAttribute);
vomsSubjectAttributes.add(primaryRoleAttribute);
}
if (!issuerRoleAttributeHT.isEmpty()) {
Collection<Attribute> roleAttributes= issuerRoleAttributeHT.values();
log.debug("Role attributes: {}", roleAttributes);
vomsSubjectAttributes.addAll(roleAttributes);
}
return vomsSubjectAttributes;
}
/**
* Validate and extract the VOMS ACs from the certificate chain.
*
* @param certChain
* the certificate chain
* @return the list of VOMS ACs or <code>null</code> if the cert chain
* doesn't contain any AC.
*/
@SuppressWarnings("unchecked")
private List<VOMSAttribute> extractVOMSAttributes(
X509Certificate[] certChain) {
VOMSValidator vomsValidator= new VOMSValidator(certChain,
new ACValidator(getCertVerifier()));
vomsValidator.validate();
List<VOMSAttribute> vomsAttributes= vomsValidator.getVOMSAttributes();
if (vomsAttributes == null || vomsAttributes.isEmpty()) {
return null;
}
return vomsAttributes;
}
/**
* 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 encoded
*
* @see #getCertificateAttributeId()
* @see #getCertificateAttributeDatatype()
*/
protected X509Certificate[] extractCertificateChain(Subject subject)
throws PIPProcessingException {
List<X509Certificate> certChain= new ArrayList<X509Certificate>();
for (Attribute attribute : subject.getAttributes()) {
// check attribute Id and datatype
if (Strings.safeEquals(attribute.getId(),
getCertificateAttributeId())
&& Strings.safeEquals(attribute.getDataType(),
getCertificateAttributeDatatype())) {
// each value is a base64 encoded DER certificate string
for (Object value : attribute.getValues()) {
byte[] derBytes= Base64.decode((String) value);
BufferedInputStream bis= new BufferedInputStream(new ByteArrayInputStream(derBytes));
try {
X509Certificate x509= (X509Certificate) cf_.generateCertificate(bis);
// log.trace("X.509 cert {} decoded ",
// x509.getSubjectX500Principal().getName());
certChain.add(x509);
} catch (CertificateException e) {
String error= "Fails to decode X.509 certificate: "
+ e.getMessage();
log.error(error);
throw new PIPProcessingException(error, e);
}
}
}
}
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.toArray(new X509Certificate[] {});
}
/** the "NULL" string */
private static final String NULL_STRING= "NULL";
/**
* Returns <code>true</code> iff the str is <code>null</code> or case
* insensitive equals to the "NULL" string.
*
* @param str
* string to check
* @return <code>true</code> iff the str is <code>null</code> or case
* insensitive equals to the "NULL" string.
*/
private boolean isNullorNULL(String str) {
if (str == null)
return true;
else
return NULL_STRING.equalsIgnoreCase(str);
}
}