mirror of
https://github.com/luxfi/crypto.git
synced 2026-07-27 01:54:50 +00:00
164 lines
4.6 KiB
Go
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)
|
|
}
|
|
}
|