Skip to content

Conversation

@schbol
Copy link

@schbol schbol commented Oct 24, 2025

Issue

When signing UTF-8 encoded messages containing multi-byte characters (like German umlauts ö, ü, ä), Ed25519 signatures are incorrectly sized and contain cleartext from the message.

Demonstration:

require 'rbnacl'

key = RbNaCl::SigningKey.generate
message = "Björn Müller from München"

signature = key.sign(message)
puts signature # => "\xE0\x89\xA5\xA1\xA9khߠ\x9E\xF2A\xB3<\xB1\xE3m]\x925\x8ELU\xD0F\xAAǪi\xA6П\xE1ӥ\xD9ֻ\xD0z\xF5\xED䧏\xFC\u0015Cηg%\xAF\xAE\xECg\x80G\xFB\xB5\u0013\xEA\rBjörn Müller from Münche"
puts signature.length # => 90

Security Impact

This is a security issue. The signature leaks portions of the cleartext message:

  • Ed25519 signatures should always be exactly 64 bytes of cryptographic data
  • With this bug, signatures become 80-100+ bytes and contain readable cleartext
  • The cleartext portion starts at byte 64 and continues for the length of the multi-byte character expansion

This violates the fundamental expectation that signatures are opaque cryptographic values and should not leak message content. Also, I did not immediately find any documentation that rbnacl does only accept binary encoded strings. Even if it exists somewhere, not following the documentation should lead to an exception, not leaking parts of the signed message.

Fix

Force messages to binary encoding (ASCII-8BIT) before passing them to libsodium functions:

  • SigningKey#sign: Convert message to binary before signing
  • VerifyKey#verify: Convert message to binary before concatenation

When signing UTF-8 encoded messages containing multi-byte characters
(like German umlauts ö, ü, ä), the signature could be incorrectly
sized (e.g., 90 bytes instead of 64 bytes). This occurred because
Ruby's string slicing [0, n] operates on characters (not bytes) when
the string has UTF-8 encoding.

The issue affected both signing and verification:

1. SigningKey#sign: The line `sign_attached(message)[0, signature_bytes]`
   would slice the first 64 characters instead of the first 64 bytes,
   resulting in signatures larger than 64 bytes when the combined
   signature+message buffer inherited UTF-8 encoding from the message.

2. VerifyKey#verify: The concatenation `signature + message` would fail
   with an encoding compatibility error when trying to combine an
   ASCII-8BIT signature with a UTF-8 message.

Fix: Force messages to binary encoding (ASCII-8BIT) before passing them
to the underlying libsodium functions. This ensures:
- String slicing operates on bytes, not characters
- Signatures are always exactly 64 bytes
- No encoding compatibility errors during verification

Added test case with German umlauts to prevent regression.
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