# QWAC vs QSealC: who does what, and why you've got the wrong layer

> The QWAC is read by the TLS layer, once, at the handshake. The QSealC is read by the application layer, on every request. Where each private key lives, what STET requires, and how to read a 'certificate error'.

_Published: 2026-08-24 · Category: security · Tags: #dsp2 #psd2 #open-banking #security #eidas #mtls #stet_

---

Both certificates come from the same [eIDAS TSP](https://openfinanceguide.com/en/glossary/eidas-tsp), carry the same authorisation number and the same [DSP2](https://openfinanceguide.com/en/glossary/dsp2) roles, and often arrive on the same purchase order. The difference is not in what they contain. It is in **which layer of the stack reads them**. The [QWAC](https://openfinanceguide.com/en/glossary/qwac) is read by the TLS layer, once, at the handshake. The [QSealC](https://openfinanceguide.com/en/glossary/qsealc) is read by the application layer, on every request, in an HTTP header.

A ticket that says "certificate error" without saying which layer cannot be diagnosed. This article does not redefine the two objects — the QWAC, QSealC and [mTLS](https://openfinanceguide.com/en/glossary/mtls) glossary entries do that. It places each one in the stack, in the infrastructure and in the logs, with the [STET](https://openfinanceguide.com/en/glossary/stet) 1.6.3 text alongside. For the full picture of the stack, read [Technical architecture of a PSD2 API](/en/blog/architecture-api-dsp2) first.

## In one picture: two layers, two certificates

A PSD2 request goes down the stack. Each certificate comes into play at one level only.

```mermaid
flowchart LR
    subgraph APP["Application layer"]
        BIZ["TPP business logic\nGET /accounts\nPOST /payment-requests"]
    end
    subgraph MSG["HTTP message layer — on every request"]
        SIG["Digest + Signature\nor x-jws-signature\n🔏 QSealC private key"]
        TOK["Authorization: Bearer …\ntoken bound to the client\ncertificate (RFC 8705)"]
    end
    subgraph TLS["Transport layer — per connection"]
        MT["mTLS handshake, TLS 1.2\n🪪 TPP's QWAC\n⇄ ASPSP's QWAC"]
    end
    APP --> MSG --> TLS -->|"Internet"| ASPSP["ASPSP"]
```

| | QWAC | QSealC |
|---|---|---|
| eIDAS basis | qualified website authentication certificate, art. 3(39) | qualified electronic seal certificate, art. 3(30) |
| Layer | transport (TLS) | message (HTTP) |
| Read by | the TLS stack on both sides, at the handshake | the signature verifier of the [ASPSP](https://openfinanceguide.com/en/glossary/aspsp), on every request |
| Frequency | once per connection | once per request — and per response if the ASPSP signs |
| Survives a proxy that terminates TLS | no | yes |
| Evidential value after the fact | none: nothing is kept | the signature stays in the request, verifiable later |
| Private key, on the [TPP](https://openfinanceguide.com/en/glossary/tpp) side | where the outbound connection is opened | where the request is built |
| Symptom when it breaks | broken handshake, no HTTP status code | HTTP 400 (STET §3.5) |
| What STET says about it | §3.1, §3.2, §3.9 | §3.5 |

> **Info:**
> RTS (EU) 2018/389, art. 34, allows either one for identification. The API
>   standards settled the question by stacking them: STET and the Berlin Group require
>   the QWAC for the channel and the QSealC for the signature. Two certificates, hence
>   two private keys, two expiry dates and two rotation procedures.

## What STET requires, text in hand

Three passages of the 1.6.3 Framework (Part 1) are enough. Page numbers are those of the official PDF, which the handbook displays in the margin of each paragraph.

### §3.1 and §3.2 — the QWAC, for TLS and nothing else

"Each actor must be provided with at least one eIDAS certificate (QWAC), for TLS 1.2 purpose" ([§3.1, p. 17](/en/stet/1.6.3/framework/3-prerequisites-and-technical-details#s-3.1)). The TPP verifies the ASPSP's server QWAC and presents its own; both comply with ETSI TS 119 495; "in case of authentication failure, on one side or the other, the connection must be closed"; and "no additional encrypting or authenticating feature is required" ([§3.2, p. 17–18](/en/stet/1.6.3/framework/3-prerequisites-and-technical-details#s-3.2)). The QWAC never surfaces in the request: everything it has to say, it says at the handshake.

The same paragraph fixes the format of the certificate subject's `organizationIdentifier`: `PSD`, the country code of the [NCA](https://openfinanceguide.com/en/glossary/nca), a hyphen, the NCA identifier, a hyphen, the authorisation number — `PSDFR-ACPR-12345` for an actor authorised by the [ACPR](https://openfinanceguide.com/en/glossary/acpr). This field, together with the roles in the ETSI QC statement, is what the ASPSP reads in the client certificate to know who is calling, and in what capacity.

### §3.5 — the QSealC, to sign every request

"Each request sent by the TPP has to be signed and ASPSP might also sign their responses. If the ASPSP notes that the signature is either absent or invalid for a given request, it shall reject this request with HTTP400" ([§3.5, p. 53](/en/stet/1.6.3/framework/3-prerequisites-and-technical-details#s-3.5)).

The historical mechanism is [HTTP Signature](https://openfinanceguide.com/en/glossary/http-signature) in its draft-cavage version: a SHA-256 `Digest` of the body, then an RSA-SHA256 signature computed with the QSealC over `(request-target)`, `Date`, `Content-Type`, `Content-Length`, `X-Request-Id`, the `PSU-*` headers and the `Digest` itself, carried in the `Signature` header along with `keyId`, `algorithm` and the list of signed headers ([§3.5.1.1, p. 53–55](/en/stet/1.6.3/framework/3-prerequisites-and-technical-details#s-3.5.1.1)). The `keyId` is either the identifier assigned at [OAuth2](https://openfinanceguide.com/en/glossary/oauth2) registration, or the URL of the QSealC suffixed with a `_` and its SHA-256 fingerprint ([§3.5.1.2, p. 55](/en/stet/1.6.3/framework/3-prerequisites-and-technical-details#s-3.5.1.2)).

Since 1.6.3, the Framework recommends replacing this mechanism with the Open Banking Europe JSON Web Signature profile: a detached JWS (RFC 7515) in the `x-jws-signature` header, whose protected header carries `x5t#S256`, the fingerprint of the signing QSealC, and `sigD`, the list of covered headers ([§3.5.2, p. 55–56](/en/stet/1.6.3/framework/3-prerequisites-and-technical-details#s-3.5.2)). As for draft-cavage, the IETF has superseded it with RFC 9421 — which STET does not cite as a target.

### §3.9 — the QWAC comes back a third time, in OAuth2

"Based on MTLS, the identity of the TPP is provided by its eIDAS certificate during OAuth2 procedures", with a reference to RFC 8705 ([§3.9, p. 59](/en/stet/1.6.3/framework/3-prerequisites-and-technical-details#s-3.9)). The authorisation server therefore authenticates the client through `tls_client_auth` — the QWAC presented at the handshake of the `POST /token` — and can bind the issued token to that certificate, via its `cnf` claim and the `x5t#S256` fingerprint. A token bound to a fingerprint that is no longer the one presented is rejected, even if it has not expired.

## One request, three checks

```mermaid
sequenceDiagram
    autonumber
    participant TPP as TPP (HTTP client)
    participant TLS as ASPSP — TLS termination
    participant AS as ASPSP — authorisation server
    participant API as ASPSP — API and signature verifier

    TPP->>TLS: ClientHello
    TLS-->>TPP: Certificate (ASPSP's QWAC) + CertificateRequest
    TPP->>TLS: Certificate (TPP's QWAC) + CertificateVerify
    alt QWAC unknown, expired, revoked, missing the role
        TLS-->>TPP: TLS alert — connection closed, no HTTP status code
    end
    TLS-->>TPP: Finished — mTLS channel established

    TPP->>AS: POST /token (tls_client_auth, over the mTLS channel)
    AS-->>TPP: access_token bound to the QWAC (cnf.x5t#35;S256)

    TPP->>API: GET /accounts<br/>Authorization: Bearer …<br/>Digest + Signature (QSealC)
    API->>API: token valid, bound to this client certificate?
    alt token absent, expired, bound to another certificate
        API-->>TPP: 401
    end
    API->>API: Digest recomputed, Signature verified with the QSealC behind keyId?
    alt signature absent or invalid
        API-->>TPP: 400 (STET §3.5)
    end
    API-->>TPP: 200 + body, signed by the ASPSP where applicable
```

Three checks, three layers, three symptoms that look nothing alike. The order of the two application-level checks varies from bank to bank; the symptom does not.

## Where the private keys live: the layer mistake

This is where the mistake happens. The two private keys do not live in the same place, because the two certificates are not read by the same component.

### On the TPP side, outbound

```mermaid
flowchart LR
    subgraph TPP["TPP infrastructure"]
        APP["Application service\nbuilds the request"]
        SIGN["Signing service\n🔏 QSealC private key (HSM / KMS)"]
        EGRESS["HTTP client or egress proxy\n🪪 QWAC private key\nopens the mTLS connection"]
        APP -->|"request + Digest"| SIGN
        SIGN -->|"request + Signature"| EGRESS
    end
    EGRESS -->|"mTLS"| BANK["ASPSP"]
```

The QSealC key belongs to the component that **builds the request**, because the signature covers headers — `Date`, `Content-Length`, `X-Request-Id` — that must be frozen before sending. The QWAC key belongs to the component that **opens the connection**. If an egress proxy, an application-level NAT or a service mesh opens the connections on the application's behalf, it is that component that presents the QWAC, and the application has no business holding the key. Putting the QWAC key in the application while the mesh terminates and re-opens TLS yields a handshake with no client certificate: a TLS alert, and no HTTP status code.

### On the ASPSP side, inbound

```mermaid
flowchart LR
    TPP["TPP"] -->|"mTLS"| LB
    subgraph ASPSP["ASPSP infrastructure"]
        LB["TLS termination — LB, WAF, gateway\nrequests and verifies the client QWAC\n🪪 server QWAC private key"]
        GW["Signature verifier\nrecomputes Digest, resolves keyId,\nverifies with the TPP's QSealC"]
        AUTHZ["Authorisation\ntoken, consent, role"]
        BE["Business backend"]
        LB -->|"request + identity extracted from the QWAC\n(internal header, signed)"| GW
        GW --> AUTHZ --> BE
    end
```

On the bank side, the client QWAC stops at the component that terminates TLS. What travels onward is an **extracted identity** — `organizationIdentifier`, roles, fingerprint — carried in an internal header that the rest of the chain must be able to trust, hence the point of signing it, or of accepting it only from the terminator. The QSealC signature, on the other hand, goes all the way through: a component anywhere behind can verify it, **provided it sees the original bytes**. A gateway that re-serialises the JSON, recomputes `Content-Length` or normalises `Date` before the verifier produces 400s on valid requests.

### The six layer confusions

| # | Confusion | What happens | Symptom |
|---|---|---|---|
| 1 | QWAC key on a component that does not open the connection | handshake with no client certificate | TLS alert, no HTTP |
| 2 | QSealC presented as the TLS client certificate, or QWAC used for signing | the ETSI `QcType` is the wrong one (`web` for one, `eseal` for the other); a verifier that checks it refuses | TLS alert, or 400 |
| 3 | Signature computed before a proxy that rewrites `Date`, `Content-Length` or the body | the signature covers different bytes from those received | 400, invalid signature |
| 4 | Verification done after a transformation of the request, on the ASPSP side | same cause, mirrored | 400 on valid requests |
| 5 | QWAC renewed without reissuing the bound tokens | `cnf.x5t#S256` no longer matches the certificate presented | 401 on an unexpired token |
| 6 | The bank's QTSP CA missing from the TPP's trust store | the TPP refuses the ASPSP's server QWAC | `unknown_ca`, connection closed by the TPP itself |

> **Warning:**
> Confusion 2 is the most expensive, because it is invisible as long as the bank does
>   not check the certificate type — and visible all at once the day it does. The two
>   certificates are often sold together; they are not interchangeable.

## Diagnostic matrix

| Symptom | Layer | Certificate involved | Where to look |
|---|---|---|---|
| Connection closed during the handshake, no HTTP status code; `handshake_failure`, `bad_certificate`, `certificate_expired`, `unknown_ca` | TLS | QWAC — yours, or the bank's that you do not recognise | TLS terminator logs; `openssl s_client -cert -key` |
| 401 | token | QWAC, via the RFC 8705 binding; or simply an expired token | authorisation server response; the token's `cnf` against the fingerprint of the certificate presented |
| 400 mentioning the signature (STET §3.5) | HTTP message | QSealC | recomputed `Digest`; list of signed headers; resolvable `keyId`; bytes actually sent |
| 403, or the bank's business error code | authorisation | neither | the certificate's PSD2 roles against the endpoint called; consent state |

```mermaid
flowchart TD
    Q0["“Certificate error”"] --> Q1{"Did an HTTP status\ncode come back?"}
    Q1 -->|"No"| TLSX["TLS layer → QWAC\nchain, revocation, role,\ntrust store, who opens the connection"]
    Q1 -->|"Yes"| Q2{"Which one?"}
    Q2 -->|"400"| SIGX["Message layer → QSealC\nDigest, signed headers, keyId,\nbytes rewritten along the way"]
    Q2 -->|"401"| TOKX["Token layer → RFC 8705 binding\nis the token bound to the certificate presented?"]
    Q2 -->|"403 or other"| ROLEX["Authorisation → neither\nPSD2 role, consent, scope"]
```

## Two clocks

The two certificates each expire on their own date, can each be revoked on their own, and are renewed separately. Three consequences:

- **Two expiry alerts**, not one. A valid QWAC says nothing about the state of the QSealC, and vice versa.
- **Rotating the QWAC affects the tokens.** The OAuth2 registration survives if the subject DN does not change — that is what `tls_client_auth_subject_dn` declares — but the access tokens bound to the old fingerprint (RFC 8705) no longer go through. Reissuing the tokens is part of the rotation.
- **Rotating the QSealC affects the `keyId`** — the fingerprint-suffixed URL changes, or the `x5t#S256` of the JWS profile — and it must not withdraw the old certificate: requests already signed must remain verifiable. That is the whole point of non-repudiation. The private key is withdrawn; the certificate stays published.

```mermaid
gantt
    title Two certificates, two calendars
    dateFormat YYYY-MM-DD
    axisFormat %m/%Y
    section QWAC
    QWAC n                                  :done, q1, 2026-01-01, 2027-01-01
    QWAC n+1                                :active, q2, 2026-11-15, 2027-11-15
    Reissue of the bound tokens             :crit, milestone, m1, 2026-11-15, 0d
    section QSealC
    QSealC n                                :done, s1, 2026-04-01, 2027-04-01
    QSealC n+1                              :active, s2, 2027-02-15, 2028-02-15
    Old QSealC still published              :s3, 2027-04-01, 2027-10-01
```

## To sum up

1. **QWAC = TLS layer.** Read at the handshake, by the component that opens or terminates the connection. Its key lives there, nowhere else.
2. **QSealC = message layer.** Read on every request, by a verifier that must see the original bytes. Its key lives where the request is built.
3. **STET**: QWAC for TLS 1.2 (§3.1–3.2), every request signed with the QSealC and 400 otherwise (§3.5), JWS profile recommended since 1.6.3 (§3.5.2), OAuth2 identity carried by the QWAC (§3.9, RFC 8705).
4. **No HTTP status code → QWAC. 400 → QSealC. 401 → token binding. 403 → neither.**
5. **Two clocks**: two expiries, two rotations, and the old QSealC stays published to verify the past.

For the rest of the stack — standards, [SCA](https://openfinanceguide.com/en/glossary/sca) flows, consent — see [Technical architecture of a PSD2 API](/en/blog/architecture-api-dsp2). The two signature mechanisms, byte by byte, and the most frequent handshake failures are the subject of the next two articles. The full text of the section quoted is in the [STET 1.6.3 handbook](/en/stet/1.6.3/framework/3-prerequisites-and-technical-details), with the original PDF alongside.

---

Source: https://openfinanceguide.com/en/blog/qwac-vs-qsealc
