FLUID-WP-0004 T01, T02, T05, T06. The pipeline is the only path from Candidate to Verified: no other code constructs a Verified value, and Publish takes one, so "AI-generated artifacts are untrusted until verified" is a property of the type signatures rather than a rule people are asked to remember. The policy gate is a pure function of the candidate, the governing intent and configured limits. It cannot consult a model or take an opinion as input, because a gate that can be argued with is not a gate. Two behaviours it enforces are worth naming: the tighter of the descriptor's own traffic ceiling and the gate's wins, so a descriptor can restrict itself but never widen; and a daimon cannot authorize its own promotion below FLUID-5, since generation authority is not promotion authority. Signatures cover the canonical document with the signature member removed, so a signed descriptor round-trips and a tampered one does not. The registry now refuses anything that does not verify, which is what makes the pipeline's signature mean something at the router. An unchecked pipeline stage is recorded as unchecked rather than omitted, so a pipeline with no security check cannot look identical to one that passed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014KmVxhJ35tCo7rE7UnLwWu Assistant: claude-code Assistant-Model: opus Assistant-Process: 1116572@bnt-lap001 Assistant-Session: 8ba9bb93-a72a-4883-b189-2499cce5c400
89 lines
3.1 KiB
Go
89 lines
3.1 KiB
Go
package runtime
|
|
|
|
import (
|
|
"crypto/ed25519"
|
|
"errors"
|
|
"testing"
|
|
|
|
"github.com/tegwick/fluid-core/internal/contract"
|
|
"github.com/tegwick/fluid-core/internal/signing"
|
|
)
|
|
|
|
// TestVerifiedRegistryRefusesUnsigned is the router-side half of Blueprint 35.
|
|
// The pipeline signs; this is what makes the signature mean something.
|
|
func TestVerifiedRegistryRefusesUnsigned(t *testing.T) {
|
|
signer, pub, err := signing.GenerateKey("key-1")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
reg := NewVerifiedRegistry(testInterface, signing.NewVerifier(map[string]ed25519.PublicKey{"key-1": pub}))
|
|
|
|
unsigned := descriptor("R-1", contract.RevisionStateStable)
|
|
if err := reg.PutRevision(unsigned); !errors.Is(err, signing.ErrUnsigned) {
|
|
t.Fatalf("unsigned descriptor accepted: %v", err)
|
|
}
|
|
|
|
sig, err := signer.Sign(contract.RevisionDescriptorDocument{Revision: unsigned})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
signed := unsigned
|
|
signed.Signature = &contract.RevisionSignature{
|
|
Algorithm: contract.RevisionSignatureAlgorithm(sig.Algorithm),
|
|
KeyID: sig.KeyID,
|
|
Value: sig.Value,
|
|
}
|
|
if err := reg.PutRevision(signed); err != nil {
|
|
t.Fatalf("correctly signed descriptor refused: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestVerifiedRegistryRefusesTamperedDescriptor covers the case that matters
|
|
// most: an attacker promoting a signed experiment to stable, or repointing it
|
|
// at their own adapter.
|
|
func TestVerifiedRegistryRefusesTamperedDescriptor(t *testing.T) {
|
|
signer, pub, _ := signing.GenerateKey("key-1")
|
|
reg := NewVerifiedRegistry(testInterface, signing.NewVerifier(map[string]ed25519.PublicKey{"key-1": pub}))
|
|
|
|
original := descriptor("R-1", contract.RevisionStateExperiment)
|
|
sig, _ := signer.Sign(contract.RevisionDescriptorDocument{Revision: original})
|
|
|
|
attach := func(d contract.Revision) contract.Revision {
|
|
d.Signature = &contract.RevisionSignature{
|
|
Algorithm: contract.RevisionSignatureAlgorithm(sig.Algorithm),
|
|
KeyID: sig.KeyID,
|
|
Value: sig.Value,
|
|
}
|
|
return d
|
|
}
|
|
|
|
promoted := original
|
|
promoted.State = contract.RevisionStateStable
|
|
if err := reg.PutRevision(attach(promoted)); !errors.Is(err, signing.ErrBadSignature) {
|
|
t.Errorf("a promoted descriptor was accepted: %v", err)
|
|
}
|
|
|
|
redirected := original
|
|
redirected.Runtime.Upstream = "http://attacker:8080"
|
|
if err := reg.PutRevision(attach(redirected)); !errors.Is(err, signing.ErrBadSignature) {
|
|
t.Errorf("a redirected descriptor was accepted: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestVerifiedRegistryRefusesUntrustedKey(t *testing.T) {
|
|
rogue, _, _ := signing.GenerateKey("rogue")
|
|
_, trusted, _ := signing.GenerateKey("key-1")
|
|
reg := NewVerifiedRegistry(testInterface, signing.NewVerifier(map[string]ed25519.PublicKey{"key-1": trusted}))
|
|
|
|
d := descriptor("R-1", contract.RevisionStateStable)
|
|
sig, _ := rogue.Sign(contract.RevisionDescriptorDocument{Revision: d})
|
|
d.Signature = &contract.RevisionSignature{
|
|
Algorithm: contract.RevisionSignatureAlgorithm(sig.Algorithm),
|
|
KeyID: sig.KeyID,
|
|
Value: sig.Value,
|
|
}
|
|
|
|
if err := reg.PutRevision(d); !errors.Is(err, signing.ErrUnknownKey) {
|
|
t.Errorf("a descriptor signed by an untrusted key was accepted: %v", err)
|
|
}
|
|
}
|