Number of child PDUs: 7

Found child tag: 0 content length: 12

Found child tag: 1 content length: 15

Found child tag: 2 content length: 1

Found child tag: 3 content length: 1

Found child tag: 4 content length: 5

Found child tag: 5 content length: 2034

Found child tag: 6 content length: 19

Primitive Types

So far, we have only extracted information about the structure of the BER encoding. To understand what each component represents we must make sense of the different types.

Integer, Real, Boolean, Null and String-types are primitive types which map nicely to standard C++ types. Classes within the fast_ber namespace are defined for each of these. Each of these classes contain a partially encoded type to reduce the amount of work required when encoding and decoding. For example, if a Sequence is decoded and only four of ten fields are accessed, work can be avoided by not fully decoding the remaining six fields.

Integer, Real, Boolean and Null types have a constricted range of possible values, therefore a small static buffer is used to store object’s data. As strings are of indefinite length, memory allocations are required. In effort to reduce memory allocations as much as possible, abseil’s absl::InlinedVector class is used to store the partially encoded string.

absl::InlinedVector - contiguous data container with small buffer optimisation

GeneralizedTime contains a UTC timestamp and a timezone offset in hours and minutes. Rather than building the interface around std::chrono , conversions to absl::Time and absl::CivilSecond are provided as Abseil offers a clear and simple model for handling timezone offsets.

Like most other primitive types, fast_ber::GeneralizedTime stores its data in partially encoded form. The maximum length of the time format is known, therefore memory allocations are not required to store its content.

The following code snippet demonstrates how several different primitive types share a unified interface.

fast_ber::Integer i = 5;

fast_ber::OctetString s = “40”;

fast_ber::Real r = 8.6f;

fast_ber::Boolean b = true;

fast_ber::Null n = nullptr;

fast_ber::GeneralizedTime t = absl::Now();

fast_ber::ObjectIdentifier o = {1, 2, 100, 200, 500}; std::array<uint8_t, 1000> buffer = {};

fast_ber::encode(absl::Span<uint8_t>(pdu), i);

fast_ber::encode(absl::Span<uint8_t>(pdu), s);

fast_ber::encode(absl::Span<uint8_t>(pdu), r);

fast_ber::encode(absl::Span<uint8_t>(pdu), b);

fast_ber::encode(absl::Span<uint8_t>(pdu), n);

fast_ber::encode(absl::Span<uint8_t>(pdu), t);

fast_ber::encode(absl::Span<uint8_t>(pdu), o);

Enumerated

Enumerated types are also primitive. Any C++ enum or enum class can be encoded in fast_ber . This is achieved using SFINAE. The following template is enabled when instantiated with an enum, causing std::is_enum return true. The integer value of the enum is converted to the fast_ber::Integer type, and encoded with the Enumerated identifier.

SFINAE - substitution failure is not an error

template <typename Enumerated, typename ID = ExplicitIdentifier<UniversalTag::enumerated>,

typename std::enable_if<std::is_enum<Enumerated>{}, int>::type = 0>

EncodeResult encode(absl::Span<uint8_t> output, const Enumerated& input, const ID& id = ID{})

{

fast_ber::Integer i(static_cast<int64_t>(input));

return encode(output, i, id);

} enum class Enumerated {

a,

b,

c

}; std::array<uint8_t, 1000> buffer = {};

Enumerated e = Enumerated::a;

fast_ber::encode(absl::Span<uint8_t>(pdu), e);

Each of the primitive types can be constructed by assigning to a fast_ber::BerView applied to a valid BER PDU.

Constructed Types

The content of constructed types is zero, one or many child types. Constructed types are used to organise the structure of the encoded data.

Interesting compound types include Sequence, SequenceOf and Choice.

Sequence

A Sequence is a collection of types. Sequences are achieved in C++ by generating structs from an ASN.1 specification. For the most natural feel, it is important that the generated structs contain only public fields, with no implementation data or member functions.

To encode the Sequence, a function is generated which passes each of its fields to a variadic template. The template encodes each of the members and combines them into the encoded sequence. When reflection becomes available in C++ this generated code will no longer be required.

reflection - inspection of classes, fields and methods at compile time

-- ASN.1 Definition

