mirror of
https://github.com/luxfi/gpu.git
synced 2026-10-30 16:23:49 +00:00
414 lines
12 KiB
Go
414 lines
12 KiB
Go
// Copyright (C) 2024-2026, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
//go:build cgo
|
|
|
|
// HQC (Hamming Quasi-Cyclic) GPU bindings via the luxcpp/gpu native
|
|
// kernels (libluxgpu_hqc.a). Symbols come from
|
|
// luxcpp/gpu/include/lux/gpu/hqc.h; the canonical CPU implementation
|
|
// in luxcpp/gpu/src/hqc_*.cpp is the byte-equal oracle every backend
|
|
// (Metal / CUDA / WebGPU) must match.
|
|
//
|
|
// Layering:
|
|
//
|
|
// luxfi/crypto/hqc <-- Go consumer (this is what callers import)
|
|
// luxfi/gpu (here) <-- thin C ABI binding, threshold routing
|
|
// luxfi/accel <-- session / device lifetime
|
|
// luxcpp/gpu/src <-- canonical CPU + Metal/CUDA dispatch
|
|
//
|
|
// Build expects the luxfi sibling-repo layout:
|
|
//
|
|
// ~/work/lux/mlx -> ~/work/luxcpp/gpu (symlink)
|
|
//
|
|
// so that ${SRCDIR}/../mlx/include resolves to the lux/gpu headers.
|
|
// libluxgpu_hqc.a is statically linked from the same build dir.
|
|
//
|
|
// Note on the `randombytes` symbol: libluxgpu_hqc.a has unresolved
|
|
// references to PQClean's `randombytes(uint8_t*, size_t)`. The Lux
|
|
// build system intentionally splits the provider out into a separate
|
|
// archive (libluxgpu_hqc_standalone_rng.a) so binaries that supply
|
|
// their own `randombytes` (luxfi/crypto/hqc's randombytes_shim.c
|
|
// under build tag hqc_pqclean, or luxfi/accel/ops/code's
|
|
// code_cpu_randombytes.go strong definition) can avoid linking the
|
|
// shim and get a clean single definition. Linking the shim in here
|
|
// unconditionally collides with those providers. Any consumer that
|
|
// imports luxfi/gpu's HQC entry points reaches them through
|
|
// luxfi/accel or luxfi/crypto/hqc, both of which provide
|
|
// `randombytes`. So we leave the symbol unresolved at the gpu layer
|
|
// and let the higher-level package supply it.
|
|
//
|
|
// Threshold routing: every entry point inspects the batch count and
|
|
// HQCGetThreshold(opType). Below threshold callers should keep the
|
|
// op on the CPU pure-Go path in crypto/hqc/gpu_nocgo.go to avoid the
|
|
// cgo + device dispatch overhead. The threshold is *advisory*; the
|
|
// C kernel is correct for any count >= 1.
|
|
package gpu
|
|
|
|
/*
|
|
#cgo CFLAGS: -I${SRCDIR}/../mlx/include
|
|
#cgo darwin LDFLAGS: -L${SRCDIR}/../mlx/build -lluxgpu_hqc -lc++ -framework Security
|
|
#cgo !darwin LDFLAGS: -L${SRCDIR}/../mlx/build -lluxgpu_hqc -lstdc++ -lm
|
|
|
|
#include <stdint.h>
|
|
#include <stddef.h>
|
|
#include "lux/gpu/hqc.h"
|
|
*/
|
|
import "C"
|
|
|
|
import (
|
|
"errors"
|
|
"unsafe"
|
|
)
|
|
|
|
// ErrHQCNotAvailable is returned when an HQC entry point hits a
|
|
// condition the CGO bindings can't satisfy (mode mismatch, buffer
|
|
// length, RNG starvation). Same sentinel value the non-CGO stub
|
|
// exports so callers can switch on it uniformly.
|
|
var ErrHQCNotAvailable = errors.New("HQC operation not available")
|
|
|
|
// Granular HQC error sentinels — produced by the C kernels and
|
|
// translated here. Callers normally only need ErrHQCNotAvailable but
|
|
// these allow precise error handling (e.g. surface RNG starvation
|
|
// distinctly from buffer length mismatch).
|
|
var (
|
|
ErrHQCInvalidArg = errors.New("hqc: invalid argument")
|
|
ErrHQCInvalidMode = errors.New("hqc: invalid mode")
|
|
ErrHQCInvalidLength = errors.New("hqc: invalid buffer length")
|
|
ErrHQCOOM = errors.New("hqc: out of memory")
|
|
ErrHQCDeviceError = errors.New("hqc: device error")
|
|
ErrHQCNotSupported = errors.New("hqc: kernel not supported on this backend")
|
|
ErrHQCRNGFailed = errors.New("hqc: RNG / seed buffer exhausted")
|
|
)
|
|
|
|
// HQCMode selects the parameter set. Bit pattern matches the C
|
|
// LuxHQCMode enum.
|
|
type HQCMode int
|
|
|
|
const (
|
|
HQC128 HQCMode = HQCMode(C.LUX_HQC_128)
|
|
HQC192 HQCMode = HQCMode(C.LUX_HQC_192)
|
|
HQC256 HQCMode = HQCMode(C.LUX_HQC_256)
|
|
)
|
|
|
|
// HQCParams describes the byte sizes for one parameter set. Values
|
|
// come from the C library so they cannot drift from the canonical
|
|
// PQClean reference.
|
|
type HQCParams struct {
|
|
PublicKeyBytes int
|
|
SecretKeyBytes int
|
|
CiphertextBytes int
|
|
SharedSecretBytes int
|
|
ParamN int
|
|
VecNSize64 int
|
|
ParamN1 int
|
|
ParamN2 int
|
|
ParamK int
|
|
}
|
|
|
|
// HQCParamsFor returns the canonical sizes for mode. Wraps
|
|
// lux_hqc_params() in the C header.
|
|
func HQCParamsFor(mode HQCMode) (HQCParams, error) {
|
|
var cp C.LuxHQCParams
|
|
rc := C.lux_hqc_params(C.LuxHQCMode(mode), &cp)
|
|
if rc != C.LUX_HQC_OK {
|
|
return HQCParams{}, hqcErrorFromCode(rc)
|
|
}
|
|
return HQCParams{
|
|
PublicKeyBytes: int(cp.public_key_bytes),
|
|
SecretKeyBytes: int(cp.secret_key_bytes),
|
|
CiphertextBytes: int(cp.ciphertext_bytes),
|
|
SharedSecretBytes: int(cp.shared_secret_bytes),
|
|
ParamN: int(cp.param_n),
|
|
VecNSize64: int(cp.vec_n_size_64),
|
|
ParamN1: int(cp.param_n1),
|
|
ParamN2: int(cp.param_n2),
|
|
ParamK: int(cp.param_k),
|
|
}, nil
|
|
}
|
|
|
|
// hqcErrorFromCode translates LuxHQCError to a Go sentinel.
|
|
func hqcErrorFromCode(rc C.LuxHQCError) error {
|
|
switch rc {
|
|
case C.LUX_HQC_OK:
|
|
return nil
|
|
case C.LUX_HQC_ERROR_INVALID_ARG:
|
|
return ErrHQCInvalidArg
|
|
case C.LUX_HQC_ERROR_INVALID_MODE:
|
|
return ErrHQCInvalidMode
|
|
case C.LUX_HQC_ERROR_INVALID_LENGTH:
|
|
return ErrHQCInvalidLength
|
|
case C.LUX_HQC_ERROR_OOM:
|
|
return ErrHQCOOM
|
|
case C.LUX_HQC_ERROR_DEVICE_ERROR:
|
|
return ErrHQCDeviceError
|
|
case C.LUX_HQC_ERROR_NOT_SUPPORTED:
|
|
return ErrHQCNotSupported
|
|
case C.LUX_HQC_ERROR_RNG_FAILED:
|
|
return ErrHQCRNGFailed
|
|
default:
|
|
return ErrHQCNotAvailable
|
|
}
|
|
}
|
|
|
|
// HQCGPUAvailable reports whether the HQC kernels are reachable.
|
|
// The C library is linked statically so this is always true when
|
|
// CGO is enabled. (Returns false on the !cgo stub.)
|
|
func HQCGPUAvailable() bool { return true }
|
|
|
|
// HQCGetBackend returns the active HQC backend. Right now every
|
|
// platform routes through the canonical CPU oracle in
|
|
// luxcpp/gpu/src/hqc_*.cpp; Metal/CUDA shims will surface here once
|
|
// they override the lux_hqc_* C symbols.
|
|
func HQCGetBackend() string { return "CPU/Native" }
|
|
|
|
// HQCGetThreshold returns the per-op batch crossover above which the
|
|
// GPU/native path is preferred. Below the threshold callers should
|
|
// keep the op on the CPU pure-Go path to avoid the cgo + device
|
|
// dispatch overhead.
|
|
//
|
|
// opType 1 GF(2)^N polynomial multiplication
|
|
// opType 2 Reed-Solomon decode
|
|
// opType 3 Reed-Muller decode
|
|
// opType 4 KEM batch encaps
|
|
// opType 5 KEM batch decaps
|
|
func HQCGetThreshold(opType int) int {
|
|
switch opType {
|
|
case 1:
|
|
return 32
|
|
case 2, 3:
|
|
return 64
|
|
case 4, 5:
|
|
return 8
|
|
default:
|
|
return 32
|
|
}
|
|
}
|
|
|
|
// vecN returns the per-slot uint64 count for mode's GF(2)^N polymul.
|
|
// Mirrors lux_hqc_params(mode).vec_n_size_64 without the cgo call.
|
|
func vecN(mode HQCMode) (int, error) {
|
|
switch mode {
|
|
case HQC128:
|
|
return (17669 + 63) / 64, nil // 277
|
|
case HQC192:
|
|
return (35851 + 63) / 64, nil // 561
|
|
case HQC256:
|
|
return (57637 + 63) / 64, nil // 901
|
|
default:
|
|
return 0, ErrHQCInvalidMode
|
|
}
|
|
}
|
|
|
|
// rsDims returns (PARAM_N1, PARAM_K) for mode's Reed-Solomon code.
|
|
func rsDims(mode HQCMode) (int, int, error) {
|
|
switch mode {
|
|
case HQC128:
|
|
return 46, 16, nil
|
|
case HQC192:
|
|
return 56, 24, nil
|
|
case HQC256:
|
|
return 90, 32, nil
|
|
default:
|
|
return 0, 0, ErrHQCInvalidMode
|
|
}
|
|
}
|
|
|
|
// vecN1N2Size64 returns the bit-packed RM codeword size in uint64s.
|
|
func vecN1N2Size64(mode HQCMode) (int, error) {
|
|
switch mode {
|
|
case HQC128:
|
|
return (17664 + 63) / 64, nil // 276
|
|
case HQC192:
|
|
return (35840 + 63) / 64, nil // 560
|
|
case HQC256:
|
|
return (57600 + 63) / 64, nil // 900
|
|
default:
|
|
return 0, ErrHQCInvalidMode
|
|
}
|
|
}
|
|
|
|
// GF2PolymulMod computes c(x) = a(x) * b(x) mod (X^n - 1) in GF(2)[X]
|
|
// for ONE polynomial. All three buffers must be length VecNSize64
|
|
// (per HQCParamsFor(mode)); they must be distinct (no aliasing). The
|
|
// kernel is constant-time and identical to PQClean's vect_mul.
|
|
func GF2PolymulMod(mode HQCMode, c, a, b []uint64) error {
|
|
n, err := vecN(mode)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(a) != n || len(b) != n || len(c) != n {
|
|
return ErrHQCInvalidLength
|
|
}
|
|
rc := C.lux_hqc_gf2_polymul(
|
|
C.LuxHQCMode(mode),
|
|
(*C.uint64_t)(unsafe.Pointer(&c[0])),
|
|
(*C.uint64_t)(unsafe.Pointer(&a[0])),
|
|
(*C.uint64_t)(unsafe.Pointer(&b[0])),
|
|
)
|
|
return hqcErrorFromCode(rc)
|
|
}
|
|
|
|
// GF2PolymulBatch computes count independent polymuls in one call.
|
|
// Slot i reads a[i*N:(i+1)*N] and b[i*N:(i+1)*N], writes
|
|
// c[i*N:(i+1)*N]. Failure short-circuits and returns the slot's
|
|
// error code (no partial results are guaranteed).
|
|
func GF2PolymulBatch(mode HQCMode, c, a, b []uint64, count int) error {
|
|
if count <= 0 {
|
|
return ErrHQCInvalidArg
|
|
}
|
|
n, err := vecN(mode)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(a) != count*n || len(b) != count*n || len(c) != count*n {
|
|
return ErrHQCInvalidLength
|
|
}
|
|
cMode := C.LuxHQCMode(mode)
|
|
for i := 0; i < count; i++ {
|
|
ai := a[i*n : (i+1)*n]
|
|
bi := b[i*n : (i+1)*n]
|
|
ci := c[i*n : (i+1)*n]
|
|
rc := C.lux_hqc_gf2_polymul(
|
|
cMode,
|
|
(*C.uint64_t)(unsafe.Pointer(&ci[0])),
|
|
(*C.uint64_t)(unsafe.Pointer(&ai[0])),
|
|
(*C.uint64_t)(unsafe.Pointer(&bi[0])),
|
|
)
|
|
if rc != C.LUX_HQC_OK {
|
|
return hqcErrorFromCode(rc)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ReedSolomonDecode runs constant-time RS decoding on one codeword.
|
|
// cdw is ParamN1 bytes, msg is ParamK bytes.
|
|
func ReedSolomonDecode(mode HQCMode, msg, cdw []byte) error {
|
|
n1, k, err := rsDims(mode)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(cdw) != n1 || len(msg) != k {
|
|
return ErrHQCInvalidLength
|
|
}
|
|
rc := C.lux_hqc_reed_solomon_decode(
|
|
C.LuxHQCMode(mode),
|
|
(*C.uint8_t)(unsafe.Pointer(&msg[0])),
|
|
(*C.uint8_t)(unsafe.Pointer(&cdw[0])),
|
|
)
|
|
return hqcErrorFromCode(rc)
|
|
}
|
|
|
|
// ReedMullerDecode runs duplicated RM(1,7) decoding via Fast Hadamard
|
|
// Transform. cdw is the bit-packed VEC_N1N2_SIZE_64 uint64 array,
|
|
// msg is ParamN1 bytes.
|
|
func ReedMullerDecode(mode HQCMode, msg []byte, cdw []uint64) error {
|
|
n1, _, err := rsDims(mode)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
wantN1N2, err := vecN1N2Size64(mode)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(msg) != n1 || len(cdw) != wantN1N2 {
|
|
return ErrHQCInvalidLength
|
|
}
|
|
rc := C.lux_hqc_reed_muller_decode(
|
|
C.LuxHQCMode(mode),
|
|
(*C.uint8_t)(unsafe.Pointer(&msg[0])),
|
|
(*C.uint64_t)(unsafe.Pointer(&cdw[0])),
|
|
)
|
|
return hqcErrorFromCode(rc)
|
|
}
|
|
|
|
// HQCKeypairBatch generates count HQC keypairs deterministically
|
|
// from the seed buffer.
|
|
//
|
|
// pks layout count * params.public_key_bytes
|
|
// sks layout count * params.secret_key_bytes
|
|
// seeds layout count * 48 bytes
|
|
func HQCKeypairBatch(mode HQCMode, pks, sks, seeds []byte, count int) error {
|
|
if count <= 0 {
|
|
return ErrHQCInvalidArg
|
|
}
|
|
p, err := HQCParamsFor(mode)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(pks) != count*p.PublicKeyBytes || len(sks) != count*p.SecretKeyBytes || len(seeds) != count*48 {
|
|
return ErrHQCInvalidLength
|
|
}
|
|
rc := C.lux_hqc_keypair_batch(
|
|
C.LuxHQCMode(mode),
|
|
(*C.uint8_t)(unsafe.Pointer(&pks[0])),
|
|
(*C.uint8_t)(unsafe.Pointer(&sks[0])),
|
|
(*C.uint8_t)(unsafe.Pointer(&seeds[0])),
|
|
C.size_t(count),
|
|
)
|
|
return hqcErrorFromCode(rc)
|
|
}
|
|
|
|
// HQCEncapsBatch runs count independent encapsulations.
|
|
//
|
|
// cts layout count * params.ciphertext_bytes
|
|
// sss layout count * params.shared_secret_bytes
|
|
// pks layout count * params.public_key_bytes
|
|
// seeds layout count * 32 bytes (PARAM_K + 16 salt, padded to 32)
|
|
func HQCEncapsBatch(mode HQCMode, cts, sss, pks, seeds []byte, count int) error {
|
|
if count <= 0 {
|
|
return ErrHQCInvalidArg
|
|
}
|
|
p, err := HQCParamsFor(mode)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(cts) != count*p.CiphertextBytes ||
|
|
len(sss) != count*p.SharedSecretBytes ||
|
|
len(pks) != count*p.PublicKeyBytes ||
|
|
len(seeds) != count*32 {
|
|
return ErrHQCInvalidLength
|
|
}
|
|
rc := C.lux_hqc_encaps_batch(
|
|
C.LuxHQCMode(mode),
|
|
(*C.uint8_t)(unsafe.Pointer(&cts[0])),
|
|
(*C.uint8_t)(unsafe.Pointer(&sss[0])),
|
|
(*C.uint8_t)(unsafe.Pointer(&pks[0])),
|
|
(*C.uint8_t)(unsafe.Pointer(&seeds[0])),
|
|
C.size_t(count),
|
|
)
|
|
return hqcErrorFromCode(rc)
|
|
}
|
|
|
|
// HQCDecapsBatch runs count independent decapsulations.
|
|
//
|
|
// sss layout count * params.shared_secret_bytes
|
|
// cts layout count * params.ciphertext_bytes
|
|
// sks layout count * params.secret_key_bytes
|
|
//
|
|
// Implicit rejection: a tampered ciphertext does NOT error; the
|
|
// corresponding sss slot receives a pseudorandom 64 bytes derived
|
|
// from (sk, ct) (Hofheinz-Hövelmanns-Kiltz construction).
|
|
func HQCDecapsBatch(mode HQCMode, sss, cts, sks []byte, count int) error {
|
|
if count <= 0 {
|
|
return ErrHQCInvalidArg
|
|
}
|
|
p, err := HQCParamsFor(mode)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(sss) != count*p.SharedSecretBytes ||
|
|
len(cts) != count*p.CiphertextBytes ||
|
|
len(sks) != count*p.SecretKeyBytes {
|
|
return ErrHQCInvalidLength
|
|
}
|
|
rc := C.lux_hqc_decaps_batch(
|
|
C.LuxHQCMode(mode),
|
|
(*C.uint8_t)(unsafe.Pointer(&sss[0])),
|
|
(*C.uint8_t)(unsafe.Pointer(&cts[0])),
|
|
(*C.uint8_t)(unsafe.Pointer(&sks[0])),
|
|
C.size_t(count),
|
|
)
|
|
return hqcErrorFromCode(rc)
|
|
}
|