From a6dc8857a5c59d0c29a322c1e67dd61ed3f61e33 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Tue, 26 May 2026 17:36:31 -0400 Subject: [PATCH 1/3] feat: share receipt validators --- protocol/receipt_signature.go | 535 +++++++++++++++++++++++++++++ protocol/receipt_signature_test.go | 213 ++++++++++++ 2 files changed, 748 insertions(+) create mode 100644 protocol/receipt_signature.go create mode 100644 protocol/receipt_signature_test.go diff --git a/protocol/receipt_signature.go b/protocol/receipt_signature.go new file mode 100644 index 0000000..aa2db01 --- /dev/null +++ b/protocol/receipt_signature.go @@ -0,0 +1,535 @@ +package protocol + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "strings" + "time" +) + +const ( + SoftwareAgentSignaturePrefix = "software-agent:" + SoftwareRouterSignaturePrefix = "software-router:" + CredentialProofSignaturePrefix = "credential-hmac-sha256:" + + defaultReceiptVerifierProvider = "signed_receipt" +) + +type ProofReceipt struct { + ID string `json:"id"` + OrgID string `json:"org_id"` + TaskID string `json:"task_id"` + TaskHash string `json:"task_hash"` + InputHash string `json:"input_hash"` + DependencyClosureHash string `json:"dependency_closure_hash"` + Executor ExecutorRef `json:"executor"` + WorkerID string `json:"worker_id"` + PoolID string `json:"pool_id"` + PolicyID string `json:"policy_id"` + StartedAt time.Time `json:"started_at"` + FinishedAt time.Time `json:"finished_at"` + ExitCode int `json:"exit_code,omitempty"` + ResourceUsage ResourceUsage `json:"resource_usage"` + ArtifactHash string `json:"artifact_hash"` + ResultPreview map[string]any `json:"result_preview,omitempty"` + Verifier VerifierResult `json:"verifier"` + AgentSignature string `json:"agent_signature"` +} + +type ServiceReceipt struct { + ID string `json:"id"` + OrgID string `json:"org_id"` + TaskID string `json:"task_id"` + ServiceLeaseID string `json:"service_lease_id"` + WorkerID string `json:"worker_id"` + PoolID string `json:"pool_id"` + PolicyID string `json:"policy_id"` + Executor ExecutorRef `json:"executor"` + DeploymentHash string `json:"deployment_hash"` + RequestID string `json:"request_id"` + TraceID string `json:"trace_id"` + RequestHash string `json:"request_hash"` + ResponseHash string `json:"response_hash"` + StartedAt time.Time `json:"started_at"` + FinishedAt time.Time `json:"finished_at"` + ResourceUsage ResourceUsage `json:"resource_usage"` + ResourceLimits ResourceLimits `json:"resource_limits,omitzero"` + SLOEvidence SLOEvidence `json:"slo_evidence"` + StatusEvidence ServiceStatusEvidence `json:"status_evidence,omitzero"` + Verifier VerifierResult `json:"verifier"` + AgentSignature string `json:"agent_signature"` +} + +type EdgeRequestReceipt struct { + ProtocolVersion string `json:"protocol_version,omitempty"` + ID string `json:"id"` + OrgID string `json:"org_id"` + PoolID string `json:"pool_id"` + ProductID string `json:"product_id"` + Hostname string `json:"hostname"` + RouteTarget string `json:"route_target"` + ServiceLeaseID string `json:"service_lease_id,omitempty"` + TaskID string `json:"task_id,omitempty"` + WorkerID string `json:"worker_id,omitempty"` + ContentRef string `json:"content_ref,omitempty"` + RequestID string `json:"request_id"` + TraceID string `json:"trace_id"` + Method string `json:"method"` + RequestClass string `json:"request_class"` + RequestHash string `json:"request_hash"` + ResponseHash string `json:"response_hash"` + RequestBytes int64 `json:"request_bytes,omitempty"` + ResponseBytes int64 `json:"response_bytes,omitempty"` + StartedAt time.Time `json:"started_at"` + FinishedAt time.Time `json:"finished_at"` + ResourceUsage ResourceUsage `json:"resource_usage"` + ServiceReceiptIDs []string `json:"service_receipt_ids,omitempty"` + IngressEvidenceID string `json:"ingress_evidence_id,omitempty"` + IngressEvidenceHash string `json:"ingress_evidence_hash,omitempty"` + Verifier VerifierResult `json:"verifier"` + RouterSignature string `json:"router_signature"` +} + +type ReceiptVerificationOptions struct { + CredentialSignatureVerified bool + AllowSoftwareFallback bool + VerifierProvider string + VerifierMessage string +} + +func TokenProofKey(token string) string { + if token == "" { + return "" + } + sum := sha256.Sum256([]byte(token)) + return hex.EncodeToString(sum[:]) +} + +func SoftwareAgentProofSignature(receipt ProofReceipt) string { + receipt.AgentSignature = "" + return SoftwareAgentSignaturePrefix + strings.TrimPrefix(CanonicalHash(receipt), "sha256:") +} + +func SoftwareAgentServiceSignature(receipt ServiceReceipt) string { + receipt.AgentSignature = "" + if !receipt.Executor.RequiresAttestation() { + receipt.Verifier = VerifierResult{} + } + return SoftwareAgentSignaturePrefix + strings.TrimPrefix(CanonicalHash(receipt), "sha256:") +} + +func SoftwareRouterEdgeSignature(receipt EdgeRequestReceipt) string { + receipt.RouterSignature = "" + receipt.Verifier = VerifierResult{} + return SoftwareRouterSignaturePrefix + strings.TrimPrefix(CanonicalHash(receipt), "sha256:") +} + +func CredentialProofSignature(receipt ProofReceipt, tokenProofKey string) string { + receipt.AgentSignature = "" + mac := hmac.New(sha256.New, []byte(tokenProofKey)) + _, _ = mac.Write([]byte(CanonicalHash(receipt))) + return CredentialProofSignaturePrefix + hex.EncodeToString(mac.Sum(nil)) +} + +func CredentialServiceSignature(receipt ServiceReceipt, tokenProofKey string) string { + receipt.AgentSignature = "" + if !receipt.Executor.RequiresAttestation() { + receipt.Verifier = VerifierResult{} + } + mac := hmac.New(sha256.New, []byte(tokenProofKey)) + _, _ = mac.Write([]byte(CanonicalHash(receipt))) + return CredentialProofSignaturePrefix + hex.EncodeToString(mac.Sum(nil)) +} + +func CredentialEdgeSignature(receipt EdgeRequestReceipt, tokenProofKey string) string { + receipt.RouterSignature = "" + receipt.Verifier = VerifierResult{} + mac := hmac.New(sha256.New, []byte(tokenProofKey)) + _, _ = mac.Write([]byte(CanonicalHash(receipt))) + return CredentialProofSignaturePrefix + hex.EncodeToString(mac.Sum(nil)) +} + +func VerifyCredentialProofSignature(receipt ProofReceipt, tokenProofKey string) bool { + if !strings.HasPrefix(receipt.AgentSignature, CredentialProofSignaturePrefix) { + return false + } + expected := CredentialProofSignature(receipt, tokenProofKey) + return hmac.Equal([]byte(receipt.AgentSignature), []byte(expected)) +} + +func VerifyCredentialServiceSignature(receipt ServiceReceipt, tokenProofKey string) bool { + if !strings.HasPrefix(receipt.AgentSignature, CredentialProofSignaturePrefix) { + return false + } + expected := CredentialServiceSignature(receipt, tokenProofKey) + return hmac.Equal([]byte(receipt.AgentSignature), []byte(expected)) +} + +func VerifyCredentialEdgeSignature(receipt EdgeRequestReceipt, tokenProofKey string) bool { + if !strings.HasPrefix(receipt.RouterSignature, CredentialProofSignaturePrefix) { + return false + } + expected := CredentialEdgeSignature(receipt, tokenProofKey) + return hmac.Equal([]byte(receipt.RouterSignature), []byte(expected)) +} + +func VerifyServiceReceipt(receipt ServiceReceipt, opts ReceiptVerificationOptions) (ServiceReceipt, error) { + for _, field := range []struct { + name string + value string + }{ + {name: "deployment_hash", value: receipt.DeploymentHash}, + {name: "request_hash", value: receipt.RequestHash}, + {name: "response_hash", value: receipt.ResponseHash}, + } { + if !validSHA256Digest(field.value) { + return ServiceReceipt{}, fmt.Errorf("%s must use sha256 digest", field.name) + } + } + if !serviceReceiptSignatureValid(receipt, opts) { + return ServiceReceipt{}, errors.New("service receipt agent_signature is invalid") + } + if !receipt.Executor.RequiresAttestation() { + receipt.Verifier = receiptVerifierResult(opts, "service receipt signature verified; quorum and key-backed trust handled separately") + } + if err := receipt.Validate(); err != nil { + return ServiceReceipt{}, err + } + return receipt, nil +} + +func VerifyEdgeRequestReceipt(receipt EdgeRequestReceipt, opts ReceiptVerificationOptions) (EdgeRequestReceipt, error) { + for _, field := range []struct { + name string + value string + }{ + {name: "request_hash", value: receipt.RequestHash}, + {name: "response_hash", value: receipt.ResponseHash}, + } { + if !validSHA256Digest(field.value) { + return EdgeRequestReceipt{}, fmt.Errorf("%s must use sha256 digest", field.name) + } + } + if !edgeRequestReceiptSignatureValid(receipt, opts) { + return EdgeRequestReceipt{}, errors.New("edge request receipt router_signature is invalid") + } + receipt.Verifier = receiptVerifierResult(opts, "edge request receipt signature verified; route target and accounting checked") + if err := receipt.Validate(); err != nil { + return EdgeRequestReceipt{}, err + } + return receipt, nil +} + +func receiptVerifierResult(opts ReceiptVerificationOptions, defaultMessage string) VerifierResult { + provider := strings.TrimSpace(opts.VerifierProvider) + if provider == "" { + provider = defaultReceiptVerifierProvider + } + message := opts.VerifierMessage + if message == "" { + message = defaultMessage + } + return VerifierResult{ + Provider: provider, + Status: VerificationAccepted, + Message: message, + } +} + +func serviceReceiptSignatureValid(receipt ServiceReceipt, opts ReceiptVerificationOptions) bool { + if strings.HasPrefix(receipt.AgentSignature, CredentialProofSignaturePrefix) { + return opts.CredentialSignatureVerified + } + return opts.AllowSoftwareFallback && receipt.AgentSignature == SoftwareAgentServiceSignature(receipt) +} + +func edgeRequestReceiptSignatureValid(receipt EdgeRequestReceipt, opts ReceiptVerificationOptions) bool { + if strings.HasPrefix(receipt.RouterSignature, CredentialProofSignaturePrefix) { + return opts.CredentialSignatureVerified + } + return opts.AllowSoftwareFallback && receipt.RouterSignature == SoftwareRouterEdgeSignature(receipt) +} + +func (r ProofReceipt) Validate() error { + var errs []error + require := func(name, value string) { + if value == "" { + errs = append(errs, fmt.Errorf("%s is required", name)) + } + } + + require("id", r.ID) + require("org_id", r.OrgID) + require("task_id", r.TaskID) + require("task_hash", r.TaskHash) + require("input_hash", r.InputHash) + require("dependency_closure_hash", r.DependencyClosureHash) + if err := r.Executor.ValidateForProof(); err != nil { + errs = append(errs, err) + } + require("worker_id", r.WorkerID) + require("pool_id", r.PoolID) + require("policy_id", r.PolicyID) + require("artifact_hash", r.ArtifactHash) + require("verifier.provider", r.Verifier.Provider) + require("agent_signature", r.AgentSignature) + if err := ValidateRuntimeResultPreview(r.ResultPreview); err != nil { + errs = append(errs, err) + } + if r.Verifier.Status != VerificationAccepted { + errs = append(errs, fmt.Errorf("verifier.status must be %q", VerificationAccepted)) + } + if r.Executor.RequiresAttestation() { + if err := ValidateAttestedProofBinding(AttestedProofBinding{ + Executor: r.Executor, + PolicyID: r.PolicyID, + TaskID: r.TaskID, + TaskHash: r.TaskHash, + InputHash: r.InputHash, + DependencyClosureHash: r.DependencyClosureHash, + WorkerID: r.WorkerID, + PoolID: r.PoolID, + StartedAt: r.StartedAt, + FinishedAt: r.FinishedAt, + Verifier: r.Verifier, + }); err != nil { + errs = append(errs, err) + } + } + if r.StartedAt.IsZero() { + errs = append(errs, errors.New("started_at is required")) + } + if r.FinishedAt.IsZero() { + errs = append(errs, errors.New("finished_at is required")) + } + if !r.StartedAt.IsZero() && !r.FinishedAt.IsZero() && r.FinishedAt.Before(r.StartedAt) { + errs = append(errs, errors.New("finished_at must be after started_at")) + } + return errors.Join(errs...) +} + +func (r ServiceReceipt) Validate() error { + var errs []error + require := func(name, value string) { + if value == "" { + errs = append(errs, fmt.Errorf("%s is required", name)) + } + } + require("id", r.ID) + require("org_id", r.OrgID) + require("task_id", r.TaskID) + require("service_lease_id", r.ServiceLeaseID) + require("worker_id", r.WorkerID) + require("pool_id", r.PoolID) + require("policy_id", r.PolicyID) + if err := r.Executor.ValidateForProof(); err != nil { + errs = append(errs, err) + } + require("deployment_hash", r.DeploymentHash) + require("request_id", r.RequestID) + require("trace_id", r.TraceID) + require("request_hash", r.RequestHash) + require("response_hash", r.ResponseHash) + for _, field := range []struct { + name string + value string + }{ + {name: "deployment_hash", value: r.DeploymentHash}, + {name: "request_hash", value: r.RequestHash}, + {name: "response_hash", value: r.ResponseHash}, + } { + if field.value != "" && !validSHA256Digest(field.value) { + errs = append(errs, fmt.Errorf("%s must use sha256 digest", field.name)) + } + } + require("verifier.provider", r.Verifier.Provider) + require("agent_signature", r.AgentSignature) + if !validStoredProofStatus(r.Verifier.Status) { + errs = append(errs, fmt.Errorf("verifier.status %q is unsupported", r.Verifier.Status)) + } + if r.Executor.RequiresAttestation() { + if err := ValidateAttestedServiceBinding(AttestedServiceBinding{ + Executor: r.Executor, + PolicyID: r.PolicyID, + TaskID: r.TaskID, + DeploymentHash: r.DeploymentHash, + WorkerID: r.WorkerID, + PoolID: r.PoolID, + StartedAt: r.StartedAt, + FinishedAt: r.FinishedAt, + Verifier: r.Verifier, + }); err != nil { + errs = append(errs, err) + } + } + if r.StartedAt.IsZero() { + errs = append(errs, errors.New("started_at is required")) + } + if r.FinishedAt.IsZero() { + errs = append(errs, errors.New("finished_at is required")) + } + if !r.StartedAt.IsZero() && !r.FinishedAt.IsZero() && r.FinishedAt.Before(r.StartedAt) { + errs = append(errs, errors.New("finished_at must be after started_at")) + } + if r.SLOEvidence.StatusCode <= 0 { + errs = append(errs, errors.New("slo_evidence.status_code is required")) + } + if r.SLOEvidence.LatencyMillis <= 0 { + errs = append(errs, errors.New("slo_evidence.latency_millis is required")) + } + return errors.Join(errs...) +} + +func (r EdgeRequestReceipt) Validate() error { + var errs []error + require := func(name, value string) { + if value == "" { + errs = append(errs, fmt.Errorf("%s is required", name)) + } + } + require("id", r.ID) + require("org_id", r.OrgID) + require("pool_id", r.PoolID) + require("product_id", r.ProductID) + require("hostname", r.Hostname) + require("route_target", r.RouteTarget) + require("request_id", r.RequestID) + require("trace_id", r.TraceID) + require("method", r.Method) + require("request_class", r.RequestClass) + require("request_hash", r.RequestHash) + require("response_hash", r.ResponseHash) + require("router_signature", r.RouterSignature) + for _, field := range []struct { + name string + value string + }{ + {name: "org_id", value: r.OrgID}, + {name: "pool_id", value: r.PoolID}, + {name: "product_id", value: r.ProductID}, + {name: "request_id", value: r.RequestID}, + {name: "trace_id", value: r.TraceID}, + {name: "request_class", value: r.RequestClass}, + } { + if field.value != "" { + if err := validateIdentifier(field.name, field.value); err != nil { + errs = append(errs, err) + } + } + } + if r.ID != "" { + if err := validateIdentifier("id", r.ID); err != nil { + errs = append(errs, err) + } + } + if r.Hostname != "" { + if err := validateNetworkHostname(r.Hostname); err != nil { + errs = append(errs, fmt.Errorf("hostname: %w", err)) + } + } + if r.RouteTarget != "" && !strings.HasPrefix(r.RouteTarget, "service-route:") && !strings.HasPrefix(r.RouteTarget, "content-route:") { + errs = append(errs, errors.New("route_target must be opaque service-route or content-route target")) + } + if strings.HasPrefix(r.RouteTarget, "service-route:") { + require("ingress_evidence_id", r.IngressEvidenceID) + require("ingress_evidence_hash", r.IngressEvidenceHash) + if r.IngressEvidenceID != "" { + if err := validateIdentifier("ingress_evidence_id", r.IngressEvidenceID); err != nil { + errs = append(errs, err) + } + } + if r.IngressEvidenceHash != "" && !validSHA256Digest(r.IngressEvidenceHash) { + errs = append(errs, errors.New("ingress_evidence_hash must use sha256 digest")) + } + } + if r.ContentRef != "" { + if err := validateContentRef(r.ContentRef); err != nil { + errs = append(errs, fmt.Errorf("content_ref: %w", err)) + } + } + switch r.Method { + case "GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS": + case "": + default: + errs = append(errs, fmt.Errorf("method %q is unsupported", r.Method)) + } + for _, field := range []struct { + name string + value string + }{ + {name: "request_hash", value: r.RequestHash}, + {name: "response_hash", value: r.ResponseHash}, + } { + if field.value != "" && !validSHA256Digest(field.value) { + errs = append(errs, fmt.Errorf("%s must use sha256 digest", field.name)) + } + } + if r.RequestBytes < 0 { + errs = append(errs, errors.New("request_bytes must be non-negative")) + } + if r.ResponseBytes < 0 { + errs = append(errs, errors.New("response_bytes must be non-negative")) + } + if r.StartedAt.IsZero() { + errs = append(errs, errors.New("started_at is required")) + } + if r.FinishedAt.IsZero() { + errs = append(errs, errors.New("finished_at is required")) + } + if !r.StartedAt.IsZero() && !r.FinishedAt.IsZero() && r.FinishedAt.Before(r.StartedAt) { + errs = append(errs, errors.New("finished_at must be after started_at")) + } + if len(r.ServiceReceiptIDs) > 0 { + seen := map[string]struct{}{} + for i, id := range r.ServiceReceiptIDs { + if err := validateIdentifier(fmt.Sprintf("service_receipt_ids[%d]", i), id); err != nil { + errs = append(errs, err) + } + if _, ok := seen[id]; ok { + errs = append(errs, fmt.Errorf("service_receipt_ids[%d] duplicates %q", i, id)) + } + seen[id] = struct{}{} + } + } + require("verifier.provider", r.Verifier.Provider) + if !validStoredProofStatus(r.Verifier.Status) { + errs = append(errs, fmt.Errorf("verifier.status %q is unsupported", r.Verifier.Status)) + } + return errors.Join(errs...) +} + +func validStoredProofStatus(status VerificationStatus) bool { + switch status { + case VerificationPending, VerificationAccepted, VerificationRejected, VerificationConflicted: + return true + default: + return false + } +} + +func validateContentRef(ref string) error { + ref = strings.TrimSpace(ref) + if ref == "" { + return errors.New("ref is required") + } + if strings.ContainsAny(ref, " \t\r\n?&#") || strings.Contains(ref, "://") && !strings.HasPrefix(ref, "artifact://") { + return errors.New("ref must be immutable digest/artifact ref without URL query or fragment") + } + if strings.HasPrefix(ref, "sha256:") { + if !validSHA256Digest(ref) { + return errors.New("sha256 ref must use 64 hex chars") + } + return nil + } + if strings.HasPrefix(ref, "artifact://") { + path := strings.TrimPrefix(ref, "artifact://") + if path == "" || strings.HasPrefix(path, "/") || strings.Contains(path, "..") { + return errors.New("artifact ref path is invalid") + } + return nil + } + return errors.New("ref must use sha256 or artifact scheme") +} diff --git a/protocol/receipt_signature_test.go b/protocol/receipt_signature_test.go new file mode 100644 index 0000000..61fb345 --- /dev/null +++ b/protocol/receipt_signature_test.go @@ -0,0 +1,213 @@ +package protocol + +import ( + "crypto/sha256" + "encoding/hex" + "strings" + "testing" + "time" +) + +func TestCredentialReceiptSignaturesBindPayloadAndToken(t *testing.T) { + key := TokenProofKey("agent-token") + + proof := testProofReceipt() + proof.AgentSignature = CredentialProofSignature(proof, key) + if !VerifyCredentialProofSignature(proof, key) { + t.Fatal("proof credential signature should verify with same payload and token key") + } + tamperedProof := proof + tamperedProof.ArtifactHash = digest("tampered") + if VerifyCredentialProofSignature(tamperedProof, key) { + t.Fatal("proof credential signature verified after payload tamper") + } + if VerifyCredentialProofSignature(proof, TokenProofKey("other-token")) { + t.Fatal("proof credential signature verified with different token key") + } + + service := testServiceReceipt() + service.AgentSignature = CredentialServiceSignature(service, key) + if !VerifyCredentialServiceSignature(service, key) { + t.Fatal("service credential signature should verify with same payload and token key") + } + tamperedService := service + tamperedService.ResponseHash = digest("tampered-service") + if VerifyCredentialServiceSignature(tamperedService, key) { + t.Fatal("service credential signature verified after payload tamper") + } + + routerKey := TokenProofKey("router-token") + edge := testEdgeRequestReceipt() + edge.RouterSignature = CredentialEdgeSignature(edge, routerKey) + if !VerifyCredentialEdgeSignature(edge, routerKey) { + t.Fatal("edge credential signature should verify with same payload and token key") + } + tamperedEdge := edge + tamperedEdge.ResponseHash = digest("tampered-edge") + if VerifyCredentialEdgeSignature(tamperedEdge, routerKey) { + t.Fatal("edge credential signature verified after payload tamper") + } +} + +func TestSoftwareReceiptSignaturesDoNotSatisfyCredentialVerification(t *testing.T) { + proof := testProofReceipt() + proof.AgentSignature = SoftwareAgentProofSignature(proof) + if !strings.HasPrefix(proof.AgentSignature, SoftwareAgentSignaturePrefix) { + t.Fatalf("proof signature = %q, want software-agent prefix", proof.AgentSignature) + } + if VerifyCredentialProofSignature(proof, TokenProofKey("agent-token")) { + t.Fatal("software proof signature must not satisfy credential verification") + } + + service := testServiceReceipt() + service.AgentSignature = SoftwareAgentServiceSignature(service) + if !strings.HasPrefix(service.AgentSignature, SoftwareAgentSignaturePrefix) { + t.Fatalf("service signature = %q, want software-agent prefix", service.AgentSignature) + } + if VerifyCredentialServiceSignature(service, TokenProofKey("agent-token")) { + t.Fatal("software service signature must not satisfy credential verification") + } + + edge := testEdgeRequestReceipt() + edge.RouterSignature = SoftwareRouterEdgeSignature(edge) + if !strings.HasPrefix(edge.RouterSignature, SoftwareRouterSignaturePrefix) { + t.Fatalf("edge signature = %q, want software-router prefix", edge.RouterSignature) + } + if VerifyCredentialEdgeSignature(edge, TokenProofKey("router-token")) { + t.Fatal("software edge signature must not satisfy credential verification") + } +} + +func TestVerifyServiceReceiptRequiresDigestAndSignaturePolicy(t *testing.T) { + receipt := testServiceReceipt() + receipt.AgentSignature = SoftwareAgentServiceSignature(receipt) + if _, err := VerifyServiceReceipt(receipt, ReceiptVerificationOptions{}); err == nil || !strings.Contains(err.Error(), "agent_signature") { + t.Fatalf("VerifyServiceReceipt() error = %v, want software fallback opt-in rejection", err) + } + if _, err := VerifyServiceReceipt(receipt, ReceiptVerificationOptions{AllowSoftwareFallback: true}); err != nil { + t.Fatalf("VerifyServiceReceipt() error = %v", err) + } + + badDigest := receipt + badDigest.ResponseHash = "sha256:not-hex" + if _, err := VerifyServiceReceipt(badDigest, ReceiptVerificationOptions{}); err == nil || !strings.Contains(err.Error(), "response_hash") { + t.Fatalf("VerifyServiceReceipt() error = %v, want response_hash", err) + } + + credential := testServiceReceipt() + credential.AgentSignature = CredentialServiceSignature(credential, TokenProofKey("agent-token")) + if _, err := VerifyServiceReceipt(credential, ReceiptVerificationOptions{}); err == nil || !strings.Contains(err.Error(), "agent_signature") { + t.Fatalf("VerifyServiceReceipt() error = %v, want credential verification gate", err) + } + if _, err := VerifyServiceReceipt(credential, ReceiptVerificationOptions{CredentialSignatureVerified: true}); err != nil { + t.Fatalf("VerifyServiceReceipt() credential verified error = %v", err) + } +} + +func TestVerifyEdgeRequestReceiptRequiresDigestAndSignaturePolicy(t *testing.T) { + receipt := testEdgeRequestReceipt() + receipt.RouterSignature = SoftwareRouterEdgeSignature(receipt) + if _, err := VerifyEdgeRequestReceipt(receipt, ReceiptVerificationOptions{}); err == nil || !strings.Contains(err.Error(), "router_signature") { + t.Fatalf("VerifyEdgeRequestReceipt() error = %v, want software fallback opt-in rejection", err) + } + if _, err := VerifyEdgeRequestReceipt(receipt, ReceiptVerificationOptions{AllowSoftwareFallback: true}); err != nil { + t.Fatalf("VerifyEdgeRequestReceipt() error = %v", err) + } + + badDigest := receipt + badDigest.RequestHash = "sha256:not-hex" + if _, err := VerifyEdgeRequestReceipt(badDigest, ReceiptVerificationOptions{}); err == nil || !strings.Contains(err.Error(), "request_hash") { + t.Fatalf("VerifyEdgeRequestReceipt() error = %v, want request_hash", err) + } + + credential := testEdgeRequestReceipt() + credential.RouterSignature = CredentialEdgeSignature(credential, TokenProofKey("router-token")) + if _, err := VerifyEdgeRequestReceipt(credential, ReceiptVerificationOptions{}); err == nil || !strings.Contains(err.Error(), "router_signature") { + t.Fatalf("VerifyEdgeRequestReceipt() error = %v, want credential verification gate", err) + } + if _, err := VerifyEdgeRequestReceipt(credential, ReceiptVerificationOptions{CredentialSignatureVerified: true}); err != nil { + t.Fatalf("VerifyEdgeRequestReceipt() credential verified error = %v", err) + } +} + +func testProofReceipt() ProofReceipt { + return ProofReceipt{ + ID: "proof-1", + OrgID: "org-1", + TaskID: "task-1", + TaskHash: digest("task"), + InputHash: digest("input"), + DependencyClosureHash: digest("deps"), + Executor: testExecutorRef(), + WorkerID: "worker-1", + PoolID: "pool-1", + PolicyID: "policy-1", + StartedAt: time.Unix(100, 0).UTC(), + FinishedAt: time.Unix(101, 0).UTC(), + ResourceUsage: ResourceUsage{CPUMillis: 1000, MaxMemoryBytes: 4096}, + ArtifactHash: digest("artifact"), + } +} + +func testServiceReceipt() ServiceReceipt { + return ServiceReceipt{ + ID: "svc-rec-1", + OrgID: "org-1", + TaskID: "task-1", + ServiceLeaseID: "lease-1", + WorkerID: "worker-1", + PoolID: "pool-1", + PolicyID: "policy-1", + Executor: testExecutorRef(), + DeploymentHash: digest("deployment"), + RequestID: "req-1", + TraceID: "trace-1", + RequestHash: digest("request"), + ResponseHash: digest("response"), + StartedAt: time.Unix(100, 0).UTC(), + FinishedAt: time.Unix(101, 0).UTC(), + ResourceUsage: ResourceUsage{CPUMillis: 1000, MaxMemoryBytes: 4096}, + SLOEvidence: SLOEvidence{LatencyMillis: 1, StatusCode: 200, Healthy: true}, + Verifier: VerifierResult{Provider: "signed_receipt", Status: VerificationAccepted}, + } +} + +func testEdgeRequestReceipt() EdgeRequestReceipt { + return EdgeRequestReceipt{ + ID: "edge-rec-1", + OrgID: "org-1", + PoolID: "pool-1", + ProductID: "product-1", + Hostname: "edge.example.invalid", + RouteTarget: "service-route:lease-1", + ServiceLeaseID: "lease-1", + TaskID: "task-1", + WorkerID: "worker-1", + RequestID: "req-1", + TraceID: "trace-1", + Method: "GET", + RequestClass: "edge-service", + RequestHash: digest("request"), + ResponseHash: digest("response"), + StartedAt: time.Unix(100, 0).UTC(), + FinishedAt: time.Unix(101, 0).UTC(), + ResourceUsage: ResourceUsage{NetworkRxBytes: 128, NetworkTxBytes: 2048}, + IngressEvidenceID: "evidence-1", + IngressEvidenceHash: digest("evidence"), + Verifier: VerifierResult{Provider: "signed_receipt", Status: VerificationAccepted}, + } +} + +func testExecutorRef() ExecutorRef { + return ExecutorRef{ + Provider: "sandboxed-command", + Version: "dev", + ExecutionSecurityTier: ExecutionTrustedNative, + ProofTier: ProofArtifactHash, + } +} + +func digest(seed string) string { + sum := sha256.Sum256([]byte(seed)) + return "sha256:" + hex.EncodeToString(sum[:]) +} From 8d84eceb83c432bb34b3e1c628190eac28d127f2 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Tue, 26 May 2026 17:42:31 -0400 Subject: [PATCH 2/3] fix: harden receipt verification --- protocol/receipt_signature.go | 54 +++++++++++++++---- protocol/receipt_signature_test.go | 86 ++++++++++++++++++++++++++++-- 2 files changed, 126 insertions(+), 14 deletions(-) diff --git a/protocol/receipt_signature.go b/protocol/receipt_signature.go index aa2db01..f27b134 100644 --- a/protocol/receipt_signature.go +++ b/protocol/receipt_signature.go @@ -94,10 +94,10 @@ type EdgeRequestReceipt struct { } type ReceiptVerificationOptions struct { - CredentialSignatureVerified bool - AllowSoftwareFallback bool - VerifierProvider string - VerifierMessage string + CredentialTokenProofKey string + AllowSoftwareFallback bool + VerifierProvider string + VerifierMessage string } func TokenProofKey(token string) string { @@ -128,6 +128,9 @@ func SoftwareRouterEdgeSignature(receipt EdgeRequestReceipt) string { } func CredentialProofSignature(receipt ProofReceipt, tokenProofKey string) string { + if tokenProofKey == "" { + return "" + } receipt.AgentSignature = "" mac := hmac.New(sha256.New, []byte(tokenProofKey)) _, _ = mac.Write([]byte(CanonicalHash(receipt))) @@ -135,6 +138,9 @@ func CredentialProofSignature(receipt ProofReceipt, tokenProofKey string) string } func CredentialServiceSignature(receipt ServiceReceipt, tokenProofKey string) string { + if tokenProofKey == "" { + return "" + } receipt.AgentSignature = "" if !receipt.Executor.RequiresAttestation() { receipt.Verifier = VerifierResult{} @@ -145,6 +151,9 @@ func CredentialServiceSignature(receipt ServiceReceipt, tokenProofKey string) st } func CredentialEdgeSignature(receipt EdgeRequestReceipt, tokenProofKey string) string { + if tokenProofKey == "" { + return "" + } receipt.RouterSignature = "" receipt.Verifier = VerifierResult{} mac := hmac.New(sha256.New, []byte(tokenProofKey)) @@ -153,6 +162,9 @@ func CredentialEdgeSignature(receipt EdgeRequestReceipt, tokenProofKey string) s } func VerifyCredentialProofSignature(receipt ProofReceipt, tokenProofKey string) bool { + if tokenProofKey == "" { + return false + } if !strings.HasPrefix(receipt.AgentSignature, CredentialProofSignaturePrefix) { return false } @@ -161,6 +173,9 @@ func VerifyCredentialProofSignature(receipt ProofReceipt, tokenProofKey string) } func VerifyCredentialServiceSignature(receipt ServiceReceipt, tokenProofKey string) bool { + if tokenProofKey == "" { + return false + } if !strings.HasPrefix(receipt.AgentSignature, CredentialProofSignaturePrefix) { return false } @@ -169,6 +184,9 @@ func VerifyCredentialServiceSignature(receipt ServiceReceipt, tokenProofKey stri } func VerifyCredentialEdgeSignature(receipt EdgeRequestReceipt, tokenProofKey string) bool { + if tokenProofKey == "" { + return false + } if !strings.HasPrefix(receipt.RouterSignature, CredentialProofSignaturePrefix) { return false } @@ -241,14 +259,14 @@ func receiptVerifierResult(opts ReceiptVerificationOptions, defaultMessage strin func serviceReceiptSignatureValid(receipt ServiceReceipt, opts ReceiptVerificationOptions) bool { if strings.HasPrefix(receipt.AgentSignature, CredentialProofSignaturePrefix) { - return opts.CredentialSignatureVerified + return VerifyCredentialServiceSignature(receipt, opts.CredentialTokenProofKey) } return opts.AllowSoftwareFallback && receipt.AgentSignature == SoftwareAgentServiceSignature(receipt) } func edgeRequestReceiptSignatureValid(receipt EdgeRequestReceipt, opts ReceiptVerificationOptions) bool { if strings.HasPrefix(receipt.RouterSignature, CredentialProofSignaturePrefix) { - return opts.CredentialSignatureVerified + return VerifyCredentialEdgeSignature(receipt, opts.CredentialTokenProofKey) } return opts.AllowSoftwareFallback && receipt.RouterSignature == SoftwareRouterEdgeSignature(receipt) } @@ -267,6 +285,19 @@ func (r ProofReceipt) Validate() error { require("task_hash", r.TaskHash) require("input_hash", r.InputHash) require("dependency_closure_hash", r.DependencyClosureHash) + for _, field := range []struct { + name string + value string + }{ + {name: "task_hash", value: r.TaskHash}, + {name: "input_hash", value: r.InputHash}, + {name: "dependency_closure_hash", value: r.DependencyClosureHash}, + {name: "artifact_hash", value: r.ArtifactHash}, + } { + if field.value != "" && !validSHA256Digest(field.value) { + errs = append(errs, fmt.Errorf("%s must use sha256 digest", field.name)) + } + } if err := r.Executor.ValidateForProof(); err != nil { errs = append(errs, err) } @@ -374,11 +405,14 @@ func (r ServiceReceipt) Validate() error { if !r.StartedAt.IsZero() && !r.FinishedAt.IsZero() && r.FinishedAt.Before(r.StartedAt) { errs = append(errs, errors.New("finished_at must be after started_at")) } - if r.SLOEvidence.StatusCode <= 0 { - errs = append(errs, errors.New("slo_evidence.status_code is required")) + if err := r.ResourceLimits.Validate(); err != nil { + errs = append(errs, fmt.Errorf("resource_limits: %w", err)) + } + if err := r.SLOEvidence.Validate(); err != nil { + errs = append(errs, fmt.Errorf("slo_evidence: %w", err)) } - if r.SLOEvidence.LatencyMillis <= 0 { - errs = append(errs, errors.New("slo_evidence.latency_millis is required")) + if err := r.StatusEvidence.Validate(); err != nil { + errs = append(errs, fmt.Errorf("status_evidence: %w", err)) } return errors.Join(errs...) } diff --git a/protocol/receipt_signature_test.go b/protocol/receipt_signature_test.go index 61fb345..54e7cfa 100644 --- a/protocol/receipt_signature_test.go +++ b/protocol/receipt_signature_test.go @@ -49,6 +49,35 @@ func TestCredentialReceiptSignaturesBindPayloadAndToken(t *testing.T) { } } +func TestCredentialReceiptSignaturesRejectEmptyTokenProofKey(t *testing.T) { + proof := testProofReceipt() + if got := CredentialProofSignature(proof, ""); got != "" { + t.Fatalf("CredentialProofSignature(empty key) = %q, want empty", got) + } + proof.AgentSignature = CredentialProofSignature(proof, TokenProofKey("agent-token")) + if VerifyCredentialProofSignature(proof, "") { + t.Fatal("proof credential signature verified with empty token proof key") + } + + service := testServiceReceipt() + if got := CredentialServiceSignature(service, ""); got != "" { + t.Fatalf("CredentialServiceSignature(empty key) = %q, want empty", got) + } + service.AgentSignature = CredentialServiceSignature(service, TokenProofKey("agent-token")) + if VerifyCredentialServiceSignature(service, "") { + t.Fatal("service credential signature verified with empty token proof key") + } + + edge := testEdgeRequestReceipt() + if got := CredentialEdgeSignature(edge, ""); got != "" { + t.Fatalf("CredentialEdgeSignature(empty key) = %q, want empty", got) + } + edge.RouterSignature = CredentialEdgeSignature(edge, TokenProofKey("router-token")) + if VerifyCredentialEdgeSignature(edge, "") { + t.Fatal("edge credential signature verified with empty token proof key") + } +} + func TestSoftwareReceiptSignaturesDoNotSatisfyCredentialVerification(t *testing.T) { proof := testProofReceipt() proof.AgentSignature = SoftwareAgentProofSignature(proof) @@ -95,13 +124,41 @@ func TestVerifyServiceReceiptRequiresDigestAndSignaturePolicy(t *testing.T) { } credential := testServiceReceipt() - credential.AgentSignature = CredentialServiceSignature(credential, TokenProofKey("agent-token")) + key := TokenProofKey("agent-token") + credential.AgentSignature = CredentialServiceSignature(credential, key) if _, err := VerifyServiceReceipt(credential, ReceiptVerificationOptions{}); err == nil || !strings.Contains(err.Error(), "agent_signature") { t.Fatalf("VerifyServiceReceipt() error = %v, want credential verification gate", err) } - if _, err := VerifyServiceReceipt(credential, ReceiptVerificationOptions{CredentialSignatureVerified: true}); err != nil { + if _, err := VerifyServiceReceipt(credential, ReceiptVerificationOptions{CredentialTokenProofKey: key}); err != nil { t.Fatalf("VerifyServiceReceipt() credential verified error = %v", err) } + + malformed := credential + malformed.AgentSignature = CredentialProofSignaturePrefix + "not-hex" + if _, err := VerifyServiceReceipt(malformed, ReceiptVerificationOptions{CredentialTokenProofKey: key}); err == nil || !strings.Contains(err.Error(), "agent_signature") { + t.Fatalf("VerifyServiceReceipt() error = %v, want malformed credential signature rejection", err) + } + + nested := receipt + nested.SLOEvidence.DeadlineMS = -1 + nested.AgentSignature = SoftwareAgentServiceSignature(nested) + if _, err := VerifyServiceReceipt(nested, ReceiptVerificationOptions{AllowSoftwareFallback: true}); err == nil || !strings.Contains(err.Error(), "deadline_ms") { + t.Fatalf("VerifyServiceReceipt() error = %v, want nested slo validation", err) + } + + nested = receipt + nested.StatusEvidence.CommandHash = "not-a-digest" + nested.AgentSignature = SoftwareAgentServiceSignature(nested) + if _, err := VerifyServiceReceipt(nested, ReceiptVerificationOptions{AllowSoftwareFallback: true}); err == nil || !strings.Contains(err.Error(), "command_hash") { + t.Fatalf("VerifyServiceReceipt() error = %v, want nested status evidence validation", err) + } + + nested = receipt + nested.ResourceLimits.CPUPercent = -1 + nested.AgentSignature = SoftwareAgentServiceSignature(nested) + if _, err := VerifyServiceReceipt(nested, ReceiptVerificationOptions{AllowSoftwareFallback: true}); err == nil || !strings.Contains(err.Error(), "cpu_percent") { + t.Fatalf("VerifyServiceReceipt() error = %v, want nested resource limit validation", err) + } } func TestVerifyEdgeRequestReceiptRequiresDigestAndSignaturePolicy(t *testing.T) { @@ -121,13 +178,34 @@ func TestVerifyEdgeRequestReceiptRequiresDigestAndSignaturePolicy(t *testing.T) } credential := testEdgeRequestReceipt() - credential.RouterSignature = CredentialEdgeSignature(credential, TokenProofKey("router-token")) + key := TokenProofKey("router-token") + credential.RouterSignature = CredentialEdgeSignature(credential, key) if _, err := VerifyEdgeRequestReceipt(credential, ReceiptVerificationOptions{}); err == nil || !strings.Contains(err.Error(), "router_signature") { t.Fatalf("VerifyEdgeRequestReceipt() error = %v, want credential verification gate", err) } - if _, err := VerifyEdgeRequestReceipt(credential, ReceiptVerificationOptions{CredentialSignatureVerified: true}); err != nil { + if _, err := VerifyEdgeRequestReceipt(credential, ReceiptVerificationOptions{CredentialTokenProofKey: key}); err != nil { t.Fatalf("VerifyEdgeRequestReceipt() credential verified error = %v", err) } + + malformed := credential + malformed.RouterSignature = CredentialProofSignaturePrefix + "not-hex" + if _, err := VerifyEdgeRequestReceipt(malformed, ReceiptVerificationOptions{CredentialTokenProofKey: key}); err == nil || !strings.Contains(err.Error(), "router_signature") { + t.Fatalf("VerifyEdgeRequestReceipt() error = %v, want malformed credential signature rejection", err) + } +} + +func TestProofReceiptValidateRequiresDigestFields(t *testing.T) { + receipt := testProofReceipt() + receipt.AgentSignature = SoftwareAgentProofSignature(receipt) + receipt.Verifier = VerifierResult{Provider: "signed_receipt", Status: VerificationAccepted} + if err := receipt.Validate(); err != nil { + t.Fatalf("Validate() error = %v", err) + } + + receipt.ArtifactHash = "sha256:not-hex" + if err := receipt.Validate(); err == nil || !strings.Contains(err.Error(), "artifact_hash") { + t.Fatalf("Validate() error = %v, want artifact_hash digest rejection", err) + } } func testProofReceipt() ProofReceipt { From 12c021015843d7ac7fd83ddd46269334e236e2c6 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Tue, 26 May 2026 17:48:20 -0400 Subject: [PATCH 3/3] fix: tighten edge receipt validation --- protocol/receipt_signature.go | 49 ++++++++++++++++++++++++++-- protocol/receipt_signature_test.go | 51 ++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 2 deletions(-) diff --git a/protocol/receipt_signature.go b/protocol/receipt_signature.go index f27b134..e12bdc9 100644 --- a/protocol/receipt_signature.go +++ b/protocol/receipt_signature.go @@ -234,7 +234,7 @@ func VerifyEdgeRequestReceipt(receipt EdgeRequestReceipt, opts ReceiptVerificati if !edgeRequestReceiptSignatureValid(receipt, opts) { return EdgeRequestReceipt{}, errors.New("edge request receipt router_signature is invalid") } - receipt.Verifier = receiptVerifierResult(opts, "edge request receipt signature verified; route target and accounting checked") + receipt.Verifier = receiptVerifierResult(opts, "edge request receipt signature and shape verified; host must still check route, authz, replay, and accounting") if err := receipt.Validate(); err != nil { return EdgeRequestReceipt{}, err } @@ -285,6 +285,23 @@ func (r ProofReceipt) Validate() error { require("task_hash", r.TaskHash) require("input_hash", r.InputHash) require("dependency_closure_hash", r.DependencyClosureHash) + for _, field := range []struct { + name string + value string + }{ + {name: "id", value: r.ID}, + {name: "org_id", value: r.OrgID}, + {name: "task_id", value: r.TaskID}, + {name: "worker_id", value: r.WorkerID}, + {name: "pool_id", value: r.PoolID}, + {name: "policy_id", value: r.PolicyID}, + } { + if field.value != "" { + if err := validateIdentifier(field.name, field.value); err != nil { + errs = append(errs, err) + } + } + } for _, field := range []struct { name string value string @@ -356,6 +373,26 @@ func (r ServiceReceipt) Validate() error { require("worker_id", r.WorkerID) require("pool_id", r.PoolID) require("policy_id", r.PolicyID) + for _, field := range []struct { + name string + value string + }{ + {name: "id", value: r.ID}, + {name: "org_id", value: r.OrgID}, + {name: "task_id", value: r.TaskID}, + {name: "service_lease_id", value: r.ServiceLeaseID}, + {name: "worker_id", value: r.WorkerID}, + {name: "pool_id", value: r.PoolID}, + {name: "policy_id", value: r.PolicyID}, + {name: "request_id", value: r.RequestID}, + {name: "trace_id", value: r.TraceID}, + } { + if field.value != "" { + if err := validateIdentifier(field.name, field.value); err != nil { + errs = append(errs, err) + } + } + } if err := r.Executor.ValidateForProof(); err != nil { errs = append(errs, err) } @@ -468,6 +505,9 @@ func (r EdgeRequestReceipt) Validate() error { errs = append(errs, errors.New("route_target must be opaque service-route or content-route target")) } if strings.HasPrefix(r.RouteTarget, "service-route:") { + if r.ContentRef != "" { + errs = append(errs, errors.New("content_ref must be empty for service route receipt")) + } require("ingress_evidence_id", r.IngressEvidenceID) require("ingress_evidence_hash", r.IngressEvidenceHash) if r.IngressEvidenceID != "" { @@ -479,6 +519,9 @@ func (r EdgeRequestReceipt) Validate() error { errs = append(errs, errors.New("ingress_evidence_hash must use sha256 digest")) } } + if strings.HasPrefix(r.RouteTarget, "content-route:") { + require("content_ref", r.ContentRef) + } if r.ContentRef != "" { if err := validateContentRef(r.ContentRef); err != nil { errs = append(errs, fmt.Errorf("content_ref: %w", err)) @@ -545,10 +588,12 @@ func validStoredProofStatus(status VerificationStatus) bool { } func validateContentRef(ref string) error { - ref = strings.TrimSpace(ref) if ref == "" { return errors.New("ref is required") } + if strings.TrimSpace(ref) != ref { + return errors.New("ref must not contain leading or trailing whitespace") + } if strings.ContainsAny(ref, " \t\r\n?&#") || strings.Contains(ref, "://") && !strings.HasPrefix(ref, "artifact://") { return errors.New("ref must be immutable digest/artifact ref without URL query or fragment") } diff --git a/protocol/receipt_signature_test.go b/protocol/receipt_signature_test.go index 54e7cfa..6d8b755 100644 --- a/protocol/receipt_signature_test.go +++ b/protocol/receipt_signature_test.go @@ -194,6 +194,40 @@ func TestVerifyEdgeRequestReceiptRequiresDigestAndSignaturePolicy(t *testing.T) } } +func TestEdgeRequestReceiptBindsContentRefByRouteClass(t *testing.T) { + content := testEdgeRequestReceipt() + content.RouteTarget = "content-route:route-1" + content.ServiceLeaseID = "" + content.IngressEvidenceID = "" + content.IngressEvidenceHash = "" + content.ContentRef = digest("content") + content.RouterSignature = SoftwareRouterEdgeSignature(content) + if _, err := VerifyEdgeRequestReceipt(content, ReceiptVerificationOptions{AllowSoftwareFallback: true}); err != nil { + t.Fatalf("VerifyEdgeRequestReceipt(content) error = %v", err) + } + + missingContent := content + missingContent.ContentRef = "" + missingContent.RouterSignature = SoftwareRouterEdgeSignature(missingContent) + if _, err := VerifyEdgeRequestReceipt(missingContent, ReceiptVerificationOptions{AllowSoftwareFallback: true}); err == nil || !strings.Contains(err.Error(), "content_ref") { + t.Fatalf("VerifyEdgeRequestReceipt() error = %v, want content_ref required", err) + } + + service := testEdgeRequestReceipt() + service.ContentRef = digest("content") + service.RouterSignature = SoftwareRouterEdgeSignature(service) + if _, err := VerifyEdgeRequestReceipt(service, ReceiptVerificationOptions{AllowSoftwareFallback: true}); err == nil || !strings.Contains(err.Error(), "content_ref") { + t.Fatalf("VerifyEdgeRequestReceipt() error = %v, want service route content_ref rejection", err) + } + + padded := content + padded.ContentRef = " " + digest("content") + " " + padded.RouterSignature = SoftwareRouterEdgeSignature(padded) + if _, err := VerifyEdgeRequestReceipt(padded, ReceiptVerificationOptions{AllowSoftwareFallback: true}); err == nil || !strings.Contains(err.Error(), "whitespace") { + t.Fatalf("VerifyEdgeRequestReceipt() error = %v, want padded content_ref rejection", err) + } +} + func TestProofReceiptValidateRequiresDigestFields(t *testing.T) { receipt := testProofReceipt() receipt.AgentSignature = SoftwareAgentProofSignature(receipt) @@ -208,6 +242,23 @@ func TestProofReceiptValidateRequiresDigestFields(t *testing.T) { } } +func TestReceiptPayloadIdentifiersRejectInvalidShapes(t *testing.T) { + proof := testProofReceipt() + proof.AgentSignature = SoftwareAgentProofSignature(proof) + proof.Verifier = VerifierResult{Provider: "signed_receipt", Status: VerificationAccepted} + proof.ID = "bad/id" + if err := proof.Validate(); err == nil || !strings.Contains(err.Error(), "id") { + t.Fatalf("ProofReceipt.Validate() error = %v, want id rejection", err) + } + + service := testServiceReceipt() + service.AgentSignature = SoftwareAgentServiceSignature(service) + service.TraceID = "trace/1" + if err := service.Validate(); err == nil || !strings.Contains(err.Error(), "trace_id") { + t.Fatalf("ServiceReceipt.Validate() error = %v, want trace_id rejection", err) + } +} + func testProofReceipt() ProofReceipt { return ProofReceipt{ ID: "proof-1",