mirror of
https://github.com/luxfi/mpc.git
synced 2026-09-24 00:26:24 +00:00
v0.61 — wire the scheduler workload-policy gate into the MPC SignGate.
SignGate gains optional Scheduler + Worker fields; AuthorizeSign now
runs scheduler.Eligible(worker, ci.PolicyID) BEFORE the policy
verifier and short-circuits on insufficient trust posture. Existing
callers that pass nil Scheduler are unaffected.
pkg/scheduler/policy_gate.go — TrustMode/IOLevel enums,
WorkloadPolicy.Eligible(Worker)
pkg/scheduler/service_drain.go — AuthorizeDrain + AuthorizeSession
pkg/mpc/sign_orchestrator.go — Scheduler/Worker fields,
WithScheduler(...), two-stage gate
v0.62 — NVIDIA Confidential-Compute attestation primitives. Clean-room
parsers + verifiers against the publicly documented nvTrust schema
(Apache-2 derived); no NVIDIA proprietary code vendored.
pkg/attestation/nvidia/gpu_report.go — GPU evidence-report parser
pkg/attestation/nvidia/nras_client.go — NRAS HTTP client + JWS verify
pkg/attestation/nvidia/rim.go — RIM signature + value match
pkg/attestation/nvidia/composite.go — VerifiedBundle (CPU+GPU bind)
Tests (57 passing, -race clean):
pkg/scheduler — 11 (truth-table, drain,
session, orchestrator
integration)
pkg/attestation/nvidia — 46 (parser, RIM Ed25519/ECDSA/
RSA-PSS, NRAS happy/error/
signature paths, ECDSA JOSE)
pkg/attestation/nvidia (hwattest) — 3, env-skipped without real GPU
Sibling v0.60 reconciliation: pkg/registry + pkg/attestation already
land WorkerCapability + CompositeAttestation envelope using lattice
trust types. nvidia.VerifiedBundle is renamed (was Composite-) to
avoid collision with pkg/attestation.CompositeAttestation; the
canonical envelope wraps the verified blobs this package emits.
Residual: lattice/v7/types is not yet published, so pkg/registry +
pkg/attestation cannot compile against luxfi/[email protected]; the
sibling v0.60 work tracks that. pkg/policy still has a pre-existing
unresolved import of luxfi/fhe/threshold (should be /pkg/threshold);
sign_orchestrator_scheduler_test.go is ready to run as soon as that
upstream issue is fixed by its owner.
244 lines
6.6 KiB
Go
244 lines
6.6 KiB
Go
package scheduler
|
|
|
|
import (
|
|
"errors"
|
|
"testing"
|
|
)
|
|
|
|
// canonical policy used across the truth table — model confidentiality
|
|
// over private lane on attested CPU + GPU.
|
|
var privatePolicy = WorkloadPolicy{
|
|
WorkloadID: "model-private",
|
|
MinTrust: TrustGPU,
|
|
MinIO: IOPrivateLane,
|
|
RequireModelConfidentiality: true,
|
|
RequireGPUAttested: true,
|
|
RequireCPUAttested: true,
|
|
ApprovedKernelRoots: [][32]byte{{1, 2, 3}, {4, 5, 6}},
|
|
}
|
|
|
|
func okWorker() Worker {
|
|
return Worker{
|
|
ID: "node-1",
|
|
TrustMode: TrustGPU,
|
|
IOLevel: IOPrivateLane,
|
|
KernelManifestRoot: [32]byte{1, 2, 3},
|
|
GPUAttested: true,
|
|
CPUAttested: true,
|
|
Online: true,
|
|
}
|
|
}
|
|
|
|
func TestEligible_TruthTable(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
mutate func(*Worker)
|
|
wantErr bool
|
|
match string
|
|
}{
|
|
{"happy path", func(w *Worker) {}, false, ""},
|
|
{"offline", func(w *Worker) { w.Online = false }, true, "offline"},
|
|
{"trust below floor", func(w *Worker) { w.TrustMode = TrustCVM }, true, "trust"},
|
|
{"io below floor", func(w *Worker) { w.IOLevel = IOPrivateNet }, true, "io"},
|
|
{"gpu unattested", func(w *Worker) { w.GPUAttested = false }, true, "GPU attestation"},
|
|
{"cpu unattested", func(w *Worker) { w.CPUAttested = false }, true, "CPU attestation"},
|
|
{"kernel root not approved", func(w *Worker) { w.KernelManifestRoot = [32]byte{99} }, true, "KernelManifestRoot not in approved set"},
|
|
{"kernel root second match", func(w *Worker) { w.KernelManifestRoot = [32]byte{4, 5, 6} }, false, ""},
|
|
{"missing kernel root", func(w *Worker) { w.KernelManifestRoot = [32]byte{} }, true, "missing KernelManifestRoot"},
|
|
{"missing ID", func(w *Worker) { w.ID = "" }, true, "missing ID"},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
w := okWorker()
|
|
tc.mutate(&w)
|
|
err := privatePolicy.Eligible(w)
|
|
if tc.wantErr {
|
|
if err == nil {
|
|
t.Fatalf("expected error containing %q, got nil", tc.match)
|
|
}
|
|
if tc.match != "" && !contains(err.Error(), tc.match) {
|
|
t.Fatalf("expected %q in error, got %q", tc.match, err.Error())
|
|
}
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("expected nil, got %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEligible_NilPolicy(t *testing.T) {
|
|
var p *WorkloadPolicy
|
|
if err := p.Eligible(okWorker()); err == nil {
|
|
t.Fatal("expected error for nil policy")
|
|
}
|
|
}
|
|
|
|
func TestEligible_PublicWorkloadAcceptsBareMetal(t *testing.T) {
|
|
publicPolicy := WorkloadPolicy{
|
|
WorkloadID: "public",
|
|
MinTrust: TrustNone,
|
|
MinIO: IOPublic,
|
|
}
|
|
w := Worker{ID: "n", Online: true}
|
|
if err := publicPolicy.Eligible(w); err != nil {
|
|
t.Fatalf("public workload should accept bare worker: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestEligible_ConfidentialityWithoutKernelRoots(t *testing.T) {
|
|
p := WorkloadPolicy{
|
|
WorkloadID: "broken",
|
|
MinTrust: TrustGPU,
|
|
MinIO: IOPrivateLane,
|
|
RequireModelConfidentiality: true,
|
|
RequireGPUAttested: true,
|
|
// no ApprovedKernelRoots — must reject
|
|
}
|
|
if err := p.Eligible(okWorker()); err == nil {
|
|
t.Fatal("expected error when confidentiality required without approved roots")
|
|
}
|
|
}
|
|
|
|
func TestStaticPolicySource(t *testing.T) {
|
|
src := NewStaticPolicySource(map[string]*WorkloadPolicy{
|
|
"a": &privatePolicy,
|
|
})
|
|
got, err := src.Lookup("a")
|
|
if err != nil {
|
|
t.Fatalf("lookup a: %v", err)
|
|
}
|
|
if got == nil || got.WorkloadID != "model-private" {
|
|
t.Fatalf("got %+v", got)
|
|
}
|
|
if _, err := src.Lookup("missing"); err == nil {
|
|
t.Fatal("expected error for missing workload")
|
|
}
|
|
}
|
|
|
|
func TestScheduler_Eligible(t *testing.T) {
|
|
src := NewStaticPolicySource(map[string]*WorkloadPolicy{"a": &privatePolicy})
|
|
s := New(src)
|
|
if err := s.Eligible(okWorker(), "a"); err != nil {
|
|
t.Fatalf("happy: %v", err)
|
|
}
|
|
bad := okWorker()
|
|
bad.TrustMode = TrustCVM
|
|
if err := s.Eligible(bad, "a"); err == nil {
|
|
t.Fatal("expected eligibility failure")
|
|
}
|
|
if err := s.Eligible(okWorker(), "missing"); err == nil {
|
|
t.Fatal("expected lookup failure")
|
|
}
|
|
}
|
|
|
|
func TestScheduler_AuthorizeSession_PrivateLaneRefusal(t *testing.T) {
|
|
src := NewStaticPolicySource(map[string]*WorkloadPolicy{"a": &privatePolicy})
|
|
s := New(src)
|
|
|
|
good1 := okWorker()
|
|
good1.ID = "n1"
|
|
good2 := okWorker()
|
|
good2.ID = "n2"
|
|
bad := okWorker()
|
|
bad.ID = "n3"
|
|
bad.IOLevel = IOPublic // private-lane refusal: this worker MUST be excluded
|
|
|
|
out, err := s.AuthorizeSession("a", []Worker{good1, bad, good2}, 2)
|
|
if err != nil {
|
|
t.Fatalf("expected 2 eligible: %v", err)
|
|
}
|
|
if len(out) != 2 {
|
|
t.Fatalf("expected 2 eligible, got %d", len(out))
|
|
}
|
|
for _, w := range out {
|
|
if w.ID == "n3" {
|
|
t.Fatalf("public-IO worker n3 leaked through gate")
|
|
}
|
|
}
|
|
|
|
if _, err := s.AuthorizeSession("a", []Worker{bad}, 1); !errors.Is(err, ErrInsufficientTrust) {
|
|
t.Fatalf("expected ErrInsufficientTrust, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestScheduler_AuthorizeDrain(t *testing.T) {
|
|
src := NewStaticPolicySource(map[string]*WorkloadPolicy{"a": &privatePolicy})
|
|
s := New(src)
|
|
|
|
w1 := okWorker()
|
|
w1.ID = "n1"
|
|
w2 := okWorker()
|
|
w2.ID = "n2"
|
|
w3 := okWorker()
|
|
w3.ID = "n3"
|
|
|
|
// drain n1 with 3 healthy → 2 remain → ok for threshold=2
|
|
if err := s.AuthorizeDrain(DrainRequest{
|
|
NodeID: "n1",
|
|
WorkloadID: "a",
|
|
Workers: []Worker{w1, w2, w3},
|
|
Threshold: 2,
|
|
}); err != nil {
|
|
t.Fatalf("happy drain: %v", err)
|
|
}
|
|
|
|
// drain n1 with 3 healthy → 2 remain → fail for threshold=3
|
|
err := s.AuthorizeDrain(DrainRequest{
|
|
NodeID: "n1",
|
|
WorkloadID: "a",
|
|
Workers: []Worker{w1, w2, w3},
|
|
Threshold: 3,
|
|
})
|
|
if !errors.Is(err, ErrInsufficientTrust) {
|
|
t.Fatalf("expected ErrInsufficientTrust, got %v", err)
|
|
}
|
|
|
|
// pre-existing ineligible worker doesn't count toward remaining
|
|
w3bad := w3
|
|
w3bad.GPUAttested = false
|
|
err = s.AuthorizeDrain(DrainRequest{
|
|
NodeID: "n1",
|
|
WorkloadID: "a",
|
|
Workers: []Worker{w1, w2, w3bad},
|
|
Threshold: 2,
|
|
})
|
|
if !errors.Is(err, ErrInsufficientTrust) {
|
|
t.Fatalf("expected ErrInsufficientTrust when only 1 eligible remains, got %v", err)
|
|
}
|
|
|
|
// missing workload
|
|
err = s.AuthorizeDrain(DrainRequest{
|
|
NodeID: "n1",
|
|
WorkloadID: "missing",
|
|
Workers: []Worker{w1, w2},
|
|
Threshold: 1,
|
|
})
|
|
if err == nil || errors.Is(err, ErrInsufficientTrust) {
|
|
t.Fatalf("expected lookup error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestNew_NilSourcePanics(t *testing.T) {
|
|
defer func() {
|
|
if recover() == nil {
|
|
t.Fatal("expected panic")
|
|
}
|
|
}()
|
|
New(nil)
|
|
}
|
|
|
|
// contains is a tiny strings.Contains shim to keep test deps minimal.
|
|
func contains(s, sub string) bool {
|
|
if sub == "" {
|
|
return true
|
|
}
|
|
for i := 0; i+len(sub) <= len(s); i++ {
|
|
if s[i:i+len(sub)] == sub {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|