DEV Community

Ivan Rossouw
Ivan Rossouw

Posted on AI-assisted

An Empty Field Is Still Signed Data

An empty form field looks unimportant. In ordinary application logic, treating an empty value like a missing value is often reasonable. In signature verification, that assumption can reject a legitimate message—or, in a different protocol, verify a representation the sender never signed.

The important lesson is simple: canonicalization is not generic data cleanup. It is part of the cryptographic protocol.

The tidy abstraction that hid a real difference

Consider an integration with two flows:

  1. The application builds an outbound form and signs it.
  2. A provider later sends an inbound form notification that the application verifies.

Both flows need URL encoding, ordered key=value pairs, a digest, and a comparison. A shared helper therefore seems sensible.

But suppose the protocol defines different empty-field rules. The outbound form omits empty values. The inbound notification includes every posted field, even an empty one represented as description=. Reusing the outbound rule during verification silently removes part of the inbound canonical representation.

Nothing is wrong with the network message. Nothing is wrong with the secret. The application simply hashes different bytes from the sender.

That is the dangerous kind of bug: the code is deterministic, the crypto primitive behaves correctly, and the abstraction looks clean. The failure lives in the representation contract.

Empty and absent are different inputs

Application code tends to reason semantically:

  • note= means there is no useful note.
  • no note field also means there is no useful note.

A signing algorithm reasons syntactically. It may see:

reference=abc&note=&status=complete
Enter fullscreen mode Exit fullscreen mode

as a different message from:

reference=abc&status=complete
Enter fullscreen mode Exit fullscreen mode

The same applies to field order, repeated keys, character encoding, spaces, escaping, decimal formatting, and line endings. Two representations can produce the same business meaning while producing different signatures.

That is why “normalize before verifying” is unsafe advice unless the protocol defines the normalization precisely. The verifier’s job is not to improve the input. Its job is to reconstruct exactly what the other side signed.

Share mechanics, not contracts

The repair pattern I prefer keeps the truly common pieces together while making direction-specific rules visible.

Shared mechanics may include:

  • the exact form-encoding routine;
  • digest calculation;
  • secret or passphrase handling;
  • fixed-time signature comparison;
  • consistent fail-closed behaviour.

The canonicalization policies remain separate:

  • outbound signing can omit empty fields when the outbound specification says so;
  • inbound verification can retain empty fields when the inbound specification says so;
  • both can exclude the signature field itself;
  • both can preserve their required ordering.

This introduces a small amount of repetition. That is a healthy trade when the alternative is a boolean option buried inside a generic helper whose callers must remember invisible protocol rules. If a shared primitive is retained, give the public operations names and documentation that make the distinction impossible to miss.

Test the representation, not your implementation

Signature tests often contain a subtle circularity: the test calls the production signing helper to create the expected signature, then asks the same helper to verify it. The test proves internal consistency, not compatibility with the protocol.

A stronger test builds the canonical input independently. For the inbound case, start from a literal ordered field sequence containing several key= entries. Calculate the expected digest with a small test-only oracle or a published fixture. Then submit the body to the real verification path.

Useful negative cases include:

  • remove one empty field and expect rejection;
  • change field order and expect rejection when order is significant;
  • replace an empty field with an absent field;
  • vary spaces and reserved characters;
  • include a blank expected signature and fail closed;
  • change only the signature field and confirm it is excluded from canonical input.

These tests describe the protocol boundary directly. They are harder for a refactor to accidentally “update” into agreeing with the bug.

Exercise the framework pipeline too

A pure canonicalization test is necessary, but it is not sufficient for a web endpoint.

Framework model binding and form parsing can consume a request stream, decode characters, merge values, or present fields through an API with different ordering behaviour. Middleware can also affect buffering or body access. A verifier that works against a handcrafted dictionary may still fail when the real HTTP pipeline handles the request first.

Add one focused integration test that:

  1. creates the literal encoded body;
  2. independently calculates the signature over that exact representation;
  3. sends an actual form POST through the application’s HTTP stack;
  4. verifies that the original body remains available where required;
  5. proves the request reaches the normal accepted outcome.

This is not an argument for turning every edge case into a slow end-to-end test. Keep the combinatorial matrix in fast unit tests. Use the HTTP-level case as a boundary witness that the framework and your canonicalization contract still agree.

Verification is necessary, not complete trust

Correct signature verification answers a narrow question: does this message match the shared signing contract and secret?

It does not automatically prove that the event is fresh, unique, authorized for the referenced resource, or safe to apply twice. Production handlers may still need replay protection, timestamp or nonce validation, amount and ownership checks, idempotency, audit evidence, and careful failure handling.

Keeping that claim bounded matters. Strong security engineering is usually a chain of explicit checks, not one impressive cryptographic step.

A practical review checklist

When reviewing a signed integration, ask:

  • What exact bytes or characters are signed in each direction?
  • Are empty and missing values distinct?
  • Is field order defined and preserved?
  • When does decoding or encoding happen?
  • Are repeated fields possible?
  • Does the comparison avoid timing leaks?
  • Do invalid inputs fail closed?
  • Is at least one expected signature calculated independently?
  • Does a real HTTP test cover framework parsing?

The broader engineering principle is worth carrying elsewhere: an abstraction should share sameness, not erase difference. At a cryptographic boundary, the awkward representational details are often the contract itself.

Top comments (0)