66 lines
4.4 KiB
Markdown
66 lines
4.4 KiB
Markdown
---
|
|
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.
|