/* * JOSSO: Java Open Single Sign-On * * Copyright 2004-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.josso.auth.scheme; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.josso.auth.CertificatePrincipal; import org.josso.auth.Credential; import org.josso.auth.CredentialProvider; import org.josso.auth.exceptions.SSOAuthenticationException; import org.josso.auth.scheme.validation.X509CertificateValidationException; import org.josso.auth.scheme.validation.X509CertificateValidator; import sun.security.util.DerValue; 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.ArrayList; import java.util.HashMap; import java.util.List; import java.util.StringTokenizer; import javax.security.auth.x500.X500Principal; /** * Certificate-based Authentication Scheme. * * @author <a href="mailto:gbrigand@josso.org">Gianluca Brigandi</a> * @version CVS $Id: X509CertificateAuthScheme.java 568 2008-07-31 18:39:20Z sgonzalez $ * * @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("strong-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; } } } 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; } // Find UID // (We could just use getUID() to authenticate user // without previous validation against known certificates?) _uid = getUID(); 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 vaue 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; } /*------------------------------------------------------------ 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; } }