org.atricore.idbus.capabilities.clientcertauthn.X509CertificateAuthScheme.java Source code

Java tutorial

Introduction

Here is the source code for org.atricore.idbus.capabilities.clientcertauthn.X509CertificateAuthScheme.java

Source

/*
 * Atricore IDBus
 *
 * Copyright (c) 2009, Atricore Inc.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.atricore.idbus.capabilities.clientcertauthn;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.atricore.idbus.capabilities.clientcertauthn.validation.X509CertificateValidationException;
import org.atricore.idbus.capabilities.clientcertauthn.validation.X509CertificateValidator;
import org.atricore.idbus.kernel.main.authn.scheme.AbstractAuthenticationScheme;
import org.atricore.idbus.kernel.main.authn.exceptions.SSOAuthenticationException;
import org.atricore.idbus.kernel.main.authn.CredentialProvider;
import org.atricore.idbus.kernel.main.authn.Credential;
import sun.security.util.DerValue;

import javax.security.auth.x500.X500Principal;
import java.io.ByteArrayInputStream;
import java.security.Principal;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.*;

/**
 * Certificate-based Authentication Scheme.
 *
 * @author <a href="mailto:gbrigand@josso.org">Gianluca Brigandi</a>
 * @version CVS $Id: X509CertificateAuthScheme.java 1040 2009-03-05 00:56:52Z gbrigand $
 *
 * @org.apache.xbean.XBean element="strong-auth-scheme"
 */
public class X509CertificateAuthScheme extends AbstractAuthenticationScheme {
    private static final Log logger = LogFactory.getLog(X509CertificateAuthScheme.class);

    /* Component Properties */
    private String _uidOID;

    /* User UID */
    private String _uid;

    /* X509 Certificate validators */
    private List<X509CertificateValidator> _validators;

    public X509CertificateAuthScheme() {
        this.setName("client-cert-authentication");
    }

    /**
     * @throws SSOAuthenticationException
     */
    public boolean authenticate() throws SSOAuthenticationException {

        setAuthenticated(false);

        //String username = getUsername(_inputCredentials);
        X509Certificate x509Certificate = getX509Certificate(_inputCredentials);

        // Check if all credentials are present.
        if (x509Certificate == null) {

            if (logger.isDebugEnabled())
                logger.debug("X.509 Certificate not provided");

            // We don't support empty values !
            return false;
        }

        // validate certificate
        if (_validators != null) {
            for (X509CertificateValidator validator : _validators) {
                try {
                    validator.validate(x509Certificate);
                } catch (X509CertificateValidationException e) {
                    logger.error("Certificate is not valid!", e);
                    return false;
                }
            }
        }

        // TODO STRONG-AUTH: Local certificate storage should be optional
        List<X509Certificate> knownX509Certificates = getX509Certificates(getKnownCredentials());

        StringBuffer buf = new StringBuffer("\n\tSupplied Credential: ");
        buf.append(x509Certificate.getSerialNumber().toString(16));
        buf.append("\n\t\t");
        buf.append(x509Certificate.getSubjectX500Principal().getName());
        buf.append("\n\n\tExisting Credentials: ");
        for (int i = 0; i < knownX509Certificates.size(); i++) {
            X509Certificate knownX509Certificate = knownX509Certificates.get(i);
            buf.append(i + 1);
            buf.append("\n\t\t");
            buf.append(knownX509Certificate.getSerialNumber().toString(16));
            buf.append("\n\t\t");
            buf.append(knownX509Certificate.getSubjectX500Principal().getName());
            buf.append("\n");
        }

        logger.debug(buf.toString());

        // Validate user identity ...
        boolean valid = false;
        X509Certificate validCertificate = null;
        for (X509Certificate knownX509Certificate : knownX509Certificates) {
            if (validateX509Certificate(x509Certificate, knownX509Certificate)) {
                validCertificate = knownX509Certificate;
                break;
            }
        }

        if (validCertificate == null) {
            return false;
        }

        // TODO STRONG-AUTH resolve User ID, take it from the certificate ..

        // Find UID
        // (We could just use getUID() to authenticate user
        // without previous validation against known certificates?)
        _uid = resolveUID(validCertificate);
        if (_uid == null) {
            return false;
        }

        if (logger.isDebugEnabled())
            logger.debug(
                    "[authenticate()], Principal authenticated : " + x509Certificate.getSubjectX500Principal());

        // We have successfully authenticated this user.
        setAuthenticated(true);
        return true;
    }

    /**
     * Create a X.509 Certificate Credential Provider instance
     *
     * @return
     */
    protected CredentialProvider doMakeCredentialProvider() {
        return new X509CertificateCredentialProvider();
    }

