gematik lib-vau / lib-vau-csharp
VAU Handshake Performs Only 2 of 6 Required Server-Key Checks
lib-vau and lib-vau-csharp are gematik's reference implementations of the VAU protocol — the security layer that protects traffic to Germany's electronic patient record (ePA) backend, in use across the ~73 million statutory-insured citizens enrolled since 2025 unless they have opted out. Both reference VAU clients deserialise the server's SignedPublicVauKeys structure during handshake Message 2 but never read the signatureEs256, certHash, or ocspResponse fields. Of the six verification steps gemSpec_Krypt A_24624-01 requires (chain validation, OCSP, role OID, ES256 signature, key format, expiry), only key-format and expiry are checked. The libraries expose no API for consumers to plug verification in. A network MITM positioned inside the TI / Klinik-Netzwerk segment in front of the VAU endpoint — the boundary the inner-VAU layer was designed to harden against — can substitute their own ECDH and Kyber public keys into Message 2 and decrypt or modify all subsequent VAU traffic. Tracked as MS-LIB-VAU-eaea8d (Java) and MS-LIB-VAU-CSHARP-9c69c4 (C#).
Description
gemSpec_Krypt A_24624-01 requires the VAU client to perform six verification steps on the server's SignedPublicVauKeys during the handshake: certificate-chain validation against the TI PKI root, OCSP revocation checking (max 24 h), role OID verification (oid_epa_vau), ES256 signature verification over the key material, ECC/Kyber key format validation, and expiry checking (exp > now). Both reference implementations perform only the last two.
In the Java reference implementation (lib-vau), the handshake gap is here:
lib-vau VauClientStateMachine.java:108-120
SignedPublicVauKeys signedPublicVauKeys;try { signedPublicVauKeys = decodeCborMessageToClass( transferredSignedServerPublicKey, SignedPublicVauKeys.class);} catch (Exception e) { throw new IllegalArgumentException( "Could not CBOR decode Signed Server Public Keys...", e);}
VauPublicKeys transferredSignedServerPublicKeyList = signedPublicVauKeys.extractVauKeys();checkCertificateExpired( transferredSignedServerPublicKeyList.getExp());verifyClientMessageIsWellFormed( transferredSignedServerPublicKeyList.getEcdhPublicKey(), transferredSignedServerPublicKeyList);// Proceeds directly to KEM encapsulation with unverified keys.The SignedPublicVauKeys data class deserialises the relevant fields but never reads them:
lib-vau SignedPublicVauKeys.java:45-58
@JsonProperty("signature-ES256")byte[] signatureEs256;
@JsonProperty("cert_hash")byte[] certHash;
@JsonProperty("ocsp_response")byte[] ocspResponse;The class exposes a sign() method for the server side but no corresponding verify() method, and no interface or callback for consumers to inject verification externally. The README states "Es werden keine Zertifikate geprüft." — it documents the state of the library, but does not document that consumers must provide the verification themselves, or how.
The C# reference implementation (lib-vau-csharp) has the same shape:
lib-vau-csharp VauClientStateMachine.cs:109-113
SignedPublicVauKeys signedPublicVauKeysClient = SignedPublicVauKeys.fromCbor(transferredSignedServerPublicKey);VauPublicKeys transferredSignedServerPublicKeyList = signedPublicVauKeysClient.ExtractVauKeys();KeyUtils.CheckCertificateExpired(transferredSignedServerPublicKeyList.Exp);KeyUtils.VerifyClientMessageIsWellFormed(transferredSignedServerPublicKeyList.EcdhPublicKey, transferredSignedServerPublicKeyList);// Proceeds directly to KEM encapsulation with unverified keys.lib-vau-csharp SignedPublicVauKeys.cs:33-36
readonly byte[] SignatureEs256;readonly byte[] CertHash;readonly byte[] OcspResponse;A_24624-01 compliance summary across both libraries:
A_24624-01 implementation status
Requirement Java C#--------------------------------------------------------OCSP verification (max 24 h) Missing MissingCertificate chain to TI PKI root Missing MissingRole OID oid_epa_vau Missing MissingES256 signature verification Missing MissingECC/Kyber key format validation Present PresentExpiry check (exp > now) Present PresentDownstream adopters embed the reference implementation as-is. The missing verification propagated into shipping clients: med-united/epa4all embeds lib-vau as a submodule and inherits the missing verification unchanged, and oviva-ag/epa4all-client added a SignedPublicKeysTrustValidator interface but discarded the boolean return value of Signature.verify(), so a signature verification failure did not cause the validator to return false (CVE-2026-44900). gematik's official example code in epa4all-examples (v0.1.6, now archived) also demonstrates calling receiveMessage2() without any server-key verification step.
Impact
- The inner-VAU layer was designed to defend patient data against compromise of TLS infrastructure that sits outside the TEE boundary. The attack requires a position inside the TI / Klinik-Netzwerk segment in front of the VAU endpoint — exactly the boundary the inner-VAU layer was specifically designed to harden against.
- From that position, a network MITM can intercept handshake Message 1, perform KEM encapsulation with the client's keys, and respond with a Message 2 carrying attacker-controlled ECDH and Kyber public keys in the AEAD payload. The
signatureEs256field can be filled with arbitrary bytes. - Because the client never verifies the signature, certificate chain, OCSP response, or role OID, it derives session keys from the attacker's keys. The MITM then holds both halves of the session and can decrypt and re-encrypt every subsequent VAU message in clear: a patient's medication records, diagnostic reports, and prescription history read by the ePA client, documents written back to the ePA backend, and the authorization transactions that control who has access to the patient's record.
Mitigation
Downstream consumers that ship either library in a production deployment should add a SignedPublicKeysTrustValidator interface (the name Oviva's fork uses works well) which the VauClientStateMachine constructor *requires* the consumer to supply. Call it from receiveMessage2() before KEM encapsulation. Default to deny — if no validator is provided, fail the handshake rather than proceed without verification. The validator should perform the full A_24624-01 chain: fetch the certificate by cert_hash, validate the OCSP response, verify the chain against TI PKI roots, check for the oid_epa_vau role OID, and verify the ES256 signature over the signed key block. Use the boolean return value of Signature.verify() (Java) / VerifyData() (C#); a discarded return value is the same as no check at all.
Defender's Checklist
Identify whether you ship lib-vau / lib-vau-csharp.
If your ePA client (or any product that speaks VAU) embeds lib-vau or lib-vau-csharp directly, via a fork, or via a git submodule, the gap applies. Two shipping ePA implementations inherited the missing verification unchanged: med-united/epa4all (lib-vau as a git submodule) and oviva-ag/epa4all-client (a validator was added but wired incorrectly — see CVE-2026-44900).
Add a trust validator with default-deny.
Require the
VauClientStateMachineconstructor to take aSignedPublicKeysTrustValidatorparameter and call it inreceiveMessage2()before KEM encapsulation. If no validator is provided, fail the handshake; do not silently proceed. The constructor signature itself must change — Java: add a required parameter to the existing no-arg constructor; C#: equivalent change toVauClientStateMachine().Implement the full A_24624-01 chain.
Cert-hash lookup, OCSP within 24 h, chain to TI PKI root,
oid_epa_vaurole OID, ES256 signature verification over the signed key block. All six checks must succeed for the handshake to continue. TI PKI root certificates are not in the standard system trust store — source them from gematik's published TI PKI (gemSpec_PKI, Appendix — TI Root CA) and pin them explicitly in your validator.Verify the boolean is read.
Both Java's
Signature.verify()and C#'sVerifyData()return a boolean for a bad signature rather than throwing. A discarded return value is a silent pass. The Oviva incident (CVE-2026-44900) is exactly this.Audit your verifier with a forged Message 2.
Stand up a local VAU peer that responds to Message 1 with a Message 2 containing well-formed but unsigned public keys and arbitrary
signatureEs256bytes. The client must refuse the handshake. If it doesn't, the validator is not wired in.
Severity Reasoning
References
How We Can Help
Who We Are
The security researchers behind this advisory.

Dr. rer. nat. Simon Weber
Senior Pentester & MedSec Researcher
I evaluate your SaMD with the same industry-defining security insight I contributed to the BAK MV for the revision of the B3S standard.
- PhD on Hospital Cybersecurity
- Critical vulnerabilities found in hospital systems
- Alumni of THB MedSec Research Group
- gematik Security Hero

Dipl.-Inf. Volker Schönefeld
Senior Application Security Expert
As a former CTO and developer turned pentester, I work alongside your team to uncover vulnerabilities and find solutions that fit your architecture.
- 20+ years as CTO, 50M+ app downloads
- Architected and secured large-scale IoT fleets
- Certified Web Exploitation Specialist
- gematik Security Hero
Looking for a Penetration Test?
Machine Spirits specializes in security assessments for medical devices and healthcare IT. From MDR penetration testing to C5 cloud compliance, we help MedTech companies meet regulatory requirements.
