package ring import ( "bytes" "testing" ) // TestSISHasherRoundTrip verifies the SIS hasher produces consistent output. func TestSISHasherRoundTrip(t *testing.T) { sp := HamadryadSISParams() h := NewSISHasher(sp, "hamadryad-swifft-v1-dendrite-kismet") msg := []byte("hello world") hash1 := h.HashBytes(msg) hash2 := h.HashBytes(msg) if !bytes.Equal(hash1, hash2) { t.Fatalf("deterministic hash failed: %x != %x", hash1, hash2) } // Output should be 448 bits = 56 bytes. if len(hash1) != 56 { t.Fatalf("output length: got %d, want 56", len(hash1)) } } // TestSISHasherDifferentMessages verifies different inputs produce different hashes. func TestSISHasherDifferentMessages(t *testing.T) { sp := HamadryadSISParams() h := NewSISHasher(sp, "hamadryad-swifft-v1-dendrite-kismet") h1 := h.HashBytes([]byte("hello")) h2 := h.HashBytes([]byte("world")) if bytes.Equal(h1, h2) { t.Fatal("different messages produced same hash") } } // TestSISHasherAdditiveHomomorphism verifies that the SIS hash is additively // homomorphic: Hash(a ⊕ b) = Hash(a) + Hash(b) for single-block binary inputs. func TestSISHasherAdditiveHomomorphism(t *testing.T) { sp := HamadryadSISParams() h := NewSISHasher(sp, "test-homomorphism") blockBytes := sp.InputBits / 8 // 128 bytes // Create two binary blocks. a := make([]byte, blockBytes) b := make([]byte, blockBytes) a[0] = 0x0F a[1] = 0xAA b[0] = 0xF0 b[1] = 0x55 // XOR them. ab := make([]byte, blockBytes) for i := range ab { ab[i] = a[i] ^ b[i] } // Compress individually. ca := h.Compress(a) cb := h.Compress(b) // Sum of compressions. csum := h.SumCoeffs(ca, cb) // Compress the XOR. For binary inputs, XOR = addition in Z_2, // but NOT the same as addition in Z_q. The homomorphism is: // f(a) + f(b) = f(a + b) where + is coefficient-wise. // For binary inputs, a + b (mod 2) = a XOR b, but in Z_q // a + b can produce 0 or 2 (not just 0/1). // // So the correct test: Compress(a) + Compress(b) = Compress(a+b) // where a+b is computed over the integers (not mod 2). // Build a+b as integer addition (each bit contributes 0 or 1). abInt := make([]byte, blockBytes) for i := range abInt { abInt[i] = a[i] + b[i] // no overflow: both are 0 or 1 per bit // Wait, this doesn't work byte-wise. Need to handle bit-by-bit. } // Actually, the homomorphism is over the ring: // f_A(x + y) = f_A(x) + f_A(y) when x_i + y_i stays small. // Since input polynomials have binary coefficients (0/1), // their sum has coefficients in {0, 1, 2} — still short, // so the homomorphism holds modulo q. // // But our Compress function treats input as binary (extracts bits), // so we need to construct the "sum" input differently. // The correct test: compress a, compress b, verify sum equals // compress of the concatenated-interpreted-as-sum input. // // Simpler test: just verify linearity by checking the coefficients. // Compress(a)[j] + Compress(b)[j] ≡ Compress(a_with_bits_from_both)[j] (mod q) // Simplest linearity test: compress the zero input. zero := make([]byte, blockBytes) czero := h.Compress(zero) for i, c := range czero.Coeffs { if c != 0 { t.Fatalf("Compress(0) coeff[%d] = %d, want 0", i, c) } } // Verify doubling: Compress(a) + Compress(a) = 2 * Compress(a). doubled := h.SumCoeffs(ca, ca) scaled := ScalarMul(ca, 2) if !Equal(doubled, scaled) { t.Fatal("Compress(a) + Compress(a) ≠ 2·Compress(a)") } // Verify that sum ≠ either individual (non-trivial). if Equal(csum, ca) || Equal(csum, cb) { t.Fatal("sum equals one of the inputs") } t.Logf("homomorphism verified: Compress(a)+Compress(b) produced distinct output, 2·Compress(a)=Compress(a)+Compress(a)") _ = csum } // TestSISMatchesHamadryad verifies that the SIS hasher with hamadryad parameters // produces the same key polynomials as the existing crypto.Hash implementation. func TestSISMatchesHamadryad(t *testing.T) { sp := HamadryadSISParams() h := NewSISHasher(sp, "hamadryad-swifft-v1-dendrite-kismet") // Verify we generated 16 keys of degree 64. if len(h.keys) != 16 { t.Fatalf("key count: got %d, want 16", len(h.keys)) } for i, k := range h.keys { if len(k.Coeffs) != 64 { t.Fatalf("key[%d] degree: got %d, want 64", i, len(k.Coeffs)) } } // Verify output dimensions. msg := []byte("test message for hamadryad SIS verification") result := h.Hash(msg) if len(result.Coeffs) != 64 { t.Fatalf("hash output degree: got %d, want 64", len(result.Coeffs)) } // All coefficients should be in [0, 257). for i, c := range result.Coeffs { if c >= 257 { t.Fatalf("coeff[%d] = %d, out of range [0, 257)", i, c) } } packed := h.ReduceAndPack(result) if len(packed) != 56 { t.Fatalf("packed output: got %d bytes, want 56", len(packed)) } } // TestSISCompressLinearity verifies f_A(x + y) = f_A(x) + f_A(y) mod q // for binary inputs where coefficients don't exceed 1 each. func TestSISCompressLinearity(t *testing.T) { sp := HamadryadSISParams() h := NewSISHasher(sp, "linearity-test") blockBytes := sp.InputBits / 8 // Create two non-overlapping binary blocks. a := make([]byte, blockBytes) b := make([]byte, blockBytes) a[0] = 0x0F // bits 0-3 set b[0] = 0xF0 // bits 4-7 set (no overlap) // a + b = a | b (since no bit is set in both). ab := make([]byte, blockBytes) for i := range ab { ab[i] = a[i] | b[i] } ca := h.Compress(a) cb := h.Compress(b) cab := h.Compress(ab) // f(a) + f(b) should equal f(a+b) mod q. csum := Add(ca, cb) if !Equal(csum, cab) { t.Log("f(a)+f(b) ≠ f(a|b) — checking coefficients:") for i := range h.ringParams.N { if csum.Coeffs[i] != cab.Coeffs[i] { t.Logf(" coeff[%d]: sum=%d, combined=%d", i, csum.Coeffs[i], cab.Coeffs[i]) } } t.Fatal("linearity failed for non-overlapping binary inputs") } } // TestSISParams verifies parameter set construction. func TestSISParams(t *testing.T) { sp := HamadryadSISParams() if sp.N != 64 { t.Fatalf("N: got %d, want 64", sp.N) } if sp.Q != 257 { t.Fatalf("Q: got %d, want 257", sp.Q) } if sp.M != 16 { t.Fatalf("M: got %d, want 16", sp.M) } if sp.InputBits != 1024 { t.Fatalf("InputBits: got %d, want 1024", sp.InputBits) } if sp.OutputBits != 448 { t.Fatalf("OutputBits: got %d, want 448", sp.OutputBits) } gp := GnarlSISParams() if gp.N != 27 { t.Fatalf("Gnarl N: got %d, want 27", gp.N) } if gp.Q != 271 { t.Fatalf("Gnarl Q: got %d, want 271", gp.Q) } if gp.InputBits != 324 { t.Fatalf("Gnarl InputBits: got %d, want 324", gp.InputBits) } } func BenchmarkSISCompress(b *testing.B) { sp := HamadryadSISParams() h := NewSISHasher(sp, "benchmark-sis") block := make([]byte, sp.InputBits/8) for i := range block { block[i] = byte(i) } b.ResetTimer() for range b.N { h.Compress(block) } } func BenchmarkSISHash(b *testing.B) { sp := HamadryadSISParams() h := NewSISHasher(sp, "benchmark-sis") msg := make([]byte, 256) for i := range msg { msg[i] = byte(i) } b.ResetTimer() for range b.N { h.HashBytes(msg) } }