    private X509Certificate buildX509Certificate(byte[] binaryCert) {
        X509Certificate cert = null;

        try {
            ByteArrayInputStream bais = new ByteArrayInputStream(binaryCert);
            CertificateFactory cf = CertificateFactory.getInstance("X.509");

            cert = (X509Certificate) cf.generateCertificate(bais);

            if (logger.isDebugEnabled())
                logger.debug("Building X.509 certificate result :\n " + cert);

        } catch (CertificateException ce) {
            logger.error("Error instantiating X.509 Certificate", ce);
        }

        return cert;
    }

    private X509Certificate buildX509Certificate(String plainCert) {
        return buildX509Certificate(plainCert.getBytes());
    }

    /**
     * Returns the private input credentials.
     *
     * @return the private input credentials
     */
    public Credential[] getPrivateCredentials() {
        Credential c = getX509CertificateCredential(_inputCredentials);

        if (c == null)
            return new Credential[0];

        Credential[] r = { c };
        return r;
    }

    /**
     * Returns the public input credentials.
     *
     * @return the public input credentials
     */
    public Credential[] getPublicCredentials() {
        Credential c = getX509CertificateCredential(_inputCredentials);

        if (c == null)
            return new Credential[0];

        Credential[] r = { c };
        return r;
    }

    /**
     * Instantiates a Principal for the user X509 Certificate.
     * Used as the primary key to obtain the known credentials from the associated
     * store.
     *
     * @return the Principal associated with the input credentials.
     */
    public Principal getPrincipal() {
        if (_uid != null) {
            return new CertificatePrincipal(_uid, getX509Certificate(_inputCredentials));
        } else {
            return getPrincipal(_inputCredentials);
        }
    }

    /**
     * Instantiates a Principal for the user X509 Certificate.
     * Used as the primary key to obtain the known credentials from the associated
     * store.
     *
     * @return the Principal associated with the input credentials.
     */
    public Principal getPrincipal(Credential[] credentials) {
        X509Certificate certificate = getX509Certificate(credentials);
        X500Principal p = certificate.getSubjectX500Principal();
        CertificatePrincipal targetPrincipal = null;

        if (_uidOID == null) {
            HashMap compoundName = parseCompoundName(p.getName());

            // Extract from the Distinguished Name (DN) only the Common Name (CN) since its
            // the store who sets the root naming context to be used based on the
            // store configuration.
            String cn = (String) compoundName.get("cn");

            if (cn == null)
                logger.error("Invalid Subject DN. Cannot create Principal : " + p.getName());

            targetPrincipal = new CertificatePrincipal(cn, certificate);
        } else {
            try {
                byte[] oidValue = getOIDBitStringValueFromCert(certificate, _uidOID);

                if (oidValue == null)
                    logger.error(
                            "No value obtained for OID " + _uidOID + ". Cannot create Principal : " + p.getName());

                // TODO: what if the OID is a compound value?
                targetPrincipal = new CertificatePrincipal(new String(oidValue), certificate);
            } catch (Exception e) {
                logger.error("Fatal error obtaining UID value using OID " + _uidOID + ". Cannot create Principal : "
                        + p.getName(), e);
            }
        }

        return targetPrincipal;
    }

    /**
     * Gets the credential that represents an X.509 Certificate.
     */
    protected X509CertificateCredential getX509CertificateCredential(Credential[] credentials) {

        for (int i = 0; i < credentials.length; i++) {
            if (credentials[i] instanceof X509CertificateCredential) {
                return (X509CertificateCredential) credentials[i];
            }
        }
        return null;
    }

    /**
     * Gets the list of credentials that represent X.509 Certificates.
     */
    protected List<X509CertificateCredential> getX509CertificateCredentials(Credential[] credentials) {
        List<X509CertificateCredential> certCredentials = new ArrayList<X509CertificateCredential>();
        for (int i = 0; i < credentials.length; i++) {
            if (credentials[i] instanceof X509CertificateCredential) {
                certCredentials.add((X509CertificateCredential) credentials[i]);
            }
        }
        return certCredentials;
    }

    /**
     * Gets the X.509 certificate from the supplied credentials
     *
     * @param credentials
     */
    protected X509Certificate getX509Certificate(Credential[] credentials) {
        X509CertificateCredential c = getX509CertificateCredential(credentials);
        if (c == null)
            return null;

        return (X509Certificate) c.getValue();
    }

    /**
     * Gets the list of X.509 certificates from the supplied credentials
     *
     * @param credentials
     */
    protected List<X509Certificate> getX509Certificates(Credential[] credentials) {
        List<X509Certificate> certs = new ArrayList<X509Certificate>();
        List<X509CertificateCredential> certCredentials = getX509CertificateCredentials(credentials);
        for (X509CertificateCredential c : certCredentials) {
            certs.add((X509Certificate) c.getValue());
        }
        return certs;
    }

