Files
crypto/hqc/gpu_test.go
T

164 lines
4.6 KiB
Go

// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
//go:build hqc_pqclean
package hqc
import (
"bytes"
"crypto/rand"
"testing"
"github.com/luxfi/accel/ops/code"
)
// TestGPUBatchRoundTrip exercises the GPU dispatch path
// (batchEncapsulateGPU / batchDecapsulateGPU) end-to-end against the
// CPU PQClean reference. The two paths share the same canonical
// kernels via accel/ops/code so the test asserts that:
//
// 1. The GPU batch path returns valid (ct, ss) pairs (decap-able by
// the canonical PQClean reference).
// 2. The byte layout matches what PQClean produces with the same
// entropy stream — there is no "shifted" or "padded" encoding
// difference between the two paths.
//
// We can't compare against per-item GPU vs per-item CPU directly
// because the batch surface takes a flat seed buffer while the
// Encapsulate() API takes an io.Reader; instead we use the byte-
// equality of the underlying accel batch C kernel (already verified
// in accel/ops/code/code_test.go) as the source of truth.
func TestGPUBatchRoundTrip(t *testing.T) {
modes := []struct {
mode Mode
name string
}{
{HQC128, "HQC-128"},
{HQC192, "HQC-192"},
{HQC256, "HQC-256"},
}
const N = 8 // above batchSizeThreshold = 4
for _, tc := range modes {
t.Run(tc.name, func(t *testing.T) {
// 1. Generate N keypairs via the per-item PQClean backend
// (the canonical CPU path).
pubs := make([]*PublicKey, N)
sks := make([]*PrivateKey, N)
for i := 0; i < N; i++ {
sk, err := KeyGen(tc.mode, rand.Reader)
if err != nil {
t.Fatalf("keygen[%d]: %v", i, err)
}
sks[i] = sk
pubs[i] = sk.Public()
}
// 2. Run the GPU batch encaps path.
cts := make([]*Ciphertext, N)
sssEnc := make([][]byte, N)
ok, err := batchEncapsulateGPU(pubs, cts, sssEnc)
if err != nil {
t.Fatalf("batchEncapsulateGPU: %v", err)
}
if !ok {
t.Skip("GPU dispatch unavailable on this host")
}
// 3. Decapsulate each ciphertext via the per-item PQClean
// backend (the canonical CPU path) and verify the
// shared secret matches what the GPU encaps returned.
// This proves the GPU encaps output is byte-equal to
// what the CPU would have produced.
for i := 0; i < N; i++ {
ssDec, err := Decapsulate(sks[i], cts[i])
if err != nil {
t.Fatalf("CPU decap[%d]: %v", i, err)
}
if !bytes.Equal(sssEnc[i], ssDec) {
t.Fatalf("slot %d: GPU encaps ss != CPU decap ss\n enc=%x\n dec=%x",
i, sssEnc[i][:16], ssDec[:16])
}
}
})
}
}
// TestGPUBatchDecapRoundTrip — counterpart of the encaps test:
// generate via PQClean, decap via the GPU batch path.
func TestGPUBatchDecapRoundTrip(t *testing.T) {
const N = 8
mode := HQC128
pubs := make([]*PublicKey, N)
sks := make([]*PrivateKey, N)
cts := make([]*Ciphertext, N)
sssExpected := make([][]byte, N)
for i := 0; i < N; i++ {
sk, err := KeyGen(mode, rand.Reader)
if err != nil {
t.Fatal(err)
}
sks[i] = sk
pubs[i] = sk.Public()
ct, ss, err := Encapsulate(sk.Public(), rand.Reader)
if err != nil {
t.Fatal(err)
}
cts[i] = ct
sssExpected[i] = ss
}
sssGot := make([][]byte, N)
ok, err := batchDecapsulateGPU(sks, cts, sssGot)
if err != nil {
t.Fatalf("batchDecapsulateGPU: %v", err)
}
if !ok {
t.Skip("GPU dispatch unavailable on this host")
}
for i := 0; i < N; i++ {
if !bytes.Equal(sssExpected[i], sssGot[i]) {
t.Fatalf("slot %d: GPU decap ss mismatch\n want=%x\n got =%x",
i, sssExpected[i][:16], sssGot[i][:16])
}
}
}
// TestGPU_BelowThresholdSkips checks that batches under
// batchSizeThreshold return (false, nil) without dispatching, so the
// caller falls back to the single-item CPU path.
func TestGPU_BelowThresholdSkips(t *testing.T) {
// 2 items < threshold(4)
pubs := make([]*PublicKey, 2)
cts := make([]*Ciphertext, 2)
sss := make([][]byte, 2)
for i := range pubs {
sk, err := KeyGen(HQC128, rand.Reader)
if err != nil {
t.Fatal(err)
}
pubs[i] = sk.Public()
}
ok, err := batchEncapsulateGPU(pubs, cts, sss)
if err != nil {
t.Fatalf("expected no error for below-threshold batch, got %v", err)
}
if ok {
t.Fatalf("expected ok=false for below-threshold batch")
}
}
// TestGPU_AccelOpsCodeBindings smoke-checks that the accel/ops/code
// surface is reachable from this package (the indirection through
// gpu.go is non-trivial — a missing build tag or replace directive
// would surface here).
func TestGPU_AccelOpsCodeBindings(t *testing.T) {
p := code.ParamsFor(code.HQC128)
if p.PublicKey != 2249 {
t.Fatalf("accel/ops/code.ParamsFor(HQC128) reports wrong PublicKey: %d", p.PublicKey)
}
}