Commit c9850548 authored by Guus der Kinderen's avatar Guus der Kinderen

OF-1100: Allow for subjectAltNames of type 'DNS'

In addition to subjectAltNames of type otherName with an ASN.1 Object Identifier of
"id-on-xmppAddr", subjectAltNames of type DNS should also be evaluated when processing
identities from a certificate.
parent 6f9f814c
package org.jivesoftware.util.cert; package org.jivesoftware.util.cert;
import java.io.IOException;
import java.security.cert.CertificateParsingException; import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.util.ArrayList; import java.util.ArrayList;
...@@ -19,8 +18,9 @@ import org.slf4j.Logger; ...@@ -19,8 +18,9 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
/** /**
* Certificate identity mapping that uses XMPP-OtherName SubjectAlternativeName * Certificate identity mapping that uses SubjectAlternativeName as the identity credentials. This implementation
* as the identity credentials * combines subjectAltName entries of type otherName with an ASN.1 Object Identifier of "id-on-xmppAddr" with entries
* of type DNS.
* *
* @author Victor Hong * @author Victor Hong
* *
...@@ -48,19 +48,63 @@ public class SANCertificateIdentityMapping implements CertificateIdentityMapping ...@@ -48,19 +48,63 @@ public class SANCertificateIdentityMapping implements CertificateIdentityMapping
if (altNames == null) { if (altNames == null) {
return Collections.emptyList(); return Collections.emptyList();
} }
// Use the type OtherName to search for the certified server name
for (List<?> item : altNames) { for (List<?> item : altNames) {
Integer type = (Integer) item.get(0); final Integer type = (Integer) item.get(0);
if (type == 0) { final Object value = item.get(1);
final String result;
switch ( type ) {
case 0:
// OtherName: search for "id-on-xmppAddr"
result = parseOtherName( (byte[]) value );
break;
case 2:
// DNS
result = (String) value;
break;
default:
// Other types are not applicable for XMPP, so silently ignore them
result = null;
break;
}
if ( result != null ) {
identities.add( result );
}
}
}
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 (never null).
*/
@Override
public String name() {
return "Subject Alternative Name Mapping";
}
/**
* Parses the byte-array representation of a subjectAltName 'otherName' entry, returning the "id-on-xmppAddr" value
* when that is in the entry.
*
* @param item A byte array representation of a subjectAltName 'otherName' entry (cannot be null).
* @return an "id-on-xmppAddr" value (which is expected to be a JID), or null.
*/
public static String parseOtherName( byte[] item ) {
// Type OtherName found so return the associated value // Type OtherName found so return the associated value
try (ASN1InputStream decoder = new ASN1InputStream((byte[]) item.get(1))) { try (ASN1InputStream decoder = new ASN1InputStream(item)) {
// Value is encoded using ASN.1 so decode it to get the server's identity // Value is encoded using ASN.1 so decode it to get the server's identity
Object object = decoder.readObject(); Object object = decoder.readObject();
ASN1Sequence otherNameSeq = null; ASN1Sequence otherNameSeq = null;
if (object != null && object instanceof ASN1Sequence) { if (object != null && object instanceof ASN1Sequence) {
otherNameSeq = (ASN1Sequence) object; otherNameSeq = (ASN1Sequence) object;
} else { } else {
continue; return null;
} }
// Check the object identifier // Check the object identifier
ASN1ObjectIdentifier objectId = (ASN1ObjectIdentifier) otherNameSeq.getObjectAt(0); ASN1ObjectIdentifier objectId = (ASN1ObjectIdentifier) otherNameSeq.getObjectAt(0);
...@@ -69,7 +113,7 @@ public class SANCertificateIdentityMapping implements CertificateIdentityMapping ...@@ -69,7 +113,7 @@ public class SANCertificateIdentityMapping implements CertificateIdentityMapping
if ( !OTHERNAME_XMPP_OID.equals(objectId.getId())) { if ( !OTHERNAME_XMPP_OID.equals(objectId.getId())) {
// Not a XMPP otherName // Not a XMPP otherName
Log.debug("Ignoring non-XMPP otherName, " + objectId.getId()); Log.debug("Ignoring non-XMPP otherName, " + objectId.getId());
continue; return null;
} }
// Get identity string // Get identity string
...@@ -87,36 +131,17 @@ public class SANCertificateIdentityMapping implements CertificateIdentityMapping ...@@ -87,36 +131,17 @@ public class SANCertificateIdentityMapping implements CertificateIdentityMapping
} }
if (identity != null && identity.length() > 0) { if (identity != null && identity.length() > 0) {
// Add the decoded server name to the list of identities // Add the decoded server name to the list of identities
identities.add(identity); return identity;
} }
} catch (IllegalArgumentException ex) { } catch (IllegalArgumentException ex) {
// OF-517: othername formats are extensible. If we don't recognize the format, skip it. // 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); Log.debug("Cannot parse altName, likely because of unknown record format.", ex);
} }
} catch (IOException e) {
// Ignore
} }
catch (Exception e) { catch (Exception e) {
Log.error("Error decoding subjectAltName", e); Log.error("Error decoding subjectAltName", e);
} }
} return null;
// 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";
} }
} }
package org.jivesoftware.util;
import org.bouncycastle.asn1.*;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.BasicConstraints;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.GeneralNames;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.junit.BeforeClass;
import org.junit.Test;
import sun.security.x509.SubjectAlternativeNameExtension;
import java.math.BigInteger;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import java.util.Date;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
/**
* Created by guus on 3-3-16.
*/
public class CertificateManagerTest
{
public static final ASN1ObjectIdentifier XMPP_ADDR_OID = new ASN1ObjectIdentifier( "1.3.6.1.5.5.7.8.5" );
private static KeyPairGenerator keyPairGenerator;
private static KeyPair subjectKeyPair;
private static KeyPair issuerKeyPair;
private static ContentSigner contentSigner;
@BeforeClass
public static void initialize() throws Exception
{
keyPairGenerator = KeyPairGenerator.getInstance( "RSA" );
keyPairGenerator.initialize( 512 );
subjectKeyPair = keyPairGenerator.generateKeyPair();
issuerKeyPair = keyPairGenerator.generateKeyPair();
contentSigner = new JcaContentSignerBuilder( "SHA1withRSA" ).build( issuerKeyPair.getPrivate() );
}
/**
* {@link CertificateManager#getServerIdentities(X509Certificate)} should return:
* <ul>
* <li>the Common Name</li>
* </ul>
*
* when a certificate contains:
* <ul>
* <li>no other identifiers than its CommonName</li>
* </ul>
*/
@Test
public void testServerIdentitiesCommonNameOnly() throws Exception
{
// Setup fixture.
final String subjectCommonName = "MySubjectCommonName";
final X509v3CertificateBuilder builder = new JcaX509v3CertificateBuilder(
new X500Name( "CN=MyIssuer" ), // Issuer
BigInteger.valueOf( Math.abs( new SecureRandom().nextInt() ) ), // Random serial number
new Date( System.currentTimeMillis() - ( 1000L * 60 * 60 * 24 * 30 ) ), // Not before 30 days ago
new Date( System.currentTimeMillis() + ( 1000L * 60 * 60 * 24 * 99 ) ), // Not after 99 days from now
new X500Name( "CN=" + subjectCommonName ), // Subject
subjectKeyPair.getPublic()
);
final X509CertificateHolder certificateHolder = builder.build( contentSigner );
final X509Certificate cert = new JcaX509CertificateConverter().getCertificate( certificateHolder );
// Execute system under test
final List<String> serverIdentities = CertificateManager.getServerIdentities( cert );
// Verify result
assertEquals( 1, serverIdentities.size() );
assertEquals( subjectCommonName, serverIdentities.get( 0 ) );
}
/**
* {@link CertificateManager#getServerIdentities(X509Certificate)} should return:
* <ul>
* <li>the 'xmppAddr' subjectAltName value</li>
* <li>explicitly not the Common Name</li>
* </ul>
*
* when a certificate contains:
* <ul>
* <li>a subjectAltName entry of type otherName with an ASN.1 Object Identifier of "id-on-xmppAddr"</li>
* </ul>
*/
@Test
public void testServerIdentitiesXmppAddr() throws Exception
{
// Setup fixture.
final String subjectCommonName = "MySubjectCommonName";
final String subjectAltNameXmppAddr = "MySubjectAltNameXmppAddr";
final X509v3CertificateBuilder builder = new JcaX509v3CertificateBuilder(
new X500Name( "CN=MyIssuer" ), // Issuer
BigInteger.valueOf( Math.abs( new SecureRandom().nextInt() ) ), // Random serial number
new Date( System.currentTimeMillis() - ( 1000L * 60 * 60 * 24 * 30 ) ), // Not before 30 days ago
new Date( System.currentTimeMillis() + ( 1000L * 60 * 60 * 24 * 99 ) ), // Not after 99 days from now
new X500Name( "CN=" + subjectCommonName ), // Subject
subjectKeyPair.getPublic()
);
final DERTaggedObject derTaggedDomainName = new DERTaggedObject(0, new DERUTF8String(subjectAltNameXmppAddr) );
final DLSequence otherName = new DLSequence(new ASN1Encodable[]{XMPP_ADDR_OID, derTaggedDomainName});
final GeneralNames generalNames = new GeneralNames(new GeneralName(GeneralName.otherName, otherName));
builder.addExtension( Extension.subjectAlternativeName, false, generalNames );
final X509CertificateHolder certificateHolder = builder.build( contentSigner );
final X509Certificate cert = new JcaX509CertificateConverter().getCertificate( certificateHolder );
// Execute system under test
final List<String> serverIdentities = CertificateManager.getServerIdentities( cert );
// Verify result
assertEquals( 1, serverIdentities.size() );
assertTrue( serverIdentities.contains( subjectAltNameXmppAddr ));
assertFalse( serverIdentities.contains( subjectCommonName ) );
}
/**
* {@link CertificateManager#getServerIdentities(X509Certificate)} should return:
* <ul>
* <li>the DNS subjectAltName value</li>
* <li>explicitly not the Common Name</li>
* </ul>
*
* when a certificate contains:
* <ul>
* <li>a subjectAltName entry of type DNS </li>
* </ul>
*/
@Test
public void testServerIdentitiesDNS() throws Exception
{
// Setup fixture.
final String subjectCommonName = "MySubjectCommonName";
final String subjectAltNameDNS = "MySubjectAltNameDNS";
final X509v3CertificateBuilder builder = new JcaX509v3CertificateBuilder(
new X500Name( "CN=MyIssuer" ), // Issuer
BigInteger.valueOf( Math.abs( new SecureRandom().nextInt() ) ), // Random serial number
new Date( System.currentTimeMillis() - ( 1000L * 60 * 60 * 24 * 30 ) ), // Not before 30 days ago
new Date( System.currentTimeMillis() + ( 1000L * 60 * 60 * 24 * 99 ) ), // Not after 99 days from now
new X500Name( "CN=" + subjectCommonName ), // Subject
subjectKeyPair.getPublic()
);
final GeneralNames generalNames = new GeneralNames(new GeneralName(GeneralName.dNSName, subjectAltNameDNS));
builder.addExtension( Extension.subjectAlternativeName, false, generalNames );
final X509CertificateHolder certificateHolder = builder.build( contentSigner );
final X509Certificate cert = new JcaX509CertificateConverter().getCertificate( certificateHolder );
// Execute system under test
final List<String> serverIdentities = CertificateManager.getServerIdentities( cert );
// Verify result
assertEquals( 1, serverIdentities.size() );
assertTrue( serverIdentities.contains( subjectAltNameDNS ) );
assertFalse( serverIdentities.contains( subjectCommonName ) );
}
/**
* {@link CertificateManager#getServerIdentities(X509Certificate)} should return:
* <ul>
* <li>the DNS subjectAltName value</li>
* <li>the 'xmppAddr' subjectAltName value</li>
* <li>explicitly not the Common Name</li>
* </ul>
*
* when a certificate contains:
* <ul>
* <li>a subjectAltName entry of type DNS </li>
* <li>a subjectAltName entry of type otherName with an ASN.1 Object Identifier of "id-on-xmppAddr"</li>
* </ul>
*/
@Test
public void testServerIdentitiesXmppAddrAndDNS() throws Exception
{
// Setup fixture.
final String subjectCommonName = "MySubjectCommonName";
final String subjectAltNameXmppAddr = "MySubjectAltNameXmppAddr";
final String subjectAltNameDNS = "MySubjectAltNameDNS";
final X509v3CertificateBuilder builder = new JcaX509v3CertificateBuilder(
new X500Name( "CN=MyIssuer" ), // Issuer
BigInteger.valueOf( Math.abs( new SecureRandom().nextInt() ) ), // Random serial number
new Date( System.currentTimeMillis() - ( 1000L * 60 * 60 * 24 * 30 ) ), // Not before 30 days ago
new Date( System.currentTimeMillis() + ( 1000L * 60 * 60 * 24 * 99 ) ), // Not after 99 days from now
new X500Name( "CN=" + subjectCommonName ), // Subject
subjectKeyPair.getPublic()
);
final DERTaggedObject derTaggedDomainName = new DERTaggedObject(0, new DERUTF8String(subjectAltNameXmppAddr) );
final DLSequence otherName = new DLSequence(new ASN1Encodable[]{XMPP_ADDR_OID, derTaggedDomainName});
final GeneralNames generalNames = new GeneralNames( new GeneralName[] {
new GeneralName(GeneralName.otherName, otherName),
new GeneralName(GeneralName.dNSName, subjectAltNameDNS)
});
builder.addExtension( Extension.subjectAlternativeName, false, generalNames );
final X509CertificateHolder certificateHolder = builder.build( contentSigner );
final X509Certificate cert = new JcaX509CertificateConverter().getCertificate( certificateHolder );
// Execute system under test
final List<String> serverIdentities = CertificateManager.getServerIdentities( cert );
// Verify result
assertEquals( 2, serverIdentities.size() );
assertTrue( serverIdentities.contains( subjectAltNameXmppAddr ));
assertFalse( serverIdentities.contains( subjectCommonName ) );
}
}
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