All Advisories

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.

Authored byVolker Schönefeld, Simon Weber2026-05-28
SeverityHighCVSS 7.4CVSS 3.1 VectorAV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:NCWECWE-323 (Reusing a Nonce, Key Pair in Encryption)Productgematik lib-vau / lib-vau-csharpAffected Versionslib-vau 1.0.0 through 1.0.15 (Java); lib-vau-csharp 1.0.0 through 1.0.9 (C#)Fixed InNot patched upstream. The vendor has clarified the libraries' experimental status in the README and considers no advisory necessary. Downstream consumers that ship the libraries in production are responsible for adding the missing server-side counter (see Mitigation).

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

@Override
protected long getRequestCounter() {
return clientRequestCounter;
}

View source →

Server stores the client-supplied counter with no monotonicity or replay check:

lib-vau VauServerStateMachine.java:191-194

@Override
protected void checkRequestCounter(long reqCtr) {
this.clientRequestCounter = reqCtr;
}

View source →

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);

View source →

The client side correctly increments its own counter before each encryption:

lib-vau VauClientStateMachine.java:169-173

@Override
public byte[] encryptVauMessage(byte[] cleartext) {
try {
requestCounter++;
return super.encryptVauMessage(cleartext);

View source →

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;
}

View source →

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);

View source →

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
}

View source →

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. With H, 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

AV:NReachable from any peer that can complete a VAU handshake with the affected server. In the ePA deployment topology, that includes legitimate clients and any network position that can establish a handshake.AC:HRequires collecting on the order of 2^16 to 2^32 server responses under a fixed Request-Counter, then applying the Joux GHASH-key recovery on a collision. Where a downstream consumer never increments the client counter (as in fbeta: AES-GCM Nonce Reuse via Frozen VAU Request Counter), the precondition becomes passive and the system-level AC collapses to L.PR:NThe VAU handshake itself is the only credential required; A_24623 does not enforce monotonicity on the counter the client supplies.UI:NNo second-party interaction is needed.S:UImpact is bounded to the cryptographic guarantees of the VAU channel between the client and the server's TEE.C:HPlaintext leakage from XOR of colliding ciphertexts; the inner-VAU layer protects the ePA response payloads.I:HGHASH-key recovery enables universal forgery of authenticated server-to-client VAU messages.A:NNo availability impact.

References

How We Can Help

Who We Are

The security researchers behind this advisory.

Dr. Simon Weber Profile

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
Volker Schönefeld Profile

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.