package operator import ( "bytes" "encoding/base64" "strings" "testing" "time" family "amadan.net/rastrillo/rastrillo/crypto" "github.com/go-json-experiment/json" ) func token(b byte) string { return base64.RawURLEncoding.EncodeToString(bytes.Repeat([]byte{b}, 32)) } func fixture(t *testing.T) (*family.Keypair, *Verifier, Authorization) { t.Helper() k, e := family.Generate() if e != nil { t.Fatal(e) } 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} v, e := NewVerifier(Pins{a.Role, a.Origin, a.Installation, a.Registry, a.Device, base64.RawURLEncoding.EncodeToString(k.SignPub()), []string{a.Action}}) if e != nil { t.Fatal(e) } return k, v, a } func signed(t *testing.T, k *family.Keypair, a Authorization) []byte { t.Helper() b, e := SignRequest(k, a, struct { Generation string `json:"generation"` }{token(8)}) if e != nil { t.Fatal(e) } return b } func TestVerifiedRequestRetainsExpiredRecoveryButNoFreshAuthority(t *testing.T) { k, v, a := fixture(t) raw := signed(t, k, a) r, e := v.Verify(raw) if e != nil || !r.Valid() { t.Fatal(e) } for _, test := range []struct { now int64 want bool }{{a.Issued - 1, false}, {a.Issued, true}, {a.Expires - 1, true}, {a.Expires, false}, {a.Expires + 1, false}} { if r.Fresh(time.Unix(test.now, 0)) != test.want { t.Fatalf("boundary %d", test.now) } } if (Request{}).Valid() || (Request{}).Fresh(time.Unix(a.Issued, 0)) { t.Fatal("zero authority") } copy := r.Input() copy[0] = '[' auth := r.Authorization() auth.Action = "directory.activate-consumer" var input struct { Generation string `json:"generation"` } if DecodeInput(r, &input) != nil || input.Generation != token(8) || r.Authorization().Action != a.Action { t.Fatal("mutable verified input") } if r.Head() == "" { t.Fatal("missing stream head") } } func TestSignedScopeCannotCrossRoleOriginOrRegistry(t *testing.T) { k, v, a := fixture(t) 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" }} { foreign := a mutate(&foreign) if _, e := v.Verify(signed(t, k, foreign)); e != ErrRefused { t.Fatal("foreign scope accepted", e) } } other, e := family.Generate() if e != nil { t.Fatal(e) } if _, e = v.Verify(signed(t, other, a)); e != ErrRefused { t.Fatal("foreign signer") } } func TestStrictWireTamperPurposeAndErrorPrivacy(t *testing.T) { k, v, a := fixture(t) raw := signed(t, k, a) 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)} var w wrapper if json.Unmarshal(raw, &w) != nil { t.Fatal("fixture") } var env envelope if json.Unmarshal(w.Authorization, &env) != nil { t.Fatal("fixture") } payload, _ := canonical(env.Payload, -1) wrong, e := family.Sign(k, OwnerContext, payload) if e != nil { t.Fatal(e) } env.Signature = base64.RawURLEncoding.EncodeToString(wrong) w.Authorization, _ = encode(env) wrongRaw, _ := encode(w) cases = append(cases, wrongRaw) for i, b := range cases { if _, e := v.Verify(b); e != ErrRefused { t.Fatalf("case %d accepted or leaked: %v", i, e) } } var dst struct { Different string `json:"different"` } r, _ := v.Verify(raw) if DecodeInput(r, &dst) != ErrRefused { t.Fatal("input unknown fields") } if strings.Contains(ErrRefused.Error(), "private@") { t.Fatal("error leaked input") } } func TestConfigurationAndUnsignedActionsRefuse(t *testing.T) { k, v, a := fixture(t) _ = k 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."} { p := v.pins p.Origin = o if _, e := NewVerifier(p); e != ErrRefused { t.Fatal("origin accepted", o) } } for _, action := range []string{"directory.restore", "raw.sql"} { p := v.pins p.Actions = []string{action} if _, e := NewVerifier(p); e != ErrRefused { t.Fatal("unapproved action", action) } } 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) }} { bad := a mutate(&bad) if _, e := SignRequest(k, bad, struct{}{}); e != ErrRefused { t.Fatal("bad claims") } } }