Authenticating against an Active Directory Server, pt. 4

Posted on September 25, 2009 by Tommy McGuire
Labels: authentication, protocols, http, ldap, active directory, java
Sending an passel of LDAP pings and waiting for a response is pretty trivial.

List possibles = getSrvRecords(dnsName);
DatagramSocket socket = null;
try
{
socket = new DatagramSocket();
sendLdapPingRequest(socket, possibles);
int messageId = receiveLdapPingReply(socket);
return possibles.get(messageId);
}
finally
{
if (socket != null)
{
socket.close();
}
}
...

/**
* Send an LDAP Ping request to a list of servers specified by SRV records.
*
* @param socket UDP socket on which to send the ping request.
* @param records List of SRV records identifying potential servers.
* @throws IOException if encoding or sending the request fails.
*/
private static void sendLdapPingRequest(DatagramSocket socket, List records) throws IOException
{
int recordId = 0;
for (SRVRecord record : records)
{
byte[] bytes = new SearchRequestMessage(recordId++).encode();
socket.send( new DatagramPacket(bytes, bytes.length, new InetSocketAddress(record.getTarget().toString(), record.getPort())) );
}
}

/**
* Receive and parse the first response to the LDAP Ping request, returning the message id of the response.
*
* @param socket UDP socket on which to listen.
* @return The id of the first successful response.
* @throws IOException if receiving or unpacking the message fails.
*/
private static int receiveLdapPingReply(DatagramSocket socket) throws IOException
{
DatagramPacket buffer = new DatagramPacket(new byte[1000], 1000);
socket.receive(buffer);
SearchResultEntryMessage reply = new SearchResultEntryMessage(new ByteArrayInputStream(buffer.getData()));
NetLogon nl = NetLogon.unpack(reply.getNetlogon());
// TODO: do something useful with nl
return reply.getMessageId();
}

The fun part is creating and encoding the initial ping request, and parsing and interpreting the results. Creating and encoding the message in sendLdapPingRequest is handled by SearchRequestMessage. This class is implemented by extending ASN1Sequence, from the Novell jldap library. (There are a number of ASN.1 libraries and compilers around, but since I was already using that library and it's support was entirely adequate, com.novell.ldap.asn1.ASN1Sequence gets the nod.)

public class SearchRequestMessage extends ASN1Sequence
{
int messageId;
String dnsDomain = null;
String host = null;
String user = null;
byte[] aac = null;
byte[] domainSid = null;
byte[] domainGuid = null;
byte[] ntVer = null;

public SearchRequestMessage(int messageId)
{
this.messageId = messageId;
}

public SearchRequestMessage setDnsDomain(String dnsDomain) { this.dnsDomain = dnsDomain; return this; }
public SearchRequestMessage setHost(String host) { this.host = host; return this; }
public SearchRequestMessage setUser(String user) { this.user = user; return this; }
public SearchRequestMessage setAac(byte[] aac) { this.aac = aac; return this; }
public SearchRequestMessage setDomainSid(byte[] domainSid) { this.domainSid = domainSid; return this; }
public SearchRequestMessage setDomainGuid(byte[] domainGuid) { this.domainGuid = domainGuid; return this; }
public SearchRequestMessage setNtVer(byte[] ntVer) { this.ntVer = ntVer; return this; }

public byte[] encode() throws IOException
{
this.add(new ASN1Integer(messageId));
this.add(new SearchRequest(dnsDomain, host, user, aac, domainSid, domainGuid, ntVer));
ByteArrayOutputStream baos = new ByteArrayOutputStream();
this.encode(new LBEREncoder(), baos);
return baos.toByteArray();
}
}

An LDAP message consists of a SEQUENCE containing an integer message identifier followed by one of the different types of specific messages. In this case, the ping uses an LDAP SearchRequest. This SearchRequest is limited to

Most of the other components seem to be unused. The collection of set* methods describe the attributes usable in the equalityMatchFilters. The exact meaning of the attributes is described in the Microsoft LDAP Ping documentation. Strangely enough, although the and filter normally requires at least one sub-filter, in this case an empty conjunction is acceptible (and in fact is what I have been using).
The SearchRequest component itself is an ASN.1 SEQUENCE with an application-specific tag of 3. Most of the following parts are familiar to anyone who has done an LDAP search: the base object of the search (an OCTET STRING), whether the search is for the base object itself, one level below, or the entire sub-tree (an enumeration), what to do about aliases in the tree, and so forth.

public class SearchRequest extends ASN1Sequence
{
public SearchRequest(String dnsDomain, String host, String user, byte[] aac, byte[] domainSid, byte[] domainGuid, byte[] ntVer)
{
this.setIdentifier(new ASN1Identifier(ASN1Identifier.APPLICATION, true, 3));
this.add(new ASN1OctetString("")); // REQUIRED: baseObject
this.add(new ASN1Enumerated(0)); // REQUIRED: scope: 0 = baseobject, 1 = single, 2 = whole subtree
this.add(new ASN1Enumerated(0)); // OPTIONAL?: derefAliases 0 = never, 1 = in searching, 2 = finding base object, 3 = always
this.add(new ASN1Integer(0)); // OPTIONAL?: sizeLimit
this.add(new ASN1Integer(0)); // OPTIONAL?: timeLimit
this.add(new ASN1Boolean(false)); // REQUIRED?: typesOnly
// REQUIRED: Filter
AndFilter filter = new AndFilter();
if (dnsDomain != null)
{
filter.add(new EqualityMatchFilter("DnsDomain", dnsDomain));
}
if (host != null)
{
filter.add(new EqualityMatchFilter("Host", host));
}
if (user != null)
{
filter.add(new EqualityMatchFilter("User", user));
}
if (aac != null)
{
filter.add(new EqualityMatchFilter("AAC", aac));
}
if (domainSid != null)
{
filter.add(new EqualityMatchFilter("DomainSid", domainSid));
}
if (domainGuid != null)
{
filter.add(new EqualityMatchFilter("DomainGuid", domainGuid));
}
if (ntVer != null)
{
filter.add(new EqualityMatchFilter("NtVer", ntVer));
}
this.add(filter);
this.add(new AttributeDescriptionList("netlogon")); // REQUIRED: AttributeDescriptionList
}
}

Because of the way application-specific components are handled by ASN.1, it is cleaner to use custom classes extending the base of the specific component. For example, the and filter is an ASN.1 SetOf composite, with a context-specific tag of 0 (rather than universal or application specific; the former indicates the generic ASN.1 types that all formats can share; the latter indicates a message type which is scoped throughout a given set of message components; context-specific tags are scoped to a particular place in the specification).

public class AndFilter extends ASN1SetOf
{
public AndFilter()
{
this.setIdentifier(new ASN1Identifier(ASN1Identifier.CONTEXT, true, 0)); // LDAP and filter
}
}

The EqualityMatchFilter class, on the other hand, is a slightly more complex ASN.1 structure. It is a subtype of an AttributeValueAssertion structure (where the assertion in question is equality) with a context-specific tag of 3 (this is the same context as the and filter above). The AttributeValueAssertion is a SEQUENCE of two OCTET STRINGs; the first is the attribuet name and the second is the value about which something is being asserted.

public class EqualityMatchFilter extends AttributeValueAssertion
{
public EqualityMatchFilter(String attribute, String value)
{
super(attribute, value);
this.setIdentifier(new ASN1Identifier(ASN1Identifier.CONTEXT, true, 3)); // LDAP equalityMatch filter
}
public EqualityMatchFilter(String attribute, byte[] value)
{
super(attribute, value);
this.setIdentifier(new ASN1Identifier(ASN1Identifier.CONTEXT, true, 3)); // LDAP equalityMatch filter
}
}

public class AttributeValueAssertion extends ASN1Sequence
{
public AttributeValueAssertion(String attribute, String value)
{
this.add(new ASN1OctetString(attribute)); // AttributeDescription
this.add(new ASN1OctetString(value)); // AssertionValue
}
public AttributeValueAssertion(String attribute, byte[] value)
{
this.add(new ASN1OctetString(attribute)); // AttributeDescription
this.add(new ASN1OctetString(value)); // AssertionValue
}
}

The final piece is the AttributeDescriptionList used to indicate the netlogon attribute in the search.

public class AttributeDescriptionList extends ASN1SequenceOf
{
public AttributeDescriptionList(String... attributes)
{
for (String attribute : attributes)
{
this.add(new ASN1OctetString(attribute));
}
}
}

This structure is mostly a helper, used to build a SEQUENCE of OCTET STRINGs.
Once the structure is built, with the set of search attributes included, converting it to a byte array for transmission is the final steps of the SearchRequestMessage.encode method. It creates a com.novell.ldap.asn1.LBEREncoder (a limited BER encoder presumably only useful for LDAP and writes the thing to a ByteArrayOutputStream.
active directory applied formal logic ashurbanipal authentication books c c++ comics conference continuations coq data structure digital humanities Dijkstra eclipse virgo electronics emacs goodreads haskell http java job Knuth ldap link linux lisp math naming nimrod notation OpenAM osgi parsing pony programming language protocols python quote R random REST ruby rust SAML scala scheme shell software development system administration theory tip toy problems unix vmware yeti
Member of The Internet Defense League
Site proudly generated by Hakyll.