Person ::= SEQUENCE {

name OCTET STRING,

age INTEGER,

height REAL,

time-of-birth GeneralizedTime

} // Corresponding Generated C++

struct Person {

fast_ber::OctetString name;

fast_ber::Integer name;

fast_ber::Real name;

fast_ber::GeneralizedTime name;

};

A field in a Sequence can be marked optional. absl::optional is used to represent this. If the field is present, it has the same encoding as if it were not optional. If it is not present the field is not encoded, taking no space in the complete PDU.

// A compiled collection with some optional members

struct Collection {

OctetString string;

Integer integer;

Optional<Boolean> optional_boolean;

Optional<GeneralizedTime> optional_timestamp;

};

SequenceOf

Similar to a vector or list, the SequenceOf type holds a number of objects of the same type. Due to this similarity, fast_ber::SequenceOf is simply a type alias to a vector, with encoding and decoding functions defined.

As with the string types, absl::InlinedVector is used instead of std::vector to reduce memory allocations. A default small buffer size of five was selected. To encode the sequence type each of the components are encoded into a buffer. The encoding is completed by wrapping the buffer with a BER header with the SequenceOf identifier.

// SequenceOf Definition

namespace fast_ber

{

constexpr const size_t default_inlined_size = 5; template <typename T, size_t N = default_inlined_size>

using SequenceOf = absl::InlinedVector<T, N>;

}

Choice

The ASN.1 Choice type allows a BER PDU to be one of many types. The type that has been selected is determined by its identifier. This behaviour is very similar to that of std::variant , introduced in C++17. The type fast_ber::Choice is simply a type alias to absl::variant .

When encoding the Choice the variant is visited and encoded. The choices within the variant must have unique tags so that they can be unambiguously identified. The Catch2 test case below demonstrates how fast_ber::Choice can hold one of multiple different types.

Catch2 - a simple to use unit testing framework

TEST_CASE("Choice: Basic choice")

{

fast_ber::Choice<fast_ber::Integer, fast_ber::OctetString> choice_1;

fast_ber::Choice<fast_ber::Integer, fast_ber::OctetString> choice_2; choice_1 = "Test string";

choice_2 = 10; REQUIRE(absl::holds_alternative<fast_ber::OctetString>(choice_1));

REQUIRE(absl::holds_alternative<fast_ber::Integer>(choice_2)); std::vector<uint8_t> buffer(100, 0x00); bool enc_success = fast_ber::encode(absl::Span<uint8_t>(buffer), choice_1).success;

bool dec_success = fast_ber::decode(absl::Span<uint8_t>(buffer), choice_2).success; REQUIRE(enc_success);

REQUIRE(dec_success); REQUIRE(absl::holds_alternative<fast_ber::OctetString>(choice_1));

REQUIRE(absl::holds_alternative<fast_ber::OctetString>(choice_2)); REQUIRE(choice_1 == choice_2);

}

Bringing it all Together

By combining what we have so far, it is possible to create a flexible but robust interface for intercommunication between applications.

As an example I will define an interface to specify a team of Pokemon characters. The following steps show definition of an ASN.1 specification, compilation into a C++ header file, and use within a C++ application.

-- pokemon.asn Pokemon DEFINITIONS AUTOMATIC TAGS ::= BEGIN Team ::= SEQUENCE {

team-name OCTET STRING,

members SEQUENCE OF Pokemon

} Pokemon ::= SEQUENCE {

name OCTET STRING,

category OCTET STRING,

type Type,

ability OCTET STRING,

weakness OCTET STRING,

weight INTEGER

} Type ::= ENUMERATED {

normal,

fire,

fighting,

water,

flying,

grass

} END

The .asn file is provided to fast_ber_compiler which generates C++ header files to be included in the user’s application.

$ ./fast_ber_compiler pokemon.asn pokemon

$ cat pokemon.hpp

#pragma once #include "fast_ber/ber_types/All.hpp" namespace fast_ber {

namespace Pokemon { enum class Type {

normal,

fire,

fighting,

water,

flying,

grass,

}; struct Pokemon {

OctetString name;

OctetString category;

Type type;

OctetString ability;

OctetString weakness;

Integer weight;

}; struct Team {

OctetString team_name;

SequenceOf<Pokemon> members;

}; } // End namespace Pokemon

} // End namespace fast_ber #include "pokemon.detail.hpp"

