Skip to content

Instantly share code, notes, and snippets.

@Konctantin
Last active May 12, 2017 08:08
Show Gist options
  • Select an option

  • Save Konctantin/d1fa4470cb7fb30595984f9a536f9e68 to your computer and use it in GitHub Desktop.

Select an option

Save Konctantin/d1fa4470cb7fb30595984f9a536f9e68 to your computer and use it in GitHub Desktop.
asn1 parser
unit ASN1;
interface
uses System.SysUtils, System.Generics.Collections, RegularExpressions, Classes, Math;
TYPE
{$REGION 'TAsn1Class'}
TAsn1Class = (
acUniversal = 0, // The type is native to ASN.1.
acApplication = 64, // The type is only valid for one specific application
acContextSpecific = 128, // Meaning of this type depends on the context (such as within a sequence, set or choice)
acPrivate = 192 // Defined in private specifications
);
{$ENDREGION 'TAsn1Class'}
{$REGION 'TAsn1NodeType'}
TAsn1NodeType = (
antPrimitive = 0, // ASN.1 type contains primitive value such as Utf8String, Integer etc.
antConstructedStart = 1, // ASN.1 type contains another ASN.1 type. This could be Sequence, Set etc.
antConstructedEnd = 2, // Virtual type of node indicating that ASN.1 constructed node has been parsed completely.
antDocumentStart = 3, // Virtual type of node indicating that parser is at the beginning of ASN.1 structure.
antDocumentEnd = 4 // Virtual type of node indicating that parsing of ASN.1 structure has been completed.
);
{$ENDREGION 'TAsn1NodeType'}
{$REGION 'TAsn1Type'}
TAsn1Type = (
atEoc = 0,
atBoolean = 1,
atInteger = 2,
atBitString = 3,
atOctetString = 4,
atNull = 5,
atObjectIdentifier = 6,
atObjectDescriptor = 7,
atExternal = 8,
atReal = 9,
atEnumerated = 10,
atEmbeddedPdv = 11,
atUtf8String = 12,
atRelativeOid = 13,
atSequence = 16,
atSet = 17,
atNumericString = 18,
atPrintableString = 19,
atT61String = 20,
atVideotexString = 21,
atIa5String = 22,
atUtcTime = 23,
atGeneralizedTime = 24,
atGraphicString = 25,
atVisibleString = 26,
atGeneralString = 27,
atUniversalString = 28,
atCharacterString = 29,
atBmpString = 30,
atLongForm = 31
);
{$ENDREGION 'TAsn1Type'}
/// <summary>
/// Class holds information about parsed ASN.1 node.
/// </summary>
TAsn1Node = class
private
m_numericStringRegex, m_printableStringRegex : TRegex;
m_childNodes : TList<TAsn1Node>;
m_raw : TBytes;
function IsNodeByOid(oid: string): boolean;
public
Asn1Class : TAsn1Class;
Constructed : boolean;
Tag : TAsn1Type;
/// <summary>
/// Length of ASN.1 node data. Identifier and Length octets of TLV are excluded. It is length of the Value octets.
/// </summary>
DataLength : integer;
/// <summary>
/// Start position of ASN.1 node in the inner stream.
/// </summary>
StartPosition : Int64;
/// <summary>
/// End position of ASN.1 node in the inner stream.
/// </summary>
EndPosition : Int64;
/// <summary>
/// Flag indicating indefinite length of ASN.1 node.
/// </summary>
HasIndefiniteLength : boolean;
/// <summary>
/// Position to the inner stream where Value octets begin.
/// </summary>
DataOffsetToStream : Int64;
/// <summary>
/// Type of the ASN.1 node
/// </summary>
NodeType : TAsn1NodeType;
constructor Create(); overload;
/// <summary>
/// Value of ASN.1 node in bytes. This property is not set automatically by Reader.
/// User should set it by calling specific method on Reader to read the value.
/// </summary>
property RawValue: TBytes read m_raw;
/// <summary>
/// Gets list of childs of this ASN.1 node.
/// </summary>
property ChildNodes : TList<TAsn1Node> read m_childNodes;
/// <summary>
/// Flag indicating that this node is not <see cref="Asn1NodeType.Primitive"/>
/// </summary>
function IsConstructed : boolean;
/// <summary>
/// Return the value of the given node as string.
/// </summary>
function AsOid : string;
/// <summary>
/// Read the value of the given node as string.
/// </summary>
function ReadContentAsString : string;
/// <summary>
/// Read the value of the given node as numeric string.
/// </summary>
function ReadContentAsNumericString : string;
/// <summary>
/// Read the value of the given node as IA5string.
/// </summary>
function ReadContentAsIA5String : string;
/// <summary>
/// Read the value of the given node as Printable string.
/// </summary>
function ReadContentAsPrintableString : string;
/// <summary>
/// Read the value of the given node as T61string.
/// </summary>
function ReadContentAsT61String : string;
/// <summary>
/// Read the value of the given node as Graphic string.
/// </summary>
function ReadContentAsGraphicString : string;
/// <summary>
/// Read the value of the given node as General string.
/// </summary>
function ReadContentAsGeneralString : string;
/// <summary>
/// Read the value of the given node as BMPstring.
/// </summary>
function ReadContentAsBmpString : string;
/// <summary>
/// Read the value of the given node as Universalstring.
/// </summary>
function ReadContentAsUniversalString : string;
/// <summary>
/// Read the value of the given node as Boolean.
/// </summary>
function ReadContentAsBoolean : boolean;
/// <summary>
/// Read the value of the given node as BitString.
/// </summary>
function ReadContentAsBitString : TBytes;
/// <summary>
/// Return the value of the given node as Real (double).
/// </summary>
function AsReal : double;
/// <summary>
/// Return the value of the given node as string.
/// </summary>
function AsString : string;
/// <summary>
/// Return the value of the given node as TDateTime.
/// </summary>
function AsDateTime : TDateTime;
function GetElementsByOid(oid : string) : TList<TAsn1Node>;
end;
/// <summary>
/// Implementation works on top o a stream. The stream should be seekable.
/// </summary>
TAsnReader = class
private
m_nodeStack : TStack<TAsn1Node>;
m_current : TAsn1Node;
m_stream : TStream;
/// <summary>
/// Parse ASN.1 node and sets the result to <see cref="InternalNode"/> class.
/// </summary>
procedure InternalReadElement;
function DetermineLengthOfIndefiniteLengthNode : integer;
function ReadLength : integer;
procedure ReadIdentifierOctet(identifierOctet : integer; var constr: boolean; var cls : TAsn1Class; var tag : TAsn1Type);
function getDepth:integer;
public
constructor Create(aStream : TStream);
destructor Destroy; override;
/// <summary>
/// Gets current node. Usable when reading of content of a ASN.1 tag is not so important at the moment.
/// </summary>
property CurrentNode : TAsn1Node read m_current;
/// <summary>
/// Inner stream on top of which this class operates.
/// </summary>
property Stream : TStream read m_stream;
/// <summary>
/// Value indicating the actual depth of parser in ASN.1 structure.
/// </summary>
property Depth : integer read getDepth;
/// <summary>
/// Parse next node in ASN.1 structure.
/// </summary>
function Read : TAsn1Node;
/// <summary>
/// Parse whole ASN.1 structure. <see cref="TInternalNode"/> class holds all the mappings. Values of the tags are not read.
/// </summary>
function ReadToEnd : TAsn1Node;
/// <summary>
/// Get raw ASN.1 node data (whole TLV => Identifier, Length and Value octets).
/// </summary>
/// <param name="node">Node that shall be read.</param>
function ExtractAsn1NodeAsRawData(node : TAsn1Node) : TBytes;
procedure SaveContentToFile(fileName : TFileName);
procedure SaveContentToStream(stream : TStream);
end;
THelpers = class
public
class function ParseEncodedOid(oid : TBytes):string;
end;
TSreamHelper = class helper for TStream
public
function ReadByte : byte;
function Eof : boolean;
end;
implementation
{ TAsn1Node }
{$REGION 'TAsn1Node'}
function TAsn1Node.AsDateTime: TDateTime;
begin
result := 0;
end;
function TAsn1Node.AsReal: double;
begin
result := 0;
end;
function TAsn1Node.AsString: string;
begin
case Tag of
atEoc: ;
atBoolean: ;
atInteger: ;
atBitString: ;
atOctetString: ;
atNull: ;
atObjectIdentifier: ;
atObjectDescriptor: ;
atExternal: ;
atReal: ;
atEnumerated: ;
atEmbeddedPdv: ;
atUtf8String: result := TEncoding.UTF8.GetString(m_raw, 0, length(m_raw));
atRelativeOid: ;
atSequence: ;
atSet: ;
atNumericString: ;
atPrintableString: ;
atT61String: ;
atVideotexString: ;
atIa5String: ;
atUtcTime: ;
atGeneralizedTime: ;
atGraphicString: ;
atVisibleString: ;
atGeneralString: ;
atUniversalString: ;
atCharacterString: ;
atBmpString: ;
atLongForm: ;
end;
end;
constructor TAsn1Node.Create;
begin
m_childNodes := TList<TAsn1Node>.Create;
m_numericStringRegex := TRegex.Create('^[\d ]*$', []);
m_printableStringRegex := TRegex.Create('^[a-zA-Z0-9 \''\(\)\+\,\-\.\/\:\=\?]*$', []);
end;
function TAsn1Node.IsNodeByOid(oid:string):boolean;
begin
result := (Tag = TAsn1Type.atObjectIdentifier)
and (AsOid = oid)
end;
function TAsn1Node.GetElementsByOid(oid: string): TList<TAsn1Node>;
var childNode : TAsn1Node;
procedure GetChild(node:TAsn1Node);
var subNode : TAsn1Node;
begin
if node.IsNodeByOid(oid) then begin
result.Add(node);
end;
for subNode in node.ChildNodes do begin
GetChild(subNode);
end;
end;
begin
result := TList<TAsn1Node>.Create;
for childNode in ChildNodes do begin
GetChild(childNode);
end;
end;
function TAsn1Node.IsConstructed: boolean;
begin
result := NodeType <> TAsn1NodeType.antPrimitive;
end;
function TAsn1Node.ReadContentAsBitString: TBytes;
var padBitesPart : byte; i : integer;
begin
if m_raw = nil then
raise Exception.Create('m_raw');
if Tag <> TAsn1Type.atBitString then
raise Exception.Create('Can only read value of BITSTRING node.');
padBitesPart := m_raw[0];
if padBitesPart > 7 then
raise Exception.Create('Value in initial octet shall be an unsigned integer in the range zero to seven');
SetLength(result, DataLength - 1);
for I := 1 to length(result)-1 do
result[i] := m_raw[i];
end;
function TAsn1Node.ReadContentAsBmpString: string;
begin
if m_raw = nil then
raise Exception.Create('m_raw');
if Tag <> TAsn1Type.atBmpString then
raise Exception.Create('Can only read value of BmpString node.');
result := TEncoding.BigEndianUnicode.GetString(m_raw, 0, length(m_raw));
end;
function TAsn1Node.ReadContentAsBoolean: boolean;
begin
if m_raw = nil then
raise Exception.Create('m_raw');
if Tag <> TAsn1Type.atBoolean then
raise Exception.Create('Can only read value of BOOLEAN node.');
if length(m_raw) <> 1 then
raise Exception.Create('Boolean ASN.1 type should have single octet value.');
// BER Encoding rules
// If the boolean value is FALSE the octet shall be zero.
result := (m_raw[0] <> 0);
end;
function TAsn1Node.ReadContentAsGeneralString: string;
begin
if Tag <> TAsn1Type.atGeneralString then
raise Exception.Create('Can only read value of GeneralString node.');
result := ReadContentAsString();
end;
function TAsn1Node.ReadContentAsGraphicString: string;
begin
if Tag <> TAsn1Type.atGraphicString then
raise Exception.Create('Can only read value of GraphicString node.');
result := ReadContentAsString();
end;
function TAsn1Node.ReadContentAsIA5String: string;
begin
if Tag <> TAsn1Type.atIa5String then
raise Exception.Create('Can only read value of Ia5String node.');
result := ReadContentAsString();
end;
function TAsn1Node.ReadContentAsNumericString: string;
begin
if Tag <> TAsn1Type.atNumericString then
raise Exception.Create('Can only read value of NumericString node.');
result := ReadContentAsString();
if not m_numericStringRegex.IsMatch(result) then
raise Exception.Create('Numeric string can contain only these characters: 0 1 2 3 4 5 6 7 8 9 0 SPACE.');
end;
function TAsn1Node.AsOid: string;
begin
if m_raw = nil then
raise Exception.Create('m_raw');
if Tag <> TAsn1Type.atObjectIdentifier then
raise Exception.Create('Can only read value of OBJECT IDENTIFIER node.');
result := THelpers.ParseEncodedOid(m_raw);
end;
function TAsn1Node.ReadContentAsPrintableString: string;
begin
if Tag <> TAsn1Type.atPrintableString then
raise Exception.Create('Can only read value of PrintableString node.');
result := ReadContentAsString();
if not m_printableStringRegex.IsMatch(result) then
raise Exception.Create('Found invalid character in input value for Printable string. Please refer to ITU-T X.680 specification.');
end;
function TAsn1Node.ReadContentAsString: string;
begin
if m_raw = nil then
raise Exception.Create('m_raw');
result := TEncoding.UTF8.GetString(m_raw, 0, length(m_raw));
end;
function TAsn1Node.ReadContentAsT61String: string;
begin
if Tag <> TAsn1Type.atT61String then
raise Exception.Create('Can only read value of T61String node.');
result := ReadContentAsString();
end;
function TAsn1Node.ReadContentAsUniversalString: string;
var enc : TEncoding;
begin
if m_raw = nil then
raise Exception.Create('m_raw');
if Tag <> TAsn1Type.atUniversalString then
raise Exception.Create('Can only read value of UniversalString node.');
enc := TEncoding.GetEncoding('utf-32BE');
if enc = nil then
raise Exception.Create('UTF-32 encoding is not supported on this platform.');
result := enc.GetString(m_raw, 0, length(m_raw));
end;
{$ENDREGION 'TAsn1Node'}
{ TAsnReader }
{$REGION 'TAsnReader'}
constructor TAsnReader.Create(aStream: TStream);
begin
self.m_stream := aStream;
m_nodeStack := TStack<TAsn1Node>.Create;
m_current := TAsn1Node.Create;
m_current.NodeType := TAsn1NodeType.antDocumentStart;
end;
destructor TAsnReader.Destroy;
begin
if assigned(m_nodeStack) then begin
m_nodeStack.Clear;
FreeAndNil(m_nodeStack);
end;
if Assigned(m_current) then begin
FreeAndNil(m_current);
end;
end;
function TAsnReader.DetermineLengthOfIndefiniteLengthNode: integer;
var streamPosition : int64;
firstByteofEocfound : boolean;
oneByte : byte;
begin
result := 0;
streamPosition := m_stream.Position;
firstByteofEocfound := false;
while not m_stream.Eof do begin
oneByte := m_stream.ReadByte();
// found possible EOC (0x00 00)
if oneByte = 0 then begin
if firstByteofEocfound then begin
m_stream.Seek(streamPosition, soBeginning);
exit;
end;
firstByteofEocfound := true;
end else begin
firstByteofEocfound := false;
inc(result);
end;
end;
end;
function TAsnReader.ExtractAsn1NodeAsRawData(node: TAsn1Node): TBytes;
begin
if node = nil then
raise Exception.Create('node');
SetLength(result, node.EndPosition - node.StartPosition);
m_stream.Seek(node.StartPosition, soBeginning);
m_stream.Read(result, 0, length(result));
end;
function TAsnReader.getDepth: integer;
begin
result := m_nodeStack.Count;
end;
procedure TAsnReader.ReadIdentifierOctet(identifierOctet : integer; var constr: boolean; var cls : TAsn1Class; var tag : TAsn1Type);
var moreBytes : boolean;
begin
// | | ( 63 -------------------------------> |
// | | 32 | ( 31 -----------------------> |
// -----------------------------------------------------------------
// | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 |
// | class | P/C | Tag Number |
// -----------------------------------------------------------------
// Tag number allows values from 0 to 31. 31 states long-form
moreBytes := (byte(identifierOctet) and ord(TAsn1Type.atLongForm)) = ord(TAsn1Type.atLongForm);
while moreBytes do begin
m_stream.ReadByte();
raise Exception.Create('NotSupportedException');
// TODO parse multi byte tag
//moreBytes = (b & 0x80) != 0;
end;
constr := (byte(identifierOctet) and $20) <> 0; // bit 6 contains P/C (Primitive/Constructed) flag. 6bit => value 0x20 (32 decimal)
cls := TAsn1Class(byte(identifierOctet) and -64);
tag := TAsn1Type(byte(identifierOctet) and 31);
end;
procedure TAsnReader.InternalReadElement;
var nodeStartPosition : int64;
len : integer; hasIndefiniteLengthForm : boolean;
val : integer;
constr: boolean;
cls : TAsn1Class;
tag : TAsn1Type;
begin
if (m_nodeStack.Count > 0) and (m_stream.Position >= m_nodeStack.Peek.EndPosition) then begin
m_current := TAsn1Node.Create();
m_current.DataLength := 0;
m_current.EndPosition := m_stream.Position;
m_current.DataOffsetToStream := m_stream.Position;
m_current.NodeType := TAsn1NodeType.antConstructedEnd;
m_current.Asn1Class := m_nodeStack.Peek.Asn1Class;
m_current.Constructed := m_nodeStack.Peek.Constructed;
m_current.Tag := m_nodeStack.Peek.Tag;
exit;
end;
nodeStartPosition := m_stream.Position;
if m_stream.Eof then begin
m_current := TAsn1Node.Create;
m_current.NodeType := TAsn1NodeType.antDocumentEnd;
exit;
end;
val := m_stream.ReadByte();
ReadIdentifierOctet(val, constr, cls, tag);
if (m_current.NodeType = TAsn1NodeType.antDocumentEnd) then
exit;
len := ReadLength();
if len >= m_stream.Size then
raise Exception.Create('Length of ASN.1 node exceeds length of current stream.');
hasIndefiniteLengthForm := false;
if len = -1 then begin
hasIndefiniteLengthForm := true;
len := DetermineLengthOfIndefiniteLengthNode();
end;
m_current := TAsn1Node.Create;
m_current.Asn1Class := cls;
m_current.Constructed := constr;
m_current.Tag := tag;
m_current.StartPosition := nodeStartPosition;
m_current.HasIndefiniteLength := hasIndefiniteLengthForm;
m_current.DataLength := len;
m_current.DataOffsetToStream := m_stream.Position;
m_current.EndPosition := m_stream.Position + ifthen(hasIndefiniteLengthForm, len+2, len);
if constr then
m_current.NodeType := TAsn1NodeType.antConstructedStart
else
m_current.NodeType := TAsn1NodeType.antPrimitive;
if m_current.NodeType = TAsn1NodeType.antPrimitive then begin
SetLength(m_current.m_raw, m_current.DataLength);
m_stream.Seek(m_current.DataOffsetToStream, soBeginning);
m_stream.Read(m_current.m_raw, 0, m_current.DataLength);
end;
end;
function TAsnReader.ReadLength: integer;
var lengthStart : byte;
numBytesLength : integer;
isMultiByteLength : boolean;
begin
// Length Octets
// read one byte from stream. This should be the Length. Length can be encoded in short or long form.
lengthStart := m_stream.ReadByte();
// 0xff => the value 11111111 (binary) shall not be used
if lengthStart = $FF then
raise Exception.Create('Invalid length format. The value 11111111 (binary) shall not be used.');
result := 0;
// indefinite form of length
if lengthStart = $80 then
exit(-1); // will be corrected later
// check if length is encoded in short form or long form
isMultiByteLength := (lengthStart and $80) <> 0;
// short form length
if not isMultiByteLength then
result := lengthStart
else begin // long form length
// All bits of the subsequent octets form the encoding of an unsigned binary integer equal to the number of octets in the contents octets.
// get number of octets
numBytesLength := lengthStart and $7F;
// in c# sizeof(int) is 4 bytes. We can not interpret more. We could use long, but reading data from stream expects int, not long
// TODO: maybe support long instead of int in Length
if numBytesLength > 4 then
raise Exception.Create('Invalid length value (too long)');
// read the value of length in long form
while numBytesLength > 0 do begin
result := (result shl 8) or byte(m_stream.ReadByte);
inc(numBytesLength, -1);
end;
end;
end;
function TAsnReader.Read: TAsn1Node;
begin
case m_current.NodeType of
TAsn1NodeType.antDocumentStart : begin
InternalReadElement();
end;
TAsn1NodeType.antPrimitive: begin
m_stream.Seek(m_current.EndPosition, soBeginning);
InternalReadElement();
end;
TAsn1NodeType.antConstructedStart: begin
m_nodeStack.Push(m_current);
InternalReadElement();
end;
TAsn1NodeType.antConstructedEnd: begin
m_nodeStack.Pop();
InternalReadElement();
end;
end;
result := m_current;
end;
function TAsnReader.ReadToEnd: TAsn1Node;
var stack : TStack<TAsn1Node>;
current, next : TAsn1Node;
begin
stack := TStack<TAsn1Node>.Create;
stack.Push(m_current);
result := m_current;
current := m_current;
next := m_current;
while next.NodeType <> TAsn1NodeType.antDocumentEnd do begin
next := self.Read();
// only construction node or primitive node will be placed in the map
if (next.NodeType = TAsn1NodeType.antConstructedStart) or (next.NodeType = TAsn1NodeType.antPrimitive) then
current.ChildNodes.Add(next);
// if we hit construction node, there will probably be child nodes under it.
// Push current head node in stack and set new current head
if next.NodeType = TAsn1NodeType.antConstructedStart then begin
m_nodeStack.Push(current);
current := next;
end;
// if we hit construction end node then return to previous head node
// as we completed mapping of inner structure of construction node
if next.NodeType = TAsn1NodeType.antConstructedEnd then
current := m_nodeStack.Pop();
end;
// after all the mapping there should be only document start node in stack
if stack.Count <> 1 then
raise Exception.Create('There should be DocumentStart node in stack. Something is not right.');
if stack.Peek.NodeType <> TAsn1NodeType.antDocumentStart then
raise Exception.Create('There should be DocumentStart node in stack. Something is not right.');
stack.Pop();
FreeAndNil(stack);
end;
procedure TAsnReader.SaveContentToFile(fileName: TFileName);
begin
//
end;
procedure TAsnReader.SaveContentToStream(stream: TStream);
begin
end;
{$ENDREGION 'TAsnReader'}
{ THelpert }
class function THelpers.ParseEncodedOid(oid: TBytes): string;
var list : TList<string>; current, I : integer;
begin
list := TList<string>.Create;
// first byte
// The numerical value of the first subidentifier is derived from the values of the first two object identifier
// components in the object identifier value being encoded, using the formula: (X*40) + Y
if length(oid) > 0 then begin
list.Add( IntToStr( trunc(oid[0] / 40) ) );
list.Add( IntToStr( oid[0] mod 40 ) );
end;
// Each subidentifier is represented as a series of (one or more) octets. Bit 8 of each octet indicates whether it is the last in
// the series: bit 8 of the last octet is zero; bit 8 of each preceding octet is one. Bits 7 to 1 of the octets in the series
// collectively encode the subidentifier. Conceptually, these groups of bits are concatenated to form an unsigned binary
// number whose most significant bit is bit 7 of the first octet and whose least significant bit is bit 1 of the last octet. The
// subidentifier shall be encoded in the fewest possible octets, that is, the leading octet of the subidentifier shall not have
// the value 80 HEX.
current := 0;
for I := 1 to length(oid)-1 do begin
current := (current shl 7) or oid[i] and $7F;
//check if last byte
if ((oid[i] and $80) = 0) then begin
list.Add(IntToStr(current));
current := 0;
end;
end;
result := string.Join('.', list.ToArray);
list.Clear;
FreeAndNil(list);
end;
{ TSreamHelper }
function TSreamHelper.Eof: boolean;
begin
result := not (self.Position < self.size);
end;
function TSreamHelper.ReadByte: byte;
begin
self.Read(result, 1);
end;
end.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment