Commit c2308294 authored by Dave Cridland's avatar Dave Cridland

Merge pull request #249 from victorqhong-bah/cert-identity-mapping

Allow users to specifiy custom classes to map credentials from certificates
parents c145141e 3bc97e95
...@@ -77,7 +77,7 @@ public class ClearspaceX509TrustManager implements X509TrustManager { ...@@ -77,7 +77,7 @@ public class ClearspaceX509TrustManager implements X509TrustManager {
if (verify) { if (verify) {
int nSize = x509Certificates.length; int nSize = x509Certificates.length;
List<String> peerIdentities = CertificateManager.getPeerIdentities(x509Certificates[0]); List<String> peerIdentities = CertificateManager.getServerIdentities(x509Certificates[0]);
if (getBooleanProperty("clearspace.certificate.verify.chain", true)) { if (getBooleanProperty("clearspace.certificate.verify.chain", true)) {
// Working down the chain, for every certificate in the chain, // Working down the chain, for every certificate in the chain,
......
...@@ -189,7 +189,7 @@ public class ClientTrustManager implements X509TrustManager { ...@@ -189,7 +189,7 @@ public class ClientTrustManager implements X509TrustManager {
if (verify) { if (verify) {
int nSize = x509Certificates.length; int nSize = x509Certificates.length;
List<String> peerIdentities = CertificateManager.getPeerIdentities(x509Certificates[0]); List<String> peerIdentities = CertificateManager.getClientIdentities(x509Certificates[0]);
if (JiveGlobals.getBooleanProperty("xmpp.client.certificate.verify.chain", true)) { if (JiveGlobals.getBooleanProperty("xmpp.client.certificate.verify.chain", true)) {
// Working down the chain, for every certificate in the chain, // Working down the chain, for every certificate in the chain,
......
...@@ -590,7 +590,7 @@ public class SASLAuthentication { ...@@ -590,7 +590,7 @@ public class SASLAuthentication {
authenticationFailed(session, Failure.NOT_AUTHORIZED); authenticationFailed(session, Failure.NOT_AUTHORIZED);
return Status.failed; return Status.failed;
} }
principals.addAll(CertificateManager.getPeerIdentities((X509Certificate)trusted)); principals.addAll(CertificateManager.getClientIdentities((X509Certificate)trusted));
if(principals.size() == 1) { if(principals.size() == 1) {
principal = principals.get(0); principal = principals.get(0);
...@@ -640,7 +640,7 @@ public class SASLAuthentication { ...@@ -640,7 +640,7 @@ public class SASLAuthentication {
} }
public static boolean verifyCertificate(X509Certificate trustedCert, String hostname) { public static boolean verifyCertificate(X509Certificate trustedCert, String hostname) {
for (String identity : CertificateManager.getPeerIdentities(trustedCert)) { for (String identity : CertificateManager.getServerIdentities(trustedCert)) {
// Verify that either the identity is the same as the hostname, or for wildcarded // Verify that either the identity is the same as the hostname, or for wildcarded
// identities that the hostname ends with .domainspecified or -is- domainspecified. // identities that the hostname ends with .domainspecified or -is- domainspecified.
if ((identity.startsWith("*.") if ((identity.startsWith("*.")
......
...@@ -49,7 +49,6 @@ import java.security.cert.CertificateFactory; ...@@ -49,7 +49,6 @@ import java.security.cert.CertificateFactory;
import java.security.cert.CertificateParsingException; import java.security.cert.CertificateParsingException;
import java.security.cert.CollectionCertStoreParameters; import java.security.cert.CollectionCertStoreParameters;
import java.security.cert.PKIXBuilderParameters; import java.security.cert.PKIXBuilderParameters;
import java.security.cert.TrustAnchor;
import java.security.cert.X509CertSelector; import java.security.cert.X509CertSelector;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.util.ArrayList; import java.util.ArrayList;
...@@ -58,11 +57,11 @@ import java.util.Collections; ...@@ -58,11 +57,11 @@ import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.Enumeration; import java.util.Enumeration;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable; import java.util.Hashtable;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.StringTokenizer;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
...@@ -70,7 +69,6 @@ import java.util.regex.Pattern; ...@@ -70,7 +69,6 @@ import java.util.regex.Pattern;
import org.bouncycastle.asn1.ASN1Encodable; import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1InputStream; import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1TaggedObject; import org.bouncycastle.asn1.ASN1TaggedObject;
import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.DERSequence; import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.asn1.ASN1ObjectIdentifier; import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.DERObjectIdentifier; import org.bouncycastle.asn1.DERObjectIdentifier;
...@@ -87,11 +85,13 @@ import org.bouncycastle.jce.provider.BouncyCastleProvider; ...@@ -87,11 +85,13 @@ import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMParser; import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.PEMDecryptorProvider; import org.bouncycastle.openssl.PEMDecryptorProvider;
import org.bouncycastle.openssl.PEMEncryptedKeyPair; import org.bouncycastle.openssl.PEMEncryptedKeyPair;
import org.bouncycastle.openssl.PasswordFinder;
import org.bouncycastle.openssl.PEMKeyPair; import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder; import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder;
import org.bouncycastle.x509.X509V3CertificateGenerator; import org.bouncycastle.x509.X509V3CertificateGenerator;
import org.jivesoftware.util.cert.CertificateIdentityMapping;
import org.jivesoftware.util.cert.CNCertificateIdentityMapping;
import org.jivesoftware.util.cert.SANCertificateIdentityMapping;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
...@@ -105,9 +105,6 @@ public class CertificateManager { ...@@ -105,9 +105,6 @@ public class CertificateManager {
private static final Logger Log = LoggerFactory.getLogger(CertificateManager.class); private static final Logger Log = LoggerFactory.getLogger(CertificateManager.class);
private static final String OTHERNAME_XMPP_OID = "1.3.6.1.5.5.7.8.5";
private static Pattern cnPattern = Pattern.compile("(?i)(cn=)([^,]*)");
private static Pattern valuesPattern = Pattern.compile("(?i)(=)([^,]*)"); private static Pattern valuesPattern = Pattern.compile("(?i)(=)([^,]*)");
private static Provider provider = new BouncyCastleProvider(); private static Provider provider = new BouncyCastleProvider();
...@@ -119,9 +116,60 @@ public class CertificateManager { ...@@ -119,9 +116,60 @@ public class CertificateManager {
private static List<CertificateEventListener> listeners = new CopyOnWriteArrayList<CertificateEventListener>(); private static List<CertificateEventListener> listeners = new CopyOnWriteArrayList<CertificateEventListener>();
private static List<CertificateIdentityMapping> serverCertMapping = new ArrayList<CertificateIdentityMapping>();
private static List<CertificateIdentityMapping> clientCertMapping = new ArrayList<CertificateIdentityMapping>();
static { static {
// Add the BC provider to the list of security providers // Add the BC provider to the list of security providers
Security.addProvider(provider); Security.addProvider(provider);
String serverCertIdentityMapList = JiveGlobals.getProperty("provider.serverCertIdentityMap.classList");
if (serverCertIdentityMapList != null) {
StringTokenizer st = new StringTokenizer(serverCertIdentityMapList, " ,\t\n\r\f");
while (st.hasMoreTokens()) {
String s_provider = st.nextToken();
try {
Class c_provider = ClassUtils.forName(s_provider);
CertificateIdentityMapping provider =
(CertificateIdentityMapping)(c_provider.newInstance());
Log.debug("CertificateManager: Loaded server identity mapping " + s_provider);
serverCertMapping.add(provider);
}
catch (Exception e) {
Log.error("CertificateManager: Error loading CertificateIdentityMapping: " + s_provider + "\n" + e);
}
}
}
if (serverCertMapping.isEmpty()) {
Log.debug("CertificateManager: No server CertificateIdentityMapping's found. Loading default mappings");
serverCertMapping.add(new SANCertificateIdentityMapping());
serverCertMapping.add(new CNCertificateIdentityMapping());
}
String clientCertMapList = JiveGlobals.getProperty("provider.clientCertIdentityMap.classList");
if (clientCertMapList != null) {
StringTokenizer st = new StringTokenizer(clientCertMapList, " ,\t\n\r\f");
while (st.hasMoreTokens()) {
String s_provider = st.nextToken();
try {
Class c_provider = ClassUtils.forName(s_provider);
CertificateIdentityMapping provider =
(CertificateIdentityMapping)(c_provider.newInstance());
Log.debug("CertificateManager: Loaded client identity mapping " + s_provider);
clientCertMapping.add(provider);
}
catch (Exception e) {
Log.error("CertificateManager: Error loading CertificateIdentityMapping: " + s_provider + "\n" + e);
}
}
}
if (clientCertMapping.isEmpty()) {
Log.debug("CertificateManager: No client CertificateIdentityMapping's found. Loading default mappings");
clientCertMapping.add(new CNCertificateIdentityMapping());
}
} }
/** /**
...@@ -336,111 +384,52 @@ public class CertificateManager { ...@@ -336,111 +384,52 @@ public class CertificateManager {
} }
/** /**
* Returns the identities of the remote server as defined in the specified certificate. The * Returns the identities of the remote client as defined in the specified certificate. The
* identities are defined in the subjectDN of the certificate and it can also be defined in * identities are mapped by the classes in the "provider.clientCertIdentityMap.classList" property.
* the subjectAltName extensions of type "xmpp". When the extension is being used then the * By default, the subjectDN of the certificate is used.
* identities defined in the extension are going to be returned. Otherwise, the value stored in
* the subjectDN is returned.
* *
* @param x509Certificate the certificate the holds the identities of the remote server. * @param x509Certificate the certificate the holds the identities of the remote server.
* @return the identities of the remote server as defined in the specified certificate. * @return the identities of the remote client as defined in the specified certificate.
*/ */
public static List<String> getPeerIdentities(X509Certificate x509Certificate) { public static List<String> getClientIdentities(X509Certificate x509Certificate) {
// Look the identity in the subjectAltName extension if available
List<String> names = getSubjectAlternativeNames(x509Certificate); List<String> names = new ArrayList<String>();
if (names.isEmpty()) { for (CertificateIdentityMapping mapping : clientCertMapping) {
String name = x509Certificate.getSubjectDN().getName(); List<String> identities = mapping.mapIdentity(x509Certificate);
Matcher matcher = cnPattern.matcher(name); Log.debug("CertificateManager: " + mapping.name() + " returned " + identities.toString());
// Create an array with the detected identities if (!identities.isEmpty()) {
names = new ArrayList<String>(); names.addAll(identities);
while (matcher.find()) { break;
names.add(matcher.group(2));
} }
} }
return names; return names;
} }
/** /**
* Returns the JID representation of an XMPP entity contained as a SubjectAltName extension * Returns the identities of the remote server as defined in the specified certificate. The
* in the certificate. If none was found then return <tt>null</tt>. * identities are mapped by the classes in the "provider.serverCertIdentityMap.classList" property.
* By default, the identities are defined in the subjectDN of the certificate and it can also be
* defined in the subjectAltName extensions of type "xmpp". When the extension is being used then the
* identities defined in the extension are going to be returned. Otherwise, the value stored in
* the subjectDN is returned.
* *
* @param certificate the certificate presented by the remote entity. * @param x509Certificate the certificate the holds the identities of the remote server.
* @return the JID representation of an XMPP entity contained as a SubjectAltName extension * @return the identities of the remote server as defined in the specified certificate.
* in the certificate. If none was found then return <tt>null</tt>.
*/ */
private static List<String> getSubjectAlternativeNames(X509Certificate certificate) { public static List<String> getServerIdentities(X509Certificate x509Certificate) {
List<String> identities = new ArrayList<String>();
try { List<String> names = new ArrayList<String>();
Collection<List<?>> altNames = certificate.getSubjectAlternativeNames(); for (CertificateIdentityMapping mapping : serverCertMapping) {
// Check that the certificate includes the SubjectAltName extension List<String> identities = mapping.mapIdentity(x509Certificate);
if (altNames == null) { Log.debug("CertificateManager: " + mapping.name() + " returned " + identities.toString());
return Collections.emptyList(); if (!identities.isEmpty()) {
} names.addAll(identities);
// Use the type OtherName to search for the certified server name break;
for (List<?> item : altNames) {
Integer type = (Integer) item.get(0);
if (type == 0) {
// Type OtherName found so return the associated value
try {
// Value is encoded using ASN.1 so decode it to get the server's identity
ASN1InputStream decoder = new ASN1InputStream((byte[]) item.get(1));
Object object = decoder.readObject();
ASN1Sequence otherNameSeq = null;
if (object != null && object instanceof ASN1Sequence) {
otherNameSeq = (ASN1Sequence) object;
} else {
continue;
} }
// Check the object identifier
ASN1ObjectIdentifier objectId = (ASN1ObjectIdentifier) otherNameSeq.getObjectAt(0);
Log.debug("Parsing otherName for subject alternative names: " + objectId.toString() );
if ( !OTHERNAME_XMPP_OID.equals(objectId.getId())) {
// Not a XMPP otherName
Log.debug("Ignoring non-XMPP otherName, " + objectId.getId());
continue;
} }
// Get identity string return names;
try {
final String identity;
ASN1Encodable o = otherNameSeq.getObjectAt(1);
if (o instanceof DERTaggedObject) {
ASN1TaggedObject ato = DERTaggedObject.getInstance(o);
Log.debug("... processing DERTaggedObject: " + ato.toString());
// TODO: there's bound to be a better way...
identity = ato.toString().substring(ato.toString().lastIndexOf(']')+1).trim();
} else {
DERUTF8String derStr = DERUTF8String.getInstance(o);
identity = derStr.getString();
}
if (identity != null && identity.length() > 0) {
// Add the decoded server name to the list of identities
identities.add(identity);
}
decoder.close();
} catch (IllegalArgumentException ex) {
// OF-517: othername formats are extensible. If we don't recognize the format, skip it.
Log.debug("Cannot parse altName, likely because of unknown record format.", ex);
}
}
catch (UnsupportedEncodingException e) {
// Ignore
}
catch (IOException e) {
// Ignore
}
catch (Exception e) {
Log.error("Error decoding subjectAltName", e);
}
}
// Other types are not applicable for XMPP, so silently ignore them
}
}
catch (CertificateParsingException e) {
Log.error("Error parsing SubjectAltName in certificate: " + certificate.getSubjectDN(), e);
}
return identities;
} }
/** /**
...@@ -501,7 +490,7 @@ public class CertificateManager { ...@@ -501,7 +490,7 @@ public class CertificateManager {
} }
else { else {
// Only accept certified domains that match the specified domain // Only accept certified domains that match the specified domain
for (String identity : getPeerIdentities(certificate)) { for (String identity : getServerIdentities(certificate)) {
if (identity.endsWith(domain) && certificate.getPublicKey().getAlgorithm().equals(algorithm)) { if (identity.endsWith(domain) && certificate.getPublicKey().getAlgorithm().equals(algorithm)) {
result = true; result = true;
} }
......
package org.jivesoftware.util.cert;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Certificate identity mapping that uses the CommonName as the
* identity credentials
*
* @author Victor Hong
*
*/
public class CNCertificateIdentityMapping implements CertificateIdentityMapping {
private static Pattern cnPattern = Pattern.compile("(?i)(cn=)([^,]*)");
/**
* Maps certificate CommonName as identity credentials
*
* @param certificate
* @return
*/
@Override
public List<String> mapIdentity(X509Certificate certificate) {
String name = certificate.getSubjectDN().getName();
Matcher matcher = cnPattern.matcher(name);
// Create an array with the detected identities
List<String> names = new ArrayList<String>();
while (matcher.find()) {
names.add(matcher.group(2));
}
return names;
}
/**
* Returns the short name of mapping
*
* @return The short name of the mapping
*/
@Override
public String name() {
return "Common Name Mapping";
}
}
package org.jivesoftware.util.cert;
import java.security.cert.X509Certificate;
import java.util.List;
/**
* This is the interface used to map identity credentials from certificates.
* Users may implement this class to map authentication credentials (i.e. usernames)
* from certificate data (e.g. CommonName or SubjectAlternativeName)
*
* @author Victor Hong
*
*/
public interface CertificateIdentityMapping {
/**
* Maps identities from X509Certificates
*
* @param certificate The certificate from which to map identities
* @return A list of identities mapped from the certificate
*/
List<String> mapIdentity(X509Certificate certificate);
/**
* Returns the short name of the mapping
*
* @return The short name of the mapping
*/
String name();
}
package org.jivesoftware.util.cert;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.ASN1TaggedObject;
import org.bouncycastle.asn1.DERTaggedObject;
import org.bouncycastle.asn1.DERUTF8String;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Certificate identity mapping that uses XMPP-OtherName SubjectAlternativeName
* as the identity credentials
*
* @author Victor Hong
*
*/
public class SANCertificateIdentityMapping implements CertificateIdentityMapping {
private static final Logger Log = LoggerFactory.getLogger(SANCertificateIdentityMapping.class);
private static final String OTHERNAME_XMPP_OID = "1.3.6.1.5.5.7.8.5";
/**
* Returns the JID representation of an XMPP entity contained as a SubjectAltName extension
* in the certificate. If none was found then return an empty list.
*
* @param certificate the certificate presented by the remote entity.
* @return the JID representation of an XMPP entity contained as a SubjectAltName extension
* in the certificate. If none was found then return an empty list.
*/
@Override
public List<String> mapIdentity(X509Certificate certificate) {
List<String> identities = new ArrayList<String>();
try {
Collection<List<?>> altNames = certificate.getSubjectAlternativeNames();
// Check that the certificate includes the SubjectAltName extension
if (altNames == null) {
return Collections.emptyList();
}
// Use the type OtherName to search for the certified server name
for (List<?> item : altNames) {
Integer type = (Integer) item.get(0);
if (type == 0) {
// Type OtherName found so return the associated value
try {
// Value is encoded using ASN.1 so decode it to get the server's identity
ASN1InputStream decoder = new ASN1InputStream((byte[]) item.get(1));
Object object = decoder.readObject();
ASN1Sequence otherNameSeq = null;
if (object != null && object instanceof ASN1Sequence) {
otherNameSeq = (ASN1Sequence) object;
} else {
continue;
}
// Check the object identifier
ASN1ObjectIdentifier objectId = (ASN1ObjectIdentifier) otherNameSeq.getObjectAt(0);
Log.debug("Parsing otherName for subject alternative names: " + objectId.toString() );
if ( !OTHERNAME_XMPP_OID.equals(objectId.getId())) {
// Not a XMPP otherName
Log.debug("Ignoring non-XMPP otherName, " + objectId.getId());
continue;
}
// Get identity string
try {
final String identity;
ASN1Encodable o = otherNameSeq.getObjectAt(1);
if (o instanceof DERTaggedObject) {
ASN1TaggedObject ato = DERTaggedObject.getInstance(o);
Log.debug("... processing DERTaggedObject: " + ato.toString());
// TODO: there's bound to be a better way...
identity = ato.toString().substring(ato.toString().lastIndexOf(']')+1).trim();
} else {
DERUTF8String derStr = DERUTF8String.getInstance(o);
identity = derStr.getString();
}
if (identity != null && identity.length() > 0) {
// Add the decoded server name to the list of identities
identities.add(identity);
}
decoder.close();
} catch (IllegalArgumentException ex) {
// OF-517: othername formats are extensible. If we don't recognize the format, skip it.
Log.debug("Cannot parse altName, likely because of unknown record format.", ex);
}
}
catch (UnsupportedEncodingException e) {
// Ignore
}
catch (IOException e) {
// Ignore
}
catch (Exception e) {
Log.error("Error decoding subjectAltName", e);
}
}
// Other types are not applicable for XMPP, so silently ignore them
}
}
catch (CertificateParsingException e) {
Log.error("Error parsing SubjectAltName in certificate: " + certificate.getSubjectDN(), e);
}
return identities;
}
/**
* Returns the short name of mapping
*
* @return The short name of the mapping
*/
@Override
public String name() {
return "Subject Alternative Name Mapping";
}
}
...@@ -210,7 +210,7 @@ ...@@ -210,7 +210,7 @@
String a = (String) aliases.nextElement(); String a = (String) aliases.nextElement();
X509Certificate c = (X509Certificate) keyStore.getCertificate(a); X509Certificate c = (X509Certificate) keyStore.getCertificate(a);
StringBuffer identities = new StringBuffer(); StringBuffer identities = new StringBuffer();
for (String identity : CertificateManager.getPeerIdentities(c)) { for (String identity : CertificateManager.getServerIdentities(c)) {
identities.append(identity).append(", "); identities.append(identity).append(", ");
} }
if (identities.length() > 0) { if (identities.length() > 0) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment