Commit 57b37b11 authored by Victor Hong's avatar Victor Hong

Added the ability to use user-defined classes to map identities from

certificates.
parent bc19c579
......@@ -49,7 +49,6 @@ import java.security.cert.CertificateFactory;
import java.security.cert.CertificateParsingException;
import java.security.cert.CollectionCertStoreParameters;
import java.security.cert.PKIXBuilderParameters;
import java.security.cert.TrustAnchor;
import java.security.cert.X509CertSelector;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
......@@ -58,11 +57,11 @@ import java.util.Collections;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
......@@ -70,7 +69,6 @@ import java.util.regex.Pattern;
import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1TaggedObject;
import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.DERObjectIdentifier;
......@@ -87,11 +85,13 @@ import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.PEMDecryptorProvider;
import org.bouncycastle.openssl.PEMEncryptedKeyPair;
import org.bouncycastle.openssl.PasswordFinder;
import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder;
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.LoggerFactory;
......@@ -105,9 +105,6 @@ public class CertificateManager {
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 Provider provider = new BouncyCastleProvider();
......@@ -119,9 +116,35 @@ public class CertificateManager {
private static List<CertificateEventListener> listeners = new CopyOnWriteArrayList<CertificateEventListener>();
private static List<CertificateIdentityMapping> certIdentityMapping = new ArrayList<CertificateIdentityMapping>();
static {
// Add the BC provider to the list of security providers
Security.addProvider(provider);
String classList = JiveGlobals.getProperty("provider.certIdentityMapping.classList");
if (classList != null) {
StringTokenizer st = new StringTokenizer(classList, " ,\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 " + s_provider);
certIdentityMapping.add(provider);
}
catch (Exception e) {
Log.error("CertificateManager: Error loading CertificateIdentityMapping: " + s_provider + "\n" + e);
}
}
}
if (certIdentityMapping.isEmpty()) {
Log.debug("CertificateManager: No CertificateIdentityMapping's found. Loading default mappings");
certIdentityMapping.add(new SANCertificateIdentityMapping());
certIdentityMapping.add(new CNCertificateIdentityMapping());
}
}
/**
......@@ -346,101 +369,15 @@ public class CertificateManager {
* @return the identities of the remote server as defined in the specified certificate.
*/
public static List<String> getPeerIdentities(X509Certificate x509Certificate) {
// Look the identity in the subjectAltName extension if available
List<String> names = getSubjectAlternativeNames(x509Certificate);
if (names.isEmpty()) {
String name = x509Certificate.getSubjectDN().getName();
Matcher matcher = cnPattern.matcher(name);
// Create an array with the detected identities
names = new ArrayList<String>();
while (matcher.find()) {
names.add(matcher.group(2));
}
}
return names;
}
/**
* Returns the JID representation of an XMPP entity contained as a SubjectAltName extension
* in the certificate. If none was found then return <tt>null</tt>.
*
* @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 <tt>null</tt>.
*/
private static List<String> getSubjectAlternativeNames(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;
List<String> names = new ArrayList<String>();
for (CertificateIdentityMapping mapping : certIdentityMapping) {
List<String> identities = mapping.mapIdentity(x509Certificate);
Log.debug("CertificateManager: " + mapping.name() + " returned " + identities.toString());
names.addAll(identities);
}
// 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;
return names;
}
/**
......
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";
}
}
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