tests: add signature verification tests for narinfo, mesh, and config
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I06ef2f37f174d278ce4f727836f339dd6a6a6964
This commit is contained in:
parent
f4804d2150
commit
de100ee611
3 changed files with 246 additions and 10 deletions
|
|
@ -149,6 +149,27 @@ func TestValidateMeshEnabledNoPeers(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestValidateMeshBadPeerKey(t *testing.T) {
|
||||||
|
cfg, _ := config.Load("")
|
||||||
|
cfg.Mesh.Enabled = true
|
||||||
|
cfg.Mesh.Peers = []config.PeerConfig{
|
||||||
|
{Addr: "127.0.0.1:7946", PublicKey: "not-hex!"},
|
||||||
|
}
|
||||||
|
if err := cfg.Validate(); err == nil {
|
||||||
|
t.Error("expected error for invalid mesh peer public key")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateUpstreamBadPublicKey(t *testing.T) {
|
||||||
|
cfg, _ := config.Load("")
|
||||||
|
cfg.Upstreams = []config.UpstreamConfig{
|
||||||
|
{URL: "https://cache.nixos.org", PublicKey: "no-colon-here"},
|
||||||
|
}
|
||||||
|
if err := cfg.Validate(); err == nil {
|
||||||
|
t.Error("expected error for upstream public_key missing ':'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestInvalidDuration(t *testing.T) {
|
func TestInvalidDuration(t *testing.T) {
|
||||||
yamlContent := `
|
yamlContent := `
|
||||||
server:
|
server:
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,17 @@ import (
|
||||||
"notashelf.dev/ncro/internal/mesh"
|
"notashelf.dev/ncro/internal/mesh"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func freeUDPAddr(t *testing.T) string {
|
||||||
|
t.Helper()
|
||||||
|
conn, err := net.ListenPacket("udp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
addr := conn.LocalAddr().String()
|
||||||
|
conn.Close()
|
||||||
|
return addr
|
||||||
|
}
|
||||||
|
|
||||||
func TestAnnounceAndReceive(t *testing.T) {
|
func TestAnnounceAndReceive(t *testing.T) {
|
||||||
store := mesh.NewRouteStore()
|
store := mesh.NewRouteStore()
|
||||||
node, err := mesh.NewNode("", store)
|
node, err := mesh.NewNode("", store)
|
||||||
|
|
@ -16,15 +27,9 @@ func TestAnnounceAndReceive(t *testing.T) {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bind to an ephemeral port.
|
addr := freeUDPAddr(t)
|
||||||
conn, err := net.ListenPacket("udp", "127.0.0.1:0")
|
// Allow messages from our own node (its public key is the only allowed key).
|
||||||
if err != nil {
|
if err := mesh.ListenAndServe(addr, store, node.PublicKey()); err != nil {
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
addr := conn.LocalAddr().String()
|
|
||||||
conn.Close()
|
|
||||||
|
|
||||||
if err := mesh.ListenAndServe(addr, store); err != nil {
|
|
||||||
t.Fatalf("ListenAndServe: %v", err)
|
t.Fatalf("ListenAndServe: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -41,7 +46,6 @@ func TestAnnounceAndReceive(t *testing.T) {
|
||||||
t.Fatalf("Announce: %v", err)
|
t.Fatalf("Announce: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Give the listener goroutine time to process the packet.
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
|
||||||
entry := store.Get("test-pkg-abc")
|
entry := store.Get("test-pkg-abc")
|
||||||
|
|
@ -52,3 +56,62 @@ func TestAnnounceAndReceive(t *testing.T) {
|
||||||
t.Errorf("UpstreamURL = %q", entry.UpstreamURL)
|
t.Errorf("UpstreamURL = %q", entry.UpstreamURL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRejectUnknownSender(t *testing.T) {
|
||||||
|
store := mesh.NewRouteStore()
|
||||||
|
|
||||||
|
// Listener node — will reject messages not from trusted
|
||||||
|
trusted, err := mesh.NewNode("", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// Untrusted sender
|
||||||
|
untrusted, err := mesh.NewNode("", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
addr := freeUDPAddr(t)
|
||||||
|
// Only allow trusted node's key.
|
||||||
|
if err := mesh.ListenAndServe(addr, store, trusted.PublicKey()); err != nil {
|
||||||
|
t.Fatalf("ListenAndServe: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
routes := []cache.RouteEntry{
|
||||||
|
{StorePath: "untrusted-pkg", UpstreamURL: "https://evil.example.com",
|
||||||
|
TTL: time.Now().Add(time.Hour)},
|
||||||
|
}
|
||||||
|
mesh.Announce(addr, untrusted, routes)
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
|
||||||
|
if entry := store.Get("untrusted-pkg"); entry != nil {
|
||||||
|
t.Error("route from untrusted sender should have been rejected")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRejectTamperedMessage(t *testing.T) {
|
||||||
|
// This is covered by TestVerifyFailsOnTamper in mesh_test.go at the crypto level.
|
||||||
|
// Here we verify the full pipeline rejects a re-signed-but-tampered body.
|
||||||
|
store := mesh.NewRouteStore()
|
||||||
|
node, err := mesh.NewNode("", store)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
addr := freeUDPAddr(t)
|
||||||
|
if err := mesh.ListenAndServe(addr, store, node.PublicKey()); err != nil {
|
||||||
|
t.Fatalf("ListenAndServe: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send a valid message first to confirm it works.
|
||||||
|
routes := []cache.RouteEntry{
|
||||||
|
{StorePath: "legit-pkg", UpstreamURL: "https://cache.nixos.org",
|
||||||
|
TTL: time.Now().Add(time.Hour)},
|
||||||
|
}
|
||||||
|
mesh.Announce(addr, node, routes)
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
|
||||||
|
if store.Get("legit-pkg") == nil {
|
||||||
|
t.Fatal("valid message should have been accepted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
package narinfo_test
|
package narinfo_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/ed25519"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
|
@ -113,7 +116,6 @@ func TestParseMalformedLine(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseNarSizeOverflow(t *testing.T) {
|
func TestParseNarSizeOverflow(t *testing.T) {
|
||||||
// uint64 max: 18446744073709551615 — verify it parses correctly
|
|
||||||
input := "StorePath: /nix/store/abc-test\nNarSize: 18446744073709551615\n"
|
input := "StorePath: /nix/store/abc-test\nNarSize: 18446744073709551615\n"
|
||||||
ni, err := narinfo.Parse(strings.NewReader(input))
|
ni, err := narinfo.Parse(strings.NewReader(input))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -164,3 +166,153 @@ func TestParseInvalidFileSize(t *testing.T) {
|
||||||
t.Error("expected error for invalid FileSize")
|
t.Error("expected error for invalid FileSize")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fingerprint and signature verification
|
||||||
|
func TestFingerprint(t *testing.T) {
|
||||||
|
ni := &narinfo.NarInfo{
|
||||||
|
StorePath: "/nix/store/s66mzxpvicwklp6cpph4dc53k5l6bfhe-hello-2.12.1",
|
||||||
|
NarHash: "sha256:04rrn5x6lnzrfkcy3bh7gf7x6hq3w1kap4wdss2n6n4s19pgbkr7",
|
||||||
|
NarSize: 226512,
|
||||||
|
References: []string{"s66mzxpvicwklp6cpph4dc53k5l6bfhe-hello-2.12.1"},
|
||||||
|
}
|
||||||
|
fp := ni.Fingerprint()
|
||||||
|
want := "1;/nix/store/s66mzxpvicwklp6cpph4dc53k5l6bfhe-hello-2.12.1;" +
|
||||||
|
"sha256:04rrn5x6lnzrfkcy3bh7gf7x6hq3w1kap4wdss2n6n4s19pgbkr7;226512;" +
|
||||||
|
"/nix/store/s66mzxpvicwklp6cpph4dc53k5l6bfhe-hello-2.12.1"
|
||||||
|
if fp != want {
|
||||||
|
t.Errorf("Fingerprint() =\n%q\nwant\n%q", fp, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFingerprintNoRefs(t *testing.T) {
|
||||||
|
ni := &narinfo.NarInfo{
|
||||||
|
StorePath: "/nix/store/abc-test",
|
||||||
|
NarHash: "sha256:abc",
|
||||||
|
NarSize: 1234,
|
||||||
|
}
|
||||||
|
fp := ni.Fingerprint()
|
||||||
|
if !strings.HasSuffix(fp, ";") {
|
||||||
|
t.Errorf("Fingerprint with no refs should end with ';', got: %q", fp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFingerprintRefsAlreadyPrefixed(t *testing.T) {
|
||||||
|
ni := &narinfo.NarInfo{
|
||||||
|
StorePath: "/nix/store/abc-test",
|
||||||
|
NarHash: "sha256:abc",
|
||||||
|
NarSize: 1234,
|
||||||
|
References: []string{"/nix/store/dep-pkg"}, // already prefixed
|
||||||
|
}
|
||||||
|
fp := ni.Fingerprint()
|
||||||
|
if strings.Contains(fp, "/nix/store//nix/store/") {
|
||||||
|
t.Errorf("Fingerprint double-prefixed refs: %q", fp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParsePublicKeyValid(t *testing.T) {
|
||||||
|
name, key, err := narinfo.ParsePublicKey("cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ParsePublicKey: %v", err)
|
||||||
|
}
|
||||||
|
if name != "cache.nixos.org-1" {
|
||||||
|
t.Errorf("name = %q", name)
|
||||||
|
}
|
||||||
|
if len(key) != ed25519.PublicKeySize {
|
||||||
|
t.Errorf("key len = %d, want %d", len(key), ed25519.PublicKeySize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParsePublicKeyMissingColon(t *testing.T) {
|
||||||
|
_, _, err := narinfo.ParsePublicKey("no-colon-here")
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected error for missing ':'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParsePublicKeyBadBase64(t *testing.T) {
|
||||||
|
_, _, err := narinfo.ParsePublicKey("name:!!!not-base64!!!")
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected error for invalid base64")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParsePublicKeyWrongSize(t *testing.T) {
|
||||||
|
// 16 bytes encoded in base64 = 24 chars with padding
|
||||||
|
b16 := base64.StdEncoding.EncodeToString(make([]byte, 16))
|
||||||
|
_, _, err := narinfo.ParsePublicKey("name:" + b16)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected error for wrong key size (16 bytes, not 32)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generates a fresh ed25519 key, signs a narinfo fingerprint,
|
||||||
|
// embeds the signature, and verifies it. This covers the full sign/verify path.
|
||||||
|
func TestVerifyRoundtrip(t *testing.T) {
|
||||||
|
pub, priv, err := ed25519.GenerateKey(rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ni := &narinfo.NarInfo{
|
||||||
|
StorePath: "/nix/store/abc123-test-pkg",
|
||||||
|
NarHash: "sha256:abcdef123456",
|
||||||
|
NarSize: 98765,
|
||||||
|
References: []string{"abc123-test-pkg"},
|
||||||
|
}
|
||||||
|
|
||||||
|
fp := ni.Fingerprint()
|
||||||
|
sig := ed25519.Sign(priv, []byte(fp))
|
||||||
|
pubKeyStr := "test-key-1:" + base64.StdEncoding.EncodeToString(pub)
|
||||||
|
ni.Sig = []string{"test-key-1:" + base64.StdEncoding.EncodeToString(sig)}
|
||||||
|
|
||||||
|
ok, err := ni.Verify(pubKeyStr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Verify error: %v", err)
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
t.Error("Verify returned false for valid signature")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVerifyWrongKey(t *testing.T) {
|
||||||
|
_, priv, _ := ed25519.GenerateKey(rand.Reader)
|
||||||
|
wrongPub, _, _ := ed25519.GenerateKey(rand.Reader) // different key
|
||||||
|
|
||||||
|
ni := &narinfo.NarInfo{
|
||||||
|
StorePath: "/nix/store/abc123-test-pkg",
|
||||||
|
NarHash: "sha256:abcdef",
|
||||||
|
NarSize: 1234,
|
||||||
|
}
|
||||||
|
fp := ni.Fingerprint()
|
||||||
|
sig := ed25519.Sign(priv, []byte(fp))
|
||||||
|
// Register wrong public key but correct key name
|
||||||
|
wrongKeyStr := "test-key-1:" + base64.StdEncoding.EncodeToString(wrongPub)
|
||||||
|
ni.Sig = []string{"test-key-1:" + base64.StdEncoding.EncodeToString(sig)}
|
||||||
|
|
||||||
|
ok, err := ni.Verify(wrongKeyStr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
t.Error("Verify should return false for mismatched key")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVerifyNoMatchingKeyName(t *testing.T) {
|
||||||
|
pub, _, _ := ed25519.GenerateKey(rand.Reader)
|
||||||
|
ni := &narinfo.NarInfo{
|
||||||
|
StorePath: "/nix/store/abc123-test-pkg",
|
||||||
|
NarHash: "sha256:abcdef",
|
||||||
|
NarSize: 1234,
|
||||||
|
}
|
||||||
|
ni.Sig = []string{"other-key-1:invalidsig="}
|
||||||
|
pubKeyStr := "my-key-1:" + base64.StdEncoding.EncodeToString(pub)
|
||||||
|
|
||||||
|
ok, err := ni.Verify(pubKeyStr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
t.Error("Verify should return false when no Sig line matches key name")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue