oficina / home Public

Clone
git clone https://amadan.net/oficina/home

Plain git — no account needed to clone.

Download

Download this file

1package operator
2
3import (
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
14func token(b byte) string { return base64.RawURLEncoding.EncodeToString(bytes.Repeat([]byte{b}, 32)) }
15func 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}
28func 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}
38func 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}
70func 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}
87func 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}
124func 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