| new file mode 100644 |
| index 000000000..c56a891b2 |
| --- /dev/null |
| +++ b/internal/release/pointerdigests_test.go |
| @@ -0,0 +1,129 @@ |
| +package release |
| + |
| +import ( |
| + "crypto/ed25519" |
| + "encoding/base64" |
| + "encoding/json" |
| + "strings" |
| + "testing" |
| + "time" |
| +) |
| + |
| +// legacyPointer is ChannelPointer exactly as every binary before #253 knows |
| +// it. An old binary decodes into this and nothing else. |
| +type legacyPointer struct { |
| + Schema int `json:"schema"` |
| + Account string `json:"account,omitempty"` |
| + App string `json:"app"` |
| + Channel string `json:"channel"` |
| + Version string `json:"version"` |
| + PromotedAt time.Time `json:"promoted_at"` |
| + History []string `json:"history,omitempty"` |
| + Audit []Promotion `json:"audit,omitempty"` |
| + FastTrack bool `json:"fast_track,omitempty"` |
| +} |
| + |
| +func signedPointerBytes(t *testing.T, priv ed25519.PrivateKey, p *ChannelPointer) []byte { |
| + t.Helper() |
| + b, err := signPointer(RootSigner(priv), p) |
| + if err != nil { |
| + t.Fatalf("signPointer: %v", err) |
| + } |
| + return b |
| +} |
| + |
| +var ( |
| + dA = ManifestDigest([]byte("manifest a")) |
| + dB = ManifestDigest([]byte("manifest b")) |
| +) |
| + |
| +func TestDigestOf(t *testing.T) { |
| + p := &ChannelPointer{Version: "v3", Digest: dA, History: []string{"v2", "v1"}, HistoryDigests: []string{"", dB}} |
| + for v, want := range map[string]string{"v3": dA, "v2": "", "v1": dB, "v9": ""} { |
| + if got := p.DigestOf(v); got != want { |
| + t.Errorf("DigestOf(%s) = %q, want %q", v, got, want) |
| + } |
| + } |
| + if !p.CarriesDigests() || !p.ChannelHasDigests() { |
| + t.Fatal("a pointer with a head digest carries digests") |
| + } |
| + tail := &ChannelPointer{Version: "v3", History: []string{"v2"}, HistoryDigests: []string{dB}} |
| + if tail.CarriesDigests() || !tail.ChannelHasDigests() { |
| + t.Fatal("history digests alone: the channel has digests, the pointer carries none") |
| + } |
| + if (&ChannelPointer{Version: "v1"}).ChannelHasDigests() { |
| + t.Fatal("a legacy pointer has no digests") |
| + } |
| + var nilp *ChannelPointer |
| + if nilp.DigestOf("v1") != "" { |
| + t.Fatal("a nil pointer has no digests") |
| + } |
| +} |
| + |
| +// Spec §8: an old binary verifies a pointer with digests. |
| +func TestAnOldBinaryVerifiesAPointerWithDigests(t *testing.T) { |
| + priv, pub := testKeys(t) |
| + wire := signedPointerBytes(t, priv, &ChannelPointer{Account: "bab", App: "km", Channel: "stable", Version: "v2", |
| + Digest: dA, History: []string{"v1"}, HistoryDigests: []string{dB}}) |
| + var sw signedWrapper |
| + if err := json.Unmarshal(wire, &sw); err != nil { |
| + t.Fatal(err) |
| + } |
| + sig, _ := base64.StdEncoding.DecodeString(sw.Sig) |
| + if !ed25519.Verify(pub, []byte(sw.Payload), sig) { |
| + t.Fatal("the signature does not cover the payload as an old binary reads it") |
| + } |
| + payload, _ := base64.StdEncoding.DecodeString(sw.Payload) |
| + var old legacyPointer |
| + if err := json.Unmarshal(payload, &old); err != nil || old.Version != "v2" || len(old.History) != 1 { |
| + t.Fatalf("old decode = %+v, %v", old, err) |
| + } |
| +} |
| + |
| +func TestAPointerWithoutDigestsMarshalsAsBefore(t *testing.T) { |
| + b, err := json.Marshal(&ChannelPointer{Schema: 1, App: "km", Channel: "stable", Version: "v1", History: []string{"v0"}}) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if strings.Contains(string(b), "digest") { |
| + t.Fatalf("an empty digest leaked into the payload: %s", b) |
| + } |
| +} |
| + |
| +// Review Focus 1. |
| +func TestValidatePointerRefusesMisalignedHistoryDigests(t *testing.T) { |
| + base := func() *ChannelPointer { |
| + return &ChannelPointer{Schema: 1, Account: "bab", App: "km", Channel: "stable", Version: "v3", |
| + PromotedAt: time.Now().UTC().Truncate(time.Second), History: []string{"v2", "v1"}} |
| + } |
| + ok := base() |
| + ok.Digest, ok.HistoryDigests = dA, []string{"", dB} |
| + if err := validatePointer(ok, "bab", "km", "stable"); err != nil { |
| + t.Fatalf("aligned: %v", err) |
| + } |
| + short := base() |
| + short.Digest, short.HistoryDigests = dA, []string{dB} |
| + if err := validatePointer(short, "bab", "km", "stable"); err == nil { |
| + t.Fatal("history_digests shorter than history was accepted") |
| + } |
| + bad := base() |
| + bad.Digest = "not-a-digest" |
| + if err := validatePointer(bad, "bab", "km", "stable"); err == nil { |
| + t.Fatal("a malformed head digest was accepted") |
| + } |
| + badHist := base() |
| + badHist.HistoryDigests = []string{"", strings.ToUpper(dB)} |
| + if err := validatePointer(badHist, "bab", "km", "stable"); err == nil { |
| + t.Fatal("a malformed history digest was accepted") |
| + } |
| +} |
| + |
| +func TestVerifyPointerRoundTripsDigests(t *testing.T) { |
| + priv, pub := testKeys(t) |
| + wire := signedPointerBytes(t, priv, &ChannelPointer{Account: "bab", App: "km", Channel: "stable", Version: "v2", |
| + Digest: dA, History: []string{"v1"}, HistoryDigests: []string{""}}) |
| + p, err := verifyPointer(pub, wire, "bab", "km", "stable") |
| + if err != nil || p.Digest != dA || len(p.HistoryDigests) != 1 || p.HistoryDigests[0] != "" { |
| + t.Fatalf("verifyPointer = %+v, %v", p, err) |
| + } |
| +} |