    /**
     * This method validates the input x509 certificate agaist the expected x509 certificate.
     *
     * @param inputX509Certificate    the X.509 Certificate supplied on authentication.
     * @param expectedX509Certificate the actual X.509 Certificate
     * @return true if the certificates match or false otherwise.
     */
    protected boolean validateX509Certificate(X509Certificate inputX509Certificate,
            X509Certificate expectedX509Certificate) {

        if (inputX509Certificate == null && expectedX509Certificate == null)
            return false;

        return inputX509Certificate.equals(expectedX509Certificate);
    }

    /**
     * Parses a Compound name
     * (ie. CN=Java Duke, OU=Java Software Division, O=Sun Microsystems Inc, C=US) and
     * builds a HashMap object with key-value pairs.
     *
     * @param s a string containing the compound name to be parsed
     * @return a HashMap object built from the parsed key-value pairs
     * @throws IllegalArgumentException if the compound name
     *                                  is invalid
     */
    private HashMap parseCompoundName(String s) {

        String valArray[] = null;

        if (s == null) {
            throw new IllegalArgumentException();
        }
        HashMap hm = new HashMap();

        // Escape characters noticed, so use "extended/escaped parser"
        if ((s.indexOf("\"") > 0) || (s.indexOf("\\") > 0)) {
            StringBuffer sb = new StringBuffer(s);
            boolean escaped = false;
            StringBuffer buff = new StringBuffer();
            String key = "";
            String value = "";
            for (int i = 0; i < sb.length(); i++) {
                // Quotes are begin/end, so keep a flag of escape-state
                if ('"' == sb.charAt(i)) {
                    if (escaped) {
                        escaped = false;
                        continue;
                    } else {
                        escaped = true;
                        continue;
                    }

                    // Single-character escape/advance
                    // but check the length, too.
                } else if ('\\' == sb.charAt(i)) {
                    i++;
                    if (i >= sb.length()) {
                        break;
                    }

                    // Split on '=' between key/value
                } else if ('=' == sb.charAt(i)) {
                    key = buff.toString();
                    buff = new StringBuffer();
                    continue;

                    // We've reached a valid delimiter, as long as we're not
                    // still reading 'escaped' data
                } else if ((',' == sb.charAt(i)) && (!escaped)) {
                    value = buff.toString();
                    buff = new StringBuffer();

                    key = key.trim().toLowerCase();
                    value = value.trim();
                    hm.put(key, value);

                    continue;
                }
                buff.append(sb.charAt(i));
            } // for...

            // And the last one...
            value = buff.toString();
            key = key.trim().toLowerCase();
            value = value.trim();
            hm.put(key, value);

        } else { // Otherwise, no (known) escape characters, so continue on with
            // the faster parse.
            StringTokenizer st = new StringTokenizer(s, ",");
            while (st.hasMoreTokens()) {
                String pair = (String) st.nextToken();
                int pos = pair.indexOf('=');
                if (pos == -1) {
                    // XXX
                    // should give more detail about the illegal argument
                    throw new IllegalArgumentException();
                }
                String key = pair.substring(0, pos).trim().toLowerCase();
                String val = pair.substring(pos + 1, pair.length()).trim();
                hm.put(key, val);
            }
        }

        return hm;
    }

    private byte[] getOIDBitStringValueFromCert(X509Certificate cert, String oid) throws Exception {

        byte[] derEncodedValue = cert.getExtensionValue(oid);
        byte[] extensionValue = null;

        DerValue dervalue = new DerValue(derEncodedValue);
        if (dervalue == null) {
            throw new IllegalArgumentException("extension not found for OID : " + oid);
        }
        if (dervalue.tag != DerValue.tag_BitString) {
            throw new IllegalArgumentException("extension value for OID not of type BIT_STRING: " + oid);
        }

        extensionValue = dervalue.getBitString();

        byte extensionValueBytes[] = new byte[extensionValue.length - 2];

        System.arraycopy(extensionValue, 2, extensionValueBytes, 0, extensionValueBytes.length);

        return extensionValueBytes;
    }

    protected String resolveUID(X509Certificate cert) throws SSOAuthenticationException {
        try {

            // If CN is used, UID is CN

            // If DN is used, we need to resolve it using the credentials store

            // If Certificate is used, we need to resolve it using the credential store

            // If Email is used, we need to resolve it using the credential store

            Principal dn = cert.getSubjectDN();

            java.util.Collection an = (java.util.Collection) cert.getSubjectAlternativeNames();

            X500Principal x500 = cert.getSubjectX500Principal();

            return null;
        } catch (Exception e) {
            throw new SSOAuthenticationException(e);
        }

    }

    /*------------------------------------------------------------ Properties
        
    /**
     * Sets the OID for the UID
     */
    public void setUidOID(String uidOID) {
        _uidOID = uidOID;
    }

    /**
     * Obtains the UID OID
     */
    public String getUidOID() {
        return _uidOID;
    }

    public List<X509CertificateValidator> getValidators() {
        return _validators;
    }

    public void setValidators(List<X509CertificateValidator> validators) {
        _validators = validators;
    }

}