3

Erlang & ASN.1(Abstract Syntax Notation One)

 2 years ago
source link: https://medium.com/erlang-battleground/erlang-asn-1-abstract-syntax-notation-one-deeb8300f479
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Erlang & ASN.1(Abstract Syntax Notation One)

"Many programs don't have a well-defined interface. They should have." — Joe Armstrong.

Hi folks! Ukrainian Erlanger is here 🤘! In this article, I want to talk about Erlang and ASN.1specifically about ASN.1 in Erlang. The purpose of this article is not to teach ASN.1 but to show the capabilities and interaction between Erlang/OTP with ASN.1. However, in the article, you will find a list of resources for training yourself and getting a deeper understanding of ASN.1 standards and much more.

It is probably worth noting that not every language can support ASN.1 out of the box — but this does not apply to Erlang/OTP! And this is amazing! Because every Erlang project can use this great functionality by default.

So, let's rock with it! 🤘

So, what is ASN.1? According to the Wiki ©:

Abstract Syntax Notation One (ASN.1) is a standard interface description language for defining data structures that can be serialized and deserialized in a cross-platform way.

The main areas where ASN.1 is used are in creating cryptographic programs and in the field of telecommunications. Sounds cool, right? But how exactly does Erlang work with ASN.1, and how can we apply it using the same ASN.1 for different clients written in other languages? Let's try to figure that out!

How exactly does Erlang work with ASN.1?

Erlang/OTP includes asn1, an application that collects modules for compile-time and runtime support of ASN.1.

Let's start with a simple example that we can take from Wikipedia:

FooProtocol DEFINITIONS ::= BEGIN

FooQuestion ::= SEQUENCE {
trackingNumber INTEGER,
question IA5String
}

FooAnswer ::= SEQUENCE {
questionNumber INTEGER,
answer BOOLEAN
}

END

We can save that into a file called FooProtocol.asn1. The filename should have the same name as written before "DEFINITIONS." At the same time, the file extension can be anything, but I've chosen "*.asn1" for convenience.

As you can see, this module (as other similar ASN.1 modules) will comply with the following skeleton template:

  • The module identifier: in our example, this is "FooProtocol."
  • The keyword DEFINITIONS
  • The symbol ::=
  • The module body, which will consist of the exports and imports statements, if any, followed by the type and value assignments, all enclosed between BEGIN and END.

Of course, for a deep understanding of ASN.1 or the case when you want to know a little bit more about data types, classes, structures, etc., you will need to refer to the sources listed in the central part of ASN.1 standard:

+------------+----------------+-----------------------------------+
| Standard | ISO | Description |
+------------+----------------+-----------------------------------+
| X.680 | ISO/IEC 8824-1 | Basic ASN.1 Notation |
| X.681 | ISO/IEC 8824-2 | Information Objects Specification |
| X.682 | ISO/IEC 8824-3 | Constraint Specification |
| X.683 | ISO/IEC 8824-4 | Parameterization |
| X.690 | ISO/IEC 8825-1 | Basic Encoding Rules |
| X.691 | ISO/IEC 8825-2 | Packed Encoding Rules |
+------------+----------------+-----------------------------------+

To compile an ASN.1 module with Erlang/OTP, you can use erlcin the shell:

$ erlc FooProtocol.asn1

Or you can compile ASN.1 modules in an Erlang shell like this:

1> asn1ct:compile('FooProtocol.asn1'). %% or: "FooProtocol.asn1"

At the same time please note that rebar3 doesn't yet have support for ASN.1. To compile ASN.1 modules in rebar3 projects, you will need to find and use existing plugins or create your own rebar3 plugin 😜.

Now, let's try to compile our module using Erlang:

$ erlc FooProtocol.asn1
$ tree -a
.
├── FooProtocol.asn
├── FooProtocol.asn1db
├── FooProtocol.beam
├── FooProtocol.erl
└── FooProtocol.hrl0 directories, 5 files

As you can see, Erlang/OTP created four additional files based on our simple exemplary module.

  • FooProtocol.erl — with encoding, decoding, and value functions.
  • FooProtocol.hrl — record structures based on the SEQUENCE definitions.
  • FooProtocol.asn1db — Intermediate format file, which can be used by the compiler when there are IMPORT definitions in other modules.
  • FooProtocol.beam — the compiled module.

It is good to know that you can choose any encoding rules. But if the encoding rule is omitted, BER(Basic Encoding Rules) will be used by default.

Let's play a little bit with those modules in an Erlang shell:

$ erl
Erlang/OTP 24 [erts-12.0.3] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1]Eshell V12.0.3 (abort with ^G)
1> rr("FooProtocol.hrl").
['FooAnswer','FooQuestion']2> {ok, EncQuestionBin} =
'FooProtocol':encode('FooQuestion',
#'FooQuestion'{
trackingNumber = 1,
question = "Do you understand?"}).
{ok,<<48,23,2,1,1,22,18,68,111,32,121,111,117,32,117,110,
100,101,114,115,116,97,110,100,63>>}3> {ok, EncAnswerBin} =
'FooProtocol':encode('FooAnswer',
#'FooAnswer'{
questionNumber = 1,
answer = true}).
{ok,<<48,6,2,1,1,1,1,255>>}4> 'FooProtocol':decode('FooQuestion', EncQuestionBin).
{ok,#'FooQuestion'{trackingNumber = 1,
question = "Do you understand?"}5> 'FooProtocol':decode('FooAnswer', EncAnswerBin).
{ok,#'FooAnswer'{questionNumber = 1,answer = true}}

Looks pretty exciting! 🌟

How can we apply it using the same ASN.1?

So, here is a thought: Can we use the same ASN.1 module but for different languages?
It sounds like a good challenge. For the server-side, I will choose Erlang/OTP. For the client-side, I will choose Python 3.

Let's create a simple UDP server on Erlang and a simple client on Python 3.

Erlang UDP server

ASN.1 module — FooProtocol.asn1:

FooProtocol DEFINITIONS ::= BEGIN

FooQuestion ::= SEQUENCE {
trackingNumber INTEGER,
question IA5String
}

FooAnswer ::= SEQUENCE {
questionNumber INTEGER,
answer BOOLEAN
}

END

Our UDP server should be straightforward, just like our ASN.1 model. We open a UDP port. Then, we keep listening on it. When we receive a UDP message, we verify that it has the structure defined in our ASN.1 module, and we can expect that our Python 3 client will send us the question: "Do you understand?". If we match it, in response, we should send an answer message with true as its value and with the same number ID of the question, encoded with our ASN.1 module (of course). For other unmatched questions, we will return the same answer but with false as its value, thus letting our client know that we do not understand its question. 😃

Erlang/OTP UDP server — asn_server.erl:

-module(asn_server).-export([start/0]).-include("FooProtocol.hrl").start() -> 
spawn(fun() -> server(8181) end).server(Port) ->
{ok, Socket} = gen_udp:open(Port, [binary, {active, false}]),
loop(Socket).loop(Socket) ->
inet:setopts(Socket, [{active, once}]),
receive
{udp, Socket, Host, Port, Bin} ->
Answer = handle_message(Bin),
gen_udp:send(Socket, Host, Port, Answer),
loop(Socket)
end.handle_message(Bin) ->
{ok, Dec} = 'FooProtocol':decode('FooQuestion', Bin),
case Dec of
#'FooQuestion'{trackingNumber = N,
question = "Do you understand?"} ->
encode_answer(N, true);
#'FooQuestion'{trackingNumber = N} ->
encode_answer(N, false)
end.encode_answer(Number, Boolean) ->
Rec = #'FooAnswer'{questionNumber = Number, answer = Boolean},
{ok, FooAnswer} = 'FooProtocol':encode('FooAnswer', Rec),
FooAnswer.

Compile all this stuff and run UDP server:

$ erlc FooProtocol.asn1 asn_server.erl
$ tree -a
.
├── asn_server.beam
├── asn_server.erl
├── FooProtocol.asn1
├── FooProtocol.asn1db
├── FooProtocol.beam
├── FooProtocol.erl
└── FooProtocol.hrl
$ erl
Erlang/OTP 24 [erts-12.0.3] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1]Eshell V12.0.3 (abort with ^G)
1> asn_server:start().
<0.82.0>

Python UDP client

As I mentioned before, not all programming languages can support ASN.1 out of the box. Well, Python is one of them 😜. After some Googling, I found some exciting tools: asn1tools. As I want to work with Python 3, I installed it using pip3:

$ sudo apt install python3-pip
$ pip3 install asn1tools

Now we can create our Python UDP client.

Our Python client will also be pretty straightforward. This client will open and listen to a UDP socket; then, it will generate one correct message and one error message for our UDP server based on the same ASN.1 module. It will send each message, then try to receive the corresponding responses, decode them based on ASN.1 and print out results in the shell.

Python 3 UDP client — asn_client.py:

#!/usr/bin/env python3import socket
import asn1toolsUDP_IP = "127.0.0.1"
UDP_PORT = 8181FOO = asn1tools.compile_files('FooProtocol.asn1')MESSAGE = FOO.encode('FooQuestion', {'trackingNumber': 1, 'question': 'Do you understand?'})
ERROR = FOO.encode('FooQuestion', {'trackingNumber': 2, 'question': 'Error?'})print("UDP target IP: %s" % UDP_IP)
print("UDP target port: %s" % UDP_PORT)
print("message: %s" % MESSAGE)
print("error message: %s" % ERROR)sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.sendto(MESSAGE, (UDP_IP, UDP_PORT))data, addr = sock.recvfrom(1024) # buffer size is 1024 bytes
answer = FOO.decode('FooAnswer', data),print("received message: %s" % answer)sock.sendto(ERROR, (UDP_IP, UDP_PORT))
data, addr = sock.recvfrom(1024) # buffer size is 1024 bytes
answer = FOO.decode('FooAnswer', data),print("received error: %s" % answer)

Of course, it's expected from all our files to be placed in the same folder. Now, let's try to run our UDP client:

$ chmod 777 asn_client.py
$ tree -a
.
├── asn_client.py
├── asn_server.beam
├── asn_server.erl
├── FooProtocol.asn1
├── FooProtocol.asn1db
├── FooProtocol.beam
├── FooProtocol.erl
└── FooProtocol.hrl
$ ./asn_client.py
UDP target IP: 127.0.0.1
UDP target port: 8181
message: bytearray(b'0\x17\x02\x01\x01\x16\x12Do you understand?')
error message: bytearray(b'0\x0b\x02\x01\x02\x16\x06Error?')
received message: {'questionNumber': 1, 'answer': True}
received error: {'questionNumber': 2, 'answer': False}

Nice! It looks like it all works as we expected! As you would notice, we have just been able to use the same ASN.1 model for a simple way to decode/encode binary messages in a UDP client/server model that was written in different languages. It's not a small feat! But we can do much more…

Where is ASN.1 used?

Well, e.g., Erlang/OTP uses ASN.1 at least for public_key and public_key_records. ASN.1 can also be used for messaging or protocol encryption and, of course, in multiple applications in the Telecom area. But it's up to you to find new uses for it, like our simple UDP example above. 😎


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK