gematik lib-vau / lib-vau-csharp
AES-GCM Nonce Reuse in VAU Server Encryption
The VAU server reuses the client-supplied Request-Counter as the 64-bit counter portion of its own AES-GCM IV, instead of maintaining an independent server-side encryption counter. IV uniqueness collapses to the 4 random bytes the server prepends, exposing the server-to-client channel to the Joux forbidden attack at ~77,000 same-counter responses.
Description
The VAU protocol specification (gemSpec_Krypt V2.40.0) requires in A_24631 that the server maintain its own independent 64-bit encryption counter for the application data key K2_s2c_app_data. The counter must start at 0 and increment on each encryption. A_24632 then specifies that the server's AES-GCM IV is constructed as random(4) || server_encryption_counter(8), while the response header's Request-Counter field separately echoes the client's stored value from A_24630.
Both reference implementations diverge from this. VauServerStateMachine has no server-side counter field and does not override the encryption method. The server's getRequestCounter() returns clientRequestCounter (the value stored from the client's most recent request), and the same value is used both for the response header's Request-Counter field and for the AES-GCM IV.
In the Java reference implementation (lib-vau), the server reuses the client's counter for the IV:
lib-vau VauServerStateMachine.java:181-184
@Overrideprotected long getRequestCounter() { return clientRequestCounter;}Server stores the client-supplied counter with no monotonicity or replay check:
lib-vau VauServerStateMachine.java:191-194
@Overrideprotected void checkRequestCounter(long reqCtr) { this.clientRequestCounter = reqCtr;}IV construction uses getRequestCounter() for the counter portion:
lib-vau AbstractVauStateMachine.java:112-124
public byte[] encryptVauMessage(byte[] cleartext) { byte versionByte = 2; byte puByte = 0; byte reqByte = getRequestByte(); byte[] reqCtrBytes = ByteBuffer.allocate(8).putLong(getRequestCounter()).array(); byte[] header = unionByteArrays(versionByte, puByte, reqByte, reqCtrBytes, getKeyId());
byte[] a = new byte[4]; new SecureRandom().nextBytes(a);
byte[] iv = unionByteArrays(a, reqCtrBytes);
byte[] ciphertext = encryptWithAesGcm(encryptionVauKey.getAppData(), iv, cleartext, header);The client side correctly increments its own counter before each encryption:
lib-vau VauClientStateMachine.java:169-173
@Overridepublic byte[] encryptVauMessage(byte[] cleartext) { try { requestCounter++; return super.encryptVauMessage(cleartext);The C# reference implementation (lib-vau-csharp) carries the same logic and produces the same effect:
lib-vau-csharp VauServerStateMachine.cs:58-70
protected override void CheckRequestCounter(long requestCounter){ clientRequestCounter = requestCounter;}
protected override long GetRequestCounter(){ return clientRequestCounter;}lib-vau-csharp AbstractVauStateMachine.cs:47-61
public virtual byte[] EncryptVauMessage(byte[] plaintext){ byte versionByte = 2; byte puByte = (byte)(isPu ? 1 : 0); byte requestByte = GetRequestByte(); long requestCounter = GetRequestCounter(); byte[] requestCounterBytes = BitConverter.GetBytes(requestCounter).Reverse().ToArray(); byte[][] headerBytes = new byte[][] { new byte[] { versionByte }, new byte[] { puByte }, new byte[] { requestByte }, requestCounterBytes, KeyId }; byte[] header = Arrays.ConcatenateAll(headerBytes);
byte[] random = new byte[4]; new SecureRandom().NextBytes(random);
AesGcm aesGcm = new AesGcm(); aesGcm.initAESForEncryption(random, requestCounter, header, encryptionVauKey);The IV itself is constructed inside crypto/AesGcm.cs, where gematik's own comment names A_24628:
lib-vau-csharp crypto/AesGcm.cs:81-91
private static byte[] initializeIV(byte[] random, long lCounter){ // A_24628 -> 32 Bit Random + 64 Bit Verschlüsselungszähler if (random?.Length != 4) { throw new ArgumentNullException(nameof(random), "Invalid random value!"); }
byte[] counter = BitConverter.GetBytes(lCounter).Reverse().ToArray(); // A_24629, A_24631 -> 64 Bit encryption counter return random.Concat(counter).ToArray(); // A_24628 -> concat random and counter}Because A_24623 does not enforce replay protection or sequence ordering in the current ePA build stage (ERP=false, ESO=false), a client can send any counter value, including a previously used one. GCM nonce uniqueness for the server's responses then depends on only the 32 random bits the server prepends.
By the birthday paradox, the collision probability for the 4-byte random prefix grows quickly when the counter portion is held constant:
Birthday collision probability for a fixed 64-bit counter
Messages P(IV collision) 100 < 0.01 % 1,000 0.01 % 10,000 1.16 % 50,000 25.23 % 77,163 50.00 % 100,000 68.55 %AES-GCM provides no confidentiality or integrity guarantees on nonce reuse. The Joux forbidden attack (2006) lets an attacker who observes two ciphertexts encrypted under the same key and IV recover the GHASH authentication key H from the two authentication tags, and XOR the two ciphertexts to recover plaintext bytes.
Impact
- The inner-VAU layer protects medication records, diagnostic reports, prescription history, document reads and writes, and the authorization transactions that control access to a patient's record.
- A legitimate VAU client (or a network attacker who has completed a VAU handshake) can hold the server's IV counter portion constant by replaying a Request-Counter value, and collect server responses until two share the same 12-byte IV.
- Once two responses share the same IV, the Joux forbidden attack recovers the GHASH authentication key
H. WithH, the attacker can forge the GCM authentication tag for ciphertexts under any IV where a collision has been observed, and tamper with server-to-client VAU messages in a session where nonce reuse is ongoing. - XOR of the two colliding ciphertexts yields the XOR of the two plaintexts, leaking information about ePA response payloads protected by the inner-VAU AES-GCM layer.
- The inner-VAU layer is the defence against a compromised TLS terminator outside the TEE. Breaking it removes that defence.
- Combined with the gematik: VAU Handshake Performs Only 2 of 6 Required Server-Key Checks advisory, the attack becomes practical. A MITM who has substituted their own keys during the handshake can pin the client Request-Counter to any fixed value on every relayed request, and the birthday collection becomes passive observation of a channel they already control.
Mitigation
Downstream consumers that ship either library in production should add an independent serverEncryptionCounter to VauServerStateMachine (or its C# equivalent), initialise it to 0, and override encryptVauMessage() to increment that counter before encryption. A getRequestCounter() override alone is not enough, because super.encryptVauMessage() uses that return value for both the IV and the header; the server must fully override encryptVauMessage() and split the two counter uses. Per A_24632, the IV's counter portion uses the server's own counter, and the response header's Request-Counter field echoes the client's stored value. Until this is in place, treat the inner-VAU server-to-client channel as offering integrity and confidentiality only up to the order of 2^32 messages per session.
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. The Java library has been pulled in unchanged by at least one production-side consumer (med-united/epa4all); audit your own dependency tree before assuming you're not affected.
Add a server-side counter in your build.
Override the server's
encryptVauMessage()to maintain an independent 64-bit counter, increment it before each encryption, and use that counter (not the client's value) for the IV's counter portion. Keep the response header's Request-Counter field echoing the client's value, per A_24632.Treat session length as a security parameter.
Until a server-side counter is in place, rotate VAU sessions well below the 2^32 message boundary. The IV-collision probability passes 1 % around 10,000 same-counter responses; that is your operational margin.
Log Request-Counter values seen on the server.
Even if you do not change the protocol behaviour, a counter that repeats or steps backwards across many responses in a single session is detectable. Capture and review it; the same metric covers misconfigured clients and active abuse.
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.
