Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I06ef2f37f174d278ce4f727836f339dd6a6a6964
318 lines
9 KiB
Go
318 lines
9 KiB
Go
package narinfo_test
|
|
|
|
import (
|
|
"crypto/ed25519"
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"strings"
|
|
"testing"
|
|
|
|
"notashelf.dev/ncro/internal/narinfo"
|
|
)
|
|
|
|
var realWorldNarinfo = `StorePath: /nix/store/s66mzxpvicwklp6cpph4dc53k5l6bfhe-hello-2.12.1
|
|
URL: nar/1wwh37nhg4f5zhb2vsn1a81p3ixn69gkg5k6fvmw3nhcn19fg8xj.nar.xz
|
|
Compression: xz
|
|
FileHash: sha256:1wwh37nhg4f5zhb2vsn1a81p3ixn69gkg5k6fvmw3nhcn19fg8xj
|
|
FileSize: 50088
|
|
NarHash: sha256:04rrn5x6lnzrfkcy3bh7gf7x6hq3w1kap4wdss2n6n4s19pgbkr7
|
|
NarSize: 226512
|
|
References: s66mzxpvicwklp6cpph4dc53k5l6bfhe-hello-2.12.1 4nlgxhzzvsnr6bva0b9afnq8lbr9rk2b-glibc-2.38-23
|
|
Sig: cache.nixos.org-1:abc123+base64signature=
|
|
`
|
|
|
|
func TestParseRealWorld(t *testing.T) {
|
|
ni, err := narinfo.Parse(strings.NewReader(realWorldNarinfo))
|
|
if err != nil {
|
|
t.Fatalf("Parse: %v", err)
|
|
}
|
|
if ni.StorePath != "/nix/store/s66mzxpvicwklp6cpph4dc53k5l6bfhe-hello-2.12.1" {
|
|
t.Errorf("StorePath = %q", ni.StorePath)
|
|
}
|
|
if ni.URL != "nar/1wwh37nhg4f5zhb2vsn1a81p3ixn69gkg5k6fvmw3nhcn19fg8xj.nar.xz" {
|
|
t.Errorf("URL = %q", ni.URL)
|
|
}
|
|
if ni.Compression != "xz" {
|
|
t.Errorf("Compression = %q, want xz", ni.Compression)
|
|
}
|
|
if ni.FileSize != 50088 {
|
|
t.Errorf("FileSize = %d, want 50088", ni.FileSize)
|
|
}
|
|
if ni.NarHash != "sha256:04rrn5x6lnzrfkcy3bh7gf7x6hq3w1kap4wdss2n6n4s19pgbkr7" {
|
|
t.Errorf("NarHash = %q", ni.NarHash)
|
|
}
|
|
if ni.NarSize != 226512 {
|
|
t.Errorf("NarSize = %d, want 226512", ni.NarSize)
|
|
}
|
|
if len(ni.References) != 2 {
|
|
t.Errorf("References len = %d, want 2", len(ni.References))
|
|
}
|
|
if len(ni.Sig) != 1 {
|
|
t.Errorf("Sig len = %d, want 1", len(ni.Sig))
|
|
}
|
|
}
|
|
|
|
func TestParseNoneCompression(t *testing.T) {
|
|
input := "StorePath: /nix/store/abc-test\nURL: nar/abc.nar\nCompression: none\n"
|
|
ni, err := narinfo.Parse(strings.NewReader(input))
|
|
if err != nil {
|
|
t.Fatalf("Parse: %v", err)
|
|
}
|
|
if ni.Compression != "none" {
|
|
t.Errorf("Compression = %q, want none", ni.Compression)
|
|
}
|
|
}
|
|
|
|
func TestParseMultipleReferences(t *testing.T) {
|
|
input := "StorePath: /nix/store/abc-test\nReferences: pkg-a pkg-b pkg-c pkg-d\n"
|
|
ni, err := narinfo.Parse(strings.NewReader(input))
|
|
if err != nil {
|
|
t.Fatalf("Parse: %v", err)
|
|
}
|
|
if len(ni.References) != 4 {
|
|
t.Errorf("References = %v, want 4 entries", ni.References)
|
|
}
|
|
}
|
|
|
|
func TestParseEmptyReferences(t *testing.T) {
|
|
input := "StorePath: /nix/store/abc-test\nReferences: \n"
|
|
ni, err := narinfo.Parse(strings.NewReader(input))
|
|
if err != nil {
|
|
t.Fatalf("Parse: %v", err)
|
|
}
|
|
if len(ni.References) != 0 {
|
|
t.Errorf("References = %v, want empty", ni.References)
|
|
}
|
|
}
|
|
|
|
func TestParseMultipleSigs(t *testing.T) {
|
|
input := "StorePath: /nix/store/abc-test\nSig: key1:aaa=\nSig: key2:bbb=\n"
|
|
ni, err := narinfo.Parse(strings.NewReader(input))
|
|
if err != nil {
|
|
t.Fatalf("Parse: %v", err)
|
|
}
|
|
if len(ni.Sig) != 2 {
|
|
t.Errorf("Sig len = %d, want 2", len(ni.Sig))
|
|
}
|
|
if ni.Sig[0] != "key1:aaa=" || ni.Sig[1] != "key2:bbb=" {
|
|
t.Errorf("Sig = %v", ni.Sig)
|
|
}
|
|
}
|
|
|
|
func TestParseMissingStorePath(t *testing.T) {
|
|
input := "URL: nar/abc.nar\nNarHash: sha256:abc\n"
|
|
_, err := narinfo.Parse(strings.NewReader(input))
|
|
if err == nil {
|
|
t.Error("expected error for missing StorePath")
|
|
}
|
|
}
|
|
|
|
func TestParseMalformedLine(t *testing.T) {
|
|
input := "StorePath: /nix/store/abc-test\nbadline\n"
|
|
_, err := narinfo.Parse(strings.NewReader(input))
|
|
if err == nil {
|
|
t.Error("expected error for malformed line")
|
|
}
|
|
}
|
|
|
|
func TestParseNarSizeOverflow(t *testing.T) {
|
|
input := "StorePath: /nix/store/abc-test\nNarSize: 18446744073709551615\n"
|
|
ni, err := narinfo.Parse(strings.NewReader(input))
|
|
if err != nil {
|
|
t.Fatalf("Parse: %v", err)
|
|
}
|
|
if ni.NarSize != 18446744073709551615 {
|
|
t.Errorf("NarSize = %d", ni.NarSize)
|
|
}
|
|
}
|
|
|
|
func TestParseDeriverCA(t *testing.T) {
|
|
input := "StorePath: /nix/store/abc-test\nDeriver: abc-drv\nCA: fixed:r:sha256:abc\n"
|
|
ni, err := narinfo.Parse(strings.NewReader(input))
|
|
if err != nil {
|
|
t.Fatalf("Parse: %v", err)
|
|
}
|
|
if ni.Deriver != "abc-drv" {
|
|
t.Errorf("Deriver = %q", ni.Deriver)
|
|
}
|
|
if ni.CA != "fixed:r:sha256:abc" {
|
|
t.Errorf("CA = %q", ni.CA)
|
|
}
|
|
}
|
|
|
|
func TestParseIgnoresBlankLines(t *testing.T) {
|
|
input := "\n\nStorePath: /nix/store/abc-test\n\nNarHash: sha256:abc\n\n"
|
|
ni, err := narinfo.Parse(strings.NewReader(input))
|
|
if err != nil {
|
|
t.Fatalf("Parse: %v", err)
|
|
}
|
|
if ni.StorePath == "" {
|
|
t.Error("StorePath should be set")
|
|
}
|
|
}
|
|
|
|
func TestParseInvalidNarSize(t *testing.T) {
|
|
input := "StorePath: /nix/store/abc-test\nNarSize: not-a-number\n"
|
|
_, err := narinfo.Parse(strings.NewReader(input))
|
|
if err == nil {
|
|
t.Error("expected error for invalid NarSize")
|
|
}
|
|
}
|
|
|
|
func TestParseInvalidFileSize(t *testing.T) {
|
|
input := "StorePath: /nix/store/abc-test\nFileSize: not-a-number\n"
|
|
_, err := narinfo.Parse(strings.NewReader(input))
|
|
if err == nil {
|
|
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")
|
|
}
|
|
}
|