LDAP is a binary protocol, which helps make it compact and efficient to parse. The particular binary encoding that it uses is based on ASN.1 (Abstract Syntax Notation One), which is a framework for representing structured data. ASN.1 is actually a family of encodings that each have their own pros and cons for different situations. For example, you might use the Packed Encoding Rules (PER) if you want to make sure that the encoded representation is as small as possible, or you might use the Octet Encoding Rules (OER) if you favor encode/decode performance over size. LDAP uses the Basic Encoding Rules (BER), which finds a good middle ground between the two.
The complete BER specification has a lot of flexibility and ambiguity, and there are several special cases to consider. Covering all of it in depth would make for a somewhat daunting task, both for me trying to explain everything, and for someone trying to take it all in. If you’re interested in all the gory details, there are already some good books that tackle that much better than I can. I highly recommend Professor John Larmouth’s excellent book ASN.1 Complete, which you can get online as a free PDF download, or you can buy an honest-to-goodness paper copy if you’d prefer a physical copy. And you can always look at the official ASN.1 specifications for the authoritative source, although they can be dense and they’re not always all that easy to interpret.
Fortunately, LDAP uses a pretty well-defined subset of BER that has less ambiguity and fewer special cases. We should be able to cover all the BER that you need to understand the LDAP wire protocol without too much difficulty.
In ASN.1 BER, each piece of data is called an element, and each BER element has three parts: a type, a length, and a value. Let’s take a closer look at each of these components.
A BER element’s type is used to indicate what kind of information that element can hold, not unlike declaring the data type (string, integer, boolean, etc.) for a variable in a computer program. There are a lot of different kinds of BER types, but if we’re just talking about LDAP’s use of BER, then there are really only seven basic data types that we need to know about:
Using these seven types, we can construct any kind of LDAP request or response.
Because BER is a compact binary protocol, it uses a compact binary representation for an element’s type. Although general-purpose BER supports types that span multiple bytes, it is highly unlikely that you’ll ever encounter a BER element in an LDAP message that uses more than one byte for its type. And that byte is laid out as follows:
Bits | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 |
---|---|---|---|---|---|---|---|---|
Purpose | Class | Primitive or Constructed? | Tag Number |
The two most significant bits in this byte (i.e., the two leftmost bits in the big-endian representation of that byte) represent the class for the type. You can also think of this as the scope for the type, which lets you know how likely it is for the same BER type to have the same meaning in two different settings. Since the class is encoded as two bits, there are four possible values:
The third most significant bit in a BER type is used to indicate whether the element is primitive or constructed. If this bit is set to one, then it means that the element is constructed and that its value is comprised of a concatenation of zero or more encoded BER elements. Sequences and sets, which encapsulate elements, are constructed. On the other hand, if this third bit is set to zero, then it means that the element is primitive and that its value should not be assumed to be comprised of encoded elements. Null, Boolean, integer, octet string, and enumerated elements are all primitive.
The remaining five bits in a BER type are used to encode the tag number, which is used to differentiate between different kinds of elements within the same class. The tag number is encoded using the binary representation of that number, so 00000 represents a tag number of zero, 00001 is a tag number of one, 00010 a tag number of two, and so on. Since there are only five bits used for the tag number, you can only have tag numbers up to thirty encoded in a single byte. Fortunately, it’s extremely unlikely that you’ll ever encounter a tag number that is greater than thirty in LDAP (the highest tag number I’m aware of is twenty-five, used for the LDAP intermediate response protocol op), so you probably don’t need to worry about multi-byte types.
The following are the BER types in the universal class that you’re likely to encounter in LDAP:
Element Type | Binary Encoding | Hex Encoding |
---|---|---|
Boolean | 00000001 | 0x01 |
Integer | 00000010 | 0x02 |
Octet String | 00000100 | 0x04 |
Null | 00000101 | 0x05 |
Enumerated | 00001010 | 0x0a |
Sequence | 00110000 | 0x30 |
Set | 00110001 | 0x31 |
The following are the BER types in the application class that are defined for LDAP:
Element Type | Binary Encoding | Hex Encoding |
---|---|---|
Bind Request Protocol Op | 01100000 | 0x60 |
Bind Response Protocol Op | 01100001 | 0x61 |
Unbind Request Protocol Op | 01000010 | 0x42 |
Search Request Protocol Op | 01100011 | 0x63 |
Search Result Entry Protocol Op | 01100100 | 0x64 |
Search Result Done Protocol Op | 01100101 | 0x65 |
Modify Request Protocol Op | 01100110 | 0x66 |
Modify Response Protocol Op | 01100111 | 0x67 |
Add Request Protocol Op | 01101000 | 0x68 |
Add Response Protocol Op | 01101001 | 0x69 |
Delete Request Protocol Op | 01001010 | 0x4a |
Delete Response Protocol Op | 01101011 | 0x6b |
Modify DN Request Protocol Op | 01101100 | 0x6c |
Modify DN Response Protocol Op | 01101101 | 0x6d |
Compare Request Protocol Op | 01101110 | 0x6e |
Compare Response Protocol Op | 01101111 | 0x6f |
Abandon Request Protocol Op | 01010000 | 0x50 |
Search Result Reference Protocol Op | 01110011 | 0x73 |
Extended Request Protocol Op | 01110111 | 0x77 |
Extended Response Protocol Op | 01111000 | 0x78 |
Intermediate Response Protocol Op | 01111001 | 0x79 |
The unbind request, delete request, and abandon request protocol op types are primitive, while all the rest are constructed. This explains why their hexadecimal representations are so out-of-line with their neighboring values. The unbind request protocol op is a null element, the delete request protocol op is an octet string element, the abandon request protocol op is an integer element, and all other types are sequence elements.
A BER element’s length specifies the number of bytes in the value. There are two ways to encode the length: a single-byte representation for values of up to 127 bytes, and a multi-byte representation for values of any size.
In the single-byte representation, the length is just encoded using the binary representation of the number of bytes in the value. For example, if the value is zero bytes long (which will be the case for a null element, for a zero-byte octet string, or an empty sequence or set), then the length is encoded as 00000000 binary or 0x00 hex. If the value is five bytes long, then the length is encoded as 00000101 binary or 0x05 hex. And a value that is 123 bytes long would be encoded as 01111011 binary or 0x7b hex.
In the multi-byte representation, the first byte has its most significant bit set to one, and the lower seven bits are used to indicate how many bytes are required to represent the length. For example, let’s say that you want to encode the length for a value that is 1234 bytes long. The binary representation of 1234 is 10011010010 (0x4d2 hex), which is large enough that it will require two bytes. And then we’ll need to precede those two bytes with a third byte that has its leftmost bit set to one and the right seven bits used to hold the binary representation of the number two. So the full binary representation of a BER length for a 1234-byte-long value is 100000100000010011010010 (0x8204d2 hex).
Although the above
BER doesn’t require you to encode the length in the smallest possible number of bytes. You can use a multi-byte representation for lengths that could be encoded in just a single byte, and you can use more bytes than necessary in a multi-byte representation. For example, all of the following hexadecimal encodings are valid ways to represent a BER length of ten bytes:
Some BER libraries choose to always use multi-byte encodings for certain types of elements (especially sequences and sets). When looking at encoded LDAP traffic, it’s relatively common to see encoded lengths that start with 0x84, followed by four more bytes that actually hold the encoded length. This is usually done for efficiency, because it allows the library to just directly copy the bytes that make up the 32-bit integer representation of the length, and because it makes it possible for the library to go back and fill in the length for a sequence or set once it knows how many elements that sequence or set contains and how big those elements are.
Although it’s technically valid to use any number of bytes to encode a BER length, many libraries impose a limit on the number of bytes that they will support in multi-byte lengths. In most cases, that limit is four bytes, not counting the one extra byte used to indicate that it’s a multi-byte length, so it’s probably best to avoid generating multi-byte lengths that start with anything larger that 0x84.
Most BER libraries impose an upper limit on the size of the elements that they will accept. This is a safety feature that is intended to mitigate the risk of a malicious application claiming that it’s going to send a very large element in the hopes that it will cause the application to allocate enough memory to hold that element, which could cause the application to crash or the system to start swapping. If you’re thinking about writing a BER decoder, it’s a very good idea to ensure you have some way of rejecting elements that are unreasonably large.
BER actually offers a third way to represent the length of an element. This is called the indefinite form, and it uses a special token at the beginning to indicate the start of a value that uses the indefinite form, and then another special token after the end of the value. This is potentially useful for cases in which the size of the element may not be known in advance (for example, when starting a sequence without knowing how many elements will be added to that sequence). However, you won’t encounter the indefinite length form in LDAP because RFC 4511 section 5.1 explicitly forbids its use, so I won’t go into any more detail about it here.
A BER element’s value holds an encoded representation of the data for that element. The way that the value is encoded depends on the type of element, so we’ll cover each kind of value separately.
A null element is one that doesn’t have a value. Or, more accurately, it always has a value with a length of zero bytes. Null elements are typically used in cases where an element is needed, but the value for that element isn’t important. For example, the LDAP unbind request protocol op is a null element because an unbind request doesn’t have any parameters.
Null elements are always primitive, and the value is always empty, so the length is always zero bytes. The universal BER type for a null element is 0x05, so the full hexadecimal encoding for a universal null element is:
05 00
In LDAP, the unbind request protocol op is encoded as a null element in the application class with a tag number of two (as per RFC 4511 section 4.3). The hexadecimal representation of that element is:
42 00
A Boolean element is one whose value represents the Boolean condition of either true or false. The value of a Boolean element is always encoded as a single byte, with 0xff representing true and 0x00 representing false.
LDAP is more restrictive than general-purpose BER is when it comes to encoding Boolean values of true. In general BER, a value of false is always as represented a single byte with all bits set to zero (hex 0x00), while a value in which at least one bit is set to one represents true. But RFC 4511 section 5.1 states that LDAP messages should always encode true values with all bits set to one, which is 0xff hex.
Boolean elements are always primitive, and they always have a one-byte value. The universal BER type for a Boolean element is 0x01, so the encoding for a universal Boolean element with a value of true is:
01 01 ff
And the encoding for a universal Boolean element with a value of false is:
01 01 00
An octet is a byte, and an octet string is simply zero or more bytes strung together. Those bytes can represent text (in LDAP, it’s usually the bytes that comprise the UTF-8 representation of that text), or they can just make up some arbitrary blob of binary data. LDAP uses octet strings all over the place, including for DNs, attribute names and values, diagnostic messages, and to hold the encoded values of controls, extended requests and responses, and SASL credentials.
In LDAP, octet strings are always primitive (BER allows for the possibility of constructed octet strings, but RFC 4511 section 5.1 forbids that use in LDAP). The universal BER type for an octet string element is 0x04, and the hexadecimal bytes that correspond to the UTF-8-encoded text string “Hello!” are: 48 65 6c 6c 6f 21, so the encoding for a universal octet string element meant to hold the text string “Hello!” is:
04 06 48 65 6c 6c 6f 21
An integer is a whole number, without any decimal point or fractional portion. Integer values can be positive, negative, or zero.
In BER, integer values are encoded using the two’s complement representation of the desired numeric value, using the smallest number of bytes that can hold the specified value. The process for coming up with the two’s complement representation varies a little based on whether the value is negative or not.
An integer value of zero is always encoded as a single byte, and that byte is 00000000 binary or 00 hexadecimal. Integer elements are always primitive, and the BER type for universal integer elements is 0x02, so the hexadecimal encoding for a universal integer element with a value of zero is:
02 01 00
Positive integer values are encoded in the smallest number of bytes needed to hold the big-endian binary representation of that number, with the caveat that the most significant bit of the first byte cannot be set to one. If the binary representation of the desired integer value requires a multiple of eight bits, then you should prepend an extra byte with all bits set to zero. For example, the binary representation of the integer value 50 is 00110010 (32 hex), so the hex encoding for a universal integer element with a value of 50 is:
02 01 32
But the binary representation of the integer value 50,000 is 11000011 01010000 (c3 50 hex), which does have its most significant bit set to one so we need to pad it with an extra byte of all zeros to get 00000000 11000011 01010000 binary (00 c3 50 hex), and the hex encoding for a universal integer element with a value of 50,000 is:
02 03 00 c3 50
Negative integer values are more difficult to understand in the two’s complement notation. If the most significant bit of the first byte is a one, then it indicates that the value is negative, but it’s not sufficient to just flip that bit from zero to one in order to turn a positive value into its negative equivalent. To compute the two’s complement representation for a negative integer, you need to use the following process:
So the hex encoding for a universal integer element with a value of -12,345 is:
02 02 cf c7
Technically, BER does not impose any limits on the magnitude of the positive or negative integer values that it can represent. However, many BER libraries do define their own bounds for the sizes of integer values that they can handle. It’s probably a safe assumption that a BER library can work with signed 32-bit integer values (that is, numbers between -2,147,483,648 and 2,147,483,647), which is the range that LDAP requires, but if you’re writing your own BER library, or are looking for a library to use with LDAP, then it’s probably better to ensure that it has support for at least signed 64-bit integer values (between -9,223,372,036,854,775,808 and 9,223,372,036,854,775,807).
An enumerated element is like an integer in that its value is numeric, but each number is associated with a particular meaning. For example, LDAP result messages use an enumerated element to encode the result code (for example, a numeric value of 0 means that the operation completed successfully, 32 means that the operation targeted an entry that didn’t exist, 49 means that an authentication attempt failed because the user provided invalid credentials, etc.). LDAP also uses enumerated elements for things like modification types, search scopes, and alias dereferencing behaviors.
Enumerated elements are encoded in exactly the same way as integer elements. They’re always primitive, and the value is the two’s complement representation of the integer value that it holds. The universal BER type for an enumerated element is 0x0a, so the hexadecimal encoding for a universal enumerated element that represents the LDAP “success” result code (integer value zero) is:
0a 01 00
Although they often are, the allowed set of numeric values for an enumerated element do not have to fall in a contiguous range. For example, there are gaps in the defined set of values for LDAP result codes.
Enumerated elements should not have negative numeric values. However, values are still encoded using the two’s complement representation of the value, so that it may be necessary to add a leading byte in which all bits are set to zero if the binary representation of the numeric value would have otherwise caused the most significant bit in the first byte to be set to one.
A sequence element is a container that holds a list of zero or more other elements. The order in which elements appear in a sequence is considered significant. The value of a sequence element is simply a concatenation of the encoded representations of all of the elements contained in the sequence.
Sequences are always constructed. The universal BER type for a sequence is 0x30, so the encoded representation of a BER sequence that contains a universal octet string with a value of “Hello!”, a Boolean value of true, and an integer value of five would be encoded as follows:
30 0e 04 06 48 65 6c 6c 6f 21 01 01 ff 02 01 05
This encoding is easier to understand if you break it up into its components, like:
30 0e -- The type and length of the sequence 04 06 48 65 6c 6c 6f 21 -- The encoded octet string "Hello!" 01 01 ff -- The encoded Boolean true 02 01 05 -- The encoded integer five
LDAP makes heavy use of sequence elements. Every LDAP request and response is encapsulated in an element called an LDAP message, which is a sequence that contains a message ID (which is an integer), a protocol operation (which varies, but is often a sequence), and an optional list of controls (which, if present, is a sequence of sequences).
A set element is also a container that holds zero or more other elements, and it’s encoded in exactly the same way as a sequence. The only real differences between a sequence and a set are that the order of elements in a set is not considered significant and that the universal BER type for a set is 0x31 instead of the 0x30 type used for a universal sequence.
LDAP does not use sets nearly as much as it does for sequences. The only place that sets are used in the core LDAP protocol specification (RFC 4511) are to hold a collection of values for an attribute, and to hold the components inside an AND or OR search filter.
The X.680 standard, titled “Information technology — Abstract Syntax Notation One (ASN.1): Specification of basic notation”, defines a syntax for representing ASN.1 elements as strings. As with many things related to the ASN.1, the complete syntax is long and complicated, but if you just constrain yourself to what you need to understand to get by in LDAP, it’s pretty manageable.
The string representation of an ASN.1 element is comprised of the following components:
For example, a simple ASN.1 element definition might look like:
AttributeValue ::= OCTET STRING
In the string representation of ASN.1 elements, whitespace consists of one or more of the following characters:
Description | UTF-8 Encoding (hexadecimal) |
---|---|
Regular space | 20 |
Non-breaking space | c2 a0 |
Horizontal tab | 09 |
Vertical tab | 0b |
Line feed (aka newline) | 0a |
Form feed | 0c |
Carriage return | 0d |
There are two ways to specify comments in the string representation of ASN.1 elements:
You can specify the BER type for an ASN.1 element by enclosing it in square brackets in front of the name of the value type. The square brackets should include at least the tag number for the BER type, but may also contain a string that indicates the class for the type.
To indicate that an element should have a BER type in the universal class, you can use the string “UNIVERSAL” inside the square brackets, followed by whitespace and the tag number for that type of element. For example:
AttributeValue ::= [UNIVERSAL 4] OCTET STRING
But this is a rare occurrence because you can omit the type specification if the element is in the universal class. So the above is equivalent to:
AttributeValue ::= OCTET STRING
To indicate that an element should have a BER type in the application class, use the string “APPLICATION” inside the square brackets, followed by whitespace and the tag number. For example:
UnbindRequest ::= [APPLICATION 2] NULL
To indicate that an element should have a BER type in the context-specific class, simply place the tag number inside the square brackets without any other text. For example:
HypotheticalContextSpecificElement ::= [0] INTEGER
And although you’ll probably never encounter it in LDAP, if you want to indicate that an element should have a BER type in the private class, use the string “PRIVATE” inside the square brackets before the tag number, like:
HypotheticalPrivateElement ::= [PRIVATE 5] BOOLEAN
Since null elements don’t have values, there isn’t much variation in the string representation of null values. You just use the string “NULL”, optionally preceded by the type specification in square brackets. For example:
UnbindRequest ::= [APPLICATION 2] NULL
Unlike null elements, Boolean elements do have values. But since a Boolean value is so simple, there aren’t any constraints that you can impose, so the string representation of a Boolean value is just the string “BOOLEAN”, optionally including the type in square brackets. For example:
HypotheticalBooleanElement ::= [1] BOOLEAN
The string representation of an octet string element uses the string “OCTET STRING”, optionally preceded by the BER type specification. For example:
AttributeValue ::= OCTET STRING
Octet string elements can have any kind of value since the value is just a collection of zero or more bytes. However, just because a general-purpose octet string can have any kind of value, that doesn’t mean that every octet string element should be treated as a free-for-all. A particular octet string element might be indented to hold a particular kind of value, and therefore you might want to indicate that there should be certain constraints on the value of that element.
If an octet string’s value should have a specific size, you can indicate that with the SIZE constraint with the allowed number of bytes specified in parentheses, like:
FiveByteOctetString ::= OCTET STRING SIZE(5)
And if the value’s size should be within a specified range, you can indicate that range by separating the upper and lower bounds with two periods, like:
FiveToTenByteOctetString ::= OCTET STRING SIZE(5..10)
If you need to specify a constraint that is more complex than just restricting the number of bytes that can be in the value, then you can just use a comment to specify what that constraint is. For example:
LDAPString ::= OCTET STRING -- UTF-8 encoded, -- [ISO10646] characters
To indicate that an element has a value that is an integer, use the string “INTEGER”, like:
SomeNumber ::= INTEGER
You can specify a range of valid values by separating the upper and lower bounds with two periods and enclosing that range in parentheses, like:
NumberBetweenOneAndTen ::= INTEGER (1..10)
You can also define an integer constant, which is a named representation of a fixed value. For example, the LDAP specification defines a maxInt constant with a value of 2147483647, and it uses that constant in various places. For example:
MessageID ::= INTEGER (0..maxInt) maxInt INTEGER ::= 2147483647 -- (2^^31 - 1) --
An enumerated element has exactly the same encoded representation as an integer element, but they have very different string representations. That’s because each of the possible numeric values for an enumerated element has a specific name that indicates its meaning, and the string representation correlates the name with its numeric value.
The string representation of an enumerated element starts with the string “ENUMERATED” (optionally preceded by the type specification in square brackets), followed by an opening curly brace. It then includes a number of name-value pairs in which the name for each pair follows the same syntax as a type reference (it must start with a letter, must not contain consecutive hyphens, must not end with a hyphen, and must contain only letters, digits, and hyphens), and the numeric value follows that name in parentheses. Each name-value pair except for the last one is followed by a comma, and the last one is followed by a closing curly brace. For example:
TrafficLightColor ::= ENUMERATED { red (0), yellow (1), green (2) }
The string representation of an enumerated element typically lists the values in ascending order, but those values don’t have to represent a contiguous range, and there is no set minimum or maximum value. For example:
SparseValues ::= ENUMERATED { smallestValue (5), middleValue (10), largestValue (17) }
There may also be cases in which you want to define a given set of allowed values now, but also permit defining additional values that can be used in the future. For example, the LDAP protocol specification uses an enumerated element to define a number of possible result code values, but it also allows for other result codes to be defined in other specifications or by specific vendors. To indicate that this should be allowed, use three periods to create an ellipsis, typically at the end of the list of possible values, like:
MayIncludeAdditionalValues ::= ENUMERATED { first (1), second (2), third (3), ... }
This indicates that the three specified values are known at the time the specification was created, but that an application should be prepared to encounter other values. The application may not necessarily be able to interpret those values correctly, and it may return an error if it encounters an unrecognized value, but at least that error shouldn’t result from an inability to decode the element.
For example, RFC 4511 section 4.5.1 specification defines three possible search scope values but uses an ellipsis to indicate that there may be additional scopes defined in the future. It does this like:
scope ::= ENUMERATED { baseObject (0), singleLevel (1), wholeSubtree (2), ... }
And in fact the draft-sermersheim-ldap-subordinate-scope specification does propose a fourth scope, subordinateSubtree, with a numeric value of 3.
There are two basic kinds of sequences: those that have a well-defined set of elements, and those that have an arbitrary number of elements that are all of the same type. The first is primarily used as a data structure to represent some entity with multiple components, while the second is primarily used to hold a bunch of the same kind of thing.
The string representation of a sequence element with a specific number and type of elements is similar to that of an enumerated element. It starts with the “SEQUENCE” keyword (optionally preceded by the type specification in square brackets), followed by an opening curly brace, a comma-delimited list of the allowed elements, and a closing curly brace. Each item in the comma-delimited list of elements consists of a name, some whitespace, and the value specifier. For example:
Date ::= SEQUENCE { year INTEGER, month ENUMERATED { january (1), february (2), march (3), april (4), may (5), june (6), july (7), august (8), september (9), october (10), november (11), december (12) }, dayOfMonth INTEGER (1..31) }
All of the constraints that you can define for the elements on their own are also available for those elements in a sequence (for example, the above constraint that only allows the dayOfMonth value to be between 1 and 31). But there are also additional constraints that you can define for elements in a sequence. These include the OPTIONAL and DEFAULT constraints.
The OPTIONAL constraint indicates that the specified element is optional and doesn’t have to be present. For example, the following sequence defines a data structure for specifying the time of the day in which the hour and minute are required, but the second is optional:
TimeOfDay ::= SEQUENCE { hour INTEGER (0..23), minute INTEGER (0..59), second INTEGER (0..60) OPTIONAL }
The DEFAULT constraint is like the OPTIONAL constraint in that it indicates that the specified element doesn’t have to be there, but the DEFAULT constraint also specifies what value should be assumed if that element isn’t present by following that keyword with whitespace and the default value that should be used. For example:
Control ::= SEQUENCE { controlType LDAPOID, criticality BOOLEAN DEFAULT FALSE, controlValue OCTET STRING OPTIONAL }
As with an enumerated element, you may want to define a sequence that has a defined set of elements right now, but that may also have additional elements in the future. In that case, you can use the ellipsis (...) at the end of the sequence before the closing curly brace, just like you can in an enumerated element. For example:
ExtendableSequence ::= SEQUENCE { element1 OCTET STRING, element2 OCTET STRING, element3 OCTET STRING, ... }
Because the order of elements in a sequence is significant, you can often use the positions of each element to determine what they represent. In the ExtendableSequence defined above, the first element in the sequence corresponds to element1 in the definition, the second corresponds to element2, and the third corresponds to element3. However, this may not work if a sequence contains non-required elements. For example, consider the following:
AnInvalidSequenceDefinition ::= SEQUENCE { element1 OCTET STRING OPTIONAL, element2 OCTET STRING OPTIONAL, element3 OCTET STRING OPTIONAL }
The above sequence is not valid because, if any of the elements is omitted, it’s not possible to determine which one it was. To deal with this, you need to ensure that all of the elements (or at least all of the elements starting with the first non-required element) have a unique BER type so that you can use the type to determine which elements are present and which are absent. For example, the following is valid because even if one or two elements are missing, you can use the BER types of the elements that are present to figure out which ones they are:
AValidSequenceDefinition ::= SEQUENCE { element1 OCTET STRING OPTIONAL, element2 BOOLEAN DEFAULT TRUE, element3 INTEGER DEFAULT 1234 }
But what if you want a sequence to have multiple elements with the same data type? This is when you specify an explicit BER type (usually in the context-specific class) so that you can use it to tell the difference between them. So the following is valid:
AnotherValidSequenceDefinition ::= SEQUENCE { element1 [1] OCTET STRING OPTIONAL, element2 [2] OCTET STRING OPTIONAL, element3 [3] OCTET STRING OPTIONAL }
Sometimes you want to have a sequence that is just a list containing some number of elements of a given kind, and you may or may not know how many elements should be in that list. You can indicate this with “SEQUENCE OF” followed by the type of element that should be contained in the list. For example:
ListOfIntegers ::= SEQUENCE OF listItem INTEGER
If you want to restrict the number of elements in the sequence, you can use the SIZE constraint. In this case, the word SIZE comes immediately after the word SEQUENCE and is followed by either a single number in parentheses (to indicate exactly how many elements should be present) or a pair of numbers separated by two periods (to indicate that the number of elements should fall within a specified range). For example:
ListOfThreeIntegers ::= SEQUENCE SIZE (3) OF listItem INTEGER ListOfFiveOrSixIntegers ::= SEQUENCE SIZE (5..6) OF listItem INTEGER
If there is a lower bound on the number of items but no upper bound, you can use the word MAX in place of the upper bound in the range, like:
NonEmptyListOfIntegers ::= SEQUENCE SIZE (1..MAX) OF listItem INTEGER
Sometimes, you may want to create one sequence that contains all of the elements of another sequence, but that also allows additional elements not in the original sequence. For example, most response messages for LDAP operations allow for a result code, matched DN, diagnostic message, and a list of referral URLs, and these are all contained in an LDAPResult sequence, which is defined as follows:
LDAPResult ::= SEQUENCE { resultCode ENUMERATED { success (0), operationsError (1), protocolError (2), timeLimitExceeded (3), sizeLimitExceeded (4), compareFalse (5), compareTrue (6), authMethodNotSupported (7), strongerAuthRequired (8), -- 9 reserved -- referral (10), adminLimitExceeded (11), unavailableCriticalExtension (12), confidentialityRequired (13), saslBindInProgress (14), noSuchAttribute (16), undefinedAttributeType (17), inappropriateMatching (18), constraintViolation (19), attributeOrValueExists (20), invalidAttributeSyntax (21), -- 22-31 unused -- noSuchObject (32), aliasProblem (33), invalidDNSyntax (34), -- 35 reserved for undefined isLeaf -- aliasDereferencingProblem (36), -- 37-47 unused -- inappropriateAuthentication (48), invalidCredentials (49), insufficientAccessRights (50), busy (51), unavailable (52), unwillingToPerform (53), loopDetect (54), -- 55-63 unused -- namingViolation (64), objectClassViolation (65), notAllowedOnNonLeaf (66), notAllowedOnRDN (67), entryAlreadyExists (68), objectClassModsProhibited (69), -- 70 reserved for CLDAP -- affectsMultipleDSAs (71), -- 72-79 unused -- other (80), ... }, matchedDN LDAPDN, diagnosticMessage LDAPString, referral [3] Referral OPTIONAL } Referral ::= SEQUENCE SIZE (1..MAX) OF uri URI URI ::= LDAPString -- limited to characters permitted in -- URIs
But an LDAP bind response can include all of these LDAPResult elements, plus an additional octet string element used to hold server SASL credentials. And an LDAP extended response can include all of the LDAPResult elements, plus an additional octet string for the response OID and an additional octet string for the response value.
Rather than duplicating the entire LDAPResult element and making the desired changes, you can use the “COMPONENTS OF” keyword followed by the name of the sequence whose elements you want to import. For example:
BindResponse ::= [APPLICATION 1] SEQUENCE { COMPONENTS OF LDAPResult, serverSaslCreds [7] OCTET STRING OPTIONAL } ExtendedResponse ::= [APPLICATION 24] SEQUENCE { COMPONENTS OF LDAPResult, responseName [10] LDAPOID OPTIONAL, responseValue [11] OCTET STRING OPTIONAL }
The string representation of set elements is virtually identical to that of sequence elements. Just replace “SEQUENCE” with “SET”, and “SEQUENCE OF” with “SET OF”. However, given that the order of elements in a set is not considered significant, you are more likely to encounter the “SET OF” variant.
There may be cases in which you want to allow for one of several elements in a given slot in a sequence or set. You can accomplish that with a choice. The string representation of a choice element is very much like a sequence or a set, except that the encoded element can only contain one of the elements. For example:
NameValuePair ::= SEQUENCE { name OCTET STRING, value CHOICE { booleanValue BOOLEAN, integerValue INTEGER, stringValue OCTET STRING } }
Most of the time, the encoded representation of the choice element is just the encoded representation of the element that is selected. For example, the encoded representation of the above sequence with a name of “age” and an integer value of 35 would be:
30 08 -- Begin a universal sequence with a total value size of 8 bytes 04 03 61 67 65 -- The universal octet string age 02 01 23 -- The universal integer 35 (0x23)
This even works for most choice elements with custom element types. For example:
NameAndOctetStringValue ::= SEQUENCE { name OCTET STRING, value CHOICE { stringValue [0] OCTET STRING, binaryValue [1] OCTET STRING } }
In this case, if you have a name of “hello” and a string value of “there” then the encoded representation would be:
30 0e -- Begin a universal sequence with a total value size of 14 bytes 04 05 68 65 6c 6c 6f -- The universal octet string hello 80 05 64 68 65 72 65 -- The context-specific primitive zero octet string there
However, there is a case in which this doesn’t work, and that is the case in which the choice element itself is defined with a custom BER type. For example:
NameAndOptionalValue ::= SEQUENCE { name [0] OCTET STRING, value [1] CHOICE { booleanValue [2] BOOLEAN, integerValue [3] INTEGER, stringValue [4] OCTET STRING, binaryValue [5] OCTET STRING } OPTIONAL }
In this case, the choice element is encoded as a constructed element, with a value that is the full encoding of the selected element inside that choice. For example, if you have a name of “state” and a string value of “Texas”, the encoding would be:
30 10 -- Begin a universal sequence with a total value size of 16 bytes 80 05 73 74 61 74 65 -- The context-specific primitive zero octet string state A1 07 -- Begin a context-specific constructed one value size of 7 bytes 84 05 54 65 78 61 73 -- The context-specific primitive four octet string Texas
Previous: LDAPv3 Wire Protocol Reference | Next: The LDAPMessage Sequence |