Security › sign-01 · Constant-time review · Performance: x86_1 · arm_1
Aigis-Sig+ (sign-01)
| Candidate | Aigis-Sig+ |
|---|---|
| Family | Lattice (Module-LWE/SIS, Fiat-Shamir) |
| Archive | Aigis-Sig+.zip (SHA-256: 88242576a3ae8f9d090b0c9045020f04ee9b5ae828e01b839f25267959e9c7ea) |
| Specification | sign-01-spec.pdf PDF |
| Performance | x86_1 · arm_1 |
sign-01-1: Trivial signature malleability violates SUF-CMA
| Severity | High |
|---|---|
| Scope | implementation |
| Status | Confirmed |
| Affected | Reference implementation, all three parameter sets |
| Discovery | Trivial |
| Exploitation | Trivial |
| Credit | Markku-Juhani O. Saarinen markku-juhani.saarinen@tuni.fi, with AI assistance |
| Date | 2026-09-21 |
Follow-up source: Aigis-Sig+ team's update
The packed hint has a variable meaningful length inside a fixed-size signature buffer. Verification decodes the meaningful portion but does not require a unique, canonical encoding of the remaining bytes.
Changing a sampled unused packed-hint bit in a valid signature produces a different byte string that still verifies for the same public key and message. No secret key or signing operation is needed to construct the second signature.
This does not by itself forge a signature for a new message, so it is not an EUF-CMA break. It does directly violate the specification's strong-unforgeability claim, because an attacker transforms one valid signature into a distinct valid signature on the same message.
Proposed fixes
The Aigis-Sig+ team's update proposes fixed-length signature encoding, zero padding for unused hint bits and bytes, and verification checks for invalid markers, excessive counts, duplicate or out-of-order positions, and nonzero padding. This section records the proposal without evaluating it.
Reproducing
Commands below run in a checkout of the ngcc-harness repository with the candidate built (see its README).
Build the candidate and the reproducer, then run:
make -C api harness && make -C tools && make -C sign-01
tools/ngcc_attack sig-malleable sign-01/lib/libAigis-sig1.so
tools/reproduce.sh runs this together with the other supported runtime
witnesses and their controls. See tools/README.md.
sign-01-2: Malformed hint counts cause an attacker-controlled stack write
| Severity | High |
|---|---|
| Scope | implementation |
| Status | Confirmed |
| Affected | Reference implementation, all three parameter sets |
| Discovery | Trivial |
| Exploitation | Unauthenticated denial of service; stronger memory-corruption impact is platform-dependent |
| Credit | Markku-Juhani O. Saarinen markku-juhani.saarinen@tuni.fi, with AI assistance |
| Date | 2026-09-21 |
Follow-up source: Aigis-Sig+ team's update
unpack_h reads the first attacker-controlled hint byte into max and uses it without checking the fixed capacity of the stack array t. It then sums attacker-controlled decoded counts into k, calls unpack6bits(pos, sm, k) without checking the OMEGA-element stack array pos, and uses the decoded positions as coefficient indices.
The exhaustive signature-bit reproducer reaches this field and triggers *** stack smashing detected *** during verification; for Aigis-sig1, signature bit 15617 is one confirmed crashing input. Because verification processes unauthenticated signatures, this is a remotely reachable stack-buffer overflow in the submitted API, distinct from the noncanonical-signature malleability in sign-01-1.
Proposed fixes
The Aigis-Sig+ team's update proposes checks for invalid hint markers, excessive hint counts, duplicate or out-of-order positions, nonzero padding, and signature lengths. This section records the proposal without evaluating it.
Reproducing
Commands below run in a checkout of the ngcc-harness repository with the candidate built (see its README).
make -C api harness && make -C tools && make -C sign-01
tools/ngcc_attack sig-malleable sign-01/lib/libAigis-sig1.so
The command reports both the accepted noncanonical flips for sign-01-1 and the number of flips that crash the verifier. The source defect is shared by the three submitted parameter sets.
sign-01-3: Signature API ignores declared key-buffer lengths
| Severity | Low |
|---|---|
| Scope | implementation |
| Status | Confirmed |
| Affected | Aigis-Sig+ reference wrappers, all three parameter sets; runtime witness on set I |
| Discovery | Trivial |
| Exploitation | Out-of-bounds read or crash on a caller-supplied short key; no disclosure shown |
| Credit | Askus Li, with Askus Operator (Luna High) assistance |
| Date | 2026-09-23 |
Original source: NGCC PKC Forum post, GitHub PR #14 Follow-up source: Aigis-Sig+ team's update
The submitted sig_sign and sig_verify wrappers discard sk_len_bytes and pk_len_bytes before unpack_sk and unpack_pk read the fixed-size keys. A one-byte key declared with length zero triggers an AddressSanitizer out-of-bounds read in each path. Applications that always pass the advertised key sizes do not encounter this defect; no remote disclosure or stronger exploit is demonstrated.
Proposed fixes
The Aigis-Sig+ team's update proposes checks for key, signature, and message lengths and the signature-length output pointer, plus resetting the output length to zero on signing failure. This section records the proposal without evaluating it.
Reproducing
Commands below run in a checkout of the ngcc-harness repository with the candidate built (see its README).
bash sign-01/reproduce_memory_safety.sh
The source-built set-I tests require Linux/GCC AddressSanitizer and print separate CONFIRMED lines for the short secret and public keys.
sign-01-4: Honest signing writes one polynomial past the mask vector
| Severity | Low |
|---|---|
| Scope | implementation |
| Status | Confirmed |
| Affected | Aigis-Sig+ reference signers, all three parameter sets; runtime witness on set I |
| Discovery | Trivial |
| Exploitation | 2,048-byte stack-buffer write on the signer path; controlled corruption or key disclosure not shown |
| Credit | Askus Li, with Askus Operator (Luna High) assistance |
| Date | 2026-09-23 |
Original source: NGCC PKC Forum post, GitHub PR #14 Follow-up source: Aigis-Sig+ team's update
polyvecl_uniform_gamma1 fills PARAM_L polynomials and then calls polyz_unpack(v->vec + i, outbuf) once more after the loop, when i == PARAM_L. That writes an entire polynomial past the polyvecl object during honest signing. The extra call is present in all three reference sets. AddressSanitizer confirms the out-of-bounds stack write at polyvec.c:214; ordinary signing may appear to work because the adjacent stack layout varies. This is separate from sign-01-2's attacker-input overflow in verification.
Proposed fixes
The Aigis-Sig+ team's update proposes removing the extra unpacking call after sampling y. This section records the proposal without evaluating it.
Reproducing
Commands below run in a checkout of the ngcc-harness repository with the candidate built (see its README).
bash sign-01/reproduce_memory_safety.sh
The set-I witness invokes the same mask sampler as the signer and requires an AddressSanitizer stack-buffer-overflow diagnostic at polyvecl_uniform_gamma1.
sign-01-5: The challenge sampler collapses independent signs to one effective bit
| Severity | Critical |
|---|---|
| Scope | implementation |
| Status | Confirmed |
| Affected | Aigis-Sig+-I and -II reference, AVX2, NEON, and AArch64 implementations |
| Discovery | Trivial |
| Exploitation | Most likely challenge costs about 2^213.46 trials in set II; set I admits about 2^68.59 generic Grover iterations |
| Credit | Yijian Liu, with Doubao assistance |
| Date | 2026-09-27 |
Original source: Liu's PKC Forum post Follow-up source: Aigis-Sig+ team's confirmation and fix summary
Algorithm 24 assigns an independent sign to every nonzero challenge coefficient. In the PARAM_C <= 64 implementation branch, however, the sampler sets one coefficient from signs & 1 and then executes signs = 0 instead of shifting to the next bit. This affects both set I (24 nonzero coefficients) and set II (44). For each support, one sign-bit value gives the all-positive challenge; the other leaves one negative coefficient whose position can vary as the sampler swaps coefficients. Thus set II has about 45*binomial(512,44) = 2^217.95 distinct outputs, but the most likely output has probability 1/(2*binomial(512,44)). Its min-entropy, and the corresponding generic challenge-search work, is only 213.4609 bits. The specification instead defines
binomial(512,44) * 2^44 = 2^256.4609 possible uniformly signed challenges.
The most likely set-II challenge therefore costs about 2^213.46 trials, below its 256-bit claim. Set I similarly has 137.17 bits of min-entropy, corresponding to about 2^68.59 generic Grover queries or iterations. No end-to-end reversible-oracle gate cost is claimed for that quantum search. Set I remains above its 128-bit classical claim; set II independently sustains the Critical classification. Set III uses a separate branch and is unaffected. The team confirms the error.
Proposed fixes
The Aigis-Sig+ team's update proposes changing the random-sign handling for sets I and II in its reference, AVX2, and ARM implementations. This section records the proposal without evaluating it.
Reproducing
Commands below run in a checkout of the ngcc-harness repository with the candidate built (see its README).
python3 sign-01/reproduce_challenge_entropy.py
The script checks the defective assignment in eight archived implementation copies across sets I and II. It computes the number of distinct outputs and the maximum output probability separately, since the outputs are not equally likely.
sign-01-6: The printed abort bound exceeds one, outside the theorems' hypothesis
| Severity | Low |
|---|---|
| Scope | design |
| Status | Confirmed |
| Affected | Aigis-Sig+ PARAMS I and PARAMS III security analysis |
| Discovery | Moderate |
| Exploitation | The printed numerical bound lies outside the theorems' hypothesis; the theorems hold for any bound below one, and no forgery or key recovery is demonstrated |
| Credit | Sun Shuzhou, with GLM-5.3 assistance |
| Date | 2026-10-01 |
Original source: Sun Shuzhou's PKC Forum post Follow-up source: Aigis-Sig+ team's response
Section 7.3 sets the signing-abort upper bound to
pbar = (1-exp(-n*l*beta1/gamma1)) + (1-exp(-n*k*(beta2+eta1)/gamma2)).
At the Table 1 parameters this is 1.07668, 0.98003, and 1.16094 for sets I–III. The §3.4 repetition estimate, evaluated in its product form before the exponential approximation, gives 6.408, 5.158, and 5.706, matching Table 2's rounded 6.41, 5.16, and 5.71. Theorems 1 and 2 and their supporting reductions explicitly require 0 < pbar < 1, so the specification's chosen bound violates its own precondition for sets I and III. The sum double-counts overlap between the two abort events.
Theorems 1 and 2 are stated for any 0 < pbar < 1 and remain valid; the defect is confined to the printed instantiation. Section 3.4's own heuristic combined-abort estimate, 1-exp(-(a+b)), is 0.844, 0.806, and 0.825. That estimate agrees with instrumented abort-rate measurements of the reference implementation, but it rests on the heuristic assumptions stated in §3.4 and does not by itself establish a uniform bound for every secret key. Correcting the loose union bound is local and leaves §7.3's final bound unchanged. This is a documented inconsistency in the submitted specification rather than a gap in the proof; the consequence is therefore Low / Confirmed.
The Aigis-Sig+ team confirms that the displayed sum is loose, says that
1-exp(-a-b) matches the measured abort rates, and notes that the proof only
requires some bound pbar < 1.
Proposed fixes
In a private communication to the maintainer on 2026-10-01, the team states that a post-submission revision removes the concrete pbar value and relies only on the existence of a bound below one. This section records the revision without evaluating it; the archived submission remains the evaluated artifact.
Reproducing
Commands below run in a checkout of the ngcc-harness repository with the candidate built (see its README).
python3 sign-01/reproduce_abort_bound.py
The script evaluates the printed formula from the Table 1 parameters and checks the Table 2 repetition-rate control.
sign-01-7: The external DRBG is used as a KDF
| Severity | Low |
|---|---|
| Scope | implementation |
| Status | Confirmed |
| Affected | Aigis-Sig+-I, -II and -III ICCS reference implementations |
| Discovery | Trivial |
| Exploitation | Replacing the external RBG changes deterministic polynomial and matrix expansion and breaks interoperability; the frozen submitted build is internally consistent |
| Credit | Markku-Juhani O. Saarinen markku-juhani.saarinen@tuni.fi, with AI assistance |
| Date | 2026-10-03 |
Under USE_ICCS, kdf_init resets an ICCS DRNG_ctx from protocol input and kdf_squeezeblocks reads the stream (hashkdf.c:82–89). Signing and verification use it for deterministic polynomial and matrix expansion (polyvec.c:61–82; poly.c:358–378). The implementation therefore treats the external RBG as a scheme KDF whose exact output must agree across parties.
The archived drng.c is self-consistent, so no forgery or key recovery follows. A scheme-defined XOF/KDF should specify the required seed-to-stream mapping.
Reproducing
Commands below run in a checkout of the ngcc-harness repository with the candidate built (see its README).
python3 security/rbg_protocol_dependency.py --report-id sign-01-7
sign-01-8: Short message representatives cap Aigis-Sig+ II and III below their claimed levels
| Severity | Critical |
|---|---|
| Scope | design |
| Status | Confirmed |
| Affected | Aigis-Sig+ II and III specification, reference and AVX2 implementations |
| Discovery | Trivial |
| Exploitation | Generic collision and signature transfer in about 2^192 or 2^384 hash evaluations; no full-width collision was computed |
| Credit | Sun Shuzhou, with GLM-5.3 assistance |
| Date | 2026-10-03 |
Original source: Sun Shuzhou's PKC Forum post Follow-up source: Aigis-Sig+ team's confirmation
Table 1 claims 256- and 512-bit classical security for PARAMS II and III.
Table 3, however, fixes their unsalted message representatives to
XOF-256(tr || M, 48) and XOF-512(tr || M, 96), only 384 and 768 bits.
Signing and verification use the message only through that representative.
Consequently, a generic collision costs about 2^192 or 2^384 evaluations;
a signature requested on one colliding message transfers unchanged to the
other. PARAMS I uses the same 384-bit representative but remains above its
128-bit classical target.
The submitted reference and AVX2 implementations use the same 48- and 96-byte
values (params.h) and compute the representative before the remaining
signing or verification transcript (sign.c). Replacing the contest hash
placeholders with ideal primitives of the same external dimensions therefore
does not change the ceiling. The forum post additionally reports a
reduced-output collision-and-transfer experiment against v1.0.1; no
full-width collision was attempted. The explicit below-target forgery bounds
make this Critical under the classification policy.
The Aigis-Sig+ team confirms the issue and says that it will increase the CRH output lengths for PARAMS II and III.
Proposed fixes
The team proposes increasing the CRH output length to 512 bits for PARAMS II and to 1024 bits for PARAMS III, with no other changes.
Reproducing
Commands below run in a checkout of the ngcc-harness repository with the candidate built (see its README).
python3 security/design_parameter_audit.py --report-id sign-01-8
The certificate checks the normative output lengths and security claims and the corresponding reference source constants and message-hash data flow.
sign-01-9: 32-bit message-length arithmetic gives long-message forgeries
| Severity | Critical |
|---|---|
| Scope | implementation |
| Status | Confirmed |
| Affected | Aigis-Sig+ I, II and III reference implementation in the submitted USE_ICCS build (sign-01/lib), and the AVX2 USE_ICCS trees; NEON source-identical but not run; the spec-default USE_SHA3 reference build is not affected |
| Discovery | Trivial |
| Exploitation | A signature on a 32-byte message verified on fresh 2^29 + 32-byte messages, and one signature on a 256 MiB message verified on other contents and lengths, at all three levels in the reference library and all three AVX2 builds; the AVX2 builds also accepted a 2^32 + 32-byte message |
| Credit | Markku-Juhani O. Saarinen markku-juhani.saarinen@tuni.fi, with AI assistance |
| Date | 2026-10-17 |
Aigis-Sig+ signs and verifies the message representative µ = CRH(CRH(pk) ∥ M) (§5.5, Algorithm 30 line 3, p. 33, and Algorithm 31 line 5, p. 34) and claims SUF-CMA security (§7.1, p. 38). In the USE_ICCS build, kdf128/256/512(uint8_t *out, int outlen, uint8_t *in, int inlen) call pseudoXOF(outlen * 8, in, inlen * 8, out) (hashkdf.c:8–45). Signing and verification pass inlen = CRHBYTES + mlen (sign.c:87–98,167–174). The bit length inlen * 8 is computed in a 32-bit int. When |M| = 2^29 + j, 8 · (CRHBYTES + |M|) wraps to 8 · (CRHBYTES + j), so µ depends only on CRH(pk) ∥ M[:j]. When 2^28 ≤ CRHBYTES + |M| < 2^29, the bit length is negative, pseudoXOF's malloc fails (auxfunc.c:486–491) and kdf* ignore the error. The output buffer then still holds CRH(pk), and µ is the same for every message in that range. The AVX2 and NEON trees have the same kdf* code and also take int32_t mlen (sign.c:75,145–147), so a message of 2^32 + j bytes is truncated to its first j bytes as well.
Two one-query forgeries follow. First, a signature on any short message m also verifies on every 2^29 + |m|-byte message that begins with m, whatever the remaining bytes. Second, a signature on any message in the negative-length range (about 256 MiB) verifies on every other message in that range. Both lengths are within the call's required message range of up to 2^63 bits (Submission Requirements §2(2)), so these are fresh-message EUF-CMA forgeries and break the stronger SUF-CMA claim as well.
Natively, lib/libAigis-sig1/2/3.so and temporary AVX2 builds of all three sets accepted both forgeries in fresh processes. The AVX2 builds also accepted a 2^32 + 32-byte message under the 32-byte message's signature. Changing the first byte of each long message, verifying the 256 MiB signature on a short message or on a message just below the range, and changing the short message were all rejected. The spec-default USE_SHA3 reference (kdf* call shake* with the byte length) rejected the 2^29 + j case. The reference copy loops use int i over the 64-bit mlen (sign.c:95,170), so every reference build crashes rather than truncating at |M| ≥ 2^31. Lengths should be carried as size_t/unsigned long long throughout, and pseudoXOF failures must abort signing and verification.
Reproducing
Commands below run in a checkout of the ngcc-harness repository with the candidate built (see its README).
python3 sign-01/reproduce_kdf_length_overflow.py
The script tests the three prebuilt reference libraries and, when gcc and an AVX2/BMI2 CPU are present, builds the three unmodified AVX2 trees with -DUSE_ICCS in a temporary directory and tests them too (otherwise it skips that part). Long messages are anonymous mmap buffers, each verification runs in a fresh process, and controls are included. It takes about 80 seconds and needs about 1.5 GiB of memory.