HTTP Message Signatures (RFC 9421): what Signature-Input actually signs
What Signature-Input and Signature carry in RFC 9421, how the signature base is built, and what keyid, created, nonce and Content-Digest (RFC 9530) do not guarantee.
Say a request carries Signature-Input and Signature headers, and the signature verifies. That still doesn’t mean the whole request was signed. RFC 9421 HTTP Message Signatures build a string called the “signature base” from a selected set of message components and sign that string. The selection is written in Signature-Input, and anything not in it can change without affecting the signature. You need this structure in mind to judge what a successful verification does and doesn’t tell you.
This post is based on RFC 9421 HTTP Message Signatures and RFC 9530 Digest Fields, which defines the content digest fields. Both are IETF Standards Track documents published in February 2024 (listed by the RFC Editor as Proposed Standard), and both were checked on 2026-10-20. On that date the RFC Editor info pages showed no document updating or obsoleting either one, and showed RFC 9530 as obsoleting RFC 3230. RFC 9421 has two verified errata, both correcting @signature-input to @signature-params in Sections 7.2.8 and 7.5.3. How particular libraries or services implement the scheme is out of scope.
Two fields: Signature-Input and Signature
Per Section 4 of RFC 9421, Signature-Input identifies the components a signature covers and the parameters it was created with, while Signature holds the signature value itself. Both are Structured Fields Dictionaries, and each dictionary key is a label naming one signature.
- The value of a
Signature-Inputmember is the ordered list (an Inner List) of covered component identifiers, with signature parameters such ascreatedandkeyidattached. - The value of a
Signaturemember is a Structured Fields Byte Sequence containing the signature bytes. In the serialized RFC examples, its Base64 value is delimited by colons. - A signature must use both fields, with the same label in each. A label that appears in only one of them is an error.
The label itself is not signed. Section 7.2.5 notes that an intermediary may relabel a signature while processing the message, so applications shouldn’t attach meaning to particular labels. If signatures need to be told apart by purpose, use the tag signature parameter. Its value is public and an attacker can use the same one (Section 7.2.7), so it should only be used to pick which signatures to check.
Below is the example request from the verification walkthrough in Section 3.2 of RFC 9421. The RFC’s line wrapping (the RFC 8792 \ notation) has been undone, and the long Signature field is omitted from this excerpt. It is a non-normative example using a test key (test-key-rsa-pss); it was not signed or verified while writing this post.
POST /foo?param=Value&Pet=dog HTTP/1.1
Host: example.com
Date: Tue, 20 Apr 2021 02:07:55 GMT
Content-Type: application/json
Content-Digest: sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:
Content-Length: 18
Signature-Input: sig1=("@method" "@authority" "@path" "content-digest" "content-length" "content-type");created=1618884473;keyid="test-key-rsa-pss"
{"hello": "world"}
What gets signed is the signature base
The signer and the verifier each build the signature base from their own copy of the message. Section 1.2 explains why: HTTP allows, and sometimes requires, intermediaries to transform messages, so the recipient can’t expect a message bitwise-identical to what was sent, and the raw bytes on the wire can’t serve as the signature base.
For the request above, the algorithm in Section 2.5 produces this signature base (RFC 9421 Figure 1, with the line wrapping undone):
"@method": POST
"@authority": example.com
"@path": /foo
"content-digest": sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:
"content-length": 18
"content-type": application/json
"@signature-params": ("@method" "@authority" "@path" "content-digest" "content-length" "content-type");created=1618884473;keyid="test-key-rsa-pss"
The rules are short:
- One line per covered component, in the order given in
Signature-Input, written as"component identifier": valueand ended with a single LF. - Names starting with
@are derived components. They take their values from the message’s control data:@methodis the method,@authorityis the authority of the target URI,@pathis the path without the query, and so on (Section 2.2). - Everything else is an HTTP field. The name is lowercased, and if the field occurs more than once, the values are joined with a comma and a single space (
,) (Section 2.1). - The last line is always
"@signature-params", and its value is the same serialization as the matchingSignature-Inputmember value (without the label). No newline follows the last line. - If a covered field is missing from the message or its value can’t be obtained, no signature base is produced and the process ends in an error.
The algorithm signs these bytes. Section 3.3 defines signing as HTTP_SIGN(M, Ks) and verification as HTTP_VERIFY(M, Kv, S), where M is the signature base. Because the last line carries all the parameters, changing created or keyid changes the signature base too. Section 3.1 notes that this makes a signature always cover its own metadata, so the metadata can’t be swapped out on its own.
Compare the example signature base with the original request and you can see what’s missing:
- The request has a
Dateheader, but it isn’t in the covered list, so it doesn’t appear in the signature base. - The request target is
/foo?param=Value&Pet=dog, but the value of@pathis/foo. The query string is covered only if@queryor@query-paramis added. - The body
{"hello": "world"}isn’t in the signature base. What’s there is the value of theContent-Digestfield.
Section 1.3 says changes to components not covered by the signature should not prevent verification. In the example, the signature would still verify after a change to the query string or to Date.
The choice of components decides the security
Section 7.2.1 warns that any part of the message the signature doesn’t cover can be modified by an attacker without affecting the signature. An attacker can add or alter a header that changes how the message is processed, and the signature stays valid. The section therefore recommends that applications require as much of the message as possible to be signed, and that verifiers trust only the components that were signed.
The extreme case is a signature with an empty covered list. The example in Appendix B.2.1, Signature-Input: sig-b21=();created=...;keyid=...;nonce=..., covers no part of the message at all. The RFC notes that an attacker who intercepts such a signature could attach it to an unrelated message, and discourages empty lists.
There is guidance for signers too (Sections 3.1 and 7.2.4):
- Signers of a request SHOULD cover some or all of the control data, such as
@method,@authorityand@target-uri. - Signers SHOULD include the
createdparameter, which records when the signature was made. - In most cases, prefer
@authorityover theHostheader, andcreatedover theDateheader for time checks.
Covering too much has its own cost: the signature breaks during normal delivery. Section 7.2.3 says fields such as Via and Forwarded, which proxies rewrite or drop, should not be covered except in very limited cases, and gives the example of a framework that rewrites query parameters using individual @query-param components instead of @query. Section 1.3 lists transformations that should not prevent verification even when they touch covered components, such as reordering fields or converting between HTTP/1.1 and HTTP/2. But parsing and reserializing a covered field’s value, or changing the value of a derived component, can make verification fail.
A server behind a reverse proxy is the usual case. When the proxy rewrites Host to an internal address, the @authority of the message the server receives no longer matches what the client signed. Section 7.4.3 describes two approaches: the server can be configured with the external target URI seen by the client and derive components from that, or the proxy can verify the client’s signature and add its own (Section 4.3). Either way, signer and verifier must share the same component rules. That is also why Section 1.4 says an application or profile of RFC 9421 MUST specify, among other things, the components and parameters that must be covered, how keys are found, and which algorithms are allowed.
keyid only points at a key
Section 2.3 defines keyid as a String that is “the identifier for the key material”. It is a hint telling the verifier which key the signer used; it proves nothing about who owns that key. Anyone can sign with their own key and write whatever keyid string they like.
Step 5 of the verification procedure in Section 3.2 says the verifier dereferences the key identifier to key material, but has to decide for itself whether that key is trustworthy in this context. If the key is unknown, not trusted for this request, or doesn’t match something preconfigured, verification MUST fail. Section 7.3.4 makes the same point from the other side: a valid signature isn’t enough to prove that the appropriate party signed the message, and a verifier that doesn’t check the key and algorithm will end up accepting a message an attacker signed with their own key. Since one message can carry several signatures, only signatures from expected signers should be accepted, whatever their cryptographic validity (Section 7.2.6).
The same principle applies to algorithms:
- The verifier starts from the set of algorithms the application allows, and verification fails if anything outside that set is selected. When the algorithm is determined in more than one place (configuration, the key material, the
algparameter), all of them must agree (step 6 of Section 3.2). - Section 7.3.6 gives two downgrade examples: accepting an RSA PKCS#1 v1.5 signature for an RSA key that is expected to be used only with RSA-PSS, and using a public RSA key as an HMAC key so that an attacker’s HMAC signature verifies. It therefore encourages fixing the algorithm by other means, such as static configuration, rather than trusting a runtime
algvalue. - With a symmetric key such as HMAC, the verifier can produce valid signatures with the same key. If the verifier is compromised, the attacker can impersonate the signer (Section 7.3.3).
A successful cryptographic check tells you “this key signed these components” and no more. Whether the key’s owner may perform the requested action is a separate decision. RFC 9421 says in Section 1.4 that it defines only part of a full security system, and RFC 9530 says in Section 1.2 that it defines no means for authentication, authorization or privacy. For signatures used as an authentication mechanism, Section 1.4 gives 401 or 403 as possible failure responses; the difference between the two is covered in 401 vs 403.
created, expires, nonce and replay
Three of the Section 2.3 parameters concern time and replay:
| Parameter | Type | Meaning in RFC 9421 |
|---|---|---|
created |
Integer (UNIX timestamp, seconds) | When the signature was created. Inclusion is RECOMMENDED |
expires |
Integer (UNIX timestamp, seconds) | Expiration time. A hint marking when the signer stops vouching for the signature |
nonce |
String | A random unique value generated for this signature |
These values sit in the last line of the signature base, so changing them breaks the signature. Their presence alone doesn’t stop replay, though. Section 7.2.2 explains that because only part of a message may be signed, two different messages can verify against the same signature, and even with enough coverage a signature can be lifted onto a similar message and replayed. The countermeasures it lists are:
- Cover enough of the message to tell it apart from other messages.
- With a
nonce, the verifier can detect replay when the same value comes back. Detecting a repeat means the verifier has to remember the values it has already seen. createdandexpiresshorten the period in which a captured signature is useful.
Section 3.2.1 lists enforcing a maximum signature age, rejecting signatures past expires, and enforcing nonce uniqueness as examples of requirements an application may add. In other words, the acceptable time window, clock skew and how long nonces are remembered are application policy, and the RFC sets no numbers for them. Since expires is a hint, a verifier can reject a signature before it expires. Appendix B.2.1 adds that even when a nonce stops the same signature from being used twice, an attacker who intercepts the signature and keeps it from reaching the intended verifier can still use it on another message.
Section 7.1.2 says message signatures don’t replace TLS. A signature provides integrity for the covered components but no confidentiality, and TLS also keeps the signature data itself from being captured, which helps against replay. A verifier that wants a fresh signature from the signer can send an Accept-Signature field with a new nonce (Section 7.2.2).
The body is bound only through Content-Digest
RFC 9421 states in Section 1 and Section 7.2.8 that, on its own, it does not cover HTTP message content with the signature; it relies on the Digest specification (RFC 9530) for a hash of the content.
Content-Digest, from Section 2 of RFC 9530, carries a digest computed by applying a hash algorithm to the actual message content. It is a Dictionary whose keys are algorithms (sha-256, sha-512 and so on) and whose values are Byte Sequences holding the digest bytes. Repr-Digest, from Section 3 of the same document, is a digest over the entire selected representation data rather than the message content. The distinction matters when the content in a message differs from the representation data, as with a partial response to a Range request or a response to HEAD. Section 1.3 of RFC 9530 says the main reason for obsoleting the old Digest field (RFC 3230) was that implementations interpreted it inconsistently, some computing it over content and some over the representation.
Adding Content-Digest does nothing for the signature by itself. For a signature to bind the body, all of the following are needed:
content-digestmust be in the covered list ofSignature-Input. Only then does the digest value enter the signature base.- The verifier must recompute the digest over the received body and compare it with the field value. Section 7.2.8 warns that the signature directly covers only the field value, so skipping this step lets an attacker swap the body while leaving
Content-Digestuntouched. - The representation metadata the digest depends on should be covered as well. Section 6.3 of RFC 9530 says digests depend on values such as
Content-TypeandContent-Encoding, so if a signature covers the digest field but not those values, an attacker can changeContent-Typeand make digest validation fail at the recipient.
That is one way to read why the example covers content-type and content-length alongside content-digest. The Section 3.2 example also assumes application requirements for verification to pass: the method, authority, path, content-digest, content-length and content-type must be signed, and the creation time must be recent enough.
A few more constraints:
- The value of
Content-Digestdepends on the content encoding. If an intermediary changes the content encoding, the digest changes and the signature becomes invalid, so such an intermediary has to sign again with the new digest (RFC 9421 Section 7.2.8). - Use with signatures is likely to be deemed an adversarial setting (RFC 9530 Section 6.3). In such settings, algorithms marked “Deprecated” in the registry (such as
md5andsha) MUST NOT be used, and the algorithms marked “Active” in the initial registry aresha-256andsha-512(RFC 9530 Sections 5 and 7.2). - A digest alone doesn’t stop malicious tampering. An on-path attacker can remove the digest or replace it with one computed over altered content, so it has to be combined with TLS or signatures (RFC 9530 Section 6.1).
SignatureandSignature-Inputmay be sent as trailers, but intermediaries are allowed to drop trailers, so sending them only as header fields is RECOMMENDED (RFC 9421 Sections 4.1 and 4.2).
What a verifier should check
The list below takes the verification procedure in Section 3.2 of RFC 9421 and adds the considerations from Section 3.2.1, Section 7 and RFC 9530. The order and grouping are this post’s own arrangement, not a format the RFC prescribes.
- Find matching label pairs in
SignatureandSignature-Input, and choose which of several signatures to process according to policy. - Check that the covered list includes every component the application requires (control data,
content-digest, authorization-related headers and so on). - Find the key through
keyidor preconfiguration, and check that the key is trusted for this request. An unknown key is a failure. - Pick the algorithm from the allowed list, and check that the values determined in different places agree.
- Rebuild the signature base from the received message. Behind a proxy, decide in advance which target URI it is based on.
- Verify the signature with the algorithm’s verification function. Non-deterministic algorithms such as RSA-PSS and ECDSA can’t be verified by re-signing the same base and comparing results (Section 7.3.5).
- Check
created,expiresandnonceagainst application policy. - Recompute
Content-Digestover the received body and compare. - Check that the key’s owner is authorized to perform the requested action.
Section 7.1.1 points out that a message is still a valid HTTP message without its signature, which makes it easy to ignore the verification result and process the message anyway. Testing only with good signatures won’t reveal that mistake, so also test that invalid signatures are actually rejected.
Specifications used
All checked on 2026-10-20.
- RFC 9421: HTTP Message Signatures (February 2024, Standards Track): Sections 1.2, 1.3, 1.4, 2.1, 2.2, 2.3 (signature parameters), 2.5 (signature base), 3.1, 3.2, 3.2.1, 3.3, 4, 7.1.1, 7.1.2, 7.2.1 to 7.2.8, 7.3.3 to 7.3.6, 7.4.3, Appendix B.2.1
- RFC 9421 info page (RFC Editor): status, update relations, errata
- RFC 9530: Digest Fields (February 2024, Standards Track): Sections 1.2, 1.3, 2, 3, 5, 6.1 to 6.3, 7.2
- RFC 9530 info page (RFC Editor): status, obsoletes relation