From 97f7684088bea26a83ba5aafeafac12816d88f84 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Tue, 26 May 2026 01:52:58 -0400 Subject: [PATCH] feat: share attested proof binding validation --- protocol/types.go | 237 +++++++++++++++++++++++++++++++++++++++++ protocol/types_test.go | 158 +++++++++++++++++++++++++++ 2 files changed, 395 insertions(+) diff --git a/protocol/types.go b/protocol/types.go index 2b24500..26639f3 100644 --- a/protocol/types.go +++ b/protocol/types.go @@ -125,6 +125,16 @@ const ( ProofZKReplay ProofTier = "zk-replay" ) +type VerificationStatus string + +const ( + VerificationUnknown VerificationStatus = "" + VerificationPending VerificationStatus = "pending" + VerificationAccepted VerificationStatus = "accepted" + VerificationRejected VerificationStatus = "rejected" + VerificationConflicted VerificationStatus = "conflicted" +) + type RuntimeAdapterKind string const ( @@ -247,6 +257,233 @@ func (e ExecutorRef) RequiresAttestation() bool { } } +type SignatureEnvelope struct { + Algorithm string `json:"algorithm,omitempty"` + KeyID string `json:"key_id,omitempty"` + Value string `json:"value,omitempty"` + Verified bool `json:"verified,omitempty"` +} + +type VerifierResult struct { + Provider string `json:"provider"` + Status VerificationStatus `json:"status"` + Message string `json:"message,omitempty"` + Attestation AttestationDecision `json:"attestation,omitzero"` + KeyRelease KeyReleaseDecision `json:"key_release,omitzero"` +} + +type AttestationDecision struct { + Provider string `json:"provider,omitempty"` + VerifierID string `json:"verifier_id,omitempty"` + DecisionID string `json:"decision_id,omitempty"` + HardwareClass string `json:"hardware_class,omitempty"` + ExecutorImageDigest string `json:"executor_image_digest,omitempty"` + ExecutorRootFSDigest string `json:"executor_rootfs_digest,omitempty"` + PolicyID string `json:"policy_id,omitempty"` + Nonce string `json:"nonce,omitempty"` + IssuedAt time.Time `json:"issued_at,omitzero"` + ExpiresAt time.Time `json:"expires_at,omitzero"` + SignatureVerified bool `json:"signature_verified,omitempty"` + ConfidentialGPU bool `json:"confidential_gpu,omitempty"` + Signature SignatureEnvelope `json:"signature,omitzero"` +} + +func (a AttestationDecision) BindingDigest() string { + a.Signature = SignatureEnvelope{} + a.SignatureVerified = false + data, err := json.Marshal(a) + if err != nil { + panic(err) + } + sum := sha256.Sum256(data) + return "sha256:" + hex.EncodeToString(sum[:]) +} + +type KeyReleaseDecision struct { + Provider string `json:"provider,omitempty"` + DecisionID string `json:"decision_id,omitempty"` + AttestationDecisionID string `json:"attestation_decision_id,omitempty"` + AttestationDigest string `json:"attestation_digest,omitempty"` + AttestationProvider string `json:"attestation_provider,omitempty"` + AttestationVerifierID string `json:"attestation_verifier_id,omitempty"` + AttestationKeyID string `json:"attestation_key_id,omitempty"` + PolicyID string `json:"policy_id,omitempty"` + TaskID string `json:"task_id,omitempty"` + TaskHash string `json:"task_hash,omitempty"` + InputHash string `json:"input_hash,omitempty"` + DependencyClosureHash string `json:"dependency_closure_hash,omitempty"` + WorkerID string `json:"worker_id,omitempty"` + PoolID string `json:"pool_id,omitempty"` + KeyRefHash string `json:"key_ref_hash,omitempty"` + Released bool `json:"released,omitempty"` + ExpiresAt time.Time `json:"expires_at,omitzero"` + Signature SignatureEnvelope `json:"signature,omitzero"` +} + +type AttestedProofBinding struct { + Executor ExecutorRef `json:"executor"` + PolicyID string `json:"policy_id"` + TaskID string `json:"task_id"` + TaskHash string `json:"task_hash"` + InputHash string `json:"input_hash"` + DependencyClosureHash string `json:"dependency_closure_hash"` + WorkerID string `json:"worker_id"` + PoolID string `json:"pool_id"` + StartedAt time.Time `json:"started_at"` + FinishedAt time.Time `json:"finished_at"` + Verifier VerifierResult `json:"verifier"` +} + +func ValidateAttestedProofBinding(binding AttestedProofBinding) error { + var errs []error + require := func(name, value string) { + if value == "" { + errs = append(errs, fmt.Errorf("%s is required", name)) + } + } + + if err := binding.Executor.ValidateForProof(); err != nil { + errs = append(errs, err) + } + require("verifier.provider", binding.Verifier.Provider) + if binding.Verifier.Status != VerificationAccepted { + errs = append(errs, fmt.Errorf("verifier.status must be %q", VerificationAccepted)) + } + + attestation := binding.Verifier.Attestation + require("verifier.attestation.provider", attestation.Provider) + require("verifier.attestation.verifier_id", attestation.VerifierID) + require("verifier.attestation.decision_id", attestation.DecisionID) + require("verifier.attestation.hardware_class", attestation.HardwareClass) + require("verifier.attestation.executor_image_digest", attestation.ExecutorImageDigest) + require("verifier.attestation.executor_rootfs_digest", attestation.ExecutorRootFSDigest) + require("verifier.attestation.policy_id", attestation.PolicyID) + require("verifier.attestation.nonce", attestation.Nonce) + if err := validateVerifiedSignature("verifier.attestation.signature", attestation.Signature); err != nil { + errs = append(errs, err) + } + if !attestation.SignatureVerified { + errs = append(errs, errors.New("verifier.attestation.signature_verified is required")) + } + if attestation.IssuedAt.IsZero() { + errs = append(errs, errors.New("verifier.attestation.issued_at is required")) + } + if attestation.ExpiresAt.IsZero() { + errs = append(errs, errors.New("verifier.attestation.expires_at is required")) + } + if !attestation.IssuedAt.IsZero() && !attestation.ExpiresAt.IsZero() && !attestation.ExpiresAt.After(attestation.IssuedAt) { + errs = append(errs, errors.New("verifier.attestation.expires_at must be after issued_at")) + } + if attestation.PolicyID != "" && attestation.PolicyID != binding.PolicyID { + errs = append(errs, errors.New("verifier.attestation.policy_id must match receipt policy_id")) + } + if attestation.HardwareClass != "" && attestation.HardwareClass != string(binding.Executor.ExecutionSecurityTier) { + errs = append(errs, errors.New("verifier.attestation.hardware_class must match executor.execution_security_tier")) + } + if attestation.ExecutorImageDigest != "" && attestation.ExecutorImageDigest != binding.Executor.ImageDigest { + errs = append(errs, errors.New("verifier.attestation.executor_image_digest must match executor.image_digest")) + } + if attestation.ExecutorRootFSDigest != "" && attestation.ExecutorRootFSDigest != binding.Executor.RootFSDigest { + errs = append(errs, errors.New("verifier.attestation.executor_rootfs_digest must match executor.rootfs_digest")) + } + if binding.Executor.ExecutionSecurityTier == ExecutionConfidentialGPU && !attestation.ConfidentialGPU { + errs = append(errs, errors.New("verifier.attestation.confidential_gpu is required for confidential GPU execution")) + } + + keyRelease := binding.Verifier.KeyRelease + require("verifier.key_release.provider", keyRelease.Provider) + require("verifier.key_release.decision_id", keyRelease.DecisionID) + require("verifier.key_release.attestation_decision_id", keyRelease.AttestationDecisionID) + require("verifier.key_release.attestation_digest", keyRelease.AttestationDigest) + require("verifier.key_release.attestation_provider", keyRelease.AttestationProvider) + require("verifier.key_release.attestation_verifier_id", keyRelease.AttestationVerifierID) + require("verifier.key_release.attestation_key_id", keyRelease.AttestationKeyID) + require("verifier.key_release.policy_id", keyRelease.PolicyID) + require("verifier.key_release.task_id", keyRelease.TaskID) + require("verifier.key_release.task_hash", keyRelease.TaskHash) + require("verifier.key_release.input_hash", keyRelease.InputHash) + require("verifier.key_release.dependency_closure_hash", keyRelease.DependencyClosureHash) + require("verifier.key_release.worker_id", keyRelease.WorkerID) + require("verifier.key_release.pool_id", keyRelease.PoolID) + require("verifier.key_release.key_ref_hash", keyRelease.KeyRefHash) + if err := validateVerifiedSignature("verifier.key_release.signature", keyRelease.Signature); err != nil { + errs = append(errs, err) + } + if !keyRelease.Released { + errs = append(errs, errors.New("verifier.key_release.released is required")) + } + if keyRelease.ExpiresAt.IsZero() { + errs = append(errs, errors.New("verifier.key_release.expires_at is required")) + } + if keyRelease.AttestationDecisionID != "" && attestation.DecisionID != "" && keyRelease.AttestationDecisionID != attestation.DecisionID { + errs = append(errs, errors.New("verifier.key_release.attestation_decision_id must match verifier.attestation.decision_id")) + } + if keyRelease.AttestationDigest != "" && keyRelease.AttestationDigest != attestation.BindingDigest() { + errs = append(errs, errors.New("verifier.key_release.attestation_digest must match verifier.attestation digest")) + } + if keyRelease.AttestationProvider != "" && keyRelease.AttestationProvider != attestation.Provider { + errs = append(errs, errors.New("verifier.key_release.attestation_provider must match verifier.attestation.provider")) + } + if keyRelease.AttestationVerifierID != "" && keyRelease.AttestationVerifierID != attestation.VerifierID { + errs = append(errs, errors.New("verifier.key_release.attestation_verifier_id must match verifier.attestation.verifier_id")) + } + if keyRelease.AttestationKeyID != "" && keyRelease.AttestationKeyID != attestation.Signature.KeyID { + errs = append(errs, errors.New("verifier.key_release.attestation_key_id must match verifier.attestation.signature.key_id")) + } + if keyRelease.PolicyID != "" && keyRelease.PolicyID != binding.PolicyID { + errs = append(errs, errors.New("verifier.key_release.policy_id must match receipt policy_id")) + } + if keyRelease.TaskID != "" && keyRelease.TaskID != binding.TaskID { + errs = append(errs, errors.New("verifier.key_release.task_id must match receipt task_id")) + } + if keyRelease.TaskHash != "" && keyRelease.TaskHash != binding.TaskHash { + errs = append(errs, errors.New("verifier.key_release.task_hash must match receipt task_hash")) + } + if keyRelease.InputHash != "" && keyRelease.InputHash != binding.InputHash { + errs = append(errs, errors.New("verifier.key_release.input_hash must match receipt input_hash")) + } + if keyRelease.DependencyClosureHash != "" && keyRelease.DependencyClosureHash != binding.DependencyClosureHash { + errs = append(errs, errors.New("verifier.key_release.dependency_closure_hash must match receipt dependency_closure_hash")) + } + if keyRelease.WorkerID != "" && keyRelease.WorkerID != binding.WorkerID { + errs = append(errs, errors.New("verifier.key_release.worker_id must match receipt worker_id")) + } + if keyRelease.PoolID != "" && keyRelease.PoolID != binding.PoolID { + errs = append(errs, errors.New("verifier.key_release.pool_id must match receipt pool_id")) + } + if !keyRelease.ExpiresAt.IsZero() && !attestation.ExpiresAt.IsZero() && keyRelease.ExpiresAt.After(attestation.ExpiresAt) { + errs = append(errs, errors.New("verifier.key_release.expires_at must not exceed verifier.attestation.expires_at")) + } + if !binding.StartedAt.IsZero() && !attestation.IssuedAt.IsZero() && binding.StartedAt.Before(attestation.IssuedAt) { + errs = append(errs, errors.New("started_at must not precede verifier.attestation.issued_at")) + } + if !binding.FinishedAt.IsZero() && !attestation.ExpiresAt.IsZero() && binding.FinishedAt.After(attestation.ExpiresAt) { + errs = append(errs, errors.New("finished_at must not exceed verifier.attestation.expires_at")) + } + if !binding.FinishedAt.IsZero() && !keyRelease.ExpiresAt.IsZero() && binding.FinishedAt.After(keyRelease.ExpiresAt) { + errs = append(errs, errors.New("finished_at must not exceed verifier.key_release.expires_at")) + } + + return errors.Join(errs...) +} + +func validateVerifiedSignature(prefix string, sig SignatureEnvelope) error { + var errs []error + if sig.Algorithm == "" { + errs = append(errs, fmt.Errorf("%s.algorithm is required", prefix)) + } + if sig.KeyID == "" { + errs = append(errs, fmt.Errorf("%s.key_id is required", prefix)) + } + if sig.Value == "" { + errs = append(errs, fmt.Errorf("%s.value is required", prefix)) + } + if !sig.Verified { + errs = append(errs, fmt.Errorf("%s.verified is required", prefix)) + } + return errors.Join(errs...) +} + func ExecutorMatchesPlacementRequirements(executor ExecutorRef, req PlacementRequirements) bool { if req.ExecutorProvider != "" && executor.Provider != req.ExecutorProvider { return false diff --git a/protocol/types_test.go b/protocol/types_test.go index ce56a49..74cf51c 100644 --- a/protocol/types_test.go +++ b/protocol/types_test.go @@ -612,6 +612,84 @@ func TestHardwareCapabilitiesSatisfyPlacementRequirements(t *testing.T) { } } +func TestAttestationDecisionBindingDigest(t *testing.T) { + attestation := validAttestationDecision(protocol.ExecutionConfidentialCPU) + first := attestation.BindingDigest() + if first == "" || !strings.HasPrefix(first, "sha256:") { + t.Fatalf("binding digest = %q", first) + } + + attestation.Signature.Value = "rotated-signature" + attestation.SignatureVerified = false + if got := attestation.BindingDigest(); got != first { + t.Fatalf("binding digest included signature-only fields: got %q want %q", got, first) + } + + attestation.DecisionID = "attest-2" + if got := attestation.BindingDigest(); got == first { + t.Fatalf("binding digest did not include decision identity: %q", got) + } +} + +func TestValidateAttestedProofBinding(t *testing.T) { + valid := validAttestedProofBinding(protocol.ExecutionConfidentialCPU) + if err := protocol.ValidateAttestedProofBinding(valid); err != nil { + t.Fatalf("valid attested proof binding rejected: %v", err) + } + + for name, mutate := range map[string]func(*protocol.AttestedProofBinding){ + "missing attestation": func(binding *protocol.AttestedProofBinding) { + binding.Verifier.Attestation = protocol.AttestationDecision{} + }, + "missing key release": func(binding *protocol.AttestedProofBinding) { + binding.Verifier.KeyRelease = protocol.KeyReleaseDecision{} + }, + "missing executor proof metadata": func(binding *protocol.AttestedProofBinding) { + binding.Executor.ProofTier = "" + }, + "policy mismatch": func(binding *protocol.AttestedProofBinding) { + binding.Verifier.Attestation.PolicyID = "other-policy" + }, + "task hash mismatch": func(binding *protocol.AttestedProofBinding) { + binding.Verifier.KeyRelease.TaskHash = "sha256:other-task" + }, + "attestation digest mismatch": func(binding *protocol.AttestedProofBinding) { + binding.Verifier.KeyRelease.AttestationDigest = "sha256:other-attestation" + }, + "started before attestation": func(binding *protocol.AttestedProofBinding) { + binding.StartedAt = time.Unix(98, 0).UTC() + }, + "finished after key release": func(binding *protocol.AttestedProofBinding) { + binding.FinishedAt = time.Unix(121, 0).UTC() + }, + } { + t.Run(name, func(t *testing.T) { + binding := valid + mutate(&binding) + if err := protocol.ValidateAttestedProofBinding(binding); err == nil { + t.Fatal("expected attested proof binding to fail") + } + }) + } +} + +func TestValidateAttestedProofBindingRequiresConfidentialGPUEvidence(t *testing.T) { + binding := validAttestedProofBinding(protocol.ExecutionConfidentialGPU) + binding.Verifier.Attestation.ConfidentialGPU = false + binding.Verifier.KeyRelease = validKeyReleaseDecisionFor(binding.Verifier.Attestation) + + err := protocol.ValidateAttestedProofBinding(binding) + if err == nil || !strings.Contains(err.Error(), "confidential_gpu") { + t.Fatalf("expected confidential GPU evidence error, got %v", err) + } + + binding.Verifier.Attestation.ConfidentialGPU = true + binding.Verifier.KeyRelease = validKeyReleaseDecisionFor(binding.Verifier.Attestation) + if err := protocol.ValidateAttestedProofBinding(binding); err != nil { + t.Fatalf("confidential GPU binding rejected: %v", err) + } +} + func TestResourceLimitsRejectNegativeValues(t *testing.T) { limits := protocol.ResourceLimits{ CPUPercent: -1, @@ -630,6 +708,86 @@ func TestResourceLimitsRejectNegativeValues(t *testing.T) { } } +func validAttestedProofBinding(tier protocol.ExecutionSecurityTier) protocol.AttestedProofBinding { + attestation := validAttestationDecision(tier) + return protocol.AttestedProofBinding{ + Executor: protocol.ExecutorRef{ + Provider: "confidential-command", + Version: "dev", + ExecutionSecurityTier: tier, + ProofTier: protocol.ProofAttestedReceipt, + ImageDigest: "sha256:image", + RootFSDigest: "sha256:rootfs", + }, + PolicyID: "policy-1", + TaskID: "task-1", + TaskHash: "sha256:task", + InputHash: "sha256:input", + DependencyClosureHash: "sha256:deps", + WorkerID: "worker-1", + PoolID: "pool-1", + StartedAt: time.Unix(100, 0).UTC(), + FinishedAt: time.Unix(101, 0).UTC(), + Verifier: protocol.VerifierResult{ + Provider: "attestation_key_release", + Status: protocol.VerificationAccepted, + Attestation: attestation, + KeyRelease: validKeyReleaseDecisionFor(attestation), + }, + } +} + +func validAttestationDecision(tier protocol.ExecutionSecurityTier) protocol.AttestationDecision { + return protocol.AttestationDecision{ + Provider: "fake-attestation", + VerifierID: "verifier-1", + DecisionID: "attest-1", + HardwareClass: string(tier), + ExecutorImageDigest: "sha256:image", + ExecutorRootFSDigest: "sha256:rootfs", + PolicyID: "policy-1", + Nonce: "nonce-1", + IssuedAt: time.Unix(99, 0).UTC(), + ExpiresAt: time.Unix(120, 0).UTC(), + SignatureVerified: true, + ConfidentialGPU: tier == protocol.ExecutionConfidentialGPU, + Signature: protocol.SignatureEnvelope{ + Algorithm: "ed25519", + KeyID: "attestation-key", + Value: "sig", + Verified: true, + }, + } +} + +func validKeyReleaseDecisionFor(attestation protocol.AttestationDecision) protocol.KeyReleaseDecision { + return protocol.KeyReleaseDecision{ + Provider: "fake-key-release", + DecisionID: "key-release-1", + AttestationDecisionID: "attest-1", + AttestationDigest: attestation.BindingDigest(), + AttestationProvider: attestation.Provider, + AttestationVerifierID: attestation.VerifierID, + AttestationKeyID: attestation.Signature.KeyID, + PolicyID: "policy-1", + TaskID: "task-1", + TaskHash: "sha256:task", + InputHash: "sha256:input", + DependencyClosureHash: "sha256:deps", + WorkerID: "worker-1", + PoolID: "pool-1", + KeyRefHash: "sha256:key-ref", + Released: true, + ExpiresAt: time.Unix(120, 0).UTC(), + Signature: protocol.SignatureEnvelope{ + Algorithm: "ed25519", + KeyID: "key-release-key", + Value: "sig", + Verified: true, + }, + } +} + func TestValidateResourceLimitsAgainstCapacity(t *testing.T) { limits := protocol.ResourceLimits{ CPUPercent: 1_100,