mirror of
https://github.com/luxfi/fhe.git
synced 2026-07-26 23:16:08 +00:00
118 lines
3.9 KiB
Go
118 lines
3.9 KiB
Go
// Copyright (c) 2026, Lux Industries Inc.
|
|||
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
||
|
|
|
||
|
|
package types
|
||
|
|
|
||
|
|
import (
|
||
|
|
"crypto/sha256"
|
||
|
|
"encoding/binary"
|
||
|
|
|
||
|
|
lattice "github.com/luxfi/lattice/v7/types"
|
||
|
|
)
|
||
|
|
|
||
|
|
// FHECiphertextHeader wraps every FHE buffer with the metadata required for
|
||
|
|
// safe dispatch.
|
||
|
|
//
|
||
|
|
// Without this header, two ciphertexts produced under different parameter
|
||
|
|
// sets, evaluation keys, or domains can be combined silently and produce
|
||
|
|
// either undecryptable ciphertexts or an outright privacy break (key
|
||
|
|
// reuse across circuits).
|
||
|
|
//
|
||
|
|
// Layout is byte-stable across Go and C++. Field order MUST NOT change
|
||
|
|
// without updating the C++ mirror and matching layout tests.
|
||
|
|
//
|
||
|
|
// Total size: 144 bytes.
|
||
|
|
type FHECiphertextHeader struct {
|
||
|
|
// ParamsHash is the SHA-256 of the canonical encoding of the full
|
||
|
|
// parameter set (q, p, N, log_p, sigma, dnum, etc.).
|
||
|
|
// 32 bytes, offset 0.
|
||
|
|
ParamsHash [32]byte
|
||
|
|
|
||
|
|
// KeyID is the SHA-256 of the public/evaluation-key material that
|
||
|
|
// produced or will operate on this ciphertext.
|
||
|
|
// 32 bytes, offset 32.
|
||
|
|
KeyID [32]byte
|
||
|
|
|
||
|
|
// CircuitID is the SHA-256 of the circuit / policy program under
|
||
|
|
// which this ciphertext was produced. Used to bind ciphertexts to
|
||
|
|
// their authorised computation set.
|
||
|
|
// 32 bytes, offset 64.
|
||
|
|
CircuitID [32]byte
|
||
|
|
|
||
|
|
// Scheme identifies the FHE scheme.
|
||
|
|
// 4 bytes, offset 96.
|
||
|
|
Scheme FHEScheme
|
||
|
|
|
||
|
|
// Level is the modulus-switching level (multiplicative depth budget).
|
||
|
|
// 4 bytes, offset 100.
|
||
|
|
Level uint32
|
||
|
|
|
||
|
|
// N is the polynomial ring degree.
|
||
|
|
// 4 bytes, offset 104.
|
||
|
|
N uint32
|
||
|
|
|
||
|
|
// ModulusCount is the number of RNS moduli currently active.
|
||
|
|
// 4 bytes, offset 108.
|
||
|
|
ModulusCount uint32
|
||
|
|
|
||
|
|
// Domain is the polynomial domain the ciphertext data lives in.
|
||
|
|
// 1 byte, offset 112.
|
||
|
|
Domain lattice.PolyDomain
|
||
|
|
|
||
|
|
// _pad pads to the next 8-byte boundary so the trailing 32-byte
|
||
|
|
// fields stay aligned at offsets that are multiples of 8.
|
||
|
|
// 7 bytes, offset 113.
|
||
|
|
_pad [7]uint8
|
||
|
|
|
||
|
|
// Reserved provides 24 bytes for forward-compatible extension fields
|
||
|
|
// (e.g. attestation hashes, transcript pointers). Initialised to zero
|
||
|
|
// and ignored by Digest() unless promoted to a named field.
|
||
|
|
// 24 bytes, offset 120.
|
||
|
|
Reserved [24]byte
|
||
|
|
}
|
||
|
|
|
||
|
|
// canonicalEncoding returns the deterministic byte representation used for
|
||
|
|
// hashing. Field order matches struct layout; integers use little-endian.
|
||
|
|
func (h *FHECiphertextHeader) canonicalEncoding() []byte {
|
||
|
|
buf := make([]byte, 144)
|
||
|
|
copy(buf[0:32], h.ParamsHash[:])
|
||
|
|
copy(buf[32:64], h.KeyID[:])
|
||
|
|
copy(buf[64:96], h.CircuitID[:])
|
||
|
|
binary.LittleEndian.PutUint32(buf[96:100], uint32(h.Scheme))
|
||
|
|
binary.LittleEndian.PutUint32(buf[100:104], h.Level)
|
||
|
|
binary.LittleEndian.PutUint32(buf[104:108], h.N)
|
||
|
|
binary.LittleEndian.PutUint32(buf[108:112], h.ModulusCount)
|
||
|
|
buf[112] = uint8(h.Domain)
|
||
|
|
// bytes 113..119 are pad, fixed zero
|
||
|
|
copy(buf[120:144], h.Reserved[:])
|
||
|
|
return buf
|
||
|
|
}
|
||
|
|
|
||
|
|
// Digest returns the SHA-256 of the canonical encoding of the header.
|
||
|
|
//
|
||
|
|
// Determinism: same field values always yield the same digest, across
|
||
|
|
// processes and runs. This is the value bound into precompile artifacts and
|
||
|
|
// threshold transcripts.
|
||
|
|
func (h *FHECiphertextHeader) Digest() [32]byte {
|
||
|
|
return sha256.Sum256(h.canonicalEncoding())
|
||
|
|
}
|
||
|
|
|
||
|
|
// MatchesContext reports whether this ciphertext header is compatible with
|
||
|
|
// the given NTTContext, i.e. the polynomial degree and domain agree.
|
||
|
|
//
|
||
|
|
// Use this at the kernel boundary BEFORE dispatching a forward/inverse NTT
|
||
|
|
// or a pointwise multiply: if the ciphertext is in PolyDomainNTTMontgomery
|
||
|
|
// but the kernel's OutputDomain is PolyDomainNTTStandard, the dispatch is
|
||
|
|
// silently corrupting and must be rejected.
|
||
|
|
func (h *FHECiphertextHeader) MatchesContext(ctx *lattice.NTTContext) bool {
|
||
|
|
if ctx == nil {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
if h.N != ctx.N {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
// The header's Domain describes the *current* domain of the buffer.
|
||
|
|
// A kernel with InputDomain == h.Domain accepts this ciphertext.
|
||
|
|
return h.Domain == ctx.InputDomain
|
||
|
|
}
|