CVE-2023-46129 - The NATS.io nkeys Encryption Vulnerability Explained and Exploited

The TL;DR:
If you’re running NATS Server 2.10. through 2.10.3 or using the nkeys Go library .4. through .4.5, pay attention: any encryption handled by nkeys in these versions uses an all-zeros key. This basically wrecks the security for protected data, especially for authentication callouts added in nats-server 2.10. What follows is an exclusive, easy-to-read guide that walks through the bug, the impact, references, plus code to understand (and exploit) the problem.

What is NATS.io and nkeys?

NATS.io is a blazingly fast, open-source messaging platform used for cloud, on-premises, IoT, and edge computing. It lets different services or devices talk to one another via the *publish-subscribe* model.

To secure these chats, NATS uses nkeys, a cryptographic library (in Go) for managing key pairs and, recently, encrypting and decrypting data.

> Fun fact: nkeys originally only did signing/authentication but in 2023, v.4. added encryption support.

CVE-2023-46129 targets the nkeys crypto library. Here’s what happened

- In nkeys versions .4. through .4.5 (used by NATS Server 2.10. through 2.10.3), the new *encryption* code had a bug in how it passed around arrays (the encryption key buffers).
- A function passed the array by value (so the changes didn’t affect the real data) instead of by reference.
- When the function tried to populate the buffer with the real encryption key, it was updating a *copy*, not the buffer actually used for encryption.
- Bottom line? When you tried to encrypt data, the key being used was a 32-byte array of all zeroes—failing entirely at data secrecy.

Important: This doesn’t affect signing/authentication; only encryption is broken.

Below is a simplified, illustrative example (not the exact code, but the heart of the issue)

func xorKey(b [32]byte) {
    for i := ; i < len(b); i++ {
        b[i] = getRealKeyByte(i) // Real key bytes
    }
    // Here's the catch: 'b' is a copy, so changes lost!
}

func encrypt(data []byte) []byte {
    var key [32]byte // Starts all zeroes!
    xorKey(key)
    for i := range data {
        data[i] ^= key[i % 32] // Actually using all-zeros key :(
    }
    return data
}

Result: Data is XOR'd with zeroes—i.e., not encrypted at all. Anyone can "decrypt" it trivially.

How Should It Be?

func xorKey(b *[32]byte) {
    for i := ; i < len(b); i++ {
        b[i] = getRealKeyByte(i)
    }
}

func encrypt(data []byte) []byte {
    var key [32]byte
    xorKey(&key) // Pass by reference!
    for i := range data {
        data[i] ^= key[i % 32]
    }
    return data
}

---

Exploit: How an Attacker Could Read Your Secret Data

Let’s see how this breaks confidentiality. Assume someone intercepts encrypted authentication payloads heading to a NATS authentication callout microservice.

Suppose you catch this blob

ciphertext = b93a52...  // (just example hex)

In the broken version, actual decryption looks like this in Python

# Real NATS encrypted bytes
ciphertext = bytes.fromhex("b93a52...")

# Normally, key is secret, but here:
key = bytes([]*32)

# XOR with zeros does nothing!
plaintext = bytes([c ^ k for c, k in zip(ciphertext, key*(len(ciphertext)//32+1))])
print(plaintext)
# Output is the original data!

Or, if you like Go

for i := range ciphertext {
    plaintext[i] = ciphertext[i] ^  // XOR zero => same byte
}
// plaintext == ciphertext!

What are Auth Callouts?

Starting in NATS Server 2.10., you can configure authentication "callouts":
When a client connects and authenticates, NATS passes data to another backend system for approval. That sensitive data is supposed to be *encrypted* with nkeys.

So, What Goes Wrong?

- If you use Go for auth callouts and the affected nkeys library, your payloads are "encrypted" with the zero-key.
- Attackers who can snoop on traffic between NATS Server and the callout backend can simply read sensitive info, like authentication tokens, as if there was no encryption.
- Any secrets sent this way are fully exposed, *breaking all assumptions of secure backend comms* in your auth pipeline.

Fixing the Hole

No workarounds. Upgrade to nkeys .4.6+ (released with NATS Server 2.10.4+ where this bug is fixed):
- Key handling now passes the buffer by reference, so the real key gets filled in and used for actual secure encryption.
- Update your Go app dependencies, recompile, redeploy your servers and callout microservices together.

Are you relying on nkeys-encrypted payloads between backend components?

If yes:
Upgrade nkeys to v.4.6+ immediately, recompile and redeploy both sides!

- NATS Security Advisory: CVE-2023-46129
- GitHub Issue: NKEYS Encryption bug
- Patched nkeys v.4.6 Release
- NATS Server Releases
- Summary at cve.org

Quick Summary Table

| Version | Affected? | Fix? |
|------------------|-------------------|-------------|
| nkeys < .4. | No encryption | Not affected|
| nkeys .4.–.4.5| All encryptions bad| Yes (.4.6) |
| nkeys .4.6+ | Fixed! | Use it |
| NATS Server 2.10.–2.10.3 | Affected | Yes (2.10.4)|
| NATS Server 2.10.4+ | Safe | |

Conclusion

CVE-2023-46129 isn’t your usual edge-case crypto slip—it’s a critical bug where encryption is completely bypassed for a major server’s authentication flows. For anyone using NATS with auth callouts (new since v2.10), your sensitive traffic was naked if on vulnerable versions.

There’s *no mitigation except to update both your servers and backend Go apps* to nkeys .4.6+ and NATS Server 2.10.4+. Don’t sleep on this one!

Found this helpful? Protect your infra—upgrading takes minutes, regret can last months.

*This post is based on public CVE information and official advisories, with added code and explanation for exclusive clarity.*

Timeline

Published on: 10/31/2023 00:15:09 UTC
Last modified on: 11/29/2023 03:15:42 UTC