logo

Database

Sensitive information in source code

Need

Removal of cryptographic keys from source code in favor of runtime configuration

Context

• Usage of Elixir 1.15+ for building application services

• Usage of the Erlang :crypto module for message authentication

Description

1. Non compliant code

defmodule MyApp.Webhooks do
  def sign(payload) do
    :crypto.mac(:hmac, :sha256, "secret-key-12345", payload)
  end
end

The `sign/1` function below computes an HMAC-SHA256 of a payload with the key `"secret-key-12345"` written as a literal. A key in the source code is available to everyone with access to the repository, stays in its history after it is removed and is embedded in the compiled release. With the key, an attacker can compute valid signatures for any payload, such as webhooks, download links or tokens, and the application accepts them as genuine. It also cannot be rotated without a new build. JWT signers created with `Joken.Signer.create("HS256", "hardcoded-secret")`, `Guardian` secrets in the code and keys passed as literals to `:crypto.crypto_one_time/4` have the same problem.

2. Steps

• Remove keys written as literals from calls to `:crypto.mac`, `:crypto.crypto_one_time`, `Joken.Signer.create` and Guardian configuration.

• Load keys in `config/runtime.exs` from environment variables injected from a secrets manager, and read them with `Application.fetch_env!/2`.

• Fail at startup when a key is missing or shorter than 32 bytes.

• Compare signatures with `Plug.Crypto.secure_compare/2`.

• Rotate every key that was committed, since it remains in the repository history and in built releases.

3. Secure code example

defmodule MyApp.Webhooks do
  def sign(payload) do
    :crypto.mac(:hmac, :sha256, signing_key(), payload)
  end

  def verify(payload, signature) do
    Plug.Crypto.secure_compare(sign(payload), signature)
  end...

The corrected function reads the key from the application environment with `Application.fetch_env!/2`. The value is set in `config/runtime.exs` from the `WEBHOOK_SIGNING_KEY` environment variable, which the deployment injects from a secrets manager, so it never appears in the repository or in the release and can be rotated by updating the secret and restarting. `fetch_env!/2` raises when the key is missing, so the application never signs with an empty or default key. `verify/2` compares signatures with `Plug.Crypto.secure_compare/2`, which takes the same time wherever the values differ, so an attacker cannot guess a valid signature byte by byte from response times.