The generated header file is then included by the application which uses the types defined inside it.

// sample_encode.cpp #include "autogen/pokemon.hpp"

#include <fstream>

#include <iostream> int main()

{

fast_ber::Pokemon::Team team{"Sam's Team"};

fast_ber::Pokemon::Pokemon muchlax =

{"Munchlax",

"Big Eater",

fast_ber::Pokemon::Type::normal,

"Thick Fat, Pickup",

"Fighting",

105};

fast_ber::Pokemon::Pokemon piplup =

{"Piplup",

"Penguin",

fast_ber::Pokemon::Type::water,

"Torrent",

"Electric, Grass",

12};

team.members.push_back(muchlax);

team.members.push_back(piplup); std::array<uint8_t, 2000> buffer{};

const EncodeResult encode_result =

fast_ber::encode(absl::Span<uint8_t>(buffer), team);

if (!encode_result.success)

{

std::cout << "Failed to encode data

";

return -1;

} std::ofstream output("pokemon.ber");

if (!output.good())

{

std::cout << "Failed to open output file: pokemon.ber

";

return -1;

}

output.write(reinterpret_cast<const char*>(buffer.data()),

static_cast<std::streamsize>(encode_result.length));

return 0;

}

Running sample_encode produces a file pokemon.ber containing a PDU with information about the Pokemon team. An inverse application, sample_decode , decodes the contents of pokemon.ber and prints its contents.

$ ./sample_encode

$ ll

-rw-r--r-- 1 styler styler 125 Mar 30 15:26 pokemon.ber $ ./sample_decode

Sam's Team, 2 members Pokemon = Munchlax

Category = Big Eater

Type = Normal

Ability = Thick Fat, Pickup

Weakness = Fighting

Weight = 105 Pokemon = Piplup

Category = Penguin

Type = Water

Ability = Torrent

Weakness = Electric, Grass

Weight = 12

A third party Linux tool unber can be used to verify the encoding.

unber - tool to decode generic BER data, provided by asn1c

$ unber -m pokemon.ber

<C T="[UNIVERSAL 16]" TL="2" V="123">

<P T="[UNIVERSAL 4]" TL="2" V="10">Sam's Team</P>

<C T="[UNIVERSAL 16]" TL="2" V="109">

<C T="[UNIVERSAL 16]" TL="2" V="56">

<P T="[UNIVERSAL 4]" TL="2" V="8">Munchlax</P>

<P T="[UNIVERSAL 4]" TL="2" V="9">Big Eater</P>

<P T="[UNIVERSAL 10]" TL="2" V="1" F>0</P>

<P T="[UNIVERSAL 4]" TL="2" V="17">Thick Fat, Pickup</P>

<P T="[UNIVERSAL 4]" TL="2" V="8">Fighting</P>

<P T="[UNIVERSAL 2]" TL="2" V="1" F>105</P>

</C T="[UNIVERSAL 16]">

<C T="[UNIVERSAL 16]" TL="2" V="49">

<P T="[UNIVERSAL 4]" TL="2" V="6">Piplup</P>

<P T="[UNIVERSAL 4]" TL="2" V="7">Penguin</P>

<P T="[UNIVERSAL 10]" TL="2" V="1" F>3</P>

<P T="[UNIVERSAL 4]" TL="2" V="7">Torrent</P>

<P T="[UNIVERSAL 4]" TL="2" V="15">Electric, Grass</P>

<P T="[UNIVERSAL 2]" TL="2" V="1" F>12</P>

</C T="[UNIVERSAL 16]">

</C T="[UNIVERSAL 16]">

</C T="[UNIVERSAL 16]">

If you have made it this far, thank you, and please check out fast_ber on github!

References

A Layman’s Guide to ASN.1

http://luca.ntop.org/Teaching/Appunti/asn1.html

ITU-T X.690 ASN.1 encoding rules: Specification of Basic Encoding Rules

https://www.itu.int/rec/T-REC-X.690-201508-I/en

asn1c

https://github.com/vlm/asn1c

abseil

https://github.com/abseil/abseil-cpp

catch2

https://github.com/catchorg/Catch2

fast_ber

https://github.com/Samuel-Tyler/fast_ber