carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package release
2
3import (
4 "context"
5 "crypto/ed25519"
6 "encoding/base64"
7 "encoding/json"
8 "errors"
9 "strings"
10 "testing"
11 "time"
12)
13
14func hostRetryKey(t *testing.T) (ed25519.PublicKey, ed25519.PrivateKey) {
15 t.Helper()
16 pub, priv, err := ed25519.GenerateKey(nil)
17 if err != nil {
18 t.Fatal(err)
19 }
20 return pub, priv
21}
22
23func TestHostRetryGrantRoundTrip(t *testing.T) {
24 pub, priv := hostRetryKey(t)
25 now := time.Date(2026, 10, 11, 9, 30, 15, 500, time.UTC)
26 wire, err := SignHostRetry(RootSigner(priv), "bab", "blog", "a.example.com", "v2", "book-1", 42, now)
27 if err != nil {
28 t.Fatal(err)
29 }
30 g, err := VerifyHostRetry(pub, wire, "bab", "blog", "a.example.com", now.Add(time.Hour))
31 if err != nil {
32 t.Fatalf("verify = %v", err)
33 }
34 want := HostRetryGrant{V: 1, Account: "bab", App: "blog", Host: "a.example.com", Build: "v2", Epoch: 42, Book: "book-1", IssuedAt: "2026-10-11T09:30:15Z"}
35 if g != want {
36 t.Fatalf("grant = %+v, want %+v", g, want)
37 }
38 // The whole 24-hour window is the backstop: the epoch is what binds.
39 if _, err := VerifyHostRetry(pub, wire, "bab", "blog", "a.example.com", now.Add(HostRetryGrantTTL-time.Second)); err != nil {
40 t.Fatalf("a grant inside its window = %v", err)
41 }
42 if len(wire) > MaxRootEnvelopeBytes {
43 t.Fatalf("wire is %d bytes", len(wire))
44 }
45}
46
47func TestHostRetryGrantRefusals(t *testing.T) {
48 pub, priv := hostRetryKey(t)
49 otherPub, _ := hostRetryKey(t)
50 now := time.Date(2026, 10, 11, 9, 30, 0, 0, time.UTC)
51 wire, err := SignHostRetry(RootSigner(priv), "bab", "blog", "a.example.com", "v2", "book-1", 7, now)
52 if err != nil {
53 t.Fatal(err)
54 }
55 cases := []struct {
56 name string
57 pub ed25519.PublicKey
58 wire string
59 acct, app, host string
60 at time.Time
61 }{
62 {"wrong host", pub, wire, "bab", "blog", "b.example.com", now},
63 {"wrong app", pub, wire, "bab", "shop", "a.example.com", now},
64 {"wrong account", pub, wire, "kass", "blog", "a.example.com", now},
65 {"expired", pub, wire, "bab", "blog", "a.example.com", now.Add(HostRetryGrantTTL + time.Second)},
66 {"from the future", pub, wire, "bab", "blog", "a.example.com", now.Add(-2 * time.Minute)},
67 {"another root", otherPub, wire, "bab", "blog", "a.example.com", now},
68 {"no key", nil, wire, "bab", "blog", "a.example.com", now},
69 {"empty", pub, "", "bab", "blog", "a.example.com", now},
70 {"tampered payload", pub, tamperHostRetry(t, wire, func(g map[string]any) { g["build"] = "v3" }), "bab", "blog", "a.example.com", now},
71 {"tampered epoch", pub, tamperHostRetry(t, wire, func(g map[string]any) { g["epoch"] = 8 }), "bab", "blog", "a.example.com", now},
72 {"tampered book", pub, tamperHostRetry(t, wire, func(g map[string]any) { g["book"] = "book-2" }), "bab", "blog", "a.example.com", now},
73 }
74 for _, c := range cases {
75 if g, err := VerifyHostRetry(c.pub, c.wire, c.acct, c.app, c.host, c.at); err == nil {
76 t.Errorf("%s: verified %+v", c.name, g)
77 }
78 }
79 // A minute of clock skew is allowed the console.
80 if _, err := VerifyHostRetry(pub, wire, "bab", "blog", "a.example.com", now.Add(-30*time.Second)); err != nil {
81 t.Fatalf("thirty seconds of skew = %v", err)
82 }
83}
84
85// A grant the root signed with a zero epoch is an older box's report read by
86// the console: there is no restriction it can name, so no box acts on it.
87func TestHostRetryGrantRefusesAZeroEpoch(t *testing.T) {
88 pub, priv := hostRetryKey(t)
89 now := time.Now()
90 wire, err := SignHostRetry(RootSigner(priv), "bab", "blog", "a.example.com", "v2", "book-1", 0, now)
91 if err != nil {
92 t.Fatalf("the console signs a zero epoch: %v", err)
93 }
94 if _, err := VerifyHostRetry(pub, wire, "bab", "blog", "a.example.com", now); !errors.Is(err, ErrHostRetryGrant) {
95 t.Fatalf("zero epoch = %v, want ErrHostRetryGrant", err)
96 }
97 wire, _ = SignHostRetry(RootSigner(priv), "bab", "blog", "a.example.com", "", "book-1", 3, now)
98 if _, err := VerifyHostRetry(pub, wire, "bab", "blog", "a.example.com", now); err == nil {
99 t.Fatal("a grant naming no build verified")
100 }
101}
102
103func TestHostRetryGrantRefusesAScopedSigner(t *testing.T) {
104 pub, priv := hostRetryKey(t)
105 scoped := Signer{key: priv, scope: &Grant{Name: "ci"}, grant: &signedGrant{}, root: pub}
106 if _, err := SignHostRetry(scoped, "bab", "blog", "a.example.com", "v2", "book-1", 1, time.Now()); !errors.Is(err, ErrRootEnvelope) {
107 t.Fatalf("scoped signer = %v, want ErrRootEnvelope", err)
108 }
109 if _, err := SignHostRetry(Signer{}, "bab", "blog", "a.example.com", "v2", "book-1", 1, time.Now()); err == nil {
110 t.Fatal("a signer with no key signed a retry")
111 }
112}
113
114// tamperHostRetry re-encodes wire's payload after edit, keeping the old
115// signature.
116func tamperHostRetry(t *testing.T, wire string, edit func(map[string]any)) string {
117 t.Helper()
118 var w struct {
119 Payload string `json:"payload"`
120 Sig string `json:"sig"`
121 }
122 if err := json.Unmarshal([]byte(wire), &w); err != nil {
123 t.Fatal(err)
124 }
125 raw, err := base64.StdEncoding.DecodeString(w.Payload)
126 if err != nil {
127 t.Fatal(err)
128 }
129 var g map[string]any
130 if err := json.Unmarshal(raw, &g); err != nil {
131 t.Fatal(err)
132 }
133 edit(g)
134 // Keep the struct's field order so only the signature can refuse it.
135 order := []string{"v", "account", "app", "host", "build", "epoch", "issued_at"}
136 var b strings.Builder
137 b.WriteString("{")
138 for i, k := range order {
139 if i > 0 {
140 b.WriteString(",")
141 }
142 kv, _ := json.Marshal(k)
143 vv, _ := json.Marshal(g[k])
144 b.Write(kv)
145 b.WriteString(":")
146 b.Write(vv)
147 }
148 b.WriteString("}")
149 w.Payload = base64.StdEncoding.EncodeToString([]byte(b.String()))
150 out, _ := json.Marshal(w)
151 return string(out)
152}
153
154// The restart-scope log takes no signature at all now (grants ride
155// host-retry.json, TestValidRestartScopeRefusesARetryAndASignature), and
156// the field stays additive for the legacy entries a box still reads.
157func TestRestartScopeSignedIsAdditive(t *testing.T) {
158 if err := ValidRestartScope(RestartScope{Kind: RestartScopeHost, Value: "a.example.com", Signed: "x"}); err == nil {
159 t.Fatal("a host restart carrying a signature was valid")
160 }
161 if _, err := AppendHostRetry(context.Background(), NewDirStore(t.TempDir()), "bab", "blog", "a.example.com", strings.Repeat("x", MaxRootEnvelopeBytes+1)); err == nil {
162 t.Fatal("an oversized signature was appended")
163 }
164 // The field is additive: an older entry has none, and an older reader's
165 // lax parse steps over it.
166 raw, _ := json.Marshal(RestartScope{Kind: RestartScopeHost, Value: "a.example.com"})
167 if strings.Contains(string(raw), "signed") {
168 t.Fatalf("an unsigned scope wrote %s", raw)
169 }
170}
171