org.jivesoftware.util.cert.SANCertificateIdentityMapping Maven / Gradle / Ivy
The newest version!
package org.jivesoftware.util.cert;
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;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
/**
* Certificate identity mapping that uses SubjectAlternativeName as the identity credentials.
* This implementation returns all subjectAltName entries that are a:
*
* - GeneralName of type otherName with the "id-on-xmppAddr" Object Identifier
* - GeneralName of type otherName with the "id-on-dnsSRV" Object Identifier
* - GeneralName of type DNSName
* - GeneralName of type UniformResourceIdentifier
*
*
* @author Victor Hong
* @author Guus der Kinderen, [email protected]
*/
public class SANCertificateIdentityMapping implements CertificateIdentityMapping
{
private static final Logger Log = LoggerFactory.getLogger( SANCertificateIdentityMapping.class );
/**
* id-on-xmppAddr Object Identifier.
*
* @see RFC 4985
*/
public static final String OTHERNAME_SRV_OID = "1.3.6.1.5.5.7.8.7";
/**
* User Principal Name (UPN) Object Identifier.
*
* @see User Principal Name (UPN)
*/
public static final String OTHERNAME_UPN_OID = "1.3.6.1.4.1.311.20.2.3";
/**
* 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 mapIdentity( X509Certificate certificate )
{
List identities = new ArrayList<>();
try
{
Collection> altNames = certificate.getSubjectAlternativeNames();
// Check that the certificate includes the SubjectAltName extension
if ( altNames == null )
{
return Collections.emptyList();
}
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' or 'userPrincipalName'
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;
}
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.
*
* The provided 'OtherName' is expected to have this format:
* {@code
* OtherName ::= SEQUENCE {
* type-id OBJECT IDENTIFIER,
* value [0] EXPLICIT ANY DEFINED BY type-id }
* }
*
* @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 String parseOtherName( byte[] item )
{
if ( item == null || item.length == 0 )
{
return null;
}
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 );
case OTHERNAME_UPN_OID:
return parseOtherNameUpn( value );
default:
String otherName = parseOtherName(typeId, value);
if (otherName != null) {
return otherName;
}
Log.debug( "Ignoring subjectAltName 'otherName' type-id '{}' that's neither id-on-xmppAddr nor id-on-dnsSRV.", typeId.getId() );
return null;
}
}
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;
}
}
/**
* Allow sub-class to support additional OID values, possibly taking typeId into account
*
* @param typeId The ASN.1 object identifier (cannot be null).
* @param value The ASN.1 representation of the value (cannot be null).
* @return The parsed otherName String value.
*/
protected String parseOtherName(ASN1ObjectIdentifier typeId, ASN1Primitive value) {
return null;
}
/**
* Parses a SRVName value as specified by RFC 4985.
*
* 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.
*/
protected 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.
*/
protected 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();
}
/**
* Parse a UPN value
*
* @param value The ASN.1 representation of the UPN (cannot be null).
* @return The parsed UPN value.
*/
protected String parseOtherNameUpn( ASN1Primitive value )
{
String otherName = null;
if (value instanceof ASN1TaggedObject) {
ASN1TaggedObject taggedObject = (ASN1TaggedObject) value;
ASN1Primitive objectPrimitive = taggedObject.getObject();
if (objectPrimitive instanceof ASN1String) {
otherName = ((ASN1String)objectPrimitive).getString();
}
}
if (otherName == null) {
Log.warn("UPN type unexpected, UPN extraction failed: " + value.getClass().getName() + ":" + value.toString());
} else {
Log.debug("UPN from certificate has value of: " + otherName );
}
return otherName;
}
}