| index da712bf89..d37e79d12 100644 |
| --- a/internal/release/production_test.go |
| +++ b/internal/release/production_test.go |
| @@ -148,6 +148,60 @@ func TestProductionFlagRefusesWrongApp(t *testing.T) { |
| } |
| } |
| |
| +// The signature binds the exact base64 payload STRING, not the decoded JSON |
| +// meaning — so a payload that is decoded, edited, and re-encoded (even |
| +// leaving the original Sig in place) must fail verification. A verifier that |
| +// decoded first and checked a signature over re-marshalled bytes would let |
| +// this through if the re-marshalling happened to reproduce the tampered |
| +// bytes; this test pins the "no canonicalisation" wire rule directly against |
| +// the field an attacker would actually want to flip. |
| +func TestProductionFlagRefusesTamperedPayload(t *testing.T) { |
| + s := NewDirStore(t.TempDir()) |
| + ctx := context.Background() |
| + priv, pub := testKeys(t) |
| + if err := SetProduction(ctx, s, priv, "bab", "kass", true, "paul@keymail.dev"); err != nil { |
| + t.Fatal(err) |
| + } |
| + raw, etag, err := s.Get(ctx, ProductionKey("bab", "kass")) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + var sw signedWrapper |
| + if err := json.Unmarshal(raw, &sw); err != nil { |
| + t.Fatal(err) |
| + } |
| + payload, err := base64.StdEncoding.DecodeString(sw.Payload) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + var f ProductionFlag |
| + if err := json.Unmarshal(payload, &f); err != nil { |
| + t.Fatal(err) |
| + } |
| + if !f.Production { |
| + t.Fatal("expected production=true before tampering") |
| + } |
| + f.Production = false // flip the bit an attacker would want to flip |
| + tampered, err := json.Marshal(&f) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + // Keep the ORIGINAL Sig — it covers the ORIGINAL payload string. A |
| + // re-encoded payload, even with identical decoded meaning, must fail |
| + // verification without a fresh signature. |
| + sw.Payload = base64.StdEncoding.EncodeToString(tampered) |
| + wrapper, err := json.Marshal(sw) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err := s.Put(ctx, ProductionKey("bab", "kass"), wrapper, etag); err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err := GetProduction(ctx, s, pub, "bab", "kass"); err == nil { |
| + t.Fatal("a tampered payload (re-encoded, original sig) verified") |
| + } |
| +} |
| + |
| // timeNowTestHelper is a stamped, truncated now — what signProductionFlag |
| // itself writes — for tests that hand-build payloads. |
| func timeNowTestHelper() time.Time { return time.Now().UTC().Truncate(time.Second) } |