| 1 | package release |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "crypto/ed25519" |
| 6 | "encoding/json" |
| 7 | "errors" |
| 8 | "os" |
| 9 | "path/filepath" |
| 10 | "strings" |
| 11 | "testing" |
| 12 | "time" |
| 13 | ) |
| 14 | |
| 15 | func proverKey(t *testing.T) (ed25519.PrivateKey, ed25519.PublicKey, string) { |
| 16 | t.Helper() |
| 17 | seed, pubB64, err := GenerateKey() |
| 18 | if err != nil { |
| 19 | t.Fatal(err) |
| 20 | } |
| 21 | priv, err := ParseSigningKey(seed, "test") |
| 22 | if err != nil { |
| 23 | t.Fatal(err) |
| 24 | } |
| 25 | pub, err := ParsePublicKey(pubB64, "test") |
| 26 | if err != nil { |
| 27 | t.Fatal(err) |
| 28 | } |
| 29 | return priv, pub, pubB64 |
| 30 | } |
| 31 | |
| 32 | var testSHA = strings.Repeat("ab", 32) |
| 33 | |
| 34 | func sampleProof() Proof { |
| 35 | return Proof{App: "console", Platform: "linux-arm64", SHA256: testSHA, Version: "f4478b15", Prover: "dev", |
| 36 | ActiveSince: time.Date(2026, 10, 6, 9, 0, 0, 0, time.UTC), |
| 37 | ProvenAt: time.Date(2026, 10, 6, 9, 15, 0, 0, time.UTC)} |
| 38 | } |
| 39 | |
| 40 | func TestProofSignVerifyRoundTrip(t *testing.T) { |
| 41 | priv, pub, _ := proverKey(t) |
| 42 | p, err := SignProof(priv, sampleProof()) |
| 43 | if err != nil { |
| 44 | t.Fatal(err) |
| 45 | } |
| 46 | b, err := json.MarshalIndent(p, "", " ") |
| 47 | if err != nil { |
| 48 | t.Fatal(err) |
| 49 | } |
| 50 | got, err := ParseProof(b) |
| 51 | if err != nil { |
| 52 | t.Fatalf("ParseProof: %v", err) |
| 53 | } |
| 54 | if got.Schema != 1 { |
| 55 | t.Fatalf("schema = %d", got.Schema) |
| 56 | } |
| 57 | if err := got.Check(pub, "console", "linux-arm64", testSHA, "dev"); err != nil { |
| 58 | t.Fatalf("Check: %v", err) |
| 59 | } |
| 60 | // The signature is over the canonical form, not the file's bytes, so a |
| 61 | // proof re-encoded in transit still verifies. |
| 62 | compact, err := json.Marshal(got) |
| 63 | if err != nil { |
| 64 | t.Fatal(err) |
| 65 | } |
| 66 | again, err := ParseProof(compact) |
| 67 | if err != nil || again.Verify(pub) != nil { |
| 68 | t.Fatalf("a compact re-encoding does not verify: %v", err) |
| 69 | } |
| 70 | } |
| 71 | |
| 72 | func TestProofEditedFieldDoesNotVerify(t *testing.T) { |
| 73 | priv, pub, _ := proverKey(t) |
| 74 | p, err := SignProof(priv, sampleProof()) |
| 75 | if err != nil { |
| 76 | t.Fatal(err) |
| 77 | } |
| 78 | for name, edit := range map[string]func(*Proof){ |
| 79 | "sha256": func(p *Proof) { p.SHA256 = strings.Repeat("cd", 32) }, |
| 80 | "app": func(p *Proof) { p.App = "other" }, |
| 81 | "platform": func(p *Proof) { p.Platform = "linux-amd64" }, |
| 82 | "version": func(p *Proof) { p.Version = "v9" }, |
| 83 | "prover": func(p *Proof) { p.Prover = "staging" }, |
| 84 | "proven_at": func(p *Proof) { p.ProvenAt = p.ProvenAt.Add(time.Second) }, |
| 85 | } { |
| 86 | q := *p |
| 87 | edit(&q) |
| 88 | if err := q.Verify(pub); err == nil { |
| 89 | t.Fatalf("%s edited and the proof still verifies", name) |
| 90 | } |
| 91 | } |
| 92 | _, other, _ := proverKey(t) |
| 93 | if err := p.Verify(other); err == nil { |
| 94 | t.Fatal("the proof verifies against another prover's key") |
| 95 | } |
| 96 | } |
| 97 | |
| 98 | func TestProofCheckNamesTheMismatch(t *testing.T) { |
| 99 | priv, pub, _ := proverKey(t) |
| 100 | p, err := SignProof(priv, sampleProof()) |
| 101 | if err != nil { |
| 102 | t.Fatal(err) |
| 103 | } |
| 104 | for _, c := range []struct{ app, plat, sha, prover, want string }{ |
| 105 | {"other", "linux-arm64", testSHA, "dev", "app"}, |
| 106 | {"console", "linux-amd64", testSHA, "dev", "platform"}, |
| 107 | {"console", "linux-arm64", strings.Repeat("cd", 32), "dev", "sha256"}, |
| 108 | {"console", "linux-arm64", testSHA, "staging", "prover"}, |
| 109 | } { |
| 110 | err := p.Check(pub, c.app, c.plat, c.sha, c.prover) |
| 111 | if err == nil || !strings.Contains(err.Error(), c.want) { |
| 112 | t.Fatalf("Check(%s,%s,%s,%s) = %v, want an error naming %s", c.app, c.plat, c.sha, c.prover, err, c.want) |
| 113 | } |
| 114 | } |
| 115 | } |
| 116 | |
| 117 | func TestParseProofRejectsBadShape(t *testing.T) { |
| 118 | priv, _, _ := proverKey(t) |
| 119 | good, err := SignProof(priv, sampleProof()) |
| 120 | if err != nil { |
| 121 | t.Fatal(err) |
| 122 | } |
| 123 | for name, edit := range map[string]func(map[string]any){ |
| 124 | "schema": func(m map[string]any) { m["schema"] = 2 }, |
| 125 | "sha256": func(m map[string]any) { m["sha256"] = "../../etc" }, |
| 126 | "prover": func(m map[string]any) { m["prover"] = "Dev Box" }, |
| 127 | "app": func(m map[string]any) { m["app"] = "" }, |
| 128 | "sig": func(m map[string]any) { m["sig"] = "not base64!" }, |
| 129 | "proven_at": func(m map[string]any) { delete(m, "proven_at") }, |
| 130 | } { |
| 131 | b, _ := json.Marshal(good) |
| 132 | var m map[string]any |
| 133 | if err := json.Unmarshal(b, &m); err != nil { |
| 134 | t.Fatal(err) |
| 135 | } |
| 136 | edit(m) |
| 137 | b, _ = json.Marshal(m) |
| 138 | if _, err := ParseProof(b); err == nil { |
| 139 | t.Fatalf("%s: a malformed proof parsed", name) |
| 140 | } |
| 141 | } |
| 142 | if _, err := ParseProof([]byte("{")); err == nil { |
| 143 | t.Fatal("torn JSON parsed") |
| 144 | } |
| 145 | } |
| 146 | |
| 147 | func TestProofStoreRoundTrip(t *testing.T) { |
| 148 | s := NewDirStore(t.TempDir()) |
| 149 | ctx := context.Background() |
| 150 | priv, pub, _ := proverKey(t) |
| 151 | p, err := SignProof(priv, sampleProof()) |
| 152 | if err != nil { |
| 153 | t.Fatal(err) |
| 154 | } |
| 155 | if got, want := ProofKey("bab", "console", testSHA, "dev"), "apps/bab/console/proofs/"+testSHA+"/dev.json"; got != want { |
| 156 | t.Fatalf("ProofKey = %s, want %s", got, want) |
| 157 | } |
| 158 | if _, err := GetProof(ctx, s, "bab", "console", testSHA, "dev"); !errors.Is(err, os.ErrNotExist) { |
| 159 | t.Fatalf("absent proof = %v, want os.ErrNotExist", err) |
| 160 | } |
| 161 | if err := PutProof(ctx, s, "bab", p); err != nil { |
| 162 | t.Fatal(err) |
| 163 | } |
| 164 | if err := PutProof(ctx, s, "bab", p); err != nil { |
| 165 | t.Fatalf("carrying the same proof twice: %v", err) |
| 166 | } |
| 167 | got, err := GetProof(ctx, s, "bab", "console", testSHA, "dev") |
| 168 | if err != nil || got.Verify(pub) != nil { |
| 169 | t.Fatalf("GetProof = %+v, %v", got, err) |
| 170 | } |
| 171 | if _, err := GetProof(ctx, s, "bab", "console", "../x", "dev"); err == nil { |
| 172 | t.Fatal("a path in the sha256 reached the store") |
| 173 | } |
| 174 | } |
| 175 | |
| 176 | func TestProofPendingIsCreateOnly(t *testing.T) { |
| 177 | s := NewDirStore(t.TempDir()) |
| 178 | ctx := context.Background() |
| 179 | first := time.Date(2026, 10, 6, 9, 30, 30, 0, time.UTC) |
| 180 | rec := ProofPending{App: "console", Platform: "linux-arm64", SHA256: testSHA, Version: "v2", Prover: "dev", LiveSince: first, After: "15m"} |
| 181 | if err := PutProofPending(ctx, s, "bab", rec); err != nil { |
| 182 | t.Fatal(err) |
| 183 | } |
| 184 | rec.LiveSince = first.Add(time.Hour) |
| 185 | if err := PutProofPending(ctx, s, "bab", rec); err != nil { |
| 186 | t.Fatalf("a second sighting: %v", err) |
| 187 | } |
| 188 | got, err := GetProofPending(ctx, s, "bab", "console", testSHA, "dev") |
| 189 | if err != nil || !got.LiveSince.Equal(first) { |
| 190 | t.Fatalf("pending = %+v, %v; want the first sighting kept", got, err) |
| 191 | } |
| 192 | } |
| 193 | |
| 194 | func TestFileSHA256(t *testing.T) { |
| 195 | p := filepath.Join(t.TempDir(), "f") |
| 196 | if err := os.WriteFile(p, []byte("bad build"), 0o644); err != nil { |
| 197 | t.Fatal(err) |
| 198 | } |
| 199 | got, err := FileSHA256(p) |
| 200 | if err != nil || got != etagOf([]byte("bad build")) { |
| 201 | t.Fatalf("FileSHA256 = %q, %v", got, err) |
| 202 | } |
| 203 | } |
| 204 | |