| 1 | package operator |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "encoding/base64" |
| 6 | "strings" |
| 7 | "testing" |
| 8 | "time" |
| 9 | |
| 10 | family "amadan.net/rastrillo/rastrillo/crypto" |
| 11 | "github.com/go-json-experiment/json" |
| 12 | ) |
| 13 | |
| 14 | func token(b byte) string { return base64.RawURLEncoding.EncodeToString(bytes.Repeat([]byte{b}, 32)) } |
| 15 | func fixture(t *testing.T) (*family.Keypair, *Verifier, Authorization) { |
| 16 | t.Helper() |
| 17 | k, e := family.Generate() |
| 18 | if e != nil { |
| 19 | t.Fatal(e) |
| 20 | } |
| 21 | a := Authorization{Version: 1, Purpose: "operator-input", Role: "directory", Origin: "https://oficina.works", Installation: token(1), Registry: token(2), Device: token(3), Sequence: 1, Initial: true, JournalSequence: 1, OperationID: token(4), Action: "directory.initialize", Issued: 2000000000, Expires: 2000000300} |
| 22 | v, e := NewVerifier(Pins{a.Role, a.Origin, a.Installation, a.Registry, a.Device, base64.RawURLEncoding.EncodeToString(k.SignPub()), []string{a.Action}}) |
| 23 | if e != nil { |
| 24 | t.Fatal(e) |
| 25 | } |
| 26 | return k, v, a |
| 27 | } |
| 28 | func signed(t *testing.T, k *family.Keypair, a Authorization) []byte { |
| 29 | t.Helper() |
| 30 | b, e := SignRequest(k, a, struct { |
| 31 | Generation string `json:"generation"` |
| 32 | }{token(8)}) |
| 33 | if e != nil { |
| 34 | t.Fatal(e) |
| 35 | } |
| 36 | return b |
| 37 | } |
| 38 | func TestVerifiedRequestRetainsExpiredRecoveryButNoFreshAuthority(t *testing.T) { |
| 39 | k, v, a := fixture(t) |
| 40 | raw := signed(t, k, a) |
| 41 | r, e := v.Verify(raw) |
| 42 | if e != nil || !r.Valid() { |
| 43 | t.Fatal(e) |
| 44 | } |
| 45 | for _, test := range []struct { |
| 46 | now int64 |
| 47 | want bool |
| 48 | }{{a.Issued - 1, false}, {a.Issued, true}, {a.Expires - 1, true}, {a.Expires, false}, {a.Expires + 1, false}} { |
| 49 | if r.Fresh(time.Unix(test.now, 0)) != test.want { |
| 50 | t.Fatalf("boundary %d", test.now) |
| 51 | } |
| 52 | } |
| 53 | if (Request{}).Valid() || (Request{}).Fresh(time.Unix(a.Issued, 0)) { |
| 54 | t.Fatal("zero authority") |
| 55 | } |
| 56 | copy := r.Input() |
| 57 | copy[0] = '[' |
| 58 | auth := r.Authorization() |
| 59 | auth.Action = "directory.activate-consumer" |
| 60 | var input struct { |
| 61 | Generation string `json:"generation"` |
| 62 | } |
| 63 | if DecodeInput(r, &input) != nil || input.Generation != token(8) || r.Authorization().Action != a.Action { |
| 64 | t.Fatal("mutable verified input") |
| 65 | } |
| 66 | if r.Head() == "" { |
| 67 | t.Fatal("missing stream head") |
| 68 | } |
| 69 | } |
| 70 | func TestSignedScopeCannotCrossRoleOriginOrRegistry(t *testing.T) { |
| 71 | k, v, a := fixture(t) |
| 72 | for _, mutate := range []func(*Authorization){func(a *Authorization) { a.Origin = "https://other.oficina.works" }, func(a *Authorization) { a.Registry = token(9) }, func(a *Authorization) { a.Device = token(9) }, func(a *Authorization) { a.Installation = token(9) }, func(a *Authorization) { a.Action = "directory.seed-team" }, func(a *Authorization) { a.Role = "credential"; a.Action = "credential.owner-seed" }} { |
| 73 | foreign := a |
| 74 | mutate(&foreign) |
| 75 | if _, e := v.Verify(signed(t, k, foreign)); e != ErrRefused { |
| 76 | t.Fatal("foreign scope accepted", e) |
| 77 | } |
| 78 | } |
| 79 | other, e := family.Generate() |
| 80 | if e != nil { |
| 81 | t.Fatal(e) |
| 82 | } |
| 83 | if _, e = v.Verify(signed(t, other, a)); e != ErrRefused { |
| 84 | t.Fatal("foreign signer") |
| 85 | } |
| 86 | } |
| 87 | func TestStrictWireTamperPurposeAndErrorPrivacy(t *testing.T) { |
| 88 | k, v, a := fixture(t) |
| 89 | raw := signed(t, k, a) |
| 90 | cases := [][]byte{nil, append(append([]byte{}, raw...), raw...), append([]byte(" "), raw...), []byte(strings.Repeat("x", BodyLimit+1)), bytes.Replace(raw, []byte(`"version":1`), []byte(`"version":1,"unknown":"private@example.test"`), 1), bytes.Replace(raw, []byte(`"version":1`), []byte(`"version":1,"version":1`), 1), bytes.Replace(raw, []byte(`"version":1`), []byte(`"version":1e0`), 1), bytes.Replace(raw, []byte(token(8)), []byte(token(9)), 1)} |
| 91 | var w wrapper |
| 92 | if json.Unmarshal(raw, &w) != nil { |
| 93 | t.Fatal("fixture") |
| 94 | } |
| 95 | var env envelope |
| 96 | if json.Unmarshal(w.Authorization, &env) != nil { |
| 97 | t.Fatal("fixture") |
| 98 | } |
| 99 | payload, _ := canonical(env.Payload, -1) |
| 100 | wrong, e := family.Sign(k, OwnerContext, payload) |
| 101 | if e != nil { |
| 102 | t.Fatal(e) |
| 103 | } |
| 104 | env.Signature = base64.RawURLEncoding.EncodeToString(wrong) |
| 105 | w.Authorization, _ = encode(env) |
| 106 | wrongRaw, _ := encode(w) |
| 107 | cases = append(cases, wrongRaw) |
| 108 | for i, b := range cases { |
| 109 | if _, e := v.Verify(b); e != ErrRefused { |
| 110 | t.Fatalf("case %d accepted or leaked: %v", i, e) |
| 111 | } |
| 112 | } |
| 113 | var dst struct { |
| 114 | Different string `json:"different"` |
| 115 | } |
| 116 | r, _ := v.Verify(raw) |
| 117 | if DecodeInput(r, &dst) != ErrRefused { |
| 118 | t.Fatal("input unknown fields") |
| 119 | } |
| 120 | if strings.Contains(ErrRefused.Error(), "private@") { |
| 121 | t.Fatal("error leaked input") |
| 122 | } |
| 123 | } |
| 124 | func TestConfigurationAndUnsignedActionsRefuse(t *testing.T) { |
| 125 | k, v, a := fixture(t) |
| 126 | _ = k |
| 127 | for _, o := range []string{"http://oficina.works", "https://oficina.works/", "https://oficina.works:443", "https://OFICINA.works", "https://oficina.works?", "https://user@oficina.works", "https://oficina.works."} { |
| 128 | p := v.pins |
| 129 | p.Origin = o |
| 130 | if _, e := NewVerifier(p); e != ErrRefused { |
| 131 | t.Fatal("origin accepted", o) |
| 132 | } |
| 133 | } |
| 134 | for _, action := range []string{"directory.restore", "raw.sql"} { |
| 135 | p := v.pins |
| 136 | p.Actions = []string{action} |
| 137 | if _, e := NewVerifier(p); e != ErrRefused { |
| 138 | t.Fatal("unapproved action", action) |
| 139 | } |
| 140 | } |
| 141 | for _, mutate := range []func(*Authorization){func(a *Authorization) { a.Sequence = 0 }, func(a *Authorization) { a.Sequence = 2 }, func(a *Authorization) { a.Expires = a.Issued + 301 }, func(a *Authorization) { a.Issued = 0 }, func(a *Authorization) { a.OperationID = "" }, func(a *Authorization) { a.Previous = token(1) }} { |
| 142 | bad := a |
| 143 | mutate(&bad) |
| 144 | if _, e := SignRequest(k, bad, struct{}{}); e != ErrRefused { |
| 145 | t.Fatal("bad claims") |
| 146 | } |
| 147 | } |
| 148 | } |
| 149 | |