mirror of
https://github.com/luxfi/crypto.git
synced 2026-07-27 01:54:50 +00:00
Root cause of the never-passing 'HQC PQClean cgo e2e (NIST KAT roundtrip)' job, in two layers: 1. The lux-build runner had no C compiler (gcc/cc/clang absent), so cgo aborted before any code ran. Fixed in prior commits by installing build-essential + selecting an available CC. 2. With a compiler present, the cgo build then failed on accel@v1.1.9/ops/code/code_cpu.go's '#include "lux/gpu/hqc.h"' and '-lluxgpu_hqc' — the native HQC library (built from luxfi/mlx) that is NOT present in CI (nor in most consumers). accel v1.1.9 gated that native path on '//go:build cgo', so every cgo build hard-required the native lib/header. Fix: bump github.com/luxfi/accel v1.1.9 -> v1.2.4, which gates the native path behind '-tags=lux_hqc_native' and otherwise returns code.ErrNativeHQCUnavailable (documented: 'callers should fall back to their own CPU KEM path'). crypto/hqc now honours that contract: - gpu.go: batchEncapsulateGPU/batchDecapsulateGPU treat ErrNativeHQCUnavailable as 'declined' (return false,nil) so callers fall back to the per-item PQClean path. Secret buffers are zeroised on the decline path. - gpu_cgo.go: GF2Polymul falls back to the pure-Go Karatsuba path on ErrNativeHQCUnavailable. Removed the duplicated vecNSize64 / redMaskForMode / GF2Add (now shared). - gpu_polymul_purego.go (new, tag-neutral): the canonical pure-Go, PQClean-byte-identical GF(2)^N polymul (Karatsuba + reduce + base_mul), extracted from gpu_nocgo.go so BOTH builds share one implementation. - gpu_nocgo.go: trimmed to the thin !cgo public wrappers over the shared helpers. - randombytes_shim.c: split. Default (hqc_pqclean, no native) forwards randombytes -> hqcGoRandombytes only; the native seed-buffer dispatch (which references lux_hqc_randombytes / lux_hqc_seed_has_bytes from the native lib) moves to randombytes_shim_native.c, gated on lux_hqc_native. Previously the default build referenced those native-only symbols unconditionally, which would not link without the native lib. NOT a crypto/KAT regression: the NIST KAT roundtrip (TestKAT_HQC128/192/ 256), TestRoundTrip_AllModes, TestDeterminism, and the GF2 polymul known-answer vectors all pass byte-for-byte. The GPU batch tests skip gracefully when the native kernel is absent (their existing t.Skip path). Verified: CGO_ENABLED=1 go test -tags=hqc_pqclean ./hqc/ passes from a clean cache; cgo (with/without hqc_pqclean) and nocgo builds all green; broader suite unchanged (only pre-existing pkg-config failures in cgo/ and hash/poseidon2 remain, unrelated). Co-authored-by: Hanzo Dev <dev@hanzo.ai>
114 lines
4.4 KiB
Go
114 lines
4.4 KiB
Go
// Copyright (C) 2024-2026, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
//go:build cgo
|
|
|
|
// gpu_cgo.go — cgo-enabled single-shot HQC low-level operations.
|
|
//
|
|
// Routes through github.com/luxfi/accel/hqc, the canonical Go binding
|
|
// layer for the luxcpp/gpu native kernels (libluxgpu_hqc.a). This
|
|
// gives crypto/hqc a clean per-call entry to the bit-sliced Karatsuba
|
|
// polynomial multiplication kernel without dragging in the full
|
|
// luxfi/gpu surface (which carries ZK + ML-DSA + ML-KEM symbols and
|
|
// has its own transitive native-library link requirements).
|
|
//
|
|
// Layering rationale:
|
|
//
|
|
// crypto/hqc/gpu_cgo.go --> accel/hqc (this path)
|
|
// crypto/hqc/gpu_cgo.go -X-> luxfi/gpu/hqc_cgo (not used here)
|
|
//
|
|
// luxfi/gpu/hqc_cgo.go is also a valid binding path (it shares the
|
|
// underlying libluxgpu_hqc.a), but crypto/hqc only needs the slim
|
|
// HQC-specific surface, not the full unified GPU API. Going via
|
|
// accel/hqc keeps the dependency graph narrow:
|
|
//
|
|
// crypto/hqc -> accel/hqc -> accel/ops/code -> luxcpp/gpu C kernels
|
|
//
|
|
// The native kernels are linked only when built with -tags=lux_hqc_native
|
|
// (accel v1.2+). Without that tag the accel surface returns
|
|
// code.ErrNativeHQCUnavailable; GF2Polymul below detects this and falls
|
|
// back to the tag-neutral pure-Go implementation in
|
|
// gpu_polymul_purego.go, which is byte-for-byte identical to PQClean.
|
|
// That keeps the default cgo build (CI, pure-Go deployments) correct and
|
|
// link-clean without the native HQC library.
|
|
//
|
|
// The batch dispatchers batchEncapsulateGPU / batchDecapsulateGPU
|
|
// live in gpu.go (build-tag-agnostic) and route through the same
|
|
// accel/ops/code batch surface, with the same fallback semantics. This
|
|
// file ONLY covers single-shot primitives so callers that need one-off
|
|
// polymuls don't pay the batch surface's setup cost.
|
|
//
|
|
// Determinism contract: byte-equal to PQClean's vect_mul. Validators
|
|
// reach consensus on the output of HQC encapsulation / decapsulation,
|
|
// so any divergence from PQClean is a consensus fork.
|
|
|
|
package hqc
|
|
|
|
import (
|
|
"errors"
|
|
|
|
luxhqc "github.com/luxfi/accel/hqc"
|
|
"github.com/luxfi/accel/ops/code"
|
|
)
|
|
|
|
// toAccelHQCMode maps crypto/hqc.Mode to accel/hqc.Mode. Both enums
|
|
// share bit patterns but we keep the mapping explicit so they can
|
|
// evolve independently.
|
|
func toAccelHQCMode(mode Mode) (luxhqc.Mode, error) {
|
|
switch mode {
|
|
case HQC128:
|
|
return luxhqc.HQC128, nil
|
|
case HQC192:
|
|
return luxhqc.HQC192, nil
|
|
case HQC256:
|
|
return luxhqc.HQC256, nil
|
|
default:
|
|
return 0, ErrModeMismatch
|
|
}
|
|
}
|
|
|
|
// GF2Polymul computes c(x) = a(x) * b(x) mod (X^n - 1) in GF(2)[X].
|
|
// All three buffers must be length VecNSize64 for mode. Wraps the
|
|
// accel/hqc HQC kernel — byte-equal to PQClean's vect_mul.
|
|
//
|
|
// When the accel native HQC library is not linked (the default build,
|
|
// without -tags=lux_hqc_native), accel returns
|
|
// code.ErrNativeHQCUnavailable; we then fall back to the pure-Go
|
|
// Karatsuba path (gpu_polymul_purego.go), which is also byte-equal to
|
|
// PQClean. Either way the result is consensus-identical.
|
|
//
|
|
// Constant-time: both the native kernel and the pure-Go fallback use
|
|
// the same masked table lookup as PQClean (no data-dependent branches
|
|
// or memory indices on secret operands).
|
|
func GF2Polymul(mode Mode, c, a, b []uint64) error {
|
|
accelMode, err := toAccelHQCMode(mode)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// accel/hqc.GF2PolymulBatch processes count polynomials; count=1
|
|
// is the single-shot path. Per-slot per-cgo-crossing cost is
|
|
// ~150 ns on Apple M1 Max, which dominates only for vectors
|
|
// shorter than ~256 uint64 — at HQC-128's VEC_N_SIZE_64=277 this
|
|
// is already amortised; HQC-192 (561) and HQC-256 (901) are
|
|
// firmly cgo-favourable.
|
|
if err := luxhqc.GF2PolymulBatch(accelMode, c, a, b, 1); err != nil {
|
|
if errors.Is(err, code.ErrNativeHQCUnavailable) {
|
|
// Native kernel not linked — use the pure-Go path, which is
|
|
// byte-for-byte identical to PQClean's vect_mul.
|
|
return polymulGo(mode, c, a, b)
|
|
}
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GF2Add computes c[i] = a[i] ^ b[i] over GF(2)^N. Constant-time
|
|
// (pure XOR, no branches). Implemented in pure Go even on the cgo
|
|
// path because (a) it's a single SIMD-friendly loop the compiler
|
|
// inlines well, and (b) the cgo crossover is a hard floor of ~150ns
|
|
// that overwhelms the actual XOR cost for any vector under ~64 KB.
|
|
// Delegates to the shared helper in gpu_polymul_purego.go.
|
|
func GF2Add(c, a, b []uint64) {
|
|
gf2AddGo(c, a, b)
|
|
}
|