Skip to content

The private key no longer reaches an exception report: sign/2 answers a mistyped call; the key may be passed by reference - #4

Merged
HackTuah merged 2 commits into
mainfrom
fix/key-by-reference
Sep 23, 2026
Merged

HackTuah merged 2 commits into
mainfrom
fix/key-by-reference

Conversation

@HackTuah

Copy link
Copy Markdown
Member

Closes the key leak the badge audit found: a mistyped call raised FunctionClauseError, which Elixir prints with its arguments, the private key among them (sign("bytes", %{private_key: key}) printed all 32 bytes). beam_mcp's Canonical.signature/3 raises the same way on mistyped options before this package is reached.

Two layers, both in this package; core is not touched

  • sign/2 is total: a mistyped call is answered {:error, :bad_arguments}, which carries nothing that was passed.
  • :private_key may be a zero-arity function returning the key (private_key: fn -> key end). A function prints as #Function<...>, so a key passed this way cannot appear in any exception, crash report or log line, core's included. The README, moduledoc and pages now use this form; the bytes form still works.

Evidence

  • Four tests were added before the code: 14 tests, 2 failures (the FunctionClauseError, and the reference refused). After the change: 14 tests, 0 failures.
  • The census flagged the new is_function/2 guard. It is now allowed, with its reason, and the zero-arity reference is pinned.
  • Four mutants were planted and each caught, then restored: dropping the answering clause, dropping the reference clause, echoing the options in the error, and resolving a function of any arity.
  • Credo reports no issues.

Second commit: the README's relative links to CONTRIBUTING, CODE_OF_CONDUCT and GOVERNANCE made mix docs print six warnings on main. The description of #3 claimed "docs without warnings", but that was measured before the README edit. They are now linked by their GitHub URLs; mix docs gives 0 warnings.

Release impact: a minor at the next release (0.x), since a form is added and a raise becomes an answer. The CHANGELOG says how to tell whether you are affected.

… a mistyped call, and the key may be passed by reference

Found by this package's own audit for the OpenSSF badge: a call that broke the documented
types raised FunctionClauseError, which Elixir prints with the call's arguments, so the key
reached whatever logged it. Measured on e5dd26e: sign("bytes", %{private_key: key}) printed
"** (FunctionClauseError) no function clause matching in BeamMCP.Signer.Ed25519.sign/2 ...
sign(\"bytes\", %{private_key: <<184, 26, ...>>})". beam_mcp's Canonical.signature/3 raises the
same way on mistyped options, before this package is reached, so a fix here alone could not
cover it unless the key stops being bytes in the options.

Two layers, both here, core untouched:
- sign/2 is total: a last clause answers {:error, :bad_arguments}, carrying nothing passed.
- :private_key may be a zero-arity function returning the 32 bytes; it is called once per
  signature and not kept. A function prints as #Function<...>, so a key passed by reference
  appears in no exception, crash report or log line, core's frames included. The README,
  moduledoc and pages now pass it that way. The bytes form still works.

Red first, the four tests added before the code: "14 tests, 2 failures" -- "a mistyped call is
answered {:error, :bad_arguments}, never raised" (** (FunctionClauseError) no function clause
matching in BeamMCP.Signer.Ed25519.sign/2) and "the key may be passed by reference" (refused
as not_32_bytes). The other two pinned properties that already held: a reference not giving
32 bytes is refused; through core, a referenced key is printed nowhere while a key passed as
bytes is (that assertion fails the day core answers a mistyped call). After: 14 tests, 0
failures. The census caught the new guard ([{:erlang, :is_function, 2}] outside the list); it
is allowed with its reason, and the third census test now pins the zero-arity reference.

Mutants, each red then restored: drop the answering clause (1 failure); drop the reference
clause (2); echo the options in the error (1); resolve a function of any arity (2).
Credo "found no issues". A minor at the next release (0.x): an accepted form added, and a
raise that becomes an answer; the CHANGELOG says how to tell whether you are affected.

Signed-off-by: Ayla Croft <aylacroft@proton.me>
…CODE_OF_CONDUCT, GOVERNANCE) linked by their GitHub URLs

df5f76e linked them relatively, and ex_doc resolves a relative link only to a file in its
extras, so mix docs printed six warnings (three links, two passes). The pull request that
added them said "docs without warnings"; that was measured before the README edit and was
not true of the merged tree. After: mix docs, 0 warnings.

Signed-off-by: Ayla Croft <aylacroft@proton.me>
@HackTuah
HackTuah merged commit 070a41e into main Sep 23, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant