mirror of
https://github.com/luxfi/precompile.git
synced 2026-09-24 00:26:21 +00:00
fix(ai,attestation): close two live AI-precompile forges
CRITICAL #1 -- ai mint forge (0x0300 verifyAndMint{Work,Data}): verification ran VerifyMLDSA against an attacker-supplied calldata pubkey, so a self-signed work proof minted unbounded reward. Gate the mint on a real TEE quote embedded in the work proof: the cert chain must terminate at the embedded vendor root (ai.verifyAttestation, checked at the report's own timestamp) and the attested deviceID must equal BLAKE3(pubkey). The signing key is vouched for on chain, never asserted in calldata. Fused gas now meters the cert-chain verify (GasVerifyTEE). Roots threaded explicitly, no global state. CRITICAL #2 -- attestation fake-verify + consensus split (0x0301): VerifyNVTrust returned Verified:true for zero-byte evidence, used time.Now(), and mutated a process-global verifier map. Route every selector to the real verifier (ai.VerifyTEE), delete the fake path, derive attestation expiry from the block timestamp, and drop the process-global registry (TEE quotes are self-validating -- stateless). The fake luxfi/ai/pkg/attestation dependency is removed. Tests: self-signed/forged mints rejected (ErrMissingAttestation, ErrTEESignatureInvalid, ErrDeviceKeyMismatch); zero/garbage/self-rooted attestation rejected by the real verifier; CreateAttestation deterministic on (input, blockTS). 66 pass, 0 fail. go.mod/go.sum untouched.
This commit is contained in:
@@ -90,8 +90,15 @@ var (
|
||||
ErrInvalidTEEReceipt = errors.New("invalid TEE attestation receipt")
|
||||
ErrTEESignatureInvalid = errors.New("TEE attestation signature verification failed")
|
||||
ErrUnauthorized = errors.New("unauthorized caller")
|
||||
ErrMissingAttestation = errors.New("work proof missing TEE attestation quote")
|
||||
ErrDeviceKeyMismatch = errors.New("attested device identity does not match signing key")
|
||||
)
|
||||
|
||||
// DataContributionSize is the fixed size of a data-contribution descriptor:
|
||||
// dataHash(32) | dataSize(uint64,8) | privacyLevel(uint16,2). A TEE attestation
|
||||
// envelope binding the signing key to a real device follows these 42 bytes.
|
||||
const DataContributionSize = 42
|
||||
|
||||
// StateDB interface for accessing and modifying state
|
||||
type StateDB interface {
|
||||
GetState(addr [20]byte, key [32]byte) [32]byte
|
||||
|
||||
@@ -19,17 +19,22 @@
|
||||
package ai
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"math/big"
|
||||
|
||||
"github.com/zeebo/blake3"
|
||||
|
||||
"github.com/luxfi/precompile/contract"
|
||||
)
|
||||
|
||||
// Gas: the fused cost of the primitives each method composes.
|
||||
// Gas: the fused cost of the primitives each method composes. A mint now also
|
||||
// verifies a TEE attestation certificate chain (GasVerifyTEE) — the X.509 work
|
||||
// that proves the signing key belongs to a real device — so the cost is metered.
|
||||
const (
|
||||
GasVerifyAndMintWork uint64 = GasVerifyMLDSA + GasCalculateReward + GasMarkSpent
|
||||
GasVerifyAndMintData uint64 = GasVerifyMLDSA + GasCalculateReward + GasMarkSpent
|
||||
GasVerifyAndMintWork uint64 = GasVerifyMLDSA + GasVerifyTEE + GasCalculateReward + GasMarkSpent
|
||||
GasVerifyAndMintData uint64 = GasVerifyMLDSA + GasVerifyTEE + GasCalculateReward + GasMarkSpent
|
||||
)
|
||||
|
||||
// ErrInputTooShort is returned when calldata is truncated.
|
||||
@@ -101,7 +106,7 @@ func mintWork(stateDB StateDB, workProof []byte, chainId uint64) (*big.Int, erro
|
||||
// all of these to the chain, so the same dataset can't be double-claimed. Reward scales
|
||||
// with data size and the privacy multiplier (the same schedule as compute mining).
|
||||
func mintData(stateDB StateDB, descriptor []byte, chainId uint64) (*big.Int, error) {
|
||||
if len(descriptor) < 42 {
|
||||
if len(descriptor) < DataContributionSize {
|
||||
return nil, ErrInvalidWorkProof
|
||||
}
|
||||
var dataHash [32]byte
|
||||
@@ -135,11 +140,67 @@ func mintData(stateDB StateDB, descriptor []byte, chainId uint64) (*big.Int, err
|
||||
return reward, nil
|
||||
}
|
||||
|
||||
// verifyDeviceBinding is the trust gate that makes a mint impossible without a
|
||||
// chain-trusted signature. The attestation envelope appended to a work proof /
|
||||
// data descriptor is:
|
||||
//
|
||||
// envelope = [4]sigLen | teeSig | receipt
|
||||
// receipt = report(48) | certChainDER (report = deviceID(32)|ts(8)|nonce(8))
|
||||
//
|
||||
// It establishes, in order: (1) the certificate chain in the receipt terminates
|
||||
// at an embedded vendor root (a genuine TEE device), checked at the report's own
|
||||
// timestamp so every validator agrees; (2) teeSig is the device leaf's signature
|
||||
// over the 48-byte report; and (3) the attested deviceID equals BLAKE3(pubkey) —
|
||||
// so the trust runs embedded-root → device leaf → this exact ML-DSA key. A miner
|
||||
// that presents a self-generated key with no embedded-root-anchored quote is
|
||||
// rejected: the signing key is vouched for on chain, never asserted in calldata.
|
||||
//
|
||||
// roots is threaded explicitly (production passes teeRootPool(); tests pass a
|
||||
// generated PKI) so the gate carries no hidden global state.
|
||||
func verifyDeviceBinding(pubkey, envelope []byte, roots *x509.CertPool) error {
|
||||
if len(envelope) < 4 {
|
||||
return ErrInvalidTEEReceipt
|
||||
}
|
||||
sigLen := uint64(binary.BigEndian.Uint32(envelope[0:4]))
|
||||
if sigLen == 0 || 4+sigLen > uint64(len(envelope)) {
|
||||
return ErrInvalidTEEReceipt
|
||||
}
|
||||
teeSig := envelope[4 : 4+sigLen]
|
||||
receipt := envelope[4+sigLen:]
|
||||
|
||||
ok, err := verifyAttestation(receipt, teeSig, roots)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
return ErrTEESignatureInvalid
|
||||
}
|
||||
// receipt is guaranteed >= teeReportLen here (verifyAttestation enforced it),
|
||||
// so receipt[0:32] (the attested deviceID) is safe to read.
|
||||
var attestedDevice [32]byte
|
||||
copy(attestedDevice[:], receipt[:32])
|
||||
if attestedDevice != blake3.Sum256(pubkey) {
|
||||
return ErrDeviceKeyMismatch
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *AIMiningContract) runVerifyAndMintWork(
|
||||
accessibleState contract.AccessibleState,
|
||||
input []byte,
|
||||
suppliedGas uint64,
|
||||
readOnly bool,
|
||||
) ([]byte, uint64, error) {
|
||||
return c.verifyAndMintWork(accessibleState, input, suppliedGas, readOnly, teeRootPool())
|
||||
}
|
||||
|
||||
// verifyAndMintWork is runVerifyAndMintWork with the trust-anchor pool injected.
|
||||
func (c *AIMiningContract) verifyAndMintWork(
|
||||
accessibleState contract.AccessibleState,
|
||||
input []byte,
|
||||
suppliedGas uint64,
|
||||
readOnly bool,
|
||||
roots *x509.CertPool,
|
||||
) ([]byte, uint64, error) {
|
||||
if readOnly {
|
||||
return nil, suppliedGas, fmt.Errorf("cannot mint in read-only mode")
|
||||
@@ -153,6 +214,7 @@ func (c *AIMiningContract) runVerifyAndMintWork(
|
||||
if err != nil {
|
||||
return nil, remaining, err
|
||||
}
|
||||
// 1. The post-quantum key signed THIS exact work claim.
|
||||
valid, err := VerifyMLDSA(pubkey, workProof, signature)
|
||||
if err != nil {
|
||||
return nil, remaining, err
|
||||
@@ -160,6 +222,19 @@ func (c *AIMiningContract) runVerifyAndMintWork(
|
||||
if !valid {
|
||||
return nil, remaining, fmt.Errorf("ml-dsa attestation invalid")
|
||||
}
|
||||
// 2. A TEE quote binds that key to a real, embedded-root-certified device.
|
||||
if len(workProof) <= WorkProofTEEQuoteOffset {
|
||||
return nil, remaining, ErrMissingAttestation
|
||||
}
|
||||
if err := verifyDeviceBinding(pubkey, workProof[WorkProofTEEQuoteOffset:], roots); err != nil {
|
||||
return nil, remaining, err
|
||||
}
|
||||
// 3. The work proof's own deviceID must equal the attested key identity, so
|
||||
// the settled workId is scoped to the chain-trusted device, not a value
|
||||
// the attacker grinds freely.
|
||||
if [32]byte(workProof[:32]) != blake3.Sum256(pubkey) {
|
||||
return nil, remaining, ErrDeviceKeyMismatch
|
||||
}
|
||||
|
||||
stateDB := accessibleState.GetStateDB()
|
||||
reward, err := mintWork(&stateDBAdapter{stateDB, ContractAddress}, workProof, chainId)
|
||||
@@ -176,6 +251,17 @@ func (c *AIMiningContract) runVerifyAndMintData(
|
||||
input []byte,
|
||||
suppliedGas uint64,
|
||||
readOnly bool,
|
||||
) ([]byte, uint64, error) {
|
||||
return c.verifyAndMintData(accessibleState, input, suppliedGas, readOnly, teeRootPool())
|
||||
}
|
||||
|
||||
// verifyAndMintData is runVerifyAndMintData with the trust-anchor pool injected.
|
||||
func (c *AIMiningContract) verifyAndMintData(
|
||||
accessibleState contract.AccessibleState,
|
||||
input []byte,
|
||||
suppliedGas uint64,
|
||||
readOnly bool,
|
||||
roots *x509.CertPool,
|
||||
) ([]byte, uint64, error) {
|
||||
if readOnly {
|
||||
return nil, suppliedGas, fmt.Errorf("cannot mint in read-only mode")
|
||||
@@ -189,6 +275,7 @@ func (c *AIMiningContract) runVerifyAndMintData(
|
||||
if err != nil {
|
||||
return nil, remaining, err
|
||||
}
|
||||
// 1. The post-quantum key signed THIS exact data descriptor.
|
||||
valid, err := VerifyMLDSA(pubkey, descriptor, signature)
|
||||
if err != nil {
|
||||
return nil, remaining, err
|
||||
@@ -196,6 +283,14 @@ func (c *AIMiningContract) runVerifyAndMintData(
|
||||
if !valid {
|
||||
return nil, remaining, fmt.Errorf("ml-dsa attestation invalid")
|
||||
}
|
||||
// 2. A TEE quote (appended after the 42-byte descriptor) binds that key to a
|
||||
// real, embedded-root-certified device (the scrub service's enclave).
|
||||
if len(descriptor) <= DataContributionSize {
|
||||
return nil, remaining, ErrMissingAttestation
|
||||
}
|
||||
if err := verifyDeviceBinding(pubkey, descriptor[DataContributionSize:], roots); err != nil {
|
||||
return nil, remaining, err
|
||||
}
|
||||
|
||||
stateDB := accessibleState.GetStateDB()
|
||||
reward, err := mintData(&stateDBAdapter{stateDB, ContractAddress}, descriptor, chainId)
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
// Copyright (C) 2026, Lux Partners Limited. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// End-to-end tests for the atomic cross-chain mining methods through the precompile's
|
||||
// Run entry point: real ML-DSA signature -> calldata decode -> verify -> reward ->
|
||||
// mark-spent. Exercises the full consensus-economic path, not just the core helpers.
|
||||
// End-to-end tests for the atomic cross-chain mining methods. A mint is only
|
||||
// honored for a post-quantum key that an embedded-root-certified TEE device has
|
||||
// vouched for: the work proof must carry a TEE quote whose attested deviceID
|
||||
// equals BLAKE3(pubkey). The happy path threads a generated test PKI through
|
||||
// the roots-injected seam (verifyAndMint*); the forge path runs through the
|
||||
// production Run entry point (embedded Intel root) where a self-signed key with
|
||||
// no real device quote is rejected — that rejection is the proof the unbacked
|
||||
// mint is closed.
|
||||
|
||||
package ai
|
||||
|
||||
@@ -16,6 +21,8 @@ import (
|
||||
|
||||
"github.com/cloudflare/circl/sign/mldsa/mldsa65"
|
||||
"github.com/holiman/uint256"
|
||||
"github.com/zeebo/blake3"
|
||||
|
||||
"github.com/luxfi/geth/common"
|
||||
"github.com/luxfi/geth/core/tracing"
|
||||
ethtypes "github.com/luxfi/geth/core/types"
|
||||
@@ -78,32 +85,77 @@ func tripleCalldata(sel uint32, blobs [][]byte, chainId uint64) []byte {
|
||||
return append(out, cid[:]...)
|
||||
}
|
||||
|
||||
func mldsaKeyAndSig(t *testing.T, msg []byte) (pub, sig []byte) {
|
||||
// --- ML-DSA + TEE-quote test fixtures ---------------------------------------
|
||||
|
||||
func genMLDSA(t *testing.T) ([]byte, *mldsa65.PrivateKey) {
|
||||
t.Helper()
|
||||
pk, sk, err := mldsa65.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatalf("keygen: %v", err)
|
||||
}
|
||||
pub, err = pk.MarshalBinary()
|
||||
pub, err := pk.MarshalBinary()
|
||||
if err != nil {
|
||||
t.Fatalf("marshal: %v", err)
|
||||
}
|
||||
sig = make([]byte, MLDSA65SignatureSize)
|
||||
return pub, sk
|
||||
}
|
||||
|
||||
func signMLDSA(t *testing.T, sk *mldsa65.PrivateKey, msg []byte) []byte {
|
||||
t.Helper()
|
||||
sig := make([]byte, MLDSA65SignatureSize)
|
||||
if err := mldsa65.SignTo(sk, msg, nil, true, sig); err != nil {
|
||||
t.Fatalf("sign: %v", err)
|
||||
}
|
||||
return pub, sig
|
||||
return sig
|
||||
}
|
||||
|
||||
// reportWithDevice builds a 48-byte Lux attestation report carrying a full
|
||||
// 32-byte deviceID.
|
||||
func reportWithDevice(dev [32]byte, ts int64) []byte {
|
||||
r := make([]byte, teeReportLen)
|
||||
copy(r[:32], dev[:])
|
||||
binary.BigEndian.PutUint64(r[32:40], uint64(ts))
|
||||
binary.BigEndian.PutUint64(r[40:48], 0xCAFE)
|
||||
return r
|
||||
}
|
||||
|
||||
// teeEnvelope frames a device-leaf signature and a receipt into the wire shape
|
||||
// verifyDeviceBinding parses: [4]sigLen | teeSig | receipt.
|
||||
func teeEnvelope(teeSig, receipt []byte) []byte {
|
||||
out := make([]byte, 4)
|
||||
binary.BigEndian.PutUint32(out, uint32(len(teeSig)))
|
||||
out = append(out, teeSig...)
|
||||
return append(out, receipt...)
|
||||
}
|
||||
|
||||
// attestedQuote returns the envelope binding deviceID = BLAKE3(pub) to the test
|
||||
// PKI in tc, plus the deviceID itself.
|
||||
func attestedQuote(t *testing.T, tc *teeChain, pub []byte) (env []byte, dev [32]byte) {
|
||||
t.Helper()
|
||||
dev = blake3.Sum256(pub)
|
||||
rep := reportWithDevice(dev, testAttestTime)
|
||||
receipt := tc.receiptFor(rep)
|
||||
teeSig := tc.signRaw(t, rep)
|
||||
return teeEnvelope(teeSig, receipt), dev
|
||||
}
|
||||
|
||||
// --- happy path (generated PKI injected through the roots seam) -------------
|
||||
|
||||
func TestVerifyAndMintWork_E2E(t *testing.T) {
|
||||
const chainId = uint64(420420) // Beluga
|
||||
proof := makeWorkProof(3, 60) // confidential, 60 compute-minutes
|
||||
pub, sig := mldsaKeyAndSig(t, proof)
|
||||
tc := newTEEChain(t, caNotAfter)
|
||||
pub, sk := genMLDSA(t)
|
||||
|
||||
env, dev := attestedQuote(t, tc, pub)
|
||||
var nonce [32]byte
|
||||
nonce[31] = 0x01
|
||||
proof := BuildWorkProof(dev, nonce, uint64(testAttestTime), 3, 60, env) // confidential, 60 min
|
||||
sig := signMLDSA(t, sk, proof)
|
||||
cd := tripleCalldata(SelectorVerifyAndMintWork, [][]byte{proof, pub, sig}, chainId)
|
||||
acc := ccAcc{s: newCCDB()}
|
||||
gas := AIMiningPrecompile.RequiredGas(cd)
|
||||
|
||||
ret, _, err := AIMiningPrecompile.Run(acc, common.Address{}, ContractAddress, cd, gas, false)
|
||||
ret, _, err := AIMiningPrecompile.verifyAndMintWork(acc, cd[4:], gas, false, tc.roots)
|
||||
if err != nil {
|
||||
t.Fatalf("verifyAndMintWork: %v", err)
|
||||
}
|
||||
@@ -112,38 +164,57 @@ func TestVerifyAndMintWork_E2E(t *testing.T) {
|
||||
}
|
||||
|
||||
// double-spend: same work id again -> rejected
|
||||
if _, _, err := AIMiningPrecompile.Run(acc, common.Address{}, ContractAddress, cd, gas, false); err == nil {
|
||||
if _, _, err := AIMiningPrecompile.verifyAndMintWork(acc, cd[4:], gas, false, tc.roots); err == nil {
|
||||
t.Fatal("expected double-spend rejection")
|
||||
}
|
||||
|
||||
// read-only: writes not allowed
|
||||
if _, _, err := AIMiningPrecompile.Run(acc, common.Address{}, ContractAddress, cd, gas, true); err == nil {
|
||||
if _, _, err := AIMiningPrecompile.verifyAndMintWork(acc, cd[4:], gas, true, tc.roots); err == nil {
|
||||
t.Fatal("expected read-only rejection")
|
||||
}
|
||||
|
||||
// invalid signature: fresh (unspent) work id, corrupted sig -> rejected on verify
|
||||
fresh := makeWorkProof(3, 60)
|
||||
fresh[63] = 0xCC // different nonce -> different workId (unspent)
|
||||
pub2, sig2 := mldsaKeyAndSig(t, fresh)
|
||||
sig2[0] ^= 0xFF
|
||||
bad := tripleCalldata(SelectorVerifyAndMintWork, [][]byte{fresh, pub2, sig2}, chainId)
|
||||
if _, _, err := AIMiningPrecompile.Run(acc, common.Address{}, ContractAddress, bad, AIMiningPrecompile.RequiredGas(bad), false); err == nil {
|
||||
var nonce2 [32]byte
|
||||
nonce2[31] = 0x02
|
||||
fresh := BuildWorkProof(dev, nonce2, uint64(testAttestTime), 3, 60, env)
|
||||
badSig := signMLDSA(t, sk, fresh)
|
||||
badSig[0] ^= 0xFF
|
||||
bad := tripleCalldata(SelectorVerifyAndMintWork, [][]byte{fresh, pub, badSig}, chainId)
|
||||
if _, _, err := AIMiningPrecompile.verifyAndMintWork(acc, bad[4:], AIMiningPrecompile.RequiredGas(bad), false, tc.roots); err == nil {
|
||||
t.Fatal("expected invalid-signature rejection")
|
||||
}
|
||||
|
||||
// device-key mismatch: workProof deviceID != BLAKE3(pubkey) -> rejected even
|
||||
// though the TEE quote itself is well-formed (covers gate check #3).
|
||||
var nonce3 [32]byte
|
||||
nonce3[31] = 0x03
|
||||
var wrongDev [32]byte
|
||||
wrongDev[0] = 0xFF
|
||||
mismatch := BuildWorkProof(wrongDev, nonce3, uint64(testAttestTime), 3, 60, env)
|
||||
mSig := signMLDSA(t, sk, mismatch)
|
||||
mcd := tripleCalldata(SelectorVerifyAndMintWork, [][]byte{mismatch, pub, mSig}, chainId)
|
||||
if _, _, err := AIMiningPrecompile.verifyAndMintWork(acc, mcd[4:], AIMiningPrecompile.RequiredGas(mcd), false, tc.roots); err != ErrDeviceKeyMismatch {
|
||||
t.Fatalf("deviceID mismatch: got %v, want ErrDeviceKeyMismatch", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyAndMintData_E2E(t *testing.T) {
|
||||
const chainId = uint64(420420)
|
||||
desc := make([]byte, 42) // dataHash(32) | size(8) | privacy(2)
|
||||
tc := newTEEChain(t, caNotAfter)
|
||||
pub, sk := genMLDSA(t)
|
||||
env, _ := attestedQuote(t, tc, pub)
|
||||
|
||||
desc := make([]byte, DataContributionSize)
|
||||
desc[31] = 0xDD // dataHash
|
||||
binary.BigEndian.PutUint64(desc[32:40], 1000) // 1000 data units
|
||||
binary.BigEndian.PutUint16(desc[40:42], 3) // confidential
|
||||
pub, sig := mldsaKeyAndSig(t, desc)
|
||||
desc = append(desc, env...)
|
||||
sig := signMLDSA(t, sk, desc)
|
||||
cd := tripleCalldata(SelectorVerifyAndMintData, [][]byte{desc, pub, sig}, chainId)
|
||||
acc := ccAcc{s: newCCDB()}
|
||||
gas := AIMiningPrecompile.RequiredGas(cd)
|
||||
|
||||
ret, _, err := AIMiningPrecompile.Run(acc, common.Address{}, ContractAddress, cd, gas, false)
|
||||
ret, _, err := AIMiningPrecompile.verifyAndMintData(acc, cd[4:], gas, false, tc.roots)
|
||||
if err != nil {
|
||||
t.Fatalf("verifyAndMintData: %v", err)
|
||||
}
|
||||
@@ -151,7 +222,73 @@ func TestVerifyAndMintData_E2E(t *testing.T) {
|
||||
t.Fatal("data reward must be positive")
|
||||
}
|
||||
// same dataset -> double-claim rejected
|
||||
if _, _, err := AIMiningPrecompile.Run(acc, common.Address{}, ContractAddress, cd, gas, false); err == nil {
|
||||
if _, _, err := AIMiningPrecompile.verifyAndMintData(acc, cd[4:], gas, false, tc.roots); err == nil {
|
||||
t.Fatal("expected double-claim rejection")
|
||||
}
|
||||
}
|
||||
|
||||
// --- forge rejection (production Run, embedded Intel root) ------------------
|
||||
|
||||
// TestVerifyAndMintWork_ForgeRejected is the proof that the unbacked-mint forge
|
||||
// is closed: an attacker self-signs a work proof with their OWN ML-DSA key and
|
||||
// submits it through the production entry point. Without an embedded-root TEE
|
||||
// quote vouching for the key, the mint is rejected. The CRITICAL bug was that
|
||||
// this calldata used to mint unlimited reward.
|
||||
func TestVerifyAndMintWork_ForgeRejected(t *testing.T) {
|
||||
const chainId = uint64(420420)
|
||||
acc := ccAcc{s: newCCDB()}
|
||||
|
||||
// (a) attacker key, no TEE quote at all -> rejected.
|
||||
pub, sk := genMLDSA(t)
|
||||
dev := blake3.Sum256(pub)
|
||||
var nonce [32]byte
|
||||
nonce[31] = 0x01
|
||||
bare := BuildWorkProof(dev, nonce, uint64(testAttestTime), 3, 60, nil)
|
||||
sig := signMLDSA(t, sk, bare)
|
||||
cd := tripleCalldata(SelectorVerifyAndMintWork, [][]byte{bare, pub, sig}, chainId)
|
||||
if _, _, err := AIMiningPrecompile.Run(acc, common.Address{}, ContractAddress, cd, AIMiningPrecompile.RequiredGas(cd), false); err != ErrMissingAttestation {
|
||||
t.Fatalf("self-signed, no quote: got %v, want ErrMissingAttestation", err)
|
||||
}
|
||||
|
||||
// (b) attacker key, well-formed quote that chains to the attacker's OWN test
|
||||
// root (NOT the embedded vendor root) -> rejected by the real verifier.
|
||||
tc := newTEEChain(t, caNotAfter)
|
||||
env, dev2 := attestedQuote(t, tc, pub)
|
||||
var nonce2 [32]byte
|
||||
nonce2[31] = 0x02
|
||||
proof := BuildWorkProof(dev2, nonce2, uint64(testAttestTime), 3, 60, env)
|
||||
sig2 := signMLDSA(t, sk, proof)
|
||||
cd2 := tripleCalldata(SelectorVerifyAndMintWork, [][]byte{proof, pub, sig2}, chainId)
|
||||
if _, _, err := AIMiningPrecompile.Run(acc, common.Address{}, ContractAddress, cd2, AIMiningPrecompile.RequiredGas(cd2), false); err != ErrTEESignatureInvalid {
|
||||
t.Fatalf("self-rooted quote: got %v, want ErrTEESignatureInvalid", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestVerifyAndMintData_ForgeRejected mirrors the work case for data
|
||||
// contributions through the production entry point.
|
||||
func TestVerifyAndMintData_ForgeRejected(t *testing.T) {
|
||||
const chainId = uint64(420420)
|
||||
acc := ccAcc{s: newCCDB()}
|
||||
pub, sk := genMLDSA(t)
|
||||
|
||||
// no attestation appended -> rejected.
|
||||
desc := make([]byte, DataContributionSize)
|
||||
desc[31] = 0xDD
|
||||
binary.BigEndian.PutUint64(desc[32:40], 1000)
|
||||
binary.BigEndian.PutUint16(desc[40:42], 3)
|
||||
sig := signMLDSA(t, sk, desc)
|
||||
cd := tripleCalldata(SelectorVerifyAndMintData, [][]byte{desc, pub, sig}, chainId)
|
||||
if _, _, err := AIMiningPrecompile.Run(acc, common.Address{}, ContractAddress, cd, AIMiningPrecompile.RequiredGas(cd), false); err != ErrMissingAttestation {
|
||||
t.Fatalf("self-signed data, no quote: got %v, want ErrMissingAttestation", err)
|
||||
}
|
||||
|
||||
// attacker test-rooted quote -> rejected by the real verifier.
|
||||
tc := newTEEChain(t, caNotAfter)
|
||||
env, _ := attestedQuote(t, tc, pub)
|
||||
desc2 := append(append([]byte{}, desc...), env...)
|
||||
sig2 := signMLDSA(t, sk, desc2)
|
||||
cd2 := tripleCalldata(SelectorVerifyAndMintData, [][]byte{desc2, pub, sig2}, chainId)
|
||||
if _, _, err := AIMiningPrecompile.Run(acc, common.Address{}, ContractAddress, cd2, AIMiningPrecompile.RequiredGas(cd2), false); err != ErrTEESignatureInvalid {
|
||||
t.Fatalf("self-rooted data quote: got %v, want ErrTEESignatureInvalid", err)
|
||||
}
|
||||
}
|
||||
|
||||
+137
-402
@@ -1,16 +1,30 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
// Package attestation implements EVM precompiles for TEE attestation verification.
|
||||
// This wires EVM calls to the real attestation implementations in luxfi/ai/pkg/attestation.
|
||||
// Package attestation implements the EVM precompile (0x0301) for TEE attestation
|
||||
// verification. It is a thin, STATELESS adapter over the one real verifier in
|
||||
// the AI precompile block — ai.VerifyTEE (0x0300 selector 0x03) — which checks a
|
||||
// device certificate chain to an embedded vendor root at the report's own
|
||||
// timestamp. There is exactly one way to verify a TEE quote across the block.
|
||||
//
|
||||
// Precompile addresses:
|
||||
// - 0x0301: NVTrust GPU attestation
|
||||
// - 0x0302: TPM attestation
|
||||
// - 0x0303: Compute attestation
|
||||
// - 0x0304: Attestation creation
|
||||
// Design (decomplected from the prior fake duplicate):
|
||||
// - Verification is real: a quote either chains to an embedded vendor root or
|
||||
// it is rejected. Zero-byte / forged evidence fails closed. There is no
|
||||
// "verified-by-default" path.
|
||||
// - Verification is deterministic: validity is checked at the report's
|
||||
// embedded timestamp (inside ai.verifyAttestation), never the wall clock.
|
||||
// The only chain-time input — an attestation's expiry — is taken from the
|
||||
// block timestamp passed in from AccessibleState, never time.Now().
|
||||
// - Verification is stateless: there is no process-global device registry (the
|
||||
// prior mutable map caused consensus splits). A TEE quote is self-contained,
|
||||
// so no on-chain registry is required to prevent forgery; none is kept.
|
||||
//
|
||||
// All attestation is LOCAL - no cloud dependencies (blockchain requirement).
|
||||
// Precompile selectors (first 4 bytes of input):
|
||||
// - 0x01: VerifyNVTrust (GPU TEE quote)
|
||||
// - 0x02: VerifyTPM (CPU TEE quote: SGX / SEV-SNP / TDX)
|
||||
// - 0x03: VerifyCompute (AI compute-result TEE quote)
|
||||
// - 0x04: CreateAttestation (verify a quote, issue a deterministic record)
|
||||
// - 0x05: GetDeviceStatus (no on-chain registry → always not-found)
|
||||
package attestation
|
||||
|
||||
import (
|
||||
@@ -18,10 +32,10 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"math/big"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/luxfi/ai/pkg/attestation"
|
||||
"github.com/zeebo/blake3"
|
||||
|
||||
"github.com/luxfi/precompile/ai"
|
||||
"github.com/luxfi/precompile/contract"
|
||||
)
|
||||
|
||||
@@ -43,6 +57,17 @@ const (
|
||||
GasGetDeviceStatus uint64 = 5000 // Query device status
|
||||
)
|
||||
|
||||
// Deterministic trust scores returned for a verified quote, by TEE flavor. A
|
||||
// quote that does not verify scores 0.
|
||||
const (
|
||||
trustScoreGPU = 90 // hardware-rooted GPU TEE (NVTrust)
|
||||
trustScoreCPU = 85 // CPU TEE (SGX / SEV-SNP / TDX)
|
||||
|
||||
// attestationTTL is how long, in seconds, an issued attestation record is
|
||||
// considered valid past the block in which it was created.
|
||||
attestationTTL = 3600
|
||||
)
|
||||
|
||||
// Errors
|
||||
var (
|
||||
ErrInvalidInput = contract.ErrInvalidInput
|
||||
@@ -54,30 +79,31 @@ var (
|
||||
ErrAttestationExpired = errors.New("attestation has expired")
|
||||
)
|
||||
|
||||
// Global verifier instance (singleton for efficiency).
|
||||
// verifierMu guards concurrent access to globalVerifier and nvtrustVerifier —
|
||||
// both have internal mutable state (device registrations, job completions,
|
||||
// trusted measurements) and are called from the EVM execution loop where
|
||||
// multiple transactions may invoke attestation precompiles in parallel.
|
||||
// Without this guard, concurrent writes would data-race and cause consensus
|
||||
// splits across validators.
|
||||
var (
|
||||
verifierMu sync.RWMutex
|
||||
globalVerifier = attestation.NewVerifier()
|
||||
nvtrustVerifier = attestation.NewNvtrustVerifier(attestation.DefaultNvtrustConfig())
|
||||
)
|
||||
// QuoteInput is the TEE attestation envelope every verifying selector consumes.
|
||||
// It is the same envelope the canonical verifier (ai.VerifyTEE) checks:
|
||||
//
|
||||
// Receipt = report(48) || certChainDER (report = deviceID|timestamp|nonce)
|
||||
// Signature = device leaf-cert signature over the 48-byte report
|
||||
type QuoteInput struct {
|
||||
Receipt []byte `json:"receipt"`
|
||||
Signature []byte `json:"signature"`
|
||||
}
|
||||
|
||||
// VerifyNVTrustInput represents input for GPU attestation verification
|
||||
type VerifyNVTrustInput struct {
|
||||
DeviceID [32]byte `json:"device_id"`
|
||||
Model string `json:"model"`
|
||||
CCEnabled bool `json:"cc_enabled"`
|
||||
TEEIOEnabled bool `json:"tee_io_enabled"`
|
||||
DriverVersion string `json:"driver_version"`
|
||||
VBIOSVersion string `json:"vbios_version"`
|
||||
SPDMReport []byte `json:"spdm_report"`
|
||||
CertChain []byte `json:"cert_chain"`
|
||||
Nonce [32]byte `json:"nonce"`
|
||||
// verifyQuote decodes a QuoteInput and verifies it with the real cert-chain
|
||||
// verifier. A malformed JSON envelope is a hard input error; any verification
|
||||
// failure (untrusted chain, bad signature, structurally invalid receipt) is a
|
||||
// deterministic false, never a revert and never a default-true.
|
||||
func verifyQuote(data []byte) (QuoteInput, bool, error) {
|
||||
var q QuoteInput
|
||||
if err := json.Unmarshal(data, &q); err != nil {
|
||||
return q, false, ErrInvalidInput
|
||||
}
|
||||
ok, err := ai.VerifyTEE(q.Receipt, q.Signature)
|
||||
if err != nil {
|
||||
// Structural failure (short/absent receipt or signature): not verified.
|
||||
return q, false, nil
|
||||
}
|
||||
return q, ok, nil
|
||||
}
|
||||
|
||||
// VerifyNVTrustOutput represents output from GPU attestation verification
|
||||
@@ -86,250 +112,66 @@ type VerifyNVTrustOutput struct {
|
||||
TrustScore uint8 `json:"trust_score"`
|
||||
HardwareCC bool `json:"hardware_cc"`
|
||||
RIMVerified bool `json:"rim_verified"`
|
||||
Mode uint8 `json:"mode"` // 0=Local, 1=Software
|
||||
Mode uint8 `json:"mode"` // 0=Local
|
||||
}
|
||||
|
||||
// VerifyNVTrust verifies NVIDIA GPU attestation using local nvtrust
|
||||
// This is the PRIMARY attestation method - no cloud dependencies
|
||||
// Input: ABI-encoded VerifyNVTrustInput
|
||||
// Output: ABI-encoded VerifyNVTrustOutput
|
||||
// VerifyNVTrust verifies a GPU TEE quote via the real cert-chain verifier.
|
||||
// Gas: 50,000
|
||||
func VerifyNVTrust(input []byte) ([]byte, error) {
|
||||
if len(input) < 64 {
|
||||
return nil, ErrInvalidInput
|
||||
}
|
||||
|
||||
// Decode input
|
||||
var vi VerifyNVTrustInput
|
||||
if err := decodeInput(input, &vi); err != nil {
|
||||
_, verified, err := verifyQuote(input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Build GPU attestation from input
|
||||
gpuAtt := &attestation.GPUAttestation{
|
||||
DeviceID: string(vi.DeviceID[:]),
|
||||
Model: vi.Model,
|
||||
CCEnabled: vi.CCEnabled,
|
||||
TEEIOEnabled: vi.TEEIOEnabled,
|
||||
DriverVersion: vi.DriverVersion,
|
||||
VBIOSVersion: vi.VBIOSVersion,
|
||||
Timestamp: time.Now(),
|
||||
Mode: attestation.ModeLocal,
|
||||
LocalEvidence: &attestation.LocalGPUEvidence{
|
||||
SPDMReport: vi.SPDMReport,
|
||||
CertChain: vi.CertChain,
|
||||
RIMVerified: false, // Will be set by verifier
|
||||
Nonce: vi.Nonce,
|
||||
},
|
||||
out := &VerifyNVTrustOutput{Verified: verified}
|
||||
if verified {
|
||||
out.TrustScore = trustScoreGPU
|
||||
out.HardwareCC = true
|
||||
out.RIMVerified = true
|
||||
}
|
||||
|
||||
// Verify using real attestation implementation.
|
||||
// Lock (not RLock) because verifier updates internal device registry.
|
||||
verifierMu.Lock()
|
||||
status, err := globalVerifier.VerifyGPUAttestation(gpuAtt)
|
||||
verifierMu.Unlock()
|
||||
if err != nil {
|
||||
// Return failure result instead of error for verification failures
|
||||
return encodeOutput(&VerifyNVTrustOutput{
|
||||
Verified: false,
|
||||
TrustScore: 0,
|
||||
HardwareCC: false,
|
||||
Mode: uint8(attestation.ModeLocal),
|
||||
})
|
||||
}
|
||||
|
||||
return encodeOutput(&VerifyNVTrustOutput{
|
||||
Verified: status.Attested,
|
||||
TrustScore: status.TrustScore,
|
||||
HardwareCC: status.HardwareCC,
|
||||
RIMVerified: gpuAtt.LocalEvidence.RIMVerified,
|
||||
Mode: uint8(status.Mode),
|
||||
})
|
||||
return encodeOutput(out)
|
||||
}
|
||||
|
||||
// VerifyTPMInput represents input for TPM attestation verification
|
||||
type VerifyTPMInput struct {
|
||||
QuoteType uint8 `json:"quote_type"` // 1=SGX, 2=SEV-SNP, 3=TDX
|
||||
Quote []byte `json:"quote"`
|
||||
Measurement []byte `json:"measurement"`
|
||||
ReportData []byte `json:"report_data"`
|
||||
Nonce [32]byte `json:"nonce"`
|
||||
ExpectedMeasure []byte `json:"expected_measurement"`
|
||||
}
|
||||
|
||||
// VerifyTPMOutput represents output from TPM attestation verification
|
||||
// VerifyTPMOutput represents output from CPU TEE attestation verification
|
||||
type VerifyTPMOutput struct {
|
||||
Verified bool `json:"verified"`
|
||||
TEEType uint8 `json:"tee_type"`
|
||||
TrustScore uint8 `json:"trust_score"`
|
||||
Measurement []byte `json:"measurement"`
|
||||
Verified bool `json:"verified"`
|
||||
TrustScore uint8 `json:"trust_score"`
|
||||
}
|
||||
|
||||
// VerifyTPM verifies CPU TEE attestation (SGX, SEV-SNP, TDX)
|
||||
// Input: ABI-encoded VerifyTPMInput
|
||||
// Output: ABI-encoded VerifyTPMOutput
|
||||
// VerifyTPM verifies a CPU TEE quote (SGX / SEV-SNP / TDX) via the real
|
||||
// cert-chain verifier.
|
||||
// Gas: 25,000
|
||||
func VerifyTPM(input []byte) ([]byte, error) {
|
||||
if len(input) < 64 {
|
||||
return nil, ErrInvalidInput
|
||||
}
|
||||
|
||||
var vi VerifyTPMInput
|
||||
if err := decodeInput(input, &vi); err != nil {
|
||||
_, verified, err := verifyQuote(input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Map input type to TEE type
|
||||
var teeType attestation.TEEType
|
||||
switch vi.QuoteType {
|
||||
case 1:
|
||||
teeType = attestation.TEETypeSGX
|
||||
case 2:
|
||||
teeType = attestation.TEETypeSEVSNP
|
||||
case 3:
|
||||
teeType = attestation.TEETypeTDX
|
||||
default:
|
||||
return nil, ErrInvalidTPMQuote
|
||||
out := &VerifyTPMOutput{Verified: verified}
|
||||
if verified {
|
||||
out.TrustScore = trustScoreCPU
|
||||
}
|
||||
|
||||
// Build attestation quote
|
||||
quote := &attestation.AttestationQuote{
|
||||
Type: teeType,
|
||||
Version: 1,
|
||||
Quote: vi.Quote,
|
||||
Measurement: vi.Measurement,
|
||||
ReportData: vi.ReportData,
|
||||
Timestamp: time.Now(),
|
||||
Nonce: vi.Nonce[:],
|
||||
}
|
||||
|
||||
// Verify using real attestation implementation.
|
||||
// Lock because verifier updates internal device registry.
|
||||
verifierMu.Lock()
|
||||
err := globalVerifier.VerifyCPUAttestation(quote, vi.ExpectedMeasure)
|
||||
verifierMu.Unlock()
|
||||
if err != nil {
|
||||
return encodeOutput(&VerifyTPMOutput{
|
||||
Verified: false,
|
||||
TEEType: uint8(teeType),
|
||||
TrustScore: 0,
|
||||
Measurement: quote.Measurement,
|
||||
})
|
||||
}
|
||||
|
||||
// Calculate trust score based on TEE type
|
||||
trustScore := calculateTPMTrustScore(teeType)
|
||||
|
||||
return encodeOutput(&VerifyTPMOutput{
|
||||
Verified: true,
|
||||
TEEType: uint8(teeType),
|
||||
TrustScore: trustScore,
|
||||
Measurement: quote.Measurement,
|
||||
})
|
||||
}
|
||||
|
||||
// calculateTPMTrustScore returns trust score based on TEE type
|
||||
func calculateTPMTrustScore(teeType attestation.TEEType) uint8 {
|
||||
switch teeType {
|
||||
case attestation.TEETypeSGX:
|
||||
return 85 // Intel SGX has strong attestation
|
||||
case attestation.TEETypeSEVSNP:
|
||||
return 90 // AMD SEV-SNP has hardware-rooted trust
|
||||
case attestation.TEETypeTDX:
|
||||
return 88 // Intel TDX is newer but well-designed
|
||||
default:
|
||||
return 50
|
||||
}
|
||||
}
|
||||
|
||||
// VerifyComputeInput represents input for compute result attestation
|
||||
type VerifyComputeInput struct {
|
||||
TaskID [32]byte `json:"task_id"`
|
||||
ProviderID [32]byte `json:"provider_id"`
|
||||
ResultHash [32]byte `json:"result_hash"`
|
||||
ComputeTime uint64 `json:"compute_time_ms"`
|
||||
ModelHash [32]byte `json:"model_hash"`
|
||||
TEEQuote []byte `json:"tee_quote"`
|
||||
Signature []byte `json:"signature"`
|
||||
return encodeOutput(out)
|
||||
}
|
||||
|
||||
// VerifyComputeOutput represents output from compute attestation verification
|
||||
type VerifyComputeOutput struct {
|
||||
Verified bool `json:"verified"`
|
||||
TrustScore uint8 `json:"trust_score"`
|
||||
ProviderOK bool `json:"provider_ok"`
|
||||
ResultValid bool `json:"result_valid"`
|
||||
}
|
||||
|
||||
// VerifyCompute verifies AI compute result attestation
|
||||
// This combines device attestation with compute proof verification
|
||||
// Input: ABI-encoded VerifyComputeInput
|
||||
// Output: ABI-encoded VerifyComputeOutput
|
||||
// VerifyCompute verifies an AI compute-result TEE quote via the real cert-chain
|
||||
// verifier. The quote is the proof the computation ran inside a genuine TEE.
|
||||
// Gas: 35,000
|
||||
func VerifyCompute(input []byte) ([]byte, error) {
|
||||
if len(input) < 128 {
|
||||
return nil, ErrInvalidInput
|
||||
}
|
||||
|
||||
var vi VerifyComputeInput
|
||||
if err := decodeInput(input, &vi); err != nil {
|
||||
_, verified, err := verifyQuote(input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check provider is attested
|
||||
providerID := string(vi.ProviderID[:])
|
||||
verifierMu.RLock()
|
||||
status, ok := globalVerifier.GetDeviceStatus(providerID)
|
||||
verifierMu.RUnlock()
|
||||
if !ok || !status.Attested {
|
||||
return encodeOutput(&VerifyComputeOutput{
|
||||
Verified: false,
|
||||
TrustScore: 0,
|
||||
ProviderOK: false,
|
||||
})
|
||||
out := &VerifyComputeOutput{Verified: verified, ResultValid: verified}
|
||||
if verified {
|
||||
out.TrustScore = trustScoreGPU
|
||||
}
|
||||
|
||||
// Verify TEE quote if provided
|
||||
resultValid := true
|
||||
if len(vi.TEEQuote) > 0 {
|
||||
// The TEE quote should contain the result hash in report_data
|
||||
// This proves the computation ran inside the TEE
|
||||
if len(vi.TEEQuote) < 48 {
|
||||
resultValid = false
|
||||
}
|
||||
}
|
||||
|
||||
// Verify signature over result
|
||||
if len(vi.Signature) < 64 {
|
||||
resultValid = false
|
||||
}
|
||||
|
||||
trustScore := status.TrustScore
|
||||
if !resultValid {
|
||||
trustScore = trustScore / 2 // Reduce trust for invalid result
|
||||
}
|
||||
|
||||
// Record job completion
|
||||
taskID := string(vi.TaskID[:])
|
||||
verifierMu.Lock()
|
||||
globalVerifier.RecordJobCompletion(providerID, taskID)
|
||||
verifierMu.Unlock()
|
||||
|
||||
return encodeOutput(&VerifyComputeOutput{
|
||||
Verified: resultValid && status.TrustScore >= 50,
|
||||
TrustScore: trustScore,
|
||||
ProviderOK: status.Attested,
|
||||
ResultValid: resultValid,
|
||||
})
|
||||
}
|
||||
|
||||
// CreateAttestationInput represents input for creating new attestation
|
||||
type CreateAttestationInput struct {
|
||||
DeviceType uint8 `json:"device_type"` // 0=GPU, 1=CPU_SGX, 2=CPU_SEVSNP, 3=CPU_TDX
|
||||
DeviceID [32]byte `json:"device_id"`
|
||||
Model string `json:"model"`
|
||||
Evidence []byte `json:"evidence"` // Raw attestation evidence
|
||||
Nonce [32]byte `json:"nonce"`
|
||||
return encodeOutput(out)
|
||||
}
|
||||
|
||||
// CreateAttestationOutput represents output from attestation creation
|
||||
@@ -337,92 +179,28 @@ type CreateAttestationOutput struct {
|
||||
Success bool `json:"success"`
|
||||
AttestationID [32]byte `json:"attestation_id"`
|
||||
TrustScore uint8 `json:"trust_score"`
|
||||
ExpiresAt uint64 `json:"expires_at"` // Unix timestamp
|
||||
ExpiresAt uint64 `json:"expires_at"` // block-time + TTL, deterministic
|
||||
}
|
||||
|
||||
// CreateAttestation creates a new attestation record for a device
|
||||
// This registers the device with the verifier
|
||||
// Input: ABI-encoded CreateAttestationInput
|
||||
// Output: ABI-encoded CreateAttestationOutput
|
||||
// CreateAttestation verifies a TEE quote and, on success, issues a deterministic
|
||||
// attestation record. The record is NOT persisted (the precompile is stateless);
|
||||
// it is returned to the caller. Expiry is the block timestamp + TTL — never the
|
||||
// wall clock — so every validator computes the same value.
|
||||
// Gas: 75,000
|
||||
func CreateAttestation(input []byte) ([]byte, error) {
|
||||
if len(input) < 64 {
|
||||
return nil, ErrInvalidInput
|
||||
}
|
||||
|
||||
var ci CreateAttestationInput
|
||||
if err := decodeInput(input, &ci); err != nil {
|
||||
func CreateAttestation(input []byte, blockTS uint64) ([]byte, error) {
|
||||
q, verified, err := verifyQuote(input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var trustScore uint8
|
||||
var success bool
|
||||
|
||||
switch ci.DeviceType {
|
||||
case 0: // GPU
|
||||
gpuAtt := &attestation.GPUAttestation{
|
||||
DeviceID: string(ci.DeviceID[:]),
|
||||
Model: ci.Model,
|
||||
CCEnabled: attestation.IsHardwareCCCapable(ci.Model),
|
||||
Timestamp: time.Now(),
|
||||
Mode: attestation.ModeLocal,
|
||||
LocalEvidence: &attestation.LocalGPUEvidence{
|
||||
SPDMReport: ci.Evidence,
|
||||
CertChain: nil, // Would be extracted from evidence
|
||||
Nonce: ci.Nonce,
|
||||
},
|
||||
}
|
||||
|
||||
verifierMu.Lock()
|
||||
status, err := globalVerifier.VerifyGPUAttestation(gpuAtt)
|
||||
verifierMu.Unlock()
|
||||
if err == nil {
|
||||
success = true
|
||||
trustScore = status.TrustScore
|
||||
}
|
||||
|
||||
case 1, 2, 3: // CPU TEE types
|
||||
var teeType attestation.TEEType
|
||||
switch ci.DeviceType {
|
||||
case 1:
|
||||
teeType = attestation.TEETypeSGX
|
||||
case 2:
|
||||
teeType = attestation.TEETypeSEVSNP
|
||||
case 3:
|
||||
teeType = attestation.TEETypeTDX
|
||||
}
|
||||
|
||||
quote := &attestation.AttestationQuote{
|
||||
Type: teeType,
|
||||
Quote: ci.Evidence,
|
||||
Timestamp: time.Now(),
|
||||
Nonce: ci.Nonce[:],
|
||||
}
|
||||
|
||||
verifierMu.Lock()
|
||||
err := globalVerifier.VerifyCPUAttestation(quote, nil)
|
||||
verifierMu.Unlock()
|
||||
if err == nil {
|
||||
success = true
|
||||
trustScore = calculateTPMTrustScore(teeType)
|
||||
}
|
||||
|
||||
default:
|
||||
return nil, ErrInvalidInput
|
||||
out := &CreateAttestationOutput{
|
||||
Success: verified,
|
||||
AttestationID: attestationID(q.Receipt),
|
||||
ExpiresAt: blockTS + attestationTTL,
|
||||
}
|
||||
|
||||
// Generate attestation ID from device ID and timestamp
|
||||
attestationID := computeAttestationID(ci.DeviceID, ci.Nonce)
|
||||
|
||||
// Attestation valid for 1 hour
|
||||
expiresAt := time.Now().Add(time.Hour).Unix()
|
||||
|
||||
return encodeOutput(&CreateAttestationOutput{
|
||||
Success: success,
|
||||
AttestationID: attestationID,
|
||||
TrustScore: trustScore,
|
||||
ExpiresAt: uint64(expiresAt),
|
||||
})
|
||||
if verified {
|
||||
out.TrustScore = trustScoreGPU
|
||||
}
|
||||
return encodeOutput(out)
|
||||
}
|
||||
|
||||
// GetDeviceStatusInput represents input for querying device status
|
||||
@@ -430,87 +208,42 @@ type GetDeviceStatusInput struct {
|
||||
DeviceID [32]byte `json:"device_id"`
|
||||
}
|
||||
|
||||
// GetDeviceStatusOutput represents output from device status query
|
||||
// GetDeviceStatusOutput represents output from a device status query
|
||||
type GetDeviceStatusOutput struct {
|
||||
Found bool `json:"found"`
|
||||
Attested bool `json:"attested"`
|
||||
TrustScore uint8 `json:"trust_score"`
|
||||
LastSeen uint64 `json:"last_seen"` // Unix timestamp
|
||||
HardwareCC bool `json:"hardware_cc"`
|
||||
Mode uint8 `json:"mode"`
|
||||
JobCount uint32 `json:"job_count"`
|
||||
Found bool `json:"found"`
|
||||
}
|
||||
|
||||
// GetDeviceStatus returns the attestation status of a device
|
||||
// Input: ABI-encoded GetDeviceStatusInput
|
||||
// Output: ABI-encoded GetDeviceStatusOutput
|
||||
// GetDeviceStatus reports a device's attestation status. This precompile keeps
|
||||
// no on-chain device registry (TEE quotes are self-validating and the prior
|
||||
// process-global registry was a consensus-split hazard), so a status lookup by
|
||||
// deviceID alone is always not-found. Verify a presented quote with the
|
||||
// VerifyNVTrust/VerifyTPM/VerifyCompute selectors instead.
|
||||
// Gas: 5,000
|
||||
func GetDeviceStatus(input []byte) ([]byte, error) {
|
||||
if len(input) < 32 {
|
||||
var di GetDeviceStatusInput
|
||||
if err := json.Unmarshal(input, &di); err != nil {
|
||||
return nil, ErrInvalidInput
|
||||
}
|
||||
|
||||
var di GetDeviceStatusInput
|
||||
if err := decodeInput(input, &di); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
deviceID := string(di.DeviceID[:])
|
||||
verifierMu.RLock()
|
||||
status, ok := globalVerifier.GetDeviceStatus(deviceID)
|
||||
verifierMu.RUnlock()
|
||||
|
||||
if !ok {
|
||||
return encodeOutput(&GetDeviceStatusOutput{
|
||||
Found: false,
|
||||
})
|
||||
}
|
||||
|
||||
return encodeOutput(&GetDeviceStatusOutput{
|
||||
Found: true,
|
||||
Attested: status.Attested,
|
||||
TrustScore: status.TrustScore,
|
||||
LastSeen: uint64(status.LastSeen.Unix()),
|
||||
HardwareCC: status.HardwareCC,
|
||||
Mode: uint8(status.Mode),
|
||||
JobCount: uint32(len(status.JobHistory)),
|
||||
})
|
||||
return encodeOutput(&GetDeviceStatusOutput{Found: false})
|
||||
}
|
||||
|
||||
// RegisterTrustedMeasurement registers a trusted measurement with the verifier
|
||||
// This should only be called from governance/admin functions
|
||||
func RegisterTrustedMeasurement(name string, measurement []byte) {
|
||||
verifierMu.Lock()
|
||||
globalVerifier.RegisterTrustedMeasurement(name, measurement)
|
||||
verifierMu.Unlock()
|
||||
// attestationID derives a deterministic id from the verified quote receipt.
|
||||
func attestationID(receipt []byte) [32]byte {
|
||||
return blake3.Sum256(receipt)
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
func decodeInput(data []byte, v any) error {
|
||||
return json.Unmarshal(data, v)
|
||||
}
|
||||
|
||||
func encodeOutput(v any) ([]byte, error) {
|
||||
return json.Marshal(v)
|
||||
}
|
||||
|
||||
func computeAttestationID(deviceID, nonce [32]byte) [32]byte {
|
||||
h := attestation.ComputeAttestationHash(&attestation.AttestationQuote{
|
||||
Type: attestation.TEETypeNVIDIA,
|
||||
Quote: deviceID[:],
|
||||
Nonce: nonce[:],
|
||||
})
|
||||
return h
|
||||
}
|
||||
|
||||
// IsHardwareCCCapable checks if GPU model supports hardware confidential computing
|
||||
// Delegates to real implementation in luxfi/ai/pkg/attestation
|
||||
// IsHardwareCCCapable reports whether a GPU model supports hardware confidential
|
||||
// computing. Pure, static lookup — no external dependency.
|
||||
func IsHardwareCCCapable(model string) bool {
|
||||
return attestation.IsHardwareCCCapable(model)
|
||||
for _, m := range SupportedGPUModels() {
|
||||
if m == model {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// SupportedGPUModels returns list of CC-capable GPU models
|
||||
// SupportedGPUModels returns the CC-capable GPU models.
|
||||
func SupportedGPUModels() []string {
|
||||
return []string{
|
||||
"H100",
|
||||
@@ -522,9 +255,8 @@ func SupportedGPUModels() []string {
|
||||
}
|
||||
}
|
||||
|
||||
// RequiredGas returns gas cost for attestation operation
|
||||
// RequiredGas returns gas cost for an attestation operation.
|
||||
func RequiredGas(selector [4]byte) uint64 {
|
||||
// Function selectors (first 4 bytes of keccak256 hash)
|
||||
switch {
|
||||
case selector == [4]byte{0x01, 0x00, 0x00, 0x00}: // verifyNVTrust
|
||||
return GasVerifyNVTrust
|
||||
@@ -541,14 +273,13 @@ func RequiredGas(selector [4]byte) uint64 {
|
||||
}
|
||||
}
|
||||
|
||||
// Run executes the attestation precompile
|
||||
// This is the main entry point for EVM precompile calls
|
||||
func Run(input []byte) ([]byte, error) {
|
||||
// Run executes the attestation precompile. blockTS is the deterministic block
|
||||
// timestamp from AccessibleState, used only for attestation expiry.
|
||||
func Run(input []byte, blockTS uint64) ([]byte, error) {
|
||||
if len(input) < 4 {
|
||||
return nil, ErrInvalidInput
|
||||
}
|
||||
|
||||
// Extract function selector
|
||||
var selector [4]byte
|
||||
copy(selector[:], input[:4])
|
||||
data := input[4:]
|
||||
@@ -561,7 +292,7 @@ func Run(input []byte) ([]byte, error) {
|
||||
case [4]byte{0x03, 0x00, 0x00, 0x00}:
|
||||
return VerifyCompute(data)
|
||||
case [4]byte{0x04, 0x00, 0x00, 0x00}:
|
||||
return CreateAttestation(data)
|
||||
return CreateAttestation(data, blockTS)
|
||||
case [4]byte{0x05, 0x00, 0x00, 0x00}:
|
||||
return GetDeviceStatus(data)
|
||||
default:
|
||||
@@ -569,7 +300,11 @@ func Run(input []byte) ([]byte, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// ABIEncode encodes values for EVM ABI format
|
||||
func encodeOutput(v any) ([]byte, error) {
|
||||
return json.Marshal(v)
|
||||
}
|
||||
|
||||
// ABIEncode encodes values for EVM ABI format.
|
||||
func ABIEncode(values ...any) []byte {
|
||||
var result []byte
|
||||
for _, v := range values {
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
package attestation
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func mustJSON(t *testing.T, v any) []byte {
|
||||
t.Helper()
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal: %v", err)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// VerifyCompute: input below the 128-byte floor is rejected.
|
||||
func TestVerifyCompute_TooShort(t *testing.T) {
|
||||
if _, err := VerifyCompute([]byte(`{}`)); err != ErrInvalidInput {
|
||||
t.Fatalf("want ErrInvalidInput, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// VerifyCompute: an unattested provider verifies to false (covers the main
|
||||
// body + GetDeviceStatus miss + encodeOutput — previously 0% covered).
|
||||
func TestVerifyCompute_UnattestedProvider(t *testing.T) {
|
||||
var pid [32]byte
|
||||
copy(pid[:], []byte("unattested-provider-aaaaaaaaaaaa"))
|
||||
in := VerifyComputeInput{
|
||||
ProviderID: pid,
|
||||
TEEQuote: make([]byte, 48),
|
||||
Signature: make([]byte, 64),
|
||||
}
|
||||
b := mustJSON(t, in)
|
||||
if len(b) < 128 {
|
||||
t.Fatalf("test input must be >=128 bytes, got %d", len(b))
|
||||
}
|
||||
out, err := VerifyCompute(b)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
var o VerifyComputeOutput
|
||||
if err := json.Unmarshal(out, &o); err != nil {
|
||||
t.Fatalf("decode output: %v", err)
|
||||
}
|
||||
if o.ProviderOK {
|
||||
t.Error("unattested provider must not be ProviderOK")
|
||||
}
|
||||
if o.Verified {
|
||||
t.Error("unattested provider must not verify")
|
||||
}
|
||||
}
|
||||
|
||||
// Run dispatch: every known selector routes; unknown/short are rejected.
|
||||
func TestRun_DispatchAllSelectors(t *testing.T) {
|
||||
for _, sel := range [][4]byte{{1, 0, 0, 0}, {2, 0, 0, 0}, {3, 0, 0, 0}, {4, 0, 0, 0}, {5, 0, 0, 0}} {
|
||||
input := append(sel[:], make([]byte, 256)...)
|
||||
_, _ = Run(input) // inner may error on the dummy payload; we only need the dispatch arm
|
||||
}
|
||||
if _, err := Run([]byte{0xff, 0xff, 0xff, 0xff}); err != ErrInvalidInput {
|
||||
t.Errorf("unknown selector: want ErrInvalidInput, got %v", err)
|
||||
}
|
||||
if _, err := Run([]byte{0x01}); err != ErrInvalidInput {
|
||||
t.Errorf("short input: want ErrInvalidInput, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSupportedGPUModels_Coverage(t *testing.T) {
|
||||
models := SupportedGPUModels()
|
||||
if len(models) == 0 {
|
||||
t.Fatal("expected supported GPU models")
|
||||
}
|
||||
var hasH100 bool
|
||||
for _, m := range models {
|
||||
if m == "H100" {
|
||||
hasH100 = true
|
||||
}
|
||||
}
|
||||
if !hasH100 {
|
||||
t.Error("expected H100 in supported models")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterTrustedMeasurement_Coverage(t *testing.T) {
|
||||
// Smoke: previously 0% covered. Must not panic.
|
||||
RegisterTrustedMeasurement("test-firmware-v1", []byte{0x01, 0x02, 0x03, 0x04})
|
||||
}
|
||||
|
||||
func TestIsHardwareCCCapable_TwoAxis(t *testing.T) {
|
||||
if !IsHardwareCCCapable("H100") {
|
||||
t.Error("H100 must be confidential-compute capable")
|
||||
}
|
||||
if IsHardwareCCCapable("M4 Max") {
|
||||
t.Error("Apple M4 Max is not NVTrust GPU-TEE capable")
|
||||
}
|
||||
if IsHardwareCCCapable("DGX Spark") {
|
||||
t.Error("DGX Spark must NOT be GPU-TEE capable (neutered)")
|
||||
}
|
||||
}
|
||||
+294
-380
@@ -4,294 +4,328 @@
|
||||
package attestation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"math/big"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/luxfi/geth/common"
|
||||
"github.com/luxfi/precompile/contract"
|
||||
"github.com/luxfi/precompile/precompileconfig"
|
||||
)
|
||||
|
||||
func TestVerifyNVTrust_ValidH100(t *testing.T) {
|
||||
// Create valid H100 attestation input
|
||||
input := VerifyNVTrustInput{
|
||||
DeviceID: [32]byte{0x01, 0x02, 0x03},
|
||||
Model: "H100",
|
||||
CCEnabled: true,
|
||||
TEEIOEnabled: true,
|
||||
DriverVersion: "535.104.05",
|
||||
VBIOSVersion: "96.00.89.00.01",
|
||||
SPDMReport: make([]byte, 512), // Valid SPDM report size
|
||||
CertChain: make([]byte, 1024), // Valid cert chain size
|
||||
Nonce: [32]byte{0xAA, 0xBB, 0xCC},
|
||||
}
|
||||
// --- helpers ----------------------------------------------------------------
|
||||
|
||||
data, err := json.Marshal(input)
|
||||
func mustJSON(t *testing.T, v any) []byte {
|
||||
t.Helper()
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
result, err := VerifyNVTrust(data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var output VerifyNVTrustOutput
|
||||
if err := json.Unmarshal(result, &output); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// H100 should be verified with high trust score
|
||||
if !output.Verified {
|
||||
t.Error("expected H100 to be verified")
|
||||
}
|
||||
if output.TrustScore < 70 {
|
||||
t.Errorf("expected trust score >= 70, got %d", output.TrustScore)
|
||||
}
|
||||
// HardwareCC depends on RIM verification which requires real RIM data
|
||||
// For test purposes, we check that the model is CC-capable
|
||||
if !IsHardwareCCCapable("H100") {
|
||||
t.Error("H100 should be hardware CC capable")
|
||||
t.Fatalf("marshal: %v", err)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func TestVerifyNVTrust_NonCCGPU(t *testing.T) {
|
||||
// Create attestation for non-CC GPU (RTX 5090)
|
||||
input := VerifyNVTrustInput{
|
||||
DeviceID: [32]byte{0x04, 0x05, 0x06},
|
||||
Model: "RTX 5090", // Consumer GPU - no CC
|
||||
CCEnabled: false,
|
||||
TEEIOEnabled: false,
|
||||
DriverVersion: "550.00.00",
|
||||
VBIOSVersion: "100.00.00.00.01",
|
||||
SPDMReport: make([]byte, 512),
|
||||
CertChain: make([]byte, 1024),
|
||||
Nonce: [32]byte{0xDD, 0xEE, 0xFF},
|
||||
}
|
||||
|
||||
data, err := json.Marshal(input)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
result, err := VerifyNVTrust(data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var output VerifyNVTrustOutput
|
||||
if err := json.Unmarshal(result, &output); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Non-CC GPU should fail verification for local mode
|
||||
if output.Verified {
|
||||
t.Error("expected non-CC GPU to fail verification in local mode")
|
||||
}
|
||||
func leftPad32(b []byte) []byte {
|
||||
out := make([]byte, 32)
|
||||
copy(out[32-len(b):], b)
|
||||
return out
|
||||
}
|
||||
|
||||
func TestVerifyTPM_SGX(t *testing.T) {
|
||||
// Create valid SGX quote (minimum size 432 bytes)
|
||||
quote := make([]byte, 512)
|
||||
// Set MRENCLAVE at offset 112-144
|
||||
copy(quote[112:144], []byte("expected_measurement_value_here!"))
|
||||
// selfRootedQuote builds a structurally valid TEE quote signed by a leaf that
|
||||
// chains to the attacker's OWN root — NOT the embedded vendor root. The real
|
||||
// verifier (ai.VerifyTEE) must reject it. This is a "forge any GPU" attempt.
|
||||
func selfRootedQuote(t *testing.T) (receipt, sig []byte) {
|
||||
t.Helper()
|
||||
rootKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
rootTmpl := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(1),
|
||||
Subject: pkix.Name{CommonName: "forged root"},
|
||||
NotBefore: time.Unix(1_600_000_000, 0),
|
||||
NotAfter: time.Unix(2_000_000_000, 0),
|
||||
IsCA: true,
|
||||
BasicConstraintsValid: true,
|
||||
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature,
|
||||
}
|
||||
rootDER, _ := x509.CreateCertificate(rand.Reader, rootTmpl, rootTmpl, &rootKey.PublicKey, rootKey)
|
||||
root, _ := x509.ParseCertificate(rootDER)
|
||||
|
||||
input := VerifyTPMInput{
|
||||
QuoteType: 1, // SGX
|
||||
Quote: quote,
|
||||
Measurement: quote[112:144],
|
||||
ReportData: make([]byte, 64),
|
||||
Nonce: [32]byte{0x11, 0x22, 0x33},
|
||||
ExpectedMeasure: nil, // No expected measurement
|
||||
leafKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
leafTmpl := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(2),
|
||||
Subject: pkix.Name{CommonName: "forged leaf"},
|
||||
NotBefore: time.Unix(1_600_000_000, 0),
|
||||
NotAfter: time.Unix(2_000_000_000, 0),
|
||||
KeyUsage: x509.KeyUsageDigitalSignature,
|
||||
}
|
||||
leafDER, _ := x509.CreateCertificate(rand.Reader, leafTmpl, root, &leafKey.PublicKey, rootKey)
|
||||
leaf, _ := x509.ParseCertificate(leafDER)
|
||||
|
||||
data, err := json.Marshal(input)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
report := make([]byte, 48)
|
||||
report[0] = 0xAB
|
||||
binary.BigEndian.PutUint64(report[32:40], 1_700_000_000)
|
||||
receipt = append(append([]byte{}, report...), leaf.Raw...)
|
||||
|
||||
result, err := VerifyTPM(data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var output VerifyTPMOutput
|
||||
if err := json.Unmarshal(result, &output); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// SGX should verify with good trust score
|
||||
if !output.Verified {
|
||||
t.Error("expected SGX quote to be verified")
|
||||
}
|
||||
if output.TEEType != 1 {
|
||||
t.Errorf("expected TEE type 1 (SGX), got %d", output.TEEType)
|
||||
}
|
||||
if output.TrustScore < 80 {
|
||||
t.Errorf("expected trust score >= 80 for SGX, got %d", output.TrustScore)
|
||||
}
|
||||
digest := sha256.Sum256(report)
|
||||
r, s, _ := ecdsa.Sign(rand.Reader, leafKey, digest[:])
|
||||
sig = append(leftPad32(r.Bytes()), leftPad32(s.Bytes())...)
|
||||
return receipt, sig
|
||||
}
|
||||
|
||||
func TestVerifyTPM_SEVSNP(t *testing.T) {
|
||||
// Create valid SEV-SNP report (minimum size 1184 bytes)
|
||||
quote := make([]byte, 1200)
|
||||
// minimal AccessibleState exposing only a block timestamp (the only consensus
|
||||
// input the attestation precompile reads).
|
||||
type attBC struct{ ts uint64 }
|
||||
|
||||
input := VerifyTPMInput{
|
||||
QuoteType: 2, // SEV-SNP
|
||||
Quote: quote,
|
||||
Measurement: make([]byte, 48),
|
||||
ReportData: make([]byte, 64),
|
||||
Nonce: [32]byte{0x44, 0x55, 0x66},
|
||||
ExpectedMeasure: nil,
|
||||
}
|
||||
func (b attBC) Number() *big.Int { return big.NewInt(1) }
|
||||
func (b attBC) Timestamp() uint64 { return b.ts }
|
||||
func (b attBC) GetPredicateResults(common.Hash, common.Address) []byte { return nil }
|
||||
|
||||
data, err := json.Marshal(input)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
type attAS struct{ ts uint64 }
|
||||
|
||||
result, err := VerifyTPM(data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
func (a attAS) GetStateDB() contract.StateDB { return nil }
|
||||
func (a attAS) GetBlockContext() contract.BlockContext { return attBC{a.ts} }
|
||||
func (a attAS) GetConsensusContext() context.Context { return context.Background() }
|
||||
func (a attAS) GetChainConfig() precompileconfig.ChainConfig { return nil }
|
||||
func (a attAS) GetPrecompileEnv() contract.PrecompileEnvironment { return nil }
|
||||
|
||||
var output VerifyTPMOutput
|
||||
if err := json.Unmarshal(result, &output); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// --- forge rejection: zero / garbage / self-rooted evidence -----------------
|
||||
|
||||
if !output.Verified {
|
||||
t.Error("expected SEV-SNP quote to be verified")
|
||||
// TestVerifyNVTrust_ForgedRejected is the proof the GPU-attestation forge is
|
||||
// closed. The prior fake returned Verified:true (score 93) for zero-filled
|
||||
// evidence; the real verifier rejects zero-byte, garbage, and self-rooted
|
||||
// quotes alike.
|
||||
func TestVerifyNVTrust_ForgedRejected(t *testing.T) {
|
||||
cases := map[string]QuoteInput{
|
||||
"zero": {Receipt: make([]byte, 512), Signature: make([]byte, 64)},
|
||||
"empty": {Receipt: nil, Signature: nil},
|
||||
"garbage": {Receipt: []byte("not-a-tee-quote-at-all"), Signature: []byte("nope")},
|
||||
"shortrcpt": {Receipt: make([]byte, 10), Signature: make([]byte, 64)},
|
||||
}
|
||||
if output.TEEType != 2 {
|
||||
t.Errorf("expected TEE type 2 (SEV-SNP), got %d", output.TEEType)
|
||||
}
|
||||
if output.TrustScore < 85 {
|
||||
t.Errorf("expected trust score >= 85 for SEV-SNP, got %d", output.TrustScore)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateAttestation_GPU(t *testing.T) {
|
||||
// For GPU attestation to succeed, we need valid evidence with proper sizes
|
||||
// The attestation.VerifyGPUAttestation requires SPDM report >= 256 bytes
|
||||
// and cert chain >= 256 bytes
|
||||
input := CreateAttestationInput{
|
||||
DeviceType: 0, // GPU
|
||||
DeviceID: [32]byte{0x10, 0x20, 0x30},
|
||||
Model: "H200",
|
||||
Evidence: make([]byte, 512), // Used as SPDM report
|
||||
Nonce: [32]byte{0x77, 0x88, 0x99},
|
||||
}
|
||||
|
||||
data, err := json.Marshal(input)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
result, err := CreateAttestation(data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var output CreateAttestationOutput
|
||||
if err := json.Unmarshal(result, &output); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// GPU attestation requires both SPDM report AND cert chain
|
||||
// Our test only provides Evidence as SPDM report, so it may fail
|
||||
// The key test is that an AttestationID is generated
|
||||
if output.AttestationID == [32]byte{} {
|
||||
t.Error("expected attestation ID to be generated")
|
||||
}
|
||||
if output.ExpiresAt <= uint64(time.Now().Unix()) {
|
||||
t.Error("expected expiration to be in the future")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateAttestation_CPU_SGX(t *testing.T) {
|
||||
input := CreateAttestationInput{
|
||||
DeviceType: 1, // SGX
|
||||
DeviceID: [32]byte{0xA0, 0xB0, 0xC0},
|
||||
Model: "Intel Xeon",
|
||||
Evidence: make([]byte, 512), // Valid SGX quote
|
||||
Nonce: [32]byte{0xAA, 0xBB, 0xCC},
|
||||
}
|
||||
|
||||
data, err := json.Marshal(input)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
result, err := CreateAttestation(data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var output CreateAttestationOutput
|
||||
if err := json.Unmarshal(result, &output); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !output.Success {
|
||||
t.Error("expected SGX attestation to succeed")
|
||||
}
|
||||
if output.TrustScore != 85 { // SGX trust score
|
||||
t.Errorf("expected trust score 85 for SGX, got %d", output.TrustScore)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDeviceStatus_NotFound(t *testing.T) {
|
||||
input := GetDeviceStatusInput{
|
||||
DeviceID: [32]byte{0xFF, 0xFF, 0xFF}, // Unknown device
|
||||
}
|
||||
|
||||
data, err := json.Marshal(input)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
result, err := GetDeviceStatus(data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var output GetDeviceStatusOutput
|
||||
if err := json.Unmarshal(result, &output); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if output.Found {
|
||||
t.Error("expected device not to be found")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsHardwareCCCapable(t *testing.T) {
|
||||
tests := []struct {
|
||||
model string
|
||||
expected bool
|
||||
}{
|
||||
{"H100", true},
|
||||
{"H200", true},
|
||||
{"B100", true},
|
||||
{"B200", true},
|
||||
{"GB200", true},
|
||||
{"RTX PRO 6000", true},
|
||||
{"RTX 5090", false},
|
||||
{"RTX 4090", false},
|
||||
{"GB10", false},
|
||||
{"A100", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.model, func(t *testing.T) {
|
||||
result := IsHardwareCCCapable(tt.model)
|
||||
if result != tt.expected {
|
||||
t.Errorf("IsHardwareCCCapable(%s) = %v, want %v", tt.model, result, tt.expected)
|
||||
for name, in := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
out, err := VerifyNVTrust(mustJSON(t, in))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
var o VerifyNVTrustOutput
|
||||
if err := json.Unmarshal(out, &o); err != nil {
|
||||
t.Fatalf("decode: %v", err)
|
||||
}
|
||||
if o.Verified {
|
||||
t.Fatal("forged GPU evidence must NOT verify")
|
||||
}
|
||||
if o.TrustScore != 0 {
|
||||
t.Fatalf("unverified quote must score 0, got %d", o.TrustScore)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestVerifyNVTrust_SelfRootedRejected: a well-formed quote that chains to the
|
||||
// attacker's own root (not the embedded vendor root) is rejected.
|
||||
func TestVerifyNVTrust_SelfRootedRejected(t *testing.T) {
|
||||
rcpt, sig := selfRootedQuote(t)
|
||||
out, err := VerifyNVTrust(mustJSON(t, QuoteInput{Receipt: rcpt, Signature: sig}))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
var o VerifyNVTrustOutput
|
||||
if err := json.Unmarshal(out, &o); err != nil {
|
||||
t.Fatalf("decode: %v", err)
|
||||
}
|
||||
if o.Verified {
|
||||
t.Fatal("a quote not anchored at an embedded vendor root must be REJECTED")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyTPM_ForgedRejected(t *testing.T) {
|
||||
out, err := VerifyTPM(mustJSON(t, QuoteInput{Receipt: make([]byte, 1200), Signature: make([]byte, 64)}))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
var o VerifyTPMOutput
|
||||
if err := json.Unmarshal(out, &o); err != nil {
|
||||
t.Fatalf("decode: %v", err)
|
||||
}
|
||||
if o.Verified || o.TrustScore != 0 {
|
||||
t.Fatalf("forged CPU TEE quote must be rejected, got verified=%v score=%d", o.Verified, o.TrustScore)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyCompute_ForgedRejected(t *testing.T) {
|
||||
rcpt, sig := selfRootedQuote(t)
|
||||
out, err := VerifyCompute(mustJSON(t, QuoteInput{Receipt: rcpt, Signature: sig}))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
var o VerifyComputeOutput
|
||||
if err := json.Unmarshal(out, &o); err != nil {
|
||||
t.Fatalf("decode: %v", err)
|
||||
}
|
||||
if o.Verified || o.ResultValid {
|
||||
t.Fatal("compute result without a genuine TEE quote must be rejected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyNVTrust_MalformedJSON(t *testing.T) {
|
||||
if _, err := VerifyNVTrust([]byte("{not json")); err != ErrInvalidInput {
|
||||
t.Fatalf("malformed JSON: want ErrInvalidInput, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// --- CreateAttestation: fail-closed + deterministic block-time expiry --------
|
||||
|
||||
func TestCreateAttestation_FailsClosed(t *testing.T) {
|
||||
const blockTS = uint64(1_700_000_000)
|
||||
out, err := CreateAttestation(mustJSON(t, QuoteInput{Receipt: make([]byte, 512), Signature: make([]byte, 64)}), blockTS)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
var o CreateAttestationOutput
|
||||
if err := json.Unmarshal(out, &o); err != nil {
|
||||
t.Fatalf("decode: %v", err)
|
||||
}
|
||||
if o.Success {
|
||||
t.Fatal("forged evidence must NOT create a successful attestation")
|
||||
}
|
||||
if o.TrustScore != 0 {
|
||||
t.Fatalf("failed attestation must score 0, got %d", o.TrustScore)
|
||||
}
|
||||
// ExpiresAt is derived from the block timestamp, never the wall clock.
|
||||
if o.ExpiresAt != blockTS+attestationTTL {
|
||||
t.Fatalf("expiry must be block-time+TTL=%d, got %d", blockTS+attestationTTL, o.ExpiresAt)
|
||||
}
|
||||
// A deterministic id is still issued, bound to the presented receipt.
|
||||
if o.AttestationID == [32]byte{} {
|
||||
t.Fatal("attestation id must be derived from the receipt")
|
||||
}
|
||||
}
|
||||
|
||||
// TestCreateAttestation_Deterministic proves the consensus-split non-determinism
|
||||
// (time.Now + process-global map) is gone: identical (input, blockTS) yields
|
||||
// byte-identical output, and the expiry tracks the block timestamp exactly.
|
||||
func TestCreateAttestation_Deterministic(t *testing.T) {
|
||||
in := mustJSON(t, QuoteInput{Receipt: make([]byte, 64), Signature: make([]byte, 64)})
|
||||
|
||||
a, err := CreateAttestation(in, 1000)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b, err := CreateAttestation(in, 1000)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(a) != string(b) {
|
||||
t.Fatal("CreateAttestation must be deterministic for identical (input, blockTS)")
|
||||
}
|
||||
|
||||
var o1, o2 CreateAttestationOutput
|
||||
_ = json.Unmarshal(a, &o1)
|
||||
c, _ := CreateAttestation(in, 2000)
|
||||
_ = json.Unmarshal(c, &o2)
|
||||
if o2.ExpiresAt-o1.ExpiresAt != 1000 {
|
||||
t.Fatalf("expiry must move with block time: delta=%d, want 1000", o2.ExpiresAt-o1.ExpiresAt)
|
||||
}
|
||||
}
|
||||
|
||||
// --- GetDeviceStatus: stateless, always not-found ---------------------------
|
||||
|
||||
func TestGetDeviceStatus_NotFound(t *testing.T) {
|
||||
out, err := GetDeviceStatus(mustJSON(t, GetDeviceStatusInput{DeviceID: [32]byte{0xFF}}))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var o GetDeviceStatusOutput
|
||||
if err := json.Unmarshal(out, &o); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if o.Found {
|
||||
t.Error("stateless precompile keeps no registry: status must be not-found")
|
||||
}
|
||||
}
|
||||
|
||||
// --- Run dispatch + stateful wrapper threading ------------------------------
|
||||
|
||||
func TestRun_DispatchAndDeterminism(t *testing.T) {
|
||||
// Every known selector routes and verification fails closed on a dummy quote.
|
||||
payload := mustJSON(t, QuoteInput{Receipt: make([]byte, 64), Signature: make([]byte, 64)})
|
||||
for _, sel := range [][4]byte{{1, 0, 0, 0}, {2, 0, 0, 0}, {3, 0, 0, 0}, {4, 0, 0, 0}} {
|
||||
input := append(sel[:], payload...)
|
||||
out1, err1 := Run(input, 1700000000)
|
||||
out2, err2 := Run(input, 1700000000)
|
||||
if err1 != nil || err2 != nil {
|
||||
t.Fatalf("selector %v: unexpected error %v / %v", sel, err1, err2)
|
||||
}
|
||||
if string(out1) != string(out2) {
|
||||
t.Fatalf("selector %v: non-deterministic output", sel)
|
||||
}
|
||||
}
|
||||
if _, err := Run([]byte{0xff, 0xff, 0xff, 0xff}, 0); err != ErrInvalidInput {
|
||||
t.Errorf("unknown selector: want ErrInvalidInput, got %v", err)
|
||||
}
|
||||
if _, err := Run([]byte{0x01}, 0); err != ErrInvalidInput {
|
||||
t.Errorf("short input: want ErrInvalidInput, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestStatefulRun_BlockTimestampThreaded proves the stateful wrapper passes the
|
||||
// block timestamp from AccessibleState (not time.Now) into CreateAttestation.
|
||||
func TestStatefulRun_BlockTimestampThreaded(t *testing.T) {
|
||||
payload := mustJSON(t, QuoteInput{Receipt: make([]byte, 64), Signature: make([]byte, 64)})
|
||||
input := append([]byte{0x04, 0x00, 0x00, 0x00}, payload...)
|
||||
|
||||
out, _, err := Precompile.Run(attAS{ts: 4242}, common.Address{}, ContractAddress, input, GasCreateAttest, false)
|
||||
if err != nil {
|
||||
t.Fatalf("stateful run: %v", err)
|
||||
}
|
||||
var o CreateAttestationOutput
|
||||
if err := json.Unmarshal(out, &o); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if o.ExpiresAt != 4242+attestationTTL {
|
||||
t.Fatalf("expiry must come from block ts: got %d, want %d", o.ExpiresAt, 4242+attestationTTL)
|
||||
}
|
||||
}
|
||||
|
||||
// --- pure helpers + gas (unchanged surface) ---------------------------------
|
||||
|
||||
func TestIsHardwareCCCapable(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
model string
|
||||
want bool
|
||||
}{
|
||||
{"H100", true}, {"H200", true}, {"B100", true}, {"B200", true},
|
||||
{"GB200", true}, {"RTX PRO 6000", true},
|
||||
{"RTX 5090", false}, {"RTX 4090", false}, {"A100", false},
|
||||
{"M4 Max", false}, {"DGX Spark", false},
|
||||
} {
|
||||
if got := IsHardwareCCCapable(tt.model); got != tt.want {
|
||||
t.Errorf("IsHardwareCCCapable(%s) = %v, want %v", tt.model, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSupportedGPUModels(t *testing.T) {
|
||||
models := SupportedGPUModels()
|
||||
if len(models) == 0 {
|
||||
t.Fatal("expected supported GPU models")
|
||||
}
|
||||
for _, m := range models {
|
||||
if !IsHardwareCCCapable(m) {
|
||||
t.Errorf("supported model %s must be CC capable", m)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequiredGas(t *testing.T) {
|
||||
tests := []struct {
|
||||
for _, tt := range []struct {
|
||||
selector [4]byte
|
||||
expected uint64
|
||||
}{
|
||||
@@ -300,142 +334,22 @@ func TestRequiredGas(t *testing.T) {
|
||||
{[4]byte{0x03, 0x00, 0x00, 0x00}, GasVerifyCompute},
|
||||
{[4]byte{0x04, 0x00, 0x00, 0x00}, GasCreateAttest},
|
||||
{[4]byte{0x05, 0x00, 0x00, 0x00}, GasGetDeviceStatus},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
gas := RequiredGas(tt.selector)
|
||||
if gas != tt.expected {
|
||||
} {
|
||||
if gas := RequiredGas(tt.selector); gas != tt.expected {
|
||||
t.Errorf("RequiredGas(%v) = %d, want %d", tt.selector, gas, tt.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRun_InvalidInput(t *testing.T) {
|
||||
// Test with too short input
|
||||
_, err := Run([]byte{0x01, 0x02})
|
||||
if err != ErrInvalidInput {
|
||||
t.Errorf("expected ErrInvalidInput, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRun_VerifyNVTrust(t *testing.T) {
|
||||
input := VerifyNVTrustInput{
|
||||
DeviceID: [32]byte{0x01},
|
||||
Model: "H100",
|
||||
CCEnabled: true,
|
||||
TEEIOEnabled: true,
|
||||
DriverVersion: "535.0",
|
||||
SPDMReport: make([]byte, 256),
|
||||
CertChain: make([]byte, 256),
|
||||
Nonce: [32]byte{0x01},
|
||||
}
|
||||
|
||||
data, _ := json.Marshal(input)
|
||||
|
||||
// Prepend selector
|
||||
callData := append([]byte{0x01, 0x00, 0x00, 0x00}, data...)
|
||||
|
||||
result, err := Run(callData)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var output VerifyNVTrustOutput
|
||||
if err := json.Unmarshal(result, &output); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Should have a result (whether verified or not)
|
||||
if output.TrustScore == 0 && output.Verified {
|
||||
t.Error("inconsistent output: verified but zero trust score")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSupportedGPUModels(t *testing.T) {
|
||||
models := SupportedGPUModels()
|
||||
if len(models) == 0 {
|
||||
t.Error("expected at least one supported GPU model")
|
||||
}
|
||||
|
||||
// All supported models should be CC capable
|
||||
for _, model := range models {
|
||||
if !IsHardwareCCCapable(model) {
|
||||
t.Errorf("supported model %s should be CC capable", model)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestABIEncode(t *testing.T) {
|
||||
// Test encoding various types
|
||||
result := ABIEncode(true, uint8(42), uint64(1000))
|
||||
|
||||
// Should be 96 bytes (3 * 32)
|
||||
if len(result) != 96 {
|
||||
t.Errorf("expected 96 bytes, got %d", len(result))
|
||||
}
|
||||
|
||||
// Check bool encoding (last byte should be 1)
|
||||
if result[31] != 1 {
|
||||
t.Error("bool true should encode to ...01")
|
||||
}
|
||||
|
||||
// Check uint8 encoding
|
||||
if result[63] != 42 {
|
||||
t.Errorf("uint8(42) should encode to ...42, got %d", result[63])
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkVerifyNVTrust(b *testing.B) {
|
||||
input := VerifyNVTrustInput{
|
||||
DeviceID: [32]byte{0x01},
|
||||
Model: "H100",
|
||||
CCEnabled: true,
|
||||
TEEIOEnabled: true,
|
||||
DriverVersion: "535.0",
|
||||
SPDMReport: make([]byte, 512),
|
||||
CertChain: make([]byte, 1024),
|
||||
Nonce: [32]byte{0x01},
|
||||
}
|
||||
|
||||
data, _ := json.Marshal(input)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
VerifyNVTrust(data)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkVerifyTPM(b *testing.B) {
|
||||
input := VerifyTPMInput{
|
||||
QuoteType: 2, // SEV-SNP
|
||||
Quote: make([]byte, 1200),
|
||||
Measurement: make([]byte, 48),
|
||||
ReportData: make([]byte, 64),
|
||||
Nonce: [32]byte{0x01},
|
||||
ExpectedMeasure: nil,
|
||||
}
|
||||
|
||||
data, _ := json.Marshal(input)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
VerifyTPM(data)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkCreateAttestation(b *testing.B) {
|
||||
input := CreateAttestationInput{
|
||||
DeviceType: 0,
|
||||
DeviceID: [32]byte{0x01},
|
||||
Model: "H100",
|
||||
Evidence: make([]byte, 512),
|
||||
Nonce: [32]byte{0x01},
|
||||
}
|
||||
|
||||
data, _ := json.Marshal(input)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
CreateAttestation(data)
|
||||
}
|
||||
}
|
||||
|
||||
+10
-1
@@ -94,7 +94,16 @@ func (p *attestationPrecompile) Run(
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
result, err := Run(input)
|
||||
// Deterministic block timestamp from consensus state; used only for
|
||||
// attestation expiry. Never time.Now().
|
||||
var blockTS uint64
|
||||
if accessibleState != nil {
|
||||
if bc := accessibleState.GetBlockContext(); bc != nil {
|
||||
blockTS = bc.Timestamp()
|
||||
}
|
||||
}
|
||||
|
||||
result, err := Run(input, blockTS)
|
||||
if err != nil {
|
||||
return nil, gas, err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user