ariadne.space/content/blog/using-otp-asn-1-support-wit...

66 lines
4.4 KiB
Markdown
Raw Normal View History

2022-08-02 22:16:40 +00:00
---
title: "Using OTP ASN.1 support with Elixir"
date: "2019-10-21"
---
The OTP ecosystem which grew out of Erlang has all sorts of useful applications included with it, such as support for [encoding and decoding ASN.1 messages based on ASN.1 definition files](http://erlang.org/doc/apps/asn1/).
I recently began work on [Cacophony](https://git.pleroma.social/pleroma/cacophony), which is a programmable LDAP server implementation, intended to be embedded in the Pleroma platform as part of the authentication components. This is intended to allow applications which support LDAP-based authentication to connect to Pleroma as a single sign-on solution. More on that later, that's not what this post is about.
## Compiling ASN.1 files with `mix`
The first thing you need to do in order to make use of the `asn1` application is install a mix task to compile the files. Thankfully, [somebody already published a Mix task to accomplish this](https://github.com/vicentfg/asn1ex). To use it, you need to make a few changes to your `mix.exs` file:
1. Add `compilers: [:asn1] ++ Mix.compilers()` to your project function.
2. Add `{:asn1ex, git: "https://github.com/vicentfg/asn1ex"}` in the dependencies section.
After that, run `mix deps.get` to install the Mix task into your project.
Once you're done, you just place your ASN.1 definitions file in the `asn1` directory, and it will generate a parser in the `src` directory when you compile your project. The generated parser module will be automatically loaded into your application, so don't worry about it.
For example, if you have `asn1/LDAP.asn1`, the compiler will generate `src/LDAP.erl` and `src/LDAP.hrl`, and the generated module can be called as `:LDAP` in your Elixir code.
## How the generated ASN.1 parser works
ASN.1 objects are marshaled (encoded) and demarshaled (parsed) to and from Erlang records. Erlang records are essentially tuples which begin with an atom that identifies the type of the record.
[Elixir provides a module for working with records](https://hexdocs.pm/elixir/master/Record.html), which comes with some documentation that explain the concept in more detail, but overall the functions in the `Record` module are unnecessary and not really worth using, I just mention it for completeness.
Here is an example of a record that contains sub-records inside it. We will be using this record for our examples.
```
message = {:LDAPMessage, 1, {:unbindRequest, :NULL}, :asn1_NOVALUE}
```
This message maps to an LDAP `unbindRequest`, inside an LDAP envelope. The `unbindRequest` carries a null payload, which is represented by `:NULL`.
The LDAP envelope (the outer record) contains three fields: the message ID, the request itself, and an optional access-control modifier, which we don't want to send, so we use the special `:asn1_NOVALUE` parameter. Accordingly, this message has an ID of 1 and represents an `unbindRequest` without any special access-control modifiers.
### Encoding messages with the `encode/2` function
To encode a message, you must represent it in the form of an Erlang record, as shown in our example. Once you have the Erlang record, you pass it to the `encode/2` function:
```
iex(1)> message = {:LDAPMessage, 1, {:unbindRequest, :NULL}, :asn1_NOVALUE}
{:LDAPMessage, 1, {:unbindRequest, :NULL}, :asn1_NOVALUE}
iex(2)> {:ok, msg} = :LDAP.encode(:LDAPMessage, message)
{:ok, <<48, 5, 2, 1, 1, 66, 0>>}
```
The first parameter is the Erlang record type of the outside message. An astute observer will notice that this signature has a peculiar quality: it takes the Erlang record type as a separate parameter as well as the record. This is because the generated encode and decode functions are recursive-descent, meaning they walk the passed record as a tree and recurse downward on elements of the record!
### Decoding messages with the `decode/2` function
Now that we have encoded a message, how do we decode one? Well, lets use our `msg` as an example:
```
iex(6)> {:ok, decoded} = :LDAP.decode(:LDAPMessage, msg)
{:ok, {:LDAPMessage, 1, {:unbindRequest, :NULL}, :asn1_NOVALUE}}
iex(7)> decoded == message
true
```
As you can see, decoding works the same way as encoding, except the input and output are reversed: you pass in the binary message and get an Erlang record out.
Hopefully this blog post is useful in answering questions that I am sure people have about making use of the `asn1` application with Elixir. There are basically no documentation or guides for it anywhere, which is why I wrote this post.