XmerlC14n implements XML canonicalization (c14n) for use in XML signing.
The package can be installed by adding xmerl_c14n
to your list of
dependencies in mix.exs
:
def deps do
[
{:xmerl_c14n, "~> 0.1.0"}
]
end
Cryptographically signing XML requires that a particular subdocument, for
example, the contents of a soap:Body
element, be canonicalized. Necessary
namespace attributes that are outside the subdocument are pulled into it, and
duplicates merged at the lowest level that they are needed. Attributes are
sorted in a particular order, with xmlns
namespaces coming first, then
un-namespaced attributes sorted alphabetically, then namespaced attributes
sorted not alphabetically by name, but alphabetically by the URI of their
associated namespace.
The only existing implementation I could find for this was in the
esaml project, implemented in Erlang.
Unfortunately, I was unable to use the entire esaml project, as it required a
much older version of Cowboy than we were using. Using Erlang from an Elixir
project is very simple, just drop the module in the src
directory, and use
mix erlang.compile
. However, we ran into an edge case where we really could
have used IO.inspect
to see what was happening internally. There are ways to
do similar debugging in Erlang, but I'm not as experienced with it, and prefer
Elixir's conventions, so I decided to port the module to Elixir, and here we
are.
First, you'll need to load your XML into Erlang-compatible tuples. Technically, they're Records, and you shouldn't rely on the fact that they're tuples under the hood, but Records have been deprecated in Elixir, except specifically for doing Erlang interop.
You can use Records from Elixir fairly easy using the macros in the Record
module, or using the tuples directly.
defmodule RecordTest do
require Record
Record.defrecord(:xmlAttribute, Record.extract(:xmlAttribute, from_lib: "xmerl/include/xmerl.hrl"))
end
iex> require RecordTest
RecordTest
iex> RecordTest.xmlAttribute()
{:xmlAttribute, :undefined, [], [], [], [], :undefined, [], :undefined, :undefined}
iex> RecordTest.xmlAttribute(name: 'foo', value: 'bar')
{:xmlAttribute, 'foo', [], [], [], [], :undefined, [], 'bar', :undefined}
It's definitely easier to understand creating Records with a keyword list
instead of remembering that :name
is the second value in the tuple after
:xmlAttribute
, and that :value
is the 9th. You more than likely won't have
to deal with the tuples yourself other than loading them from the library of
your choice. I highly recommend SweetXml,
which is a thin Elixir wrapper around Erlang's xmerl
module, but if you don't
want to pull in another dependency, xmerl
is easy enough.
# Parsing with xmerl
iex> xml = "<foo><bar>some text</bar></foo>"
"<foo><bar>some text</bar></foo>"
iex> {doc, _} = :xmerl_scan.string(to_charlist(xml), namespace_conformant: true, document: true)
{{:xmlDocument,
[
{:xmlElement, :foo, :foo, [], {:xmlNamespace, [], []}, [], 1, [],
[
{:xmlElement, :bar, :bar, [], {:xmlNamespace, [], []}, [foo: 1], 1, [],
[{:xmlText, [bar: 1, foo: 1], 1, [], 'some text', :text}], [],
'', :undeclared}
], [], '', :undeclared}
]}, []}
# Parsing with SweetXml
iex> doc = SweetXml.parse(xml, namespace_conformant: true, document: true)
{:xmlDocument,
[
{:xmlElement, :foo, :foo, [], {:xmlNamespace, [], []}, [], 1, [],
[
{:xmlElement, :bar, :bar, [], {:xmlNamespace, [], []}, [foo: 1], 1, [],
[{:xmlText, [bar: 1, foo: 1], 1, [], 'some text', :text}], [],
'', :undeclared}
], [], '', :undeclared}
]}
I prefer SweetXml because you don't have to worry about converting the string to a charlist, or pulling the tuples out of the 2-tuple they're parsed into.
From there, you simply pass the tuples to XmerlC14n.canonicalize!
, and it'll
spit back out the canonicalized XML string you need.
iex> doc |> XmerlC14n.canonicalize!()
"<foo><bar>some text</bar></foo>"
iex> doc |> XmerlC14n.canonicalize()
{:ok, "<foo><bar>some text</bar></foo>"}
According to the usual Elixir way of doing things, there's a version with and
without the bang (!
). canonicalize
will return either {:ok, canonicalized_xml}
or {:error, {:failed_canonicalization, error_message}}
,
while canonicalize!
will either return just the canonicalized XML string, or
raise an ArgumentError
for you to catch.
Currently, the API only accepts tuples of the kind used by xmerl
, Erlang's
XML-processing library, but may accept strings/filenames in future versions.