Packages

Elixir code signing and verification by embedding Ed25519 signatures in BEAM files.

Current section

Files

Jump to
code_signing lib code_signing.ex
Raw

lib/code_signing.ex

defmodule CodeSigning do
@moduledoc """
Code signing and verification functions for BEAM binaries using Ed25519 signatures.
All strings for paths need to be passed as charlists for Erlang compatibility.
"""
@typedoc """
The filename as a string or the BEAM module binary.
"""
@type beam :: charlist() | binary()
@typedoc """
A tuple of the chunk ID and its binary data.
"""
@type chunkdata :: {charlist(), binary()}
@doc """
Signs the the given BEAM binary or path to `.beam` file using the Ed25519 `secret_key`.
Returns the modified binary that can be written to a file.
When given a BEAM binary, it will sign the binary.
When given a path to a `.beam` file, it will sign the binary without modifying the original file.
"""
@spec sign(beam(), Ed25519.key()) :: binary
def sign(module_or_path, secret_key) do
chunks = module_chunks(module_or_path)
chunks
|> sign_bytecode(secret_key)
|> (&write_signature_attribute(chunks, &1)).()
|> build_module
end
@doc """
Verifies the signature of the given BEAM binary or path to `.beam` file using the
Ed25519 `public_key`.
When given a BEAM binary, it will verify the signature of the binary.
When given a path to a `.beam` file, it will verify the signature of the binary without
modifying the original file.
"""
@spec valid_signature?(beam(), Ed25519.key()) :: boolean
def valid_signature?(module_or_path, public_key) do
chunks = module_chunks(module_or_path)
code = chunks |> code_binary
chunks
|> read_signature_attribute
|> Ed25519.valid_signature?(code, public_key)
end
@doc """
Verifies the signature of the given BEAM binary using the Ed25519 `public_key`. If the
signature is valid, the module will be loaded.
"""
@spec load(atom(), binary(), Ed25519.key()) :: :ok | :error
def load(module, binary, public_key) do
case valid_signature?(binary, public_key) do
true ->
:code.load_binary(module, nil, binary)
:ok
_ ->
:error
end
end
@doc """
Verifies the signature of the given path to `.beam` file using the
Ed25519 `public_key`. If the signature is valid, the module will be loaded.
Module names should be atoms prefixed with Elixir, such as `String.to_atom("Elixir.MyModule")`
"""
@spec load_file(atom(), charlist(), Ed25519.key()) :: :ok | :error
def load_file(module, beam_path, public_key) do
case valid_signature?(beam_path, public_key) do
true ->
{:ok, binary, _} = :erl_prim_loader.get_file(beam_path)
:code.load_binary(module, beam_path, binary)
:ok
_ ->
:error
end
end
@spec sign_bytecode([chunkdata()], Ed25519.key()) :: Ed25519.signature()
defp sign_bytecode(chunks, secret_key) do
chunks |> code_binary |> Ed25519.signature(secret_key)
end
@spec read_signature_attribute([chunkdata()]) :: Ed25519.signature()
defp read_signature_attribute(chunks) do
case :lists.keyfind('Attr', 1, chunks) do
{'Attr', attributes} ->
case :erlang.binary_to_term(attributes) |> Keyword.get(:signature) do
nil -> nil
signature -> signature |> hd
end
_ ->
nil
end
end
@spec write_signature_attribute([chunkdata()], Ed25519.signature()) :: [chunkdata()]
defp write_signature_attribute(chunks, signature) do
case :lists.keyfind('Attr', 1, chunks) do
{'Attr', attributes} ->
attribute_list = [signature: [signature]] ++ :erlang.binary_to_term(attributes)
:lists.keyreplace('Attr', 1, chunks, {'Attr', :erlang.term_to_binary(attribute_list)})
_ ->
attribute_list = [signature: [signature]]
:lists.append(chunks, [{'Attr', :erlang.term_to_binary(attribute_list)}])
end
end
@spec code_binary([chunkdata()]) :: binary
defp code_binary(chunks) do
with {'Code', code} <- :lists.keyfind('Code', 1, chunks) do
code
end
end
@spec module_chunks(beam()) :: [chunkdata()]
defp module_chunks(module_or_path) do
with {:ok, _, chunks} <- :beam_lib.all_chunks(module_or_path) do
chunks
end
end
@spec build_module([chunkdata()]) :: binary
defp build_module(chunks) do
with {:ok, binary} <- :beam_lib.build_module(chunks) do
binary
end
end
end