gpuattest

package
v0.0.0-...-c16d419 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 14, 2026 License: Apache-2.0 Imports: 10 Imported by: 0

Documentation

Overview

Package gpuattest implements a device attestation crypto suite for Helix Cluster OS GPU nodes. It is pure-Go, depends only on the Go standard library crypto primitives, and is fully deterministic in its accept/reject outcomes.

It covers four cohesive capabilities over a shared device Descriptor model:

(A) HXC-1568 — device-info challenge/response + stable fingerprint.
    A Verifier issues a nonce-bearing Challenge; the device signs
    (nonce || fingerprint || tick) with its ed25519 key. Verify accepts a
    genuine response and rejects expired, replayed, tampered, wrong-key and
    forged-descriptor responses, each with a distinct typed error.

(B) HXC-1569 — seeded matmul proof-of-GPU-work (PoVW). From a seed two NxN
    integer matrices are generated by a self-contained xorshift PRNG (no
    math/rand global, no clock). C = A*B is computed and a SHA-256 chain is
    folded over the rows of C. VerifyProof recomputes every link and reports
    the first broken link index on mismatch.

(C) HXC-1570 — O(1) spot-check verification. Each word of the proof carries a
    committed leaf hash inside a Merkle tree. SpotCheck verifies only the
    requested k indices in O(k log N) using Merkle inclusion proofs — never
    re-touching the other words — and names the failing index on tamper.

(D) HXC-1571 — device-sealed AES-256-GCM. SealForDevice derives a key from a
    device secret via HKDF-SHA256 and seals a payload under a fresh random
    nonce. OpenFromDevice round-trips for the sealing device and is rejected
    (GCM authentication failure) for any other device secret.

CLAUDE-2 note: this package is platform-agnostic pure Go (no /proc, no cgroup, no DRM, no cgo). It runs identically on every supported OS, so it needs no per-OS equivalents.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrExpired is returned when the challenge's expiry tick has passed at the
	// time of verification.
	ErrExpired = errors.New("gpuattest: challenge expired")
	// ErrReplay is returned when the challenge nonce has already been consumed
	// by this Verifier (single-use nonce set).
	ErrReplay = errors.New("gpuattest: nonce replayed")
	// ErrTampered is returned when the signature does not match the
	// reconstructed signed message (any signed field altered).
	ErrTampered = errors.New("gpuattest: signed payload tampered")
	// ErrWrongKey is returned when the response was signed by a key that does
	// not match the descriptor's public key.
	ErrWrongKey = errors.New("gpuattest: signature key mismatch")
	// ErrForgedDescriptor is returned when the descriptor presented at verify
	// time does not hash to the fingerprint that was signed.
	ErrForgedDescriptor = errors.New("gpuattest: descriptor fingerprint mismatch")
)

Typed errors. Each attack variant in Verify maps to a DISTINCT sentinel so a caller (and the closure tests) can prove exactly which check rejected the response, rather than seeing one opaque failure.

View Source
var (
	// ErrDuplicateGPUUUID is returned when two devices in a NodeDescriptor share
	// the same descriptor UUID. Per-device proofs are keyed by UUID, so a
	// duplicate would make a proof ambiguous; enumeration rejects the node
	// outright rather than silently collapsing the pair.
	ErrDuplicateGPUUUID = errors.New("gpuattest: duplicate GPU UUID in node")
	// ErrNoDevices is returned when a NodeDescriptor carries zero devices. A node
	// with nothing to attest is a caller error, not an empty-but-valid result.
	ErrNoDevices = errors.New("gpuattest: node has no devices")
)
View Source
var ErrProofBroken = errors.New("gpuattest: proof chain link mismatch")

ErrProofBroken is returned by VerifyProof when a chain link does not match.

View Source
var ErrSealOpen = errors.New("gpuattest: device seal open failed (auth)")

ErrSealOpen is returned by OpenFromDevice when the ciphertext fails GCM authentication under the supplied device secret — i.e. the wrong device (or tampered ciphertext).

View Source
var ErrSpotCheckFailed = errors.New("gpuattest: spot-check word mismatch")

ErrSpotCheckFailed is wrapped by SpotCheck with the offending index.

Functions

