KibiPay
HomeBlog › API Security

Authenticating an API with RFC 9421 HTTP Message Signatures

9 min read API Security
API SecurityRFC 9421
Authenticating an API with RFC 9421 HTTP Message Signatures

A bearer token is a password: whoever holds it can do anything, and if it leaks in a log or a proxy, it's game over. KibiPay authenticates its API differently — every request is signed using RFC 9421 HTTP Message Signatures, so the caller proves not just that they hold a key but that this exact request came from them and wasn't altered in transit.

What gets signed

RFC 9421 defines how to sign a set of HTTP "components" and carry the signature in headers. A KibiPay request sends four:

The covered components — the "signature base" — are, in exact order: @method, @path, @query, host, content-digest and date. Each is written as a labelled line and the lines are newline-joined, then signed. Covering @method and @path stops an attacker replaying a GET as a DELETE or pointing it at a different resource; covering @query — the raw query string — stops query-parameter tampering in transit; covering content-digest ties the signature to the body. Notably, reads are signed too, not just writes: a GET carries a full signature over an empty-body digest.

Ed25519, with the algorithm pinned

Production signatures use Ed25519, a modern elliptic-curve signature scheme — fast, small, and free of the footguns that dog RSA. Keys are generated as a standard PEM pair. There's a legacy hmac-sha256 mode for local development, but it's rejected at runtime unless an explicit dev secret is set, so it can never be the production path by accident.

One detail in the verifier is worth calling out because it's a classic vulnerability if you get it wrong: the algorithm used to verify is pinned from trusted key material, never read from the attacker-supplied Signature-Input. If you let the request tell you which algorithm to verify with, an attacker simply picks the weakest one — the "alg confusion" attack that has broken many JWT implementations. KibiPay's verifier requires the caller to supply the expected algorithm from the stored key, so the request can't choose its own verification.

Per-tenant keys, built for rotation

Each tenant registers one or more public keys, stored server-side in a keys table keyed by tenant and key id, holding the algorithm, the public PEM, and an active flag. Only the public half is ever persisted — the private key is returned exactly once when the key is issued and never stored, so a database compromise can't yield anyone's signing key. Because a tenant can have several active keys at once, rotation is a non-event: register the new key, start signing with it, retire the old one, no flag-day. The signed key id is what establishes the caller's identity — the verified tenant, not any client-asserted header, is the authoritative account for the request.

How this compares to Form3

KibiPay's scheme is explicitly built for "Form3 parity," and the developer tutorials are modelled on Form3's. The shared idea is the same: each client holds an asymmetric key and signs each request together with a digest of the body, so the server verifies against a public key rather than a shared secret. The difference is the envelope. Form3's approach signs using Authorization and Digest headers; KibiPay uses the standardized RFC 9421 header set — Signature-Input, Signature and Content-Digest — which is now an IETF standard rather than a bespoke scheme. On top of the baseline, KibiPay adds a one-time replay nonce, covered in Ed25519 request signing with a one-time replay nonce.

The net effect: a request that's been tampered with fails verification, a request captured from a log can't be replayed, and a leaked public key is worthless to an attacker. That's a materially stronger position than "hold this token and you're in."

Why signing beats bearer tokens

It's worth being explicit about what this design buys over the usual "send a secret token" approach. A bearer token is a single secret that grants everything; capture it once — from a log, a proxy, a crash dump — and you can impersonate the client until it's rotated. A signed request is different in kind. The server only ever stores the client's public key, so a database breach yields nothing an attacker can sign with. The signature covers the method, path, query and a digest of the body, so a captured request can't be altered — change any covered part and verification fails. And because the signature is bound to this request rather than being a reusable credential, a leaked signature is far less dangerous than a leaked token, especially once the one-time replay nonce ensures each one works exactly once. The trade-off is that signing is more work for the client than pasting a token into a header — which is exactly why the platform ships Form3-style tutorials and a shared signing helper, so the extra rigour costs integrators effort once, at build time, not forever.

See it in motion

KibiPay connects UK Faster Payments, Bacs, CHAPS, Mojaloop mobile money, a mock ACH rail and Solana behind one API, with a cross-rail alias directory, ISO 20022 messaging and real-time fraud & AML screening.

Open the live console Directory demo