Commit 7adf9c58 authored by Guus der Kinderen's avatar Guus der Kinderen

Merge pull request #555 from guusdk/OF-1100

OF-1100: Allow for subjectAltNames of type 'DNS'
parents 6f9f814c 465fdd5d
package org.jivesoftware.util.cert;
import java.io.IOException;
import org.bouncycastle.asn1.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
......@@ -8,28 +11,37 @@ 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
* Certificate identity mapping that uses SubjectAlternativeName as the identity credentials.
* This implementation returns all subjectAltName entries that are a:
* <ul>
* <li>GeneralName of type otherName with the "id-on-xmppAddr" Object Identifier</li>
* <li>GeneralName of type otherName with the "id-on-dnsSRV" Object Identifier</li>
* <li>GeneralName of type DNSName</li>
* <li>GeneralName of type UniformResourceIdentifier</li>
* </ul>
*
* @author Victor Hong
*
* @author Guus der Kinderen, guus@goodbytes.nl
*/
public class SANCertificateIdentityMapping implements CertificateIdentityMapping {
public class SANCertificateIdentityMapping implements CertificateIdentityMapping
{
private static final Logger Log = LoggerFactory.getLogger(SANCertificateIdentityMapping.class);
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";
/**
* id-on-xmppAddr Object Identifier.
*
* @see <a href="http://tools.ietf.org/html/rfc6120>RFC 6120</a>
*/
public static final String OTHERNAME_XMPP_OID = "1.3.6.1.5.5.7.8.5";
/**
* id-on-dnsSRV Object Identifier.
*
* @see <a href="https://tools.ietf.org/html/rfc4985">RFC 4985</a>
*/
public static final String OTHERNAME_SRV_OID = "1.3.6.1.5.5.7.8.7";
/**
* Returns the JID representation of an XMPP entity contained as a SubjectAltName extension
......@@ -40,83 +52,164 @@ public class SANCertificateIdentityMapping implements CertificateIdentityMapping
* in the certificate. If none was found then return an empty list.
*/
@Override
public List<String> mapIdentity(X509Certificate certificate) {
public List<String> mapIdentity( X509Certificate certificate )
{
List<String> identities = new ArrayList<>();
try {
try
{
Collection<List<?>> altNames = certificate.getSubjectAlternativeNames();
// Check that the certificate includes the SubjectAltName extension
if (altNames == null) {
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 (ASN1InputStream decoder = new ASN1InputStream((byte[]) item.get(1))) {
// Value is encoded using ASN.1 so decode it to get the server's identity
Object object = decoder.readObject();
ASN1Sequence otherNameSeq = null;
if (object != null && object instanceof ASN1Sequence) {
otherNameSeq = (ASN1Sequence) object;
} else {
continue;
for ( List<?> item : altNames )
{
final Integer type = (Integer) item.get( 0 );
final Object value = item.get( 1 ); // this is either a string, or a byte-array that represents the ASN.1 DER encoded form.
final String result;
switch ( type )
{
case 0:
// OtherName: search for "id-on-xmppAddr" or 'sRVName'
result = parseOtherName( (byte[]) value );
break;
case 2:
// DNS
result = (String) value;
break;
case 6:
// URI
result = (String) value;
break;
default:
// Not applicable to XMPP, so silently ignore them
result = null;
break;
}
// 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;
if ( result != null )
{
identities.add( result );
}
// 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);
}
} 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 ( CertificateParsingException e )
{
Log.error( "Error parsing SubjectAltName in certificate: " + certificate.getSubjectDN(), e );
}
} catch (IOException e) {
// Ignore
return identities;
}
catch (Exception e) {
Log.error("Error decoding subjectAltName", e);
/**
* 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.
* <p/>
* The provided 'OtherName' is expected to have this format:
* <pre>{@code
* OtherName ::= SEQUENCE {
* type-id OBJECT IDENTIFIER,
* value [0] EXPLICIT ANY DEFINED BY type-id }
* }</pre>
*
* @param item A byte array representation of a subjectAltName 'otherName' entry (cannot be null).
* @return an xmpp address, or null when the otherName entry does not relate to XMPP (or fails to parse).
*/
public static String parseOtherName( byte[] item )
{
if ( item == null || item.length == 0 )
{
return null;
}
// Other types are not applicable for XMPP, so silently ignore them
try ( final ASN1InputStream decoder = new ASN1InputStream( item ) )
{
// By specification, OtherName instances must always be an ASN.1 Sequence.
final ASN1Primitive object = decoder.readObject();
final ASN1Sequence otherNameSeq = (ASN1Sequence) object;
// By specification, an OtherName instance consists of:
// - the type-id (which is an Object Identifier), followed by:
// - a tagged value, of which the tag number is 0 (zero) and the value is defined by the type-id.
final ASN1ObjectIdentifier typeId = (ASN1ObjectIdentifier) otherNameSeq.getObjectAt( 0 );
final ASN1TaggedObject taggedValue = (ASN1TaggedObject) otherNameSeq.getObjectAt( 1 );
final int tagNo = taggedValue.getTagNo();
if ( tagNo != 0 )
{
throw new IllegalArgumentException( "subjectAltName 'otherName' sequence's second object is expected to be a tagged value of which the tag number is 0. The tag number that was detected: " + tagNo );
}
final ASN1Primitive value = taggedValue.getObject();
switch ( typeId.getId() )
{
case OTHERNAME_SRV_OID:
return parseOtherNameDnsSrv( value );
case OTHERNAME_XMPP_OID:
return parseOtherNameXmppAddr( value );
default:
Log.debug( "Ignoring subjectAltName 'otherName' type-id '{}' that's neither id-on-xmppAddr nor id-on-dnsSRV.", typeId.getId() );
return null;
}
catch (CertificateParsingException e) {
Log.error("Error parsing SubjectAltName in certificate: " + certificate.getSubjectDN(), e);
}
return identities;
catch ( Exception e )
{
Log.warn( "Unable to parse a byte array (of length {}) as a subjectAltName 'otherName'. It is ignored.", item.length, e );
return null;
}
}
/**
* Returns the short name of mapping
* Parses a SRVName value as specified by RFC 4985.
*
* @return The short name of the mapping
* This method parses the argument value as a DNS SRV Resource Record. Only when the parsed value refers to an XMPP
* related service, the corresponding DNS domain name is returned (minus the service name).
*
* @param srvName The ASN.1 representation of the srvName value (cannot be null).
* @return an XMPP address value, or null when the record does not relate to XMPP.
*/
@Override
public String name() {
return "Subject Alternative Name Mapping";
public static String parseOtherNameDnsSrv( ASN1Primitive srvName )
{
// RFC 4985 says that this should be a IA5 String. Lets be tolerant and allow all text-based values.
final String value = ( (ASN1String) srvName ).getString();
if ( value.toLowerCase().startsWith( "_xmpp-server." ) )
{
return value.substring( "_xmpp-server.".length() );
}
else if ( value.toLowerCase().startsWith( "_xmpp-client." ) )
{
return value.substring( "_xmpp-client.".length() );
}
else
{
// Not applicable to XMPP. Ignore.
Log.debug( "srvName value '{}' of id-on-dnsSRV record is neither _xmpp-server nor _xmpp-client. It is being ignored.", value );
return null;
}
}
/**
* Parse a XmppAddr value as specified in RFC 6120.
*
* @param xmppAddr The ASN.1 representation of the xmppAddr value (cannot be null).
* @return The parsed xmppAddr value.
*/
public static String parseOtherNameXmppAddr( ASN1Primitive xmppAddr )
{
// RFC 6120 says that this should be a UTF8String. Lets be tolerant and allow all text-based values.
return ( (ASN1String) xmppAddr ).getString();
}
}
package org.jivesoftware.util;
import org.bouncycastle.asn1.*;
import org.bouncycastle.asn1.x500.X500Name;
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 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;
/**
* Unit test to validate the functionality of @{link CertificateManager}.
*
* @author Guus der Kinderen, guus@goodbytes.nl
*/
public class CertificateManagerTest
{
public static final ASN1ObjectIdentifier XMPP_ADDR_OID = new ASN1ObjectIdentifier( "1.3.6.1.5.5.7.8.5" );
public static final ASN1ObjectIdentifier DNS_SRV_OID = new ASN1ObjectIdentifier( "1.3.6.1.5.5.7.8.7" );
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 DERSequence otherName = new DERSequence( new ASN1Encodable[] { XMPP_ADDR_OID, new DERUTF8String( subjectAltNameXmppAddr ) });
final GeneralNames subjectAltNames = new GeneralNames( new GeneralName(GeneralName.otherName, otherName ) );
builder.addExtension( Extension.subjectAlternativeName, true, subjectAltNames );
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 SRV' 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-dnsSRV"</li>
* </ul>
*/
@Test
public void testServerIdentitiesDnsSrv() throws Exception
{
// Setup fixture.
final String subjectCommonName = "MySubjectCommonName";
final String subjectAltNameDnsSrv = "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 DERSequence otherName = new DERSequence( new ASN1Encodable[] {DNS_SRV_OID, new DERUTF8String( "_xmpp-server."+subjectAltNameDnsSrv ) });
final GeneralNames subjectAltNames = new GeneralNames( new GeneralName(GeneralName.otherName, otherName ) );
builder.addExtension( Extension.subjectAlternativeName, true, subjectAltNames );
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( subjectAltNameDnsSrv ));
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 DERSequence otherName = new DERSequence( new ASN1Encodable[] { XMPP_ADDR_OID, new DERUTF8String( subjectAltNameXmppAddr ) });
final GeneralNames subjectAltNames = new GeneralNames( new GeneralName[] {
new GeneralName( GeneralName.otherName, otherName ),
new GeneralName( GeneralName.dNSName, subjectAltNameDNS )
});
builder.addExtension( Extension.subjectAlternativeName, true, subjectAltNames );
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