The decentralization stack overcomplicated the project. Removed:
internal/pow, internal/smt, internal/checkpoint, internal/bft,
internal/lightnode, cmd/lightnode, relay gossip/checkpoint/proof/BFT
endpoints, their docs, vectors and the blake3 dependency.
Kept: WebSocket streaming on the relay, the full protocol v1 object set
including DelegationClaim (0x07) and KeyRotation request/confirm
(0x08/0x09) with chain resolution in verify.Graph, TrustedIssuers,
batch fetch, stable cursor pagination, per-type metrics.
INV-1 reverts to its original form: the relay holds no keys again.
Everything removed remains reachable at commit 20cc52c.
204 lines
6.7 KiB
Go
204 lines
6.7 KiB
Go
package identity_test
|
|
|
|
import (
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// Invariant tests.
|
|
//
|
|
// These check properties of the source tree itself rather than of a running
|
|
// program. They exist because INV-1 and INV-9 are structural claims: they
|
|
// cannot be established by testing behaviour, only by showing that certain
|
|
// code is absent and that certain packages are not linked together.
|
|
//
|
|
// Expressing them as tests means a violation fails the build instead of
|
|
// silently contradicting a comment.
|
|
|
|
// requireToolchain skips the calling test when the Go toolchain and the module
|
|
// source are not available.
|
|
//
|
|
// These tests inspect the source tree, so they are meaningful only when run
|
|
// from a checkout with a working toolchain. A cross-compiled test binary
|
|
// executed on another machine, for example the Windows build run under an
|
|
// emulator, has neither, and must skip rather than report a failure that says
|
|
// nothing about the code.
|
|
func requireToolchain(t *testing.T) {
|
|
t.Helper()
|
|
if _, err := exec.LookPath("go"); err != nil {
|
|
t.Skip("go toolchain not available; source-tree invariants are checked on the build host")
|
|
}
|
|
if _, err := os.Stat("invariants_test.go"); err != nil {
|
|
t.Skip("module source not available; source-tree invariants are checked on the build host")
|
|
}
|
|
}
|
|
|
|
// goList runs `go list` and returns its output lines.
|
|
func goList(t *testing.T, args ...string) []string {
|
|
t.Helper()
|
|
cmd := exec.Command("go", append([]string{"list"}, args...)...)
|
|
cmd.Env = append(cmd.Environ(), "GOPROXY=off", "GOFLAGS=-mod=mod")
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
t.Fatalf("go list %v: %v\n%s", args, err, out)
|
|
}
|
|
var lines []string
|
|
for _, l := range strings.Split(string(out), "\n") {
|
|
if l = strings.TrimSpace(l); l != "" {
|
|
lines = append(lines, l)
|
|
}
|
|
}
|
|
return lines
|
|
}
|
|
|
|
const modulePath = "git.n1ko.dev/Niko/niko_trust"
|
|
|
|
// TestProtocolLayerImports enforces INV-9.
|
|
//
|
|
// The protocol layer must depend on nothing but the standard library and
|
|
// vetted cryptographic primitives. If a protocol package ever imported the
|
|
// storage, transport or application layer, application semantics would leak
|
|
// into the definition of what a signed object means, and the layering that the
|
|
// whole design rests on would be gone.
|
|
func TestProtocolLayerImports(t *testing.T) {
|
|
requireToolchain(t)
|
|
// Only vetted cryptographic and encoding primitives are permitted, along
|
|
// with their own subpackages.
|
|
allowedExternalPrefixes := []string{
|
|
"github.com/btcsuite/btcd/btcutil/bech32",
|
|
"filippo.io/edwards25519",
|
|
}
|
|
allowedExternal := func(dep string) bool {
|
|
for _, p := range allowedExternalPrefixes {
|
|
if dep == p || strings.HasPrefix(dep, p+"/") {
|
|
return true
|
|
}
|
|
}
|
|
// Standard library internal packages are versioned in recent
|
|
// toolchains (for example crypto/internal/entropy/v1.0.0), which puts
|
|
// a dot in the path even though they are part of the stdlib.
|
|
return strings.HasPrefix(dep, "crypto/internal/") ||
|
|
strings.HasPrefix(dep, "internal/")
|
|
}
|
|
|
|
protocolPackages := []string{
|
|
modulePath + "/internal/address",
|
|
modulePath + "/internal/identity",
|
|
}
|
|
|
|
for _, pkg := range protocolPackages {
|
|
t.Run(pkg, func(t *testing.T) {
|
|
for _, dep := range goList(t, "-deps", pkg) {
|
|
switch {
|
|
case !strings.Contains(dep, "."):
|
|
// No dot in the first path element means stdlib.
|
|
continue
|
|
case allowedExternal(dep):
|
|
continue
|
|
case strings.HasPrefix(dep, modulePath+"/internal/address"),
|
|
strings.HasPrefix(dep, modulePath+"/internal/identity"):
|
|
// Protocol packages may depend on each other.
|
|
continue
|
|
case strings.HasPrefix(dep, "internal/"),
|
|
strings.HasPrefix(dep, "vendor/"),
|
|
strings.HasPrefix(dep, "golang.org/x/sys"),
|
|
strings.HasPrefix(dep, "golang.org/x/crypto"):
|
|
// Toolchain-internal and transitive runtime support.
|
|
continue
|
|
default:
|
|
t.Errorf("protocol package %s depends on %s, which is outside the protocol layer", pkg, dep)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestProtocolDoesNotImportSigner enforces the client/server split behind
|
|
// INV-1.
|
|
//
|
|
// The signer package is the only place a private key exists. Nothing in the
|
|
// protocol layer may reach it, so that a server binary built from the protocol
|
|
// and transport layers cannot contain signing capability even by accident.
|
|
func TestProtocolDoesNotImportSigner(t *testing.T) {
|
|
requireToolchain(t)
|
|
signerPkg := modulePath + "/internal/identity/signer"
|
|
|
|
for _, pkg := range []string{
|
|
modulePath + "/internal/address",
|
|
modulePath + "/internal/identity",
|
|
} {
|
|
for _, dep := range goList(t, "-deps", pkg) {
|
|
if dep == signerPkg {
|
|
t.Errorf("%s imports %s: the protocol layer must not have signing capability", pkg, signerPkg)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestNoSigningOutsideSigner enforces INV-1 at the source level: the ability
|
|
// to produce an Ed25519 signature must exist in exactly one package: the
|
|
// client-only signer. A relay that starts signing anything fails here the
|
|
// moment the code is written.
|
|
func TestNoSigningOutsideSigner(t *testing.T) {
|
|
requireToolchain(t)
|
|
files := goList(t, "-f",
|
|
`{{$d := .Dir}}{{range .GoFiles}}{{$d}}/{{.}}{{"\n"}}{{end}}`,
|
|
modulePath+"/...")
|
|
|
|
signingAPIs := []string{
|
|
"ed25519.Sign(",
|
|
"ed25519.GenerateKey(",
|
|
"ed25519.NewKeyFromSeed(",
|
|
}
|
|
|
|
for _, file := range files {
|
|
if strings.Contains(file, "/internal/identity/signer/") {
|
|
continue
|
|
}
|
|
data, err := readFile(file)
|
|
if err != nil {
|
|
t.Fatalf("read %s: %v", file, err)
|
|
}
|
|
for _, api := range signingAPIs {
|
|
if strings.Contains(data, api) {
|
|
t.Errorf("%s uses %s outside the signer package; "+
|
|
"private key operations must stay in internal/identity/signer", file, api)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestNoApplicationSemantics enforces INV-5 and INV-9 from the other
|
|
// direction: the protocol layer must not know what any claim key means.
|
|
//
|
|
// The trust server is a relay for opaque statements. The moment a package
|
|
// branches on "minecraft.op" or grants a "permission", it has started making
|
|
// authorization decisions, which is exactly what the design forbids.
|
|
func TestNoApplicationSemantics(t *testing.T) {
|
|
requireToolchain(t)
|
|
files := goList(t, "-f",
|
|
`{{$d := .Dir}}{{range .GoFiles}}{{$d}}/{{.}}{{"\n"}}{{end}}`,
|
|
modulePath+"/...")
|
|
|
|
forbidden := []string{
|
|
"minecraft", "ssh.login", "server.restart",
|
|
"isAdmin", "IsAdmin", "hasPermission", "HasPermission",
|
|
"isAuthorized", "IsAuthorized", "checkPermission", "CheckPermission",
|
|
}
|
|
|
|
for _, file := range files {
|
|
data, err := readFile(file)
|
|
if err != nil {
|
|
t.Fatalf("read %s: %v", file, err)
|
|
}
|
|
lower := strings.ToLower(data)
|
|
for _, term := range forbidden {
|
|
if strings.Contains(lower, strings.ToLower(term)) {
|
|
t.Errorf("%s mentions %q: the trust layer must not contain "+
|
|
"application-specific semantics or authorization logic", file, term)
|
|
}
|
|
}
|
|
}
|
|
}
|