Watch
1
0
Fork
You've already forked RedFlag
0
RedFlag/server/internal/capability/mutation_manifest_test.go
Fimeg f5eb83ca94 feat: pin mutation manifest authority bytes
Introduce a dormant MutationEnvelope beside the current closure token. Server, Agent, and helper now agree on length-prefixed manifest and authorization bytes, with one shared fixture pinning the hash, key identity, and Ed25519 signature.\n\nExecutor-affecting provenance, location, target, backend, and backend payload now have a signed home before any runtime path migrates. Exact duplicates stay visible in the hash; ordering does not. Current APT/DNF and Windows behavior remains untouched.
2026-08-26 16:07:19 -04:00

163 lines
6.1 KiB
Go

package capability
import (
"crypto/ed25519"
"encoding/hex"
"encoding/json"
"os"
"strings"
"testing"
)
type mutationProtocolFixture struct {
Manifest MutationManifest `json:"manifest"`
Authorization MutationAuthorization `json:"authorization"`
TestSeed string `json:"test_seed"`
ExpectedManifestCanonicalHex string `json:"expected_manifest_canonical_hex"`
ExpectedManifestHash string `json:"expected_manifest_hash"`
ExpectedAuthorizationCanonical string `json:"expected_authorization_canonical_hex"`
ExpectedKeyID string `json:"expected_key_id"`
ExpectedSignature string `json:"expected_signature"`
}
func loadMutationProtocolFixture(t *testing.T) mutationProtocolFixture {
t.Helper()
raw, err := os.ReadFile("../../../protocol/testdata/mutation-golden.json")
if err != nil {
t.Fatal(err)
}
var fixture mutationProtocolFixture
if err := json.Unmarshal(raw, &fixture); err != nil {
t.Fatal(err)
}
return fixture
}
func signedMutationProtocolFixture(t *testing.T) (MutationManifest, MutationAuthorization, ed25519.PublicKey) {
t.Helper()
fixture := loadMutationProtocolFixture(t)
seed, err := hex.DecodeString(fixture.TestSeed)
if err != nil {
t.Fatal(err)
}
privateKey := ed25519.NewKeyFromSeed(seed)
authorization := fixture.Authorization
if err := authorization.Sign(privateKey, fixture.Manifest); err != nil {
t.Fatal(err)
}
return fixture.Manifest, authorization, privateKey.Public().(ed25519.PublicKey)
}
func cloneMutationManifest(t *testing.T, manifest MutationManifest) MutationManifest {
t.Helper()
raw, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
var clone MutationManifest
if err := json.Unmarshal(raw, &clone); err != nil {
t.Fatal(err)
}
return clone
}
func TestMutationProtocolGoldenVector(t *testing.T) {
fixture := loadMutationProtocolFixture(t)
manifest, authorization, publicKey := signedMutationProtocolFixture(t)
if fixture.ExpectedManifestHash == "" {
t.Fatalf(
"fill fixture: manifest_canonical=%s\nmanifest_hash=%s\nauthorization_canonical=%s\nkey_id=%s\nsignature=%s",
hex.EncodeToString(manifest.CanonicalBytes()),
manifest.Hash(),
hex.EncodeToString(authorization.CanonicalMessage()),
authorization.KeyID,
authorization.Signature,
)
}
if got := hex.EncodeToString(manifest.CanonicalBytes()); got != fixture.ExpectedManifestCanonicalHex {
t.Fatalf("manifest canonical bytes = %q, want %q", got, fixture.ExpectedManifestCanonicalHex)
}
if got := manifest.Hash(); got != fixture.ExpectedManifestHash {
t.Fatalf("manifest hash = %q, want %q", got, fixture.ExpectedManifestHash)
}
if got := hex.EncodeToString(authorization.CanonicalMessage()); got != fixture.ExpectedAuthorizationCanonical {
t.Fatalf("authorization canonical bytes = %q, want %q", got, fixture.ExpectedAuthorizationCanonical)
}
if authorization.KeyID != fixture.ExpectedKeyID {
t.Fatalf("key id = %q, want %q", authorization.KeyID, fixture.ExpectedKeyID)
}
if authorization.Signature != fixture.ExpectedSignature {
t.Fatalf("signature = %q, want %q", authorization.Signature, fixture.ExpectedSignature)
}
envelope := MutationEnvelope{Manifest: manifest, Authorization: authorization}
if err := envelope.VerifyForExecutionAt(publicKey, 1_700_000_100); err != nil {
t.Fatalf("golden authorization did not verify: %v", err)
}
}
func TestMutationProtocolOrderingAndDuplicateSemantics(t *testing.T) {
manifest, authorization, publicKey := signedMutationProtocolFixture(t)
reordered := cloneMutationManifest(t, manifest)
reordered.ResolvedActions[0], reordered.ResolvedActions[1] = reordered.ResolvedActions[1], reordered.ResolvedActions[0]
reordered.Evidence[0], reordered.Evidence[1] = reordered.Evidence[1], reordered.Evidence[0]
if reordered.Hash() != manifest.Hash() {
t.Fatal("manifest hash changed when action/evidence order changed")
}
if err := authorization.Verify(publicKey, reordered); err != nil {
t.Fatalf("authorization rejected reordered manifest: %v", err)
}
duplicate := cloneMutationManifest(t, manifest)
duplicate.ResolvedActions = append(duplicate.ResolvedActions, duplicate.ResolvedActions[0])
if duplicate.Hash() == manifest.Hash() {
t.Fatal("exact duplicate action was silently de-duplicated")
}
if err := authorization.Verify(publicKey, duplicate); err == nil {
t.Fatal("authorization accepted a duplicate resolved action")
}
}
func TestMutationProtocolExecutorAffectingTamperFails(t *testing.T) {
manifest, authorization, publicKey := signedMutationProtocolFixture(t)
tests := map[string]func(*MutationManifest){
"provenance": func(m *MutationManifest) { m.Evidence[0].Digest = strings.Repeat("c", 64) },
"execution location": func(m *MutationManifest) {
m.ResolvedActions[0].Payload = strings.Replace(m.ResolvedActions[0].Payload, "/var/cache/redflag", "/tmp", 1)
},
"target": func(m *MutationManifest) { m.TargetID = "body-other" },
"backend": func(m *MutationManifest) { m.Backend = "wua" },
"resolved action": func(m *MutationManifest) { m.ResolvedActions[0].Identity = "zsh@6.0-1" },
}
for name, tamper := range tests {
t.Run(name, func(t *testing.T) {
changed := cloneMutationManifest(t, manifest)
tamper(&changed)
if err := authorization.Verify(publicKey, changed); err == nil {
t.Fatal("authorization accepted tampered manifest")
}
})
}
changedAuthorization := authorization
changedAuthorization.IssuedAt++
if err := changedAuthorization.Verify(publicKey, manifest); err == nil {
t.Fatal("authorization accepted tampered authorization metadata")
}
}
func TestMutationProtocolUnknownVersionsFailClosed(t *testing.T) {
manifest, authorization, publicKey := signedMutationProtocolFixture(t)
manifest.ProtocolVersion++
if err := manifest.Validate(); err == nil {
t.Fatal("unknown manifest version passed validation")
}
manifest.ProtocolVersion = MutationProtocolVersion
authorization.ProtocolVersion++
if err := authorization.Verify(publicKey, manifest); err == nil {
t.Fatal("unknown authorization version passed verification")
}
}