PQC-XXJTLU · cryptanalysis

HomePublic-key schemes › Aigis-Enc+

KEM · lattice

Aigis-Enc+

Implementation

Bugs in the submitted code; the specification is sound on these points.

aigis-enc-plus-i1

The submitted decapsulation discards its re-encryption check and returns the derived key for every ciphertext

Minor break · Demonstrated · Cause: code

Property: IND-CCA security of the submitted code

Cost against claim: one decapsulation query against IND-CCA2 at 128, 256 and 512 bits, in all four implementations

In every submitted tree (reference, AVX2, NEON, AArch64) the decapsulation writes the key derived from the decrypted message to its output before the re-encryption comparison, then moves the rejection value into a local buffer instead of the output and clears the failure flag. The output is therefore H2(Dec(sk, c) ‖ H1(pk)) for every ciphertext, valid or not. Flipping one low bit of a challenge ciphertext leaves the decrypted message unchanged, so a single decapsulation query returns the challenge's own key and tells a real session key from a random one.

Parameter setClaimAttack costVerdict
Aigis-Enc+-512IND-CCA2, 128 classicalone decapsulation queryMinor break
Aigis-Enc+-1024IND-CCA2, 256 classicalone decapsulation queryMinor break
Aigis-Enc+-2048IND-CCA2, 512 classicalone decapsulation queryMinor break

What causes it

The specification is sound on this point: Algorithm 20 (Aigis-Enc+.Decap) sets K = H3(s, c) at line 8 and replaces it with K' only when the re-encryption c' equals c (lines 9 and 10), the implicit-rejection transform that Theorems 2 and 3 analyse. In kem.c, mkem_dec copies K' to the output unconditionally (line 71), computes the comparison (line 74), then writes the rejection key into the scratch buffer buf rather than into ss (line 80) and sets fail to 0 (line 81). The NGCC wrapper kem_dec adds no check and always returns 0. The official test vectors decapsulate honest ciphertexts only, so they cannot reveal the defect.

Evidence

Cause in the submitted code; the specification is not affected.

Limits

Key recovery is argued, not run: since the output is a deterministic function of the decrypted message and H1(pk) is public, the decapsulation is a plaintext-checking oracle, and the published key-mismatch attacks on LWE-type encryption recover the secret key from thousands of such queries, far inside the 280 the call allows. The fix is to move the constant-time replacement to the output; in the reference and AArch64 trees the rejection secret must also be read at its specified offset (next entry).

Designers' response

The designers posted an updated implementation on the NGCC PKC forum (2026-09-22), stating that the two decryption errors reported on ngcc.dev are fixed. PQC-X has not checked it.

What PQC-X adds

The defect is confirmed in the AVX2, NEON and AArch64 trees as well as in the reference, and the shipped decapsulation is compared with a transcription of Algorithm 20 on every single-bit modification of valid ciphertexts: it never returns the specified value. The consequence beyond a distinguisher, a plaintext-checking oracle that exposes the secret key to standard key-mismatch attacks, is argued.

Credit

First public report: M.-J. Saarinen (ngcc.dev kem-01-1, 2026-09-21). Found independently by PQC-X.

aigis-enc-plus-i2

The reference and AArch64 decapsulation read the rejection secret from before the secret key

Holds · Argued · Cause: code

Property: conformance of the rejection path; no claimed property falls in the submitted code

Cost against claim: with the first defect repaired, rejection would hash a constant instead of the secret

Scope: The reference and AArch64 trees, all three sets; the AVX2 and NEON trees are not affected.

Algorithm 18 stores the rejection secret s as the last κ' bytes of the secret key, and Algorithm 20 derives the rejection key as H3(s, c). In the reference and AArch64 trees, mkem_dec reads those bytes from just before the start of the secret key instead (kem.c line 76, sk-SEED_BYTES); the AVX2 and NEON trees read the correct offset. In the submitted code the value is discarded anyway (previous entry). Once the constant-time replacement is repaired, this second defect would still make the reference code derive its rejection key from memory outside the key: in the NGCC harness layout those bytes are a constant, so anyone could compute the rejection value, and the code would still not be the specified scheme.

Parameter setClaimAttack costVerdict
Aigis-Enc+-512, -1024, -2048, reference and AArch64 coderejection key derived from the secret snoneHolds

Evidence

Found by reading the code while establishing the previous entry. A second member of PQC-X confirmed the offset in all six reference and AArch64 trees (the same kem.c byte for byte), checked that the AVX2 and NEON trees read the last bytes of the key, and measured, with a probe that allocates the keys as the NGCC test harness does, the bytes the reference code would hash as the secret: the same 16-byte allocator header on every run, on Aigis-Enc+-512.

Cause in the submitted code; the specification is not affected.

Designers' response

The designers posted an updated implementation on the NGCC PKC forum (2026-09-22), stating that the two decryption errors reported on ngcc.dev are fixed. PQC-X has not checked it.

What PQC-X adds

What the out-of-bounds read hashes in the harness's memory layout: a constant allocator header, measured on the reference build. Once the first defect is repaired, the rejection value is therefore computable by anyone, and the code behaves as an explicit-rejection KEM, a variant the designers' proofs do not cover, rather than as Algorithm 20.

Credit

First public report: M.-J. Saarinen (ngcc.dev kem-01-2, 2026-09-21). Found independently by PQC-X.