ASN.1 Library
From CIM
|
Links: |
Contents |
ASN.1 Library
The ASN.1 Library implements the various primitives and parsing strategies for creating and parsing BER, CER and DER encoded streams using the ASN.1 encoding standard. This standard library approaches the problem of parsing an incoming or generating an outgoing BER, CER or DER encoded stream through using proxy objects and hand-rolling a parser, though the possibility of grafting these classes onto an ASN.1 specification language parser is certainly there.
New Version
On August 14, 2008 a new version of the ASN.1 library was uploaded. This version now supports relative OIDs (oops), as well as the constructs outlined in RFC 3641. This includes the external construct and the embedded pdv construct (both encoded like sequence objects, and thus inheriting from the BerConstruct object), and the UTF8, universal, character and BMP string types.
License
Note that the ASN.1 Library is licensed under a BSD style license.
Copyright 2007 William Woody, All Rights Reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
- Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
- Neither the name of Chaos In Motion nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Contact William Woody at woody@alumni.caltech.edu or at woody@chaosinmotion.com. Chaos In Motion is at http://www.chaosinmotion.com
ASN.1 Overview
The ASN.1 standard specifies a standard method for encoding and decoding data to and from a serial stream. Commonly used by a number of standards including SNMP and LDAP, the ASN.1 standard defines a way to both specify the sort of data that is encoded in a transmission packet and to define how the data is encoded.
One class of standards for encoding data that is commonly used in various protocols including LDAP and SNMP is the BER (Basic Encoding Rules) transfer syntax within the ASN.1 standard.
This library handles encoding and decoding data using the basic BER transfer syntax, and its variants, CER and DER. This library does not handle PER, which is a much more compact standard used in telecommunications, though the scheme could potentially be extended to handling PER formatted packets.
ASN.1 Library Overview
The ASN.1 library can roughly be broken up into the following classes of objects: encoding primitives which encode the various primitive and complex objects within an ASN.1 data packet, such as integers and strings, I/O stream support classes which provide for parsing primitive objects in an input stream and tracking the current position, as well as writing data to the output stream, parser support which provides a template for creating more complex ASN.1 input stream parsers which convert a byte data stream into a tree structure of encoding primitives, and exception classes.
Encoding Primitives
The ASN.1 specification fundamentally breaks all underlying objects into two types: simple primitive types, such as integers and floating point numbers whose definition is entirely self-contained, and compound types which essentially represent a collection of objects. (The one potential exception to this is the octet string type, which can either be represented as a primitive array of bytes, or a compound type containing nothing but octet strings. Functionally, however, this is handled as a special case in the input and output stream classes, so for purposes of discussion here, a string is always a primitive type.)
Each type within a BER data stream is represented by a type identifier, a flag indicating if the object is a primitive or compound object, a length and the octets representing the data itself. A primitive object's data block stores the actual integer, string or real value itself; a compound object recursively contains zero or more BER data objects optionally followed by an end of data marker if the compound object's size is set to 'indeterminate.'
The root of the encoding primitive class hierarchy is the BerNode class, an abstract class which provides support for writing itself out to an output stream. Compound objects are all derived from the BerConstruct class, which extends the BerNode class functionality by using an ArrayList to represent the contents of a compound object. The BerSequence, BerSet and BerTag objects all extend the BerConstruct class to provide a concrete representation of ASN.1 sequences, sets and explicit tag types.
Primitives include BerInteger, BerReal, BerBoolean and variations on the BerOctetString class.
The data type mentioned above ('tags' in the ASN.1 parlence) are represented by using integers within the ASN.1 library. The constants for representing a tag are all defined in the Tag class, and this should simplify constructing a tag type as an integer constant.
Stream I/O
The ASN.1 library uses two basic I/O stream classes used for encoding and decoding data into the output and input streams, respectively. Generally best practice is to wrap your input or output stream in these BER encoding streams, then pasing the BER encoding streams to the appropriate methods for reading or writing data.
The BerInputStream class adds functionality necessary to track the current read position in an input stream and for parsing the byte sequences used to store the BER style tag, length identifiers and primitive data types. The BerOutputStream class adds the complementary methods for writing tags, identifiers and primitives.
BER Parser
While writing a BER data object is as simple a matter as constructing in memory the tree structure of BerNodes and BerConstructs to represent the data to write, then calling the BerNode serialization routine with an output stream, parsing ASN.1 input streams are a little more complex. The complexity is added by the fact that the same identifier can be used to define different data types depending upon the context in which the data stream is being parsed. (For example, a node Tag.APPLICATION | 3 could represent an integer in one compound object and a real number in another.
To handle this, the BerParser abstract class provides a way to easily construct a recursive descent parser, in conjunction with the constructor of the BerConstruct class. This context is represented by a 'state' variable, which can be used to determine the current 'state' that the parser is in. This state, combined with the tag identifier, can be used to always and uniquely identify the correct object that needs to be built at any time during parsing.
Using the ASN.1 Library
The ASN.1 library should hopefully simplify the construction of an application that needs to parse ASN.1 input and output streams.
Writing an ASN.1 BER data stream
Writing a BER output stream is relatively straight forward. You would first wrap your output stream object in a BerOutputStream, optionally specifying if the output stream should conform with BER, CER or DER encoding rules:
ByteArrayOutputStream stream = new ByteArrayOutputStream(); BerOutputStream out = new BerOutputStream(stream);
In the above example we are wrapping a ByteArrayOutputStream, but you can wrap any valid OutputStream derived class.
You would then construct your output packet request by building the packet request using the various BerNode derived objects. For example, to create a sequence with two integers, one which is defined using the tag type for a primitive integer, another with an application identifier of 53:
BerSequence seq = new BerSequence(); seq.add(new BerInteger(10)); seq.add(new BerInteger(Tag.APPLICATION | 53, 20));
Then you would write the element to the output stream, optionally flushing the stream to make sure all of the bytes were written. (You would need to do this if the underlying output stream is a network connection.)
seq.writeElement(out); out.flush();
Reading an ASN.1 BER data stream
Reading a BER stream is a little more complex. It requires that you first create a BerParser derived class which can parse the ASN.1 specified protocol, then you create a BerInputStream wrapper around your input stream, and finally parse the packets as they come in.
Creating a BER Parser Object
Suppose we have a protocol we wish to parse, specified in the ASN.1 specification language (borrowed from here and modified for illustration purposes) as:
ExampleProtocol DEFINITIONS ::=
BEGIN
ExampleProtocolMessage ::= CHOICE {
rlcGeneralBroadcastInformation RlcGeneralBroadcastInformation,
rlcFrequencyList RlcFrequencyList }
RlcGeneralBroadcastInformation
::= [APPLICATION 0] SEQUENCE {
duplexMode DuplexMode,
frameOffset FrameOffset,
uplinkPowerMaxRangingStart UplinkPowerMax,
infoText InfoText }
DuplexMode ::= ENUMERATED {fdd(0), tdd(1)}
FrameOffset ::= INTEGER (0 | 8..20)
UplinkPowerMax ::= INTEGER (10..20)
InfoText ::= IA5String (SIZE (0..128))
RlcFrequencyList ::= SEQUENCE (SIZE(32)) OF PairOfCarrierFrequencies
PairOfCarrierFrequencies
::= [APPLICATION 1] SEQUENCE {
uplinkCarrierFrequency CarrierFrequency,
downlinkCarrierFrequency CarrierFrequency }
CarrierFrequency ::= INTEGER (0..130000)
END
Notice that in the above specification, we have the following parser states: ExampleProtocolMessage, RlcGeneralBroadcastInformation, DuplexMode, FrameOffset, etc. Of these, there are four which are not defined as primitive types: ExampleProtocolMessage (a choice), RlcGeneralBroadcastInformation, RlcFrequencyList and PairOfCarrierFrequencies, which are all sequences.
These parser states determine the type of information we're looking for in order to determine the meaning of non-primitive types, such as the APPLICATION type--and while that does not come into play as much here, this can be of importance when parsing a protocol such as LDAP.
To this end we defined our states:
public class TestParser extends BerParser
{
private static final int ExampleProtocolMessage = BerParser.START;
private static final int RlcGeneralBroadcastInformation = 1;
private static final int RlcFrequencyList = 2;
private static final int PairOfCarrierFrequencies = 3;
...
These states represent the different components we're parsing. We only need to track the states for compound objects, such as sets and sequences.
Once we have defined the states in our BerParser-derived class TestParser, we would next need to fill in the create method. This method takes three arguments: the current tag being parsed, the current state we're in (which corresponds to the specific sequence or set we're parsing), and the input stream we're reading from.
Note that the create method is only called if we encounter an application-defined tag, so we do not need to create code to parse the primitive types such as INTEGER or REAL; this is handled for us automatically.
Our create method then would only really need to handle the ExampleProtocolMessage state; if other states are seen it is in error.
public BerNode create(int tag, int state, BerInputStream stream) throws IOException
{
switch (state) {
case ExampleProtocolMessage:
// Application 0 == RlcGeneralBroadcastInformation
// Application 1 == RlcFrequencyList
if (tag == (Tag.APPLICATION | 0)) {
return new BerSequence(tag,RlcGeneralBroadcastInformation,this,stream);
} else if (tag == (Tag.APPLICATION | 1)) {
return new BerSequence(tag,RlcFrequencyList,this,stream);
} else {
throw new AsnEncodingException("Unknown tag: " + tag);
}
case RlcGeneralBroadcastInformation:
throw new AsnEncodingException("Unknown tag: " + tag);
case RlcFrequencyList:
throw new AsnEncodingException("Unknown tag: " + tag);
case PairOfCarrierFrequencies:
throw new AsnEncodingException("Unknown tag: " + tag);
default:
throw new AsnEncodingException("Unknown state: " + state);
}
}
}
Using the Parser
To parse an input stream into a collection of primitives which can later be manipulated by your application, you would need to do like the steps above: first, create a BerInputStream which wraps your input stream primitive:
ByteArrayInputStream inStream = new ByteArrayInputStream(stream.toByteArray()); BerInputStream in = new BerInputStream(inStream);
Next you would need to create an instance of your parser object. (This could be created globally, as by default the underlying BerParser is thread safe.)
TestParser parser = new TestParser();
You then can parse the input stream as needed. If the readPacket method returns NULL you've probably hit the end of the input stream. Your protocol may define other messages to signify the end of the data stream as well.
while (null != (node = parser.readPacket(in))) {
// do your operation.
}
