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
endThe `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.
References
• 009. Sensitive information in source code