Has anyone used Erlang's :public_key.der_decode/2 function? I can't seem to get it working

I’ve been trying to figure this out for a few hours now and I’m running into a wall. I’m trying to decode a public key to get the bit strength. I’ve done this in other languages via libraries that interface with OpenSSL but every code example I see using Elixir defer to :public_key.der_decode/2. I’ve even resorted to ChatGPT to give me working example code with sample data and those examples give me the same error.

EDIT: Originally posted this with a sample key that I couldn’t even get working with the OpenSSL code, so I’ve updated it to one that has been verified to work under OpenSSL. Updated error has been posted.

I’m using Erlang 27.1 and as I was researching this I saw that :public_key saw some deprecations for security reasons and I was wondering if it was affected, but I rolled back to OTP 26 and still get the same error.

Any ideas would be greatly appreciated.

Here is the error:

%MatchError{
      term:
        {:error,
         {:asn1,
          {{:wrong_tag,
            {{:expected, 2},
             {:got, 16, {16, [{6, <<42, 134, 72, 134, 247, 13, 1, 1, 1>>}, {5, ""}]}}}},
           [
             {:"OTP-PUB-KEY", :match_tags, 2, [file: ~c"../src/OTP-PUB-KEY.erl", line: 24244]},
             {:"OTP-PUB-KEY", :decode_integer, 2,
              [file: ~c"../src/OTP-PUB-KEY.erl", line: 23761]},
             {:"OTP-PUB-KEY", :dec_RSAPublicKey, 2,
              [file: ~c"../src/OTP-PUB-KEY.erl", line: 2976]},
             {:"OTP-PUB-KEY", :decode, 2, [file: ~c"../src/OTP-PUB-KEY.erl", line: 1239]},
             {:public_key, :der_decode, 2, [file: ~c"public_key.erl", line: 353]},
             {PublicKeyDecoder, :decode_rsa_public_key, 1,
              [file: ~c"lib/public_key_decoder.ex", line: 13]},
             {PublicKeyDecoder, :example_usage, 0,
              [file: ~c"lib/public_key_decoder.ex", line: 39]},
             {:elixir, :eval_external_handler, 3, [file: ~c"src/elixir.erl", line: 386]},
             {:erl_eval, :do_apply, 7, [file: ~c"erl_eval.erl", line: 750]},
             {:elixir, :eval_forms, 4, [file: ~c"src/elixir.erl", line: 364]},
             {Module.ParallelChecker, :verify, 1,
              [file: ~c"lib/module/parallel_checker.ex", line: 120]},
             {IEx.Evaluator, :eval_and_inspect, 3, [file: ~c"lib/iex/evaluator.ex", line: 336]},
             {IEx.Evaluator, :eval_and_inspect_parsed, 3,
              [file: ~c"lib/iex/evaluator.ex", line: 310]},
             {IEx.Evaluator, :parse_eval_inspect, 4, [file: ~c"lib/iex/evaluator.ex", line: 299]},
             {IEx.Evaluator, :loop, 1, [file: ~c"lib/iex/evaluator.ex", line: 189]},
             {IEx.Evaluator, :init, 5, [file: ~c"lib/iex/evaluator.ex", line: 34]},
             {:proc_lib, :init_p_do_apply, 3, [file: ~c"proc_lib.erl", line: 241]}
           ]}}}
    }

Below I’ll provide some sample code from ChatGPT that’s giving the same errors as my code if you’d like to reproduce it.

Just call PublicKeyDecoder.example_usage() to try it.

defmodule PublicKeyDecoder do
  @moduledoc """
  A module to demonstrate decoding a DER-encoded RSA public key using :public_key.der_decode.
  """

  @doc """
  Decodes a DER-encoded RSA public key and extracts its modulus and exponent.

  - `der_encoded_key` should be the binary representation of the public key.
  """
  def decode_rsa_public_key(der_encoded_key) do
    try do
      {:RSAPublicKey, modulus, exponent} = :public_key.der_decode(:RSAPublicKey, der_encoded_key)
      {:ok, %{modulus: modulus, exponent: exponent}}
    rescue
      e -> {:error, "Failed to decode RSA public key: #{inspect(e)}"}
    end
  end

  @doc """
  Calculate the bit strength of the modulus.

  - `modulus` is the integer modulus extracted from the decoded public key.
  """
  def calculate_bit_strength(modulus) do
    :erlang.bit_size(modulus)
  end

  def example_usage() do
    # Example Usage

    # This is a sample Base64-encoded DER-encoded RSA public key.
   base64_public_key = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDW8q5LtUKpUOLpWqiGfDzbUMjP+MEBzfYOq8q1hCST/wyoBqJznRhhKLERfWQ7GKK8X/6hQotoPBEFF2PfZaIXvahalOs7Q40EdCtooCb0Vt/sGH5DIeTWSTFTwHlINHFKBKnH/0oW24XrjxW3jcBmgIxQNBFoOQFHhmjshjLjbQIDAQAB"


    # Decode the Base64 key into binary
    {:ok, der_encoded_key} = Base.decode64(base64_public_key)

    # Decode the RSA public key
    case PublicKeyDecoder.decode_rsa_public_key(der_encoded_key) do
      {:ok, %{modulus: modulus, exponent: exponent}} ->
        IO.puts("Modulus: #{inspect(modulus)}")
        IO.puts("Exponent: #{exponent}")
        bit_strength = PublicKeyDecoder.calculate_bit_strength(modulus)
        IO.puts("Bit strength: #{bit_strength} bits")

      {:error, reason} ->
        IO.puts("Error: #{reason}")
    end
  end
end

There are two different formats for an “RSA public key”, and der_decode expects one while the input is the opposite.

Backstory:

TLDR:

There are two different ASN.1 payloads for an RSA public key:

  • a “simple” one that’s just the modulus and exponent. Used with the -----BEGIN RSA PUBLiC KEY----- PEM header
  • a “generic” one that uses SubjectPublicKeyInfo and then nests the modulus + exponent inside

Your input is the latter, but der_decode is expecting the former.

The PEM machinery can handle this correctly:

:public_key.pem_decode("-----BEGIN PUBLIC KEY-----\n" <> base64_public_key <> "\n-----END PUBLIC KEY-----\n")
|> hd()
|> :public_key.pem_entry_decode()

# gives:
{:RSAPublicKey,
 150941599098518570126874683465281552058004089482812534451212203604139744439191153555431393200565235808092997600710339447131887772472013591195906357127812043735918563281687598329506755792366482304448753019436841738444187832077672451367879120396964808983355846518616258864015758142866753194926761306968328168301,
 65537}
6 Likes

Thank you for the detailed answer. I’ve read over the Stack Overflow answer and I’ve tried your solution here and I do get the output you mentioned, but I can’t figure out how to get the bit strength from it?

Just a quick example, this is what the code looked like in Ruby.

    decoded_key = Base64.decode64(key_string.to_s)
    rsa_key = OpenSSL::PKey::RSA.new(decoded_key)
    rsa_key.n.num_bits

That would give me a simple 1024 for the key given above. All I’m trying to do is check if a DKIM public key is 512, 1024, 2048 or 4096.

I don’t see the corresponding BN_num_bits function from OpenSSL exposed anywhere in OTP’s implementation - it’s used in a few places but only from C.

A simple alternative would be :binary.encode_unsigned/1 followed by bit_size:

{_, n, _} =
  :public_key.pem_decode("-----BEGIN PUBLIC KEY-----\n" <> base64_public_key <> "\n-----END PUBLIC KEY-----\n")
  |> hd()
  |> :public_key.pem_entry_decode()

bits = n |> :binary.encode_unsigned() |> bit_size()

# bits will be 1024
3 Likes

Thanks! This worked like a charm. I’ll share the final function that resulted from it.

Still need to add some error checking probably, but this worked great. Tested it on an assortment of keys and it worked everytime.

  def calculate_key_strength(%{"k" => "ed25519"}), do: 256
  def calculate_key_strength(%{"k" => "rsa", "p" => key}) do

    { _key_type, modulus, _exponent} =
      :public_key.pem_decode("-----BEGIN PUBLIC KEY-----\n" <> key <> "\n-----END PUBLIC KEY-----\n")
      |> hd()
      |> :public_key.pem_entry_decode()

      modulus |> :binary.encode_unsigned() |> bit_size()
  end
1 Like