skip to content

On the SAML HTTP-Redirect binding, which octets do SigAlg and Signature cover, and why does re-encoding the parsed message break verification?

level: seniorimportance: should knowfreq 35%

answer

  1. not an XML signature at all
  2. the signature sits beside the message
  3. one fixed order, three parameters
  4. SAMLRequest, RelayState, SigAlg, in that order
  5. verify the octets you actually received

basics

~10 s

They cover a query string built in the fixed order SAMLRequest=value&RelayState=value&SigAlg=value, with the values URL-encoded exactly as sent. Neither DEFLATE nor percent-encoding is canonical, so a rebuilt string is different octets and fails.

solid answer

~50 s

The Redirect binding cannot carry an enveloped XML signature, so it defines a **detached** one. The signer builds an octet string by concatenating the parameters in a fixed order — `SAMLRequest=value&RelayState=value&SigAlg=value`, or `SAMLResponse=...` — with each value URL-encoded, signs those octets, and sends the result base64-encoded in the `Signature` parameter with the algorithm named in `SigAlg`. `RelayState` is omitted from the string entirely when there is none, rather than included empty. Anything else in the query string is neither included in nor protected by the signature. A verifier must therefore take the raw parameter values off the wire and assemble the string in that prescribed order. Rebuilding it by re-deflating and re-encoding the parsed message produces different octets — compression output and percent-encoding are implementation choices, not canonical forms — and the signature fails on a message that was never tampered with.

code

pseudocode · 11 lines
pseudocode
function signing_string(params):
    if params has SAMLRequest:
        s = "SAMLRequest=" + url_encode(params.SAMLRequest)
    else:
        s = "SAMLResponse=" + url_encode(params.SAMLResponse)

    if params has RelayState:                 // omit entirely if absent
        s = s + "&RelayState=" + url_encode(params.RelayState)

    s = s + "&SigAlg=" + url_encode(params.SigAlg)
    return s                                  // Signature is never part of it

go deeper

for a junior

Know that a Redirect URL can carry SigAlg and Signature parameters next to the message, and that this is a different mechanism from a signature written inside the XML document.

for a middle

Explain the split: enveloped inside the XML on HTTP-POST, detached over the query string on HTTP-Redirect. Name the three parameters in the signed string and say why Signature itself is excluded.

for a senior

Demonstrate the diagnosis — percent-escape case, a stray empty RelayState, parameters assembled in URL order — and state the rule that a verifier works from the received octets rather than a regenerated encoding.

for a principal

The wider call is where you allow detached signatures over serialisations at all, given that every such scheme depends on two parties agreeing on an encoding that no specification makes canonical.

## Two signatures live in this branch, and only one of them is XML On the HTTP-POST binding, a signature is an **enveloped `<ds:Signature>`** inside the XML document, and the binding is oblivious to it: base64 carries the signed bytes unchanged. On the HTTP-Redirect binding that is not available. The message has been compressed and re-encoded on the way into a URL, so a signature computed over the XML would have to survive a transformation the binding does not define a canonical form for. The Redirect binding solves this by moving the signature out of the message entirely. It becomes **detached**: two extra query parameters beside the message, computed over the URL's own octets. This is the most common reversal candidates make on SAML, so it is worth stating both directions explicitly — enveloped inside the XML on POST, detached in the query string on Redirect. ## The signing string The signer concatenates the parameters in this exact order, values URL-encoded: ``` SAMLRequest=value&RelayState=value&SigAlg=value ``` or, for a response, `SAMLResponse=value&RelayState=value&SigAlg=value`. Four things about that string carry weight: - **The order is fixed by the binding**, and it is not necessarily the order the parameters appear in on the wire. A verifier that reads parameters in URL order and concatenates them as it goes can produce a different string from the one that was signed. - **`Signature` is not part of it.** It cannot be: it is the output. - **`RelayState` is omitted entirely when there is none.** Not empty — absent. Including `RelayState=` with no value produces a different string and a failed verification. - **Any other content in the query string is neither included in nor protected by the signature.** A parameter appended by something along the path is simply unsigned, and a recipient that reads a value from it is reading unauthenticated input. `SigAlg` carries the URI of the algorithm, and `Signature` carries the base64 signature value. ## Why re-encoding breaks it The failure this question is really about looks like tampering and is not. The recipient decodes the message, parses it, and then — reasonably enough — re-encodes it to reconstruct what must have been signed. The signature fails on every message. The reason is that **none of the steps in the pipeline are canonical**: | Step | Why two implementations differ | |---|---| | DEFLATE | Compression level, block splitting and dictionary choices all produce valid but different output for the same input | | Base64 | Line-break policy and padding handling vary | | Percent-encoding | Which characters get escaped is a choice, and hexadecimal digits may be upper or lower case (`%2B` against `%2b`) | | Parameter assembly | Reading in URL order rather than the prescribed order changes the string | Any one of those produces a different octet string carrying the identical XML meaning. The signature was computed over octets, not over meaning, so it fails. The rule that follows is simple and general: **verify the bytes you received**. Capture the raw, still-encoded parameter values as they arrived, assemble the string in the prescribed order, and verify. Decode the message afterwards, for processing, from the same bytes you just verified. ## The SigAlg trap `SigAlg` is sent by the signer, which means it is an assertion by the sender about how the sender signed. Treating it as an instruction — "verify with whatever algorithm this names" — hands the choice of algorithm to whoever produced the URL. A recipient should check it against a policy of algorithms it is prepared to accept and reject the message otherwise, rather than following it wherever it leads. ## Diagnosing a failure in practice 1. **Does the message parse?** If the XML is well formed and the content is plausible, the failure is in the octet string, not in the message. 2. **Rebuild the string from the raw wire values** and compare it character by character with whatever the verifier actually hashed. Case differences in percent escapes and a stray `RelayState=` show up immediately. 3. **Check the order used.** Prescribed order, not URL order. 4. **Check what else is on the URL.** Extra parameters are unsigned, and if the application reads one of them, that is a finding regardless of whether the signature verifies. 5. **Ask whether the message should be on this binding at all.** If it is a `<Response>` carrying signed content, it belongs on HTTP-POST, where the signature lives inside the XML and none of this arises. The underlying lesson generalises well past SAML: a detached signature over a serialisation is only as reliable as the agreement on how that serialisation is produced, and the safe discipline is always to verify the received representation rather than a regenerated one.

  • What does the signed string look like when the message carries no RelayState?
    It becomes `SAMLRequest=value&SigAlg=value`. The parameter is left out altogether rather than sent with an empty value, and a verifier that inserts `RelayState=` to keep a three-part shape will compute a different string and reject a valid message.
  • Why should a recipient not simply use whatever algorithm SigAlg names?
    SigAlg is supplied by the sender, so obeying it lets the sender choose the algorithm the recipient verifies with, including a weak one the recipient would otherwise refuse. Check it against an accepted-algorithm policy and reject anything outside it.
  • A gateway appends a tracking parameter to the Redirect URL. What is the consequence?
    The signature still verifies, because only the three named parameters are covered. The appended value is unsigned input, so any behaviour the recipient derives from it is unauthenticated even though the SAML message itself is genuine.

saying these in an interview costs you the question

  • Calls the Signature parameter an XML signature that was moved
  • Rebuilds the signed string by re-deflating the parsed message
  • Believes every query parameter on the URL is covered
  • Includes an empty RelayState in the signing string
  • Verifies with whatever algorithm SigAlg happens to name
  • Assembles the string in URL order rather than the prescribed order