Signed, Encrypted, or Both: A Decision Guide for SAML
Somewhere in your SAML integration documentation there is probably a sentence like "assertions must be signed and encrypted." Somewhere in a customer's security questionnaire there is a question asking whether you support assertion encryption, and the only commercially viable answer is yes.
Somewhere in your SAML integration documentation there is probably a sentence like "assertions must be signed and encrypted." Somewhere in a customer's security questionnaire there is a question asking whether you support assertion encryption, and the only commercially viable answer is yes.
And somewhere in your support queue there is a ticket, open for nine days, from a customer whose SSO broke because their IdP rotated an encryption certificate and nobody told your side. The engineer working it cannot decrypt the assertion, so they cannot see which part of it is wrong, so they are debugging an opaque blob by asking the customer to send screenshots of their IdP configuration.
That trade — a real, recurring operational cost against a security benefit that in most deployments is already provided by something else — is what this article is about. Signing and encryption are not two intensities of the same control. They solve different problems, they fail differently, and the honest position is that almost every SAML deployment needs signing and most do not need encryption.
That's a claim worth defending carefully, because "most do not need encryption" sounds like negligence until you look at what the encryption is actually protecting against.
What each one gives you
Signing gives you integrity and authenticity. An XML digital signature over an assertion proves two things: the content has not been altered since it was signed, and it was signed by the holder of a specific private key. For a service provider, that's the entire basis of trust — without it, an assertion is just XML that arrived at your endpoint, and XML that arrives at your endpoint can be written by anyone who can reach your endpoint.
Encryption gives you confidentiality. An encrypted assertion (EncryptedAssertion, using XML Encryption) is unreadable to anyone without the SP's private key. It protects the content — the user's name, email, group memberships, employee number — from being read by parties who handle the message but shouldn't see inside it.
Note that these are independent. An encrypted-but-unsigned assertion is worthless: an attacker who obtains your public key (which is published in your metadata, by design) can encrypt an assertion of their own devising and you cannot tell it from a real one. Encryption authenticates nothing. Signing is not optional; encryption is a decision.
The question that determines whether you need encryption
Who handles the assertion between the IdP and your SP, and what can they read?
Work through the actual path. In the standard SP-initiated flow with HTTP-POST binding:
- The IdP generates the assertion and puts it in an auto-submitting HTML form.
- That form is delivered to the user's browser over TLS.
- The browser POSTs it to your ACS endpoint over TLS.
TLS covers both hops. So the parties who can read the assertion are: the IdP (who wrote it), your SP (who consumes it), and the user's own browser.
That's the list. Encryption's marginal benefit is entirely about that third party — hiding the assertion's contents from the user whose assertion it is, and from anything running in or alongside their browser.
Which raises the obvious question: is that a threat? Sometimes, genuinely:
- The assertion contains attributes the user shouldn't see about themselves. Risk scores, internal classifications, salary band, a manager's assessment, a clearance level. This is a real case and it's the strongest argument for encryption.
- Browser-side exposure matters in your threat model. The assertion sits in POST data, and in some flows it can end up in browser history, in a page the user can view-source on, in a bookmarked URL if someone used the Redirect binding, or in a browser extension's reach. If your assertion contains sensitive PII, that exposure is real even if it isn't dramatic.
- A regulation or contract says so. Not a technical argument, but a determinative one. Some sectors mandate it, and arguing against a control your customer's auditor requires is a losing use of your time.
- The transport isn't actually end-to-end TLS. Corporate TLS-inspecting proxies terminate and re-establish TLS, which means the assertion is in plaintext inside a middlebox. In some enterprise environments this is genuinely the case, and the customer knows it.
And the cases where it adds nothing:
- The assertion contains only what the user already knows. Their own name, their own email, the groups they're a member of and can see in their own directory. Encrypting that hides it from the person it describes, which is not a security property, it's an inconvenience.
- You believe encryption protects the assertion in transit. It doesn't add to what TLS already provides on the wire. This is the cargo-cult version of the requirement: encryption is being asked for as a defence against network eavesdropping, which TLS 1.2+ already handles, and which XML Encryption handles less well.
That last point deserves emphasis because it's the actual reason the requirement propagates. Someone reads "assertions should be encrypted," pattern-matches it to "data in transit must be encrypted," and adds it to a checklist. The control is then implemented against a threat that was already mitigated, and it brings costs that nobody accounted for.
The costs, which are mostly operational
A second certificate lifecycle. Signing uses the IdP's key; encryption uses the SP's key. That's a separate keypair, separate expiry, separate rotation, and separate coordination — and rotation now requires the IdP side to pick up your new certificate, which means asking every customer's IdP administrator to make a change on your schedule. Signing-certificate rotation is one conversation per customer. Adding encryption doubles the number of certificate-coordination events in your estate, and each one is a potential outage with a date attached.
Debugging becomes materially harder. This is the cost engineers feel. SAML errors are famously uninformative, and the debugging workflow — decode the Base64, read the XML, find the wrong audience or the mismatched NameID format — requires reading the assertion. With encryption, an integration engineer looking at a failing login sees a <xenc:CipherValue> blob. To see inside, they need the SP's private key, which they should not have on their laptop. So the debugging loop becomes: add temporary logging to a production service, redeploy, reproduce, read, remove. That turns twenty-minute problems into two-day problems, and it does it at exactly the moment when a customer's users cannot log in.
Cryptographic complexity that has historically failed. XML Encryption has a worse security history than most people realize. The 2011 Jager–Somorovsky adaptive chosen-ciphertext attack broke CBC-mode XML Encryption in a way that allowed plaintext recovery against many implementations. Backwards-compatibility attacks against implementations supporting both old and new modes came later. The remediation is to use GCM modes and RSA-OAEP key transport rather than CBC and RSA-1.5 — but the relevant point is that adding encryption adds a cryptographic implementation whose track record is worse than the signing you already depend on, and that implementations differ in which modes they support, so interoperability becomes a matrix.
More things to configure wrong. Every option is a misconfiguration opportunity. Encryption adds: which key transport algorithm, which block cipher, whether the whole assertion or only specific attributes are encrypted, and whether the SP requires encryption or merely accepts it. That last one is a genuine vulnerability class — an SP configured to accept encrypted assertions but not to require them will happily process an unencrypted one, which means the control is bypassable by an attacker who simply doesn't apply it.
The decision
flowchart TD
A["Is the assertion signed?"] -->|No| B["Stop. Fix this first.<br/>Nothing else matters."]
A -->|Yes| C{"Does the assertion carry data<br/>the user shouldn't see,<br/>or unusually sensitive PII?"}
C -->|Yes| D["Encrypt"]
C -->|No| E{"Contract or regulation<br/>requires it?"}
E -->|Yes| D
E -->|No| F{"Is end-to-end TLS<br/>actually intact?<br/>(no inspecting proxy)"}
F -->|No| D
F -->|Yes| G["Sign only.<br/>Spend the effort on<br/>signature validation correctness."]
The last box is the recommendation for the majority of deployments, and the reasoning is one of allocation rather than dismissal. The effort you'd spend on encryption key lifecycle, algorithm interoperability, and blind debugging is better spent making sure your signature validation is actually correct — because signature validation is where the real vulnerabilities in SAML deployments have always been. XML signature wrapping, accepting unsigned assertions, validating a signature over the wrong element, trusting the certificate embedded in the assertion instead of the one in configured metadata: these are the bugs that produce authentication bypasses, and every one of them is unaffected by whether you also encrypted the thing.
An SP with impeccable encryption and sloppy signature validation is trivially exploitable. An SP with rigorous signature validation and no encryption is, in most threat models, fine.
If you do encrypt, the details that matter
Encrypt the assertion, not the response. EncryptedAssertion inside a signed Response is the interoperable pattern. Encrypting the whole response is less widely supported and complicates the signing arrangement.
Sign first, then encrypt. The signature must cover the plaintext assertion, and the encryption wraps the signed assertion. The other order (encrypt then sign) lets an attacker who can strip the outer signature substitute ciphertext, and it means the signature attests to a blob rather than to the claims.
Use modern algorithms and reject the old ones. AES-GCM for the data, RSA-OAEP for key transport. Explicitly refuse RSA-1.5 and CBC modes — not deprioritize them, refuse them, because the attacks above depend on the implementation being willing to try.
Require, don't merely accept. If a connection is configured for encrypted assertions, an unencrypted assertion on that connection must be rejected. Otherwise the control is advisory.
Separate signing and encryption keys. Different keys for different purposes, so that rotating one doesn't disturb the other and so that a key's usage is unambiguous. Metadata supports use="signing" and use="encryption" on KeyDescriptor — populate it, and honour it when consuming the other side's metadata.
Monitor both certificates' expiry with a 60-day alert. Your encryption certificate expiring is an outage for every connection that uses it, and unlike your signing certificate, replacing it requires action on the other side.
What to say when a customer requires it
You will get this request, and the answer isn't to argue. It's to support it, per-connection, and to have a clear internal position:
Encryption is supported and configurable per connection. It is off by default because for assertions containing only the user's own identity attributes it duplicates a confidentiality guarantee TLS already provides, while adding a certificate lifecycle that generates outages and making integration failures substantially harder to diagnose. It should be turned on when the assertion carries attributes the subject shouldn't see, when a contract or regulation requires it, or when the customer's environment breaks end-to-end TLS.
That is a defensible engineering position stated in a way a security reviewer can evaluate, which is a better outcome than either silently defaulting it on or refusing to support it. And it makes the actual point: the interesting question in SAML security is not whether you encrypted the assertion. It's whether you validated the signature correctly — and that one has no configuration option, no per-customer toggle, and no acceptable answer other than yes.