//go:build linux package provisioner import ( "encoding/base64" "os" "path/filepath" "testing" family "amadan.net/rastrillo/rastrillo/crypto" ) func testToken(n byte) string { b := make([]byte, 32) for i := range b { b[i] = n } return base64.RawURLEncoding.EncodeToString(b) } func floorFixture(t *testing.T) (*nativeFloor, *family.Keypair) { t.Helper() path := persistentDirectory(t) dir, e := openPrivateDirectory(path) if e != nil { t.Fatal(e) } t.Cleanup(func() { dir.close() }) key, e := family.Generate() if e != nil { t.Fatal(e) } floor, e := newNativeFloor(dir, floorIdentity{testToken(1), testToken(2), base64.RawURLEncoding.EncodeToString(key.SignPub())}) if e != nil { t.Fatal(e) } return floor, key } func TestFloorDurableCompareAndSwap(t *testing.T) { f, k := floorFixture(t) if _, _, e := f.current(); e != ErrRefused { t.Fatal("absent floor accepted", e) } if e := f.initialize(k); e != nil { t.Fatal(e) } if e := f.initialize(k); e != ErrRefused { t.Fatal("floor reinitialized", e) } zero, _, e := f.current() if e != nil || zero != (floorPoint{}) { t.Fatal(e) } one := floorPoint{1, testToken(3)} if e = f.advance(k, zero, one); e != nil { t.Fatal(e) } if e = f.advance(k, zero, floorPoint{1, testToken(4)}); e != ErrRefused { t.Fatal("stale registry rolled floor back", e) } again, _, e := f.current() if e != nil || again != one { t.Fatal("floor failed durable scan", e) } // Reopening with a competing device or key never creates a fresh genesis. wrong, e := newNativeFloor(f.dir, floorIdentity{f.identity.Registry, testToken(9), f.identity.Public}) if e != nil { t.Fatal(e) } if _, _, e = wrong.current(); e != ErrRefused { t.Fatal("competing device accepted", e) } } func TestFloorRefusesPartialTamperedAndMissingHistory(t *testing.T) { for _, which := range []string{"partial", "tamper", "gap"} { t.Run(which, func(t *testing.T) { f, k := floorFixture(t) if e := f.initialize(k); e != nil { t.Fatal(e) } one := floorPoint{1, testToken(3)} if e := f.advance(k, floorPoint{}, one); e != nil { t.Fatal(e) } switch which { case "partial": if e := f.dir.create(floorName(2), []byte("{")); e != nil { t.Fatal(e) } case "tamper": path := filepath.Join(f.dir.file.Name(), floorName(1)) raw, e := os.ReadFile(path) if e != nil { t.Fatal(e) } raw[len(raw)-5] ^= 1 if e = os.WriteFile(path, raw, 0600); e != nil { t.Fatal(e) } case "gap": if e := f.advance(k, one, floorPoint{2, testToken(4)}); e != nil { t.Fatal(e) } if e := os.Remove(filepath.Join(f.dir.file.Name(), floorName(1))); e != nil { t.Fatal(e) } } if _, _, e := f.current(); e != ErrRefused { t.Fatal("damaged history accepted", e) } }) } } func TestFloorScanSeesDurableRecordBeforeMarkerUpdate(t *testing.T) { f, k := floorFixture(t) if e := f.initialize(k); e != nil { t.Fatal(e) } old, previous, e := f.current() if e != nil { t.Fatal(e) } next := floorPoint{1, testToken(8)} raw, e := f.signed(k, next, previous) if e != nil { t.Fatal(e) } if e = f.dir.create(floorName(1), raw); e != nil { t.Fatal(e) } // The marker remains genesis, as after power loss between durable record and // marker replacement. It cannot authorize reusing the previous sequence. current, _, e := f.current() if e != nil || current != next { t.Fatal("old marker hid durable record", e) } if e = f.advance(k, old, next); e != ErrRefused { t.Fatal("old marker allowed replay", e) } marker, e := canonicalJSON(next) if e != nil { t.Fatal(e) } if e = f.dir.create("head.tmp", marker); e != nil { t.Fatal(e) } if e = f.updateMarker(next); e != nil { t.Fatal("matching interrupted marker did not resume", e) } } func TestCanonicalMetadataPreservesTypedIntegerPrecision(t *testing.T) { value := struct { Signed int64 `json:"signed"` Unsigned uint64 `json:"unsigned"` }{9223372036854775807, 18446744073709551615} raw, e := canonicalJSON(value) if e != nil { t.Fatal(e) } if string(raw) != `{"signed":9223372036854775807,"unsigned":18446744073709551615}` { t.Fatal("integer precision lost") } var decoded struct { Signed int64 `json:"signed"` Unsigned uint64 `json:"unsigned"` } if closedJSON(raw, &decoded) != nil || decoded != value { t.Fatal("typed integer did not round trip") } } func TestFloorMarkerRecoveryRefusesUnsafeScratch(t *testing.T) { for _, kind := range []string{"symlink", "hardlink", "public", "directory"} { t.Run(kind, func(t *testing.T) { f, k := floorFixture(t) if e := f.initialize(k); e != nil { t.Fatal(e) } path := filepath.Join(f.dir.file.Name(), "head.tmp") target := filepath.Join(persistentDirectory(t), "target") original := []byte("must remain unchanged") if e := os.WriteFile(target, original, 0600); e != nil { t.Fatal(e) } var e error switch kind { case "symlink": e = os.Symlink(target, path) case "hardlink": e = os.Link(target, path) case "public": e = os.WriteFile(path, original, 0644) case "directory": e = os.Mkdir(path, 0700) } if e != nil { t.Fatal(e) } if e = f.advance(k, floorPoint{}, floorPoint{1, testToken(3)}); e != ErrRefused { t.Fatal("unsafe scratch accepted", e) } got, e := os.ReadFile(target) if e != nil || string(got) != string(original) { t.Fatal("target changed", e) } if _, e := os.Lstat(path); e != nil { t.Fatal("unsafe scratch removed", e) } }) } }