ASN.1 Utilities

GitLab Pipeline Coverage License
Utility for reading and writing ASN.1 based content, including handling BER and DER.

The Ups and Downs of ASN.1

There are various benefits to

Pros

  • ASN.1

Cons

  • The ASN.1 IDL syntax is very complicated, and there are few OSS systems to help with IDL parsing, BER encoding or decoding, code generation, etc. This OSS project is in part meant to counter that argument, and make

Comparison to ...

  • protobuf / grpc: ...
  • json: ...

The ASN.1 Syntax

There is a good source of material for figuring out the ASN.1 DLS syntax, where I have among others used access to various sources of ASN.1 files on the internet, and publicly available books.

First a number of terms and what is meant by them:

  • identifier: Identifiers are a string of [a-zA-Z][a-zA-Z0-9]*(-[a-zA-z0-9]+)*.
  • Type-Name: Types are identified by an identifier that starts with an upper-case letter, and indicates the name of a type, either specified throughout the ASN.1 IDL, or from the UNIVERSAL namespace. Note that in the UNIVERSAL type namespace, there are a number of types that contains a space in it, like OCTET STRING, BIT STRING or OBJECT IDENTIFIER. Only these universally known types may contain the space character as part of the name.
  • value-name: Values (non-type fields and identifiers) starts with a a lower-case letter, and identified a name pointing to a value with a type.
  • SubType: is a modification of a type, usually enclosed in (...), but sometimes not, exceptions will be mentioned later.
  • String literals may be:
    • "Double quoted for unicode string"
    • '0100'B for encoding bit string, with single-quotes.
    • '917421436587'H for hexadecimal encoded octet strings, with single-quites.
    • foo or {'foo' 9 'bar'} using single quotes, or bracket enclosed sequence of single-quote strings and decimal code points for ASCII encoded strings (aka IA5 in ASN.1 spec), where the code-points can represent non-printable characters.
  • Integer numbers are of the form -?[1-9][0-9]*, and can represent all
  • It is not allowed to represent decimal form numbers (with a dot), but are defined

Each ASN.1 file is referenced as a Module.

Module-Name { module object id }
DEFINITIONS
-- add file options here
-- ( AUTOMATIC | IMPLICIT | EXPLICIT )? TAGS
BEGIN

-- put stuff here

END

Note that the ID part is not mandatory, but if it is missing, this ASN.1 spec may not be imported into other ASN.1 spec files. If EXPORTS is specified, then ONLY those types and values may be referenced outside this module, though if no EXPORTS are present, then ALL types and values may be referenced outside the module.

EXPORTS Type-Name, Type-Name2;

For any type or value not defined in the module itself, or in the UNIVERSAL type namespace only imported IDs may be used in this module.

IMPORTS
   Type-Name, value-name FROM Other-File-Spec { id }
   Type-Name2 FROM Third-File-Spec { id };

Note that the ID part is mandatory when importing.

Type-Name ::= `Type-Definition`
Class-Name ::= CLASS `Class-Specification`
Type-Set `Type-Specification` ::= { v1 | v2 }

value-name ::= `Type-Specification` `value`
value-name `Type-Specification` ::= `value`
value-name `Type-Specification` ::= { `value specification` }

Where I use the term Type definition to be how to define a type, while the term Type specification is a reference to a type with optional sub-typing. The latter one have less flexibility when it comes to what it can specify, but both of them can take a type and modify them before using it in its relative position.

And yes, for some reason both ways of defining values are allowed (:facepalm:).

Classes

Class-Name ::= CLASS {
  &Class-Type,
  &class-value Value-Type
} WITH SYNTAX {
  [`optional string containing one or more fields from above`]
  `string containing fields from above`
  `in total all fields must be referenced exactly once`
}
class-Value Class-Name ::= {
  SYNTAX WORD value-name
  MORE WORDS Type-Name
  EVEN MORE WORDS 42
}
Class-Object-Set Class-Name ::= { class-Value | class-Value-2, ... }

-- AFAIK only SEQUENCE types may be templated in this way.
Templated-Type ::= SEQUENCE {
  identifier  Class-Name.&class-value({Class-Object-Set}),
  argument    Class-Name.&Class-Type({Class-Object-Set}{@identifier})
}
  • TODO: Figure out the ANY DEFINED BY type. It looks like the only reasonable way to represent is is to have a TLV of unknown type, and let the implementers connect the dots.

And yes, both ways

Constructed types.

There exist 3 types of constructed types:

  • CHOICE
  • SEQUENCE
  • SET
Constructed-Type ::= Base-Type {
   component Component-Type,
   ..., -- either this or ...
   ...! err1 err2, -- <- exception marker ?????
   extended Component-Type-2 -- for version2
}
err1 INTEGER ::= 501
err2 INTEGER ::= 503

Constructed types and extensibility.

Normally constructed types have a fixed component set, but by either adding the ... extensible-marker component, or by setting.

Choice

A choice is a meta-type that signifies the presence of at most one of a set of values.

Choice-Type ::= CHOICE {
  first-choice [0] Value-Type-1,
  second-choice [1] Value-Type-2
}
  • Type matching a CHOICE is the same as matching any of its fields, and it is not encoded into it's own constructed group unless used in an EXPLICIT numbered field in a sequence or set.

Sequence and Set

Sequences and set's are pretty similar, but where the SEQUENCE type can take advantage of the fact that fields must come in the order they were defined.

Sub-Types and constraints

String-Type ::= UTF8String (SIZE (from..to)) -- bounded
String-Type ::= UTF8String (SIZE (from..)) -- unbounded on higher numbers
String-Type ::= UTF8String (SIZE (fixed)) -- fixed size
String-Type ::= IA5String (FROM ("A" .. "Z")) -- limited alphabet
String-Type ::= IA5String (FROM ("0" .. "9", A" .. "Z")) (SIZE (1 ..)) -- limited alphabet
Integer-Type ::= INTEGER (min..max) -- integer with bounded value.
Integer-Type ::= INTEGER (min..) -- integer with lower-bound value.
Integer-Type ::= INTEGER (..max) -- integer with upper-bound value.
Sequence-Type ::= Origin-Type (WITH COMPONENTS {..., `additional components`})

For extending a sequence with additional components: They will be inserted where the extension point is, or at the end if