func Fingerprint

func Fingerprint(d Descriptor) [32]byte

Fingerprint is the stable SHA-256 over the descriptor's canonical encoding.

func OpenFromDevice

func OpenFromDevice(ciphertext, deviceSecret []byte) ([]byte, error)

OpenFromDevice reverses SealForDevice. It derives the key from deviceSecret, splits off the nonce prefix, and authenticates+decrypts. A wrong device secret derives a different key, so GCM authentication fails and ErrSealOpen is returned (no plaintext leaks).

func SealForDevice

func SealForDevice(payload, deviceSecret []byte) ([]byte, error)

SealForDevice encrypts payload under a key derived from deviceSecret using AES-256-GCM. A fresh 12-byte random nonce is generated per call and PREPENDED to the ciphertext, so the same payload+secret produces different output each time yet always decrypts. Output layout: nonce(12) || gcmCiphertext.

func SpotCheck

func SpotCheck(trustedRoot [32]byte, n int, opened []OpenWord) error

SpotCheck verifies ONLY the supplied opened words against the trusted root, using each word's Merkle inclusion proof. Total work is O(k log N), independent of the other N-k words. It returns nil if every opened word is genuine; on the first failure it returns an error naming that exact index (wrapping ErrSpotCheckFailed). n is the committed word count (tree width).

A check at index i fails precisely when that index's transmitted word no longer hashes — through its genuine proof path — into the trusted root, i.e. exactly when the word at i was tampered. Indices whose words are untouched always pass, regardless of tampering elsewhere.

func VerifyNode

func VerifyNode(v *Verifier, proofs []DeviceProof, nowTick int64) error

VerifyNode independently verifies every DeviceProof at nowTick. Each proof is checked in isolation against its own challenge, response and descriptor; any single failure makes VerifyNode return that proof's verification error. A nil return means every per-device proof verified genuinely.

func VerifyProof

func VerifyProof(p Proof) (ok bool, firstBrokenLink int, err error)

VerifyProof independently re-derives A,B,C from the proof's seed/size and recomputes the whole chain, comparing every link. It returns:

  • (true, -1, nil) when every link and the root match.
  • (false, i, ErrProofBroken) when link i is the FIRST that disagrees with the recomputed value (or, if all links agree but the claimed Root does not, i == N to flag the root).

Types

type Challenge

type Challenge struct {
	Nonce      [32]byte
	IssueTick  int64
	ExpiryTick int64
}

Challenge is issued by a Verifier. The device must echo the nonce and sign a message bound to the issuing tick and the device fingerprint.

type Descriptor

type Descriptor struct {
	Vendor string
	Model  string
	UUID   string
	VRAM   uint64 // bytes
	PubKey ed25519.PublicKey
}

Descriptor is the immutable, signable identity of a GPU device. PubKey is the device's ed25519 public key; the matching private key never leaves the device.

type Device

type Device struct {
	Descriptor Descriptor
	// contains filtered or unexported fields
}

Device holds a key pair and its descriptor. NewDevice generates a fresh ed25519 key (crypto/rand is fine here — tests drive accept/reject via fixed challenges and key reuse, not by predicting the key bytes).

func NewDevice

func NewDevice(vendor, model, uuid string, vram uint64) (*Device, error)

NewDevice creates a device with a fresh key pair. vendor/model/uuid/vram set the descriptor identity; the public key is filled from the generated pair.

func (*Device) Respond

func (d *Device) Respond(c Challenge, tick int64) Response

Respond produces a genuine Response to a challenge at the given production tick. It signs nonce || Fingerprint(descriptor) || tick.

type DeviceProof

type DeviceProof struct {
	GPUUUID    string
	Challenge  Challenge
	Response   Response
	Descriptor Descriptor
}

DeviceProof is the per-device attestation work product. It is keyed by GPUUUID (the attested descriptor's UUID) and bundles everything needed to independently verify this single device: the issued Challenge, the device's signed Response, and the Descriptor that was attested. VerifyNode verifies each DeviceProof on its own.

func EnumerateNode

func EnumerateNode(v *Verifier, node NodeDescriptor, issueTick, expiryTick, respondTick int64) ([]DeviceProof, error)

EnumerateNode produces one DeviceProof per device in the node. For each device it issues a fresh challenge from v, has THAT device Respond at respondTick, and records the proof keyed by the device's descriptor UUID.

It rejects an empty device list (ErrNoDevices) and any duplicate device UUID (ErrDuplicateGPUUUID) BEFORE issuing any challenge, so a malformed node never consumes verifier nonces. The returned slice has exactly len(node.Devices) proofs, in device order, each with GPUUUID equal to that device's UUID.

type NodeDescriptor

type NodeDescriptor struct {
	NodeID  string
	Devices []*Device
}

NodeDescriptor names a node and the set of GPU devices physically present on it. Devices each carry their own key pair and descriptor (see Device).

type OpenWord

type OpenWord struct {
	Index int
	Word  []byte
	Proof [][32]byte
}

OpenWord is what a prover transmits for ONE spot-checked index: the claimed word plus the Merkle inclusion proof (sibling path) for that leaf position. The verifier holds only the trusted root and recomputes inclusion in O(log N); it never receives or touches the other N-1 words.

type Proof

type Proof struct {
	Seed  uint64
	N     int
	Chain [][32]byte
	Root  [32]byte
}

Proof is a deterministic proof of having computed C = A*B for the matrices derived from Seed at size N. Chain[i] = H(Chain[i-1] || row_i(C)), with Chain[-1] taken as the zero hash. Root == Chain[N-1].

func GenerateProof

func GenerateProof(seed uint64, n int) (Proof, error)

GenerateProof derives A and B from seed, computes C=A*B, and folds a SHA-256 chain over the rows of C. n must be >= 1.

type Response

type Response struct {
	Nonce       [32]byte
	Fingerprint [32]byte
	Tick        int64
	SignerPub   ed25519.PublicKey
	Sig         []byte
}

Response is the device's signed answer. Fingerprint is the fingerprint the device claims (and signed); Tick is the device-asserted production tick. SignerPub is the public key the device claims signed this response; Verify binds it to the descriptor's key (mismatch => wrong-key) AND verifies the signature under it (failure under a matching key => tampered field).

type Verifier

type Verifier struct {
	// contains filtered or unexported fields
}

Verifier issues challenges and verifies responses. It owns the single-use nonce set used to reject replays.

func NewVerifier

func NewVerifier() *Verifier

NewVerifier returns a Verifier with an empty consumed-nonce set.

func (*Verifier) Issue

func (v *Verifier) Issue(issueTick, expiryTick int64) (Challenge, error)

Issue creates a Challenge with a fresh random nonce, bound to the issue and expiry ticks supplied by the caller's clock.

func (*Verifier) Verify

func (v *Verifier) Verify(c Challenge, r Response, d Descriptor, nowTick int64) error

Verify checks a response against the challenge and the presented descriptor at the verifier's current tick. Order of checks matters: expiry and replay are cheap pre-conditions; fingerprint binding, key match and signature are the cryptographic core. On success the nonce is consumed (single-use).

Returns nil for a genuine response, or one of the distinct sentinel errors.

type WordResponse

type WordResponse struct {
	Root [32]byte
	// contains filtered or unexported fields
}

WordResponse is a PoVW-style response made of many words, each committed under a Merkle root. The verifier holds only Root (32 bytes); to check word i it is given the word plus a logarithmic inclusion proof — never the whole set.

func NewWordResponse

func NewWordResponse(words [][]byte) *WordResponse

NewWordResponse commits to the given words and builds the Merkle tree. The words slice is copied defensively so later external mutation cannot silently change committed state.

func (*WordResponse) Len

func (w *WordResponse) Len() int

Len reports how many words were committed.

func (*WordResponse) Open

func (w *WordResponse) Open(i int) (OpenWord, bool)

Open produces the OpenWord a prover would send for index i, drawn from THIS (committed) response's genuine tree. A dishonest prover may later substitute OpenWord.Word with a tampered value; the proof path still comes from the genuine tree, so substitution at index i only ever fails the check at i.

func (*WordResponse) Word

func (w *WordResponse) Word(i int) ([]byte, bool)

Word returns a copy of word i (for a prover/transport layer). The verifier does not need the full set.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL