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.
145 lines
5.8 KiB
Go
145 lines
5.8 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 || authorization.Signature != fixture.ExpectedSignature {
|
|
t.Fatal("authorization key/signature drifted from golden vector")
|
|
}
|
|
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() || authorization.Verify(publicKey, reordered) != nil {
|
|
t.Fatal("ordering changed the authorized manifest")
|
|
}
|
|
duplicate := cloneMutationManifest(t, manifest)
|
|
duplicate.ResolvedActions = append(duplicate.ResolvedActions, duplicate.ResolvedActions[0])
|
|
if duplicate.Hash() == manifest.Hash() || authorization.Verify(publicKey, duplicate) == nil {
|
|
t.Fatal("duplicate action was silently de-duplicated or remained authorized")
|
|
}
|
|
}
|
|
|
|
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 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")
|
|
}
|
|
}
|