oficina / home Public

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

Plain git — no account needed to clone.

Download

Download this file

1package connect
2
3import (
4 "bytes"
5 "crypto/sha256"
6 "encoding/base64"
7 "encoding/hex"
8 "encoding/json"
9 "fmt"
10 "strings"
11 "testing"
12
13 assertioncrypto "amadan.net/rastrillo/rastrillo/crypto"
14)
15
16const configOrigin = "https://blue.docs.oficina.works"
17
18func homeConfigFixture(t *testing.T) (Credentials, string, []byte) {
19 t.Helper()
20 key, err := assertioncrypto.Generate()
21 if err != nil {
22 t.Fatal(err)
23 }
24 public := key.SignPub()
25 sum := sha256.Sum256(public)
26 id := hex.EncodeToString(sum[:])
27 manifest, err := json.Marshal(map[string]string{id: base64.StdEncoding.EncodeToString(public)})
28 if err != nil {
29 t.Fatal(err)
30 }
31 return Credentials{Issuer: "https://id.oficina.works", ConsumerID: "blue-docs_1", KeysBase64: base64.StdEncoding.EncodeToString(manifest), DEKBase64: base64.StdEncoding.EncodeToString(bytes.Repeat([]byte{17}, 32))}, id, public
32}
33func requireCredentialsRefused(t *testing.T, cfg *Credentials, origin string) {
34 t.Helper()
35 settings, err := DecodeCredentials(cfg, origin)
36 if err != ErrConfig || settings != nil {
37 t.Fatalf("configuration acceptance: settings present=%v, error=%v", settings != nil, err)
38 }
39 if err.Error() != "home connect: invalid configuration" {
40 t.Fatal("configuration error leaked a supplied value")
41 }
42}
43
44func TestCredentialsStandaloneAndValidPublicManifest(t *testing.T) {
45 settings, err := DecodeCredentials(nil, "")
46 if err != nil || settings != nil {
47 t.Fatal("nil configuration changed standalone behavior")
48 }
49 cfg, id, pub := homeConfigFixture(t)
50 settings, err = DecodeCredentials(&cfg, configOrigin)
51 if err != nil || settings == nil {
52 t.Fatal("actual Home key manifest refused", err)
53 }
54 if settings.Issuer != cfg.Issuer || settings.ConsumerID != cfg.ConsumerID || !bytes.Equal(settings.Keys[id], pub) || !bytes.Equal(settings.DEK, bytes.Repeat([]byte{17}, 32)) {
55 t.Fatal("decoded configuration changed values")
56 }
57 // Encoded JSON may contain ordinary JSON whitespace. The outer base64 must
58 // remain canonical; requiring canonical JSON would reject Home's own shape.
59 raw, _ := base64.StdEncoding.DecodeString(cfg.KeysBase64)
60 cfg.KeysBase64 = base64.StdEncoding.EncodeToString(append(append([]byte(" \n"), raw...), []byte("\t ")...))
61 if _, err := DecodeCredentials(&cfg, configOrigin); err != nil {
62 t.Fatal("JSON whitespace refused", err)
63 }
64}
65
66func TestCredentialsRejectsPartialConfiguration(t *testing.T) {
67 cfg, _, _ := homeConfigFixture(t)
68 for _, field := range []string{"all", "issuer", "consumer", "keys", "dek"} {
69 t.Run(field, func(t *testing.T) {
70 broken := cfg
71 switch field {
72 case "all":
73 broken = Credentials{}
74 case "issuer":
75 broken.Issuer = ""
76 case "consumer":
77 broken.ConsumerID = ""
78 case "keys":
79 broken.KeysBase64 = ""
80 case "dek":
81 broken.DEKBase64 = ""
82 }
83 requireCredentialsRefused(t, &broken, configOrigin)
84 })
85 }
86}
87
88func TestCredentialsUsesExactCanonicalAssertionOrigins(t *testing.T) {
89 cfg, _, _ := homeConfigFixture(t)
90 for _, bad := range []string{"", "http://id.oficina.works", " https://id.oficina.works", "https://id.oficina.works ", "https://id.oficina.works/", "https://id.oficina.works/path", "https://id.oficina.works?query=private", "https://id.oficina.works#private", "https://user:secret@id.oficina.works", "https://0x", "https://ID.oficina.works", "https://id.oficina.works:443", "https://id.oficina.works\n"} {
91 t.Run(bad, func(t *testing.T) {
92 broken := cfg
93 broken.Issuer = bad
94 requireCredentialsRefused(t, &broken, configOrigin)
95 requireCredentialsRefused(t, &cfg, bad)
96 })
97 }
98 requireCredentialsRefused(t, &cfg, cfg.Issuer)
99}
100
101func TestCredentialsConsumerIDGrammar(t *testing.T) {
102 cfg, _, _ := homeConfigFixture(t)
103 for _, id := range []string{"", strings.Repeat("x", 65), "docs/one", "docs.one", "docs one", " docs", "docs\n", "dócs", "private?value"} {
104 cfg.ConsumerID = id
105 requireCredentialsRefused(t, &cfg, configOrigin)
106 }
107 cfg.ConsumerID = strings.Repeat("A", 64)
108 if _, err := DecodeCredentials(&cfg, configOrigin); err != nil {
109 t.Fatal("64-byte valid consumer refused", err)
110 }
111}
112
113func TestCredentialsRejectsNoncanonicalOrWrongSizeDEK(t *testing.T) {
114 cfg, _, _ := homeConfigFixture(t)
115 for _, value := range []string{"private-invalid-secret", cfg.DEKBase64 + "\n", " " + cfg.DEKBase64, strings.TrimRight(cfg.DEKBase64, "="), cfg.DEKBase64[:42] + "B=", base64.StdEncoding.EncodeToString(bytes.Repeat([]byte{17}, 31)), base64.StdEncoding.EncodeToString(bytes.Repeat([]byte{17}, 33)), strings.Repeat("A", 100000)} {
116 broken := cfg
117 broken.DEKBase64 = value
118 requireCredentialsRefused(t, &broken, configOrigin)
119 }
120}
121
122func TestCredentialsRejectsAmbiguousKeyManifests(t *testing.T) {
123 cfg, id, pub := homeConfigFixture(t)
124 value := base64.StdEncoding.EncodeToString(pub)
125 quotedID, _ := json.Marshal(id)
126 quotedValue, _ := json.Marshal(value)
127 pair := string(quotedID) + ":" + string(quotedValue)
128 for _, raw := range []string{"", `null`, `{}`, `[]`, `"private-value"`, `{"keys":{}}`, `{"k":null}`, `{"k":1}`, `{"k":[]}`, "{" + pair + "," + pair + "}", "{" + pair + "}{}", "{" + pair + "} private-trailing", `{"private key":"` + value + `"}`, `{"":"` + value + `"}`, `{"` + strings.Repeat("x", 65) + `":"` + value + `"}`, "{\"k\":\"\xff\"}"} {
129 t.Run(fmt.Sprintf("shape-%x", sha256.Sum256([]byte(raw))), func(t *testing.T) {
130 broken := cfg
131 broken.KeysBase64 = base64.StdEncoding.EncodeToString([]byte(raw))
132 requireCredentialsRefused(t, &broken, configOrigin)
133 })
134 }
135 // Equivalent escaped names are duplicates too, not distinct trusted keys.
136 broken := cfg
137 broken.KeysBase64 = base64.StdEncoding.EncodeToString([]byte(`{"k":"` + value + `","\u006b":"` + value + `"}`))
138 requireCredentialsRefused(t, &broken, configOrigin)
139 for _, value := range []string{cfg.KeysBase64 + "\n", " " + cfg.KeysBase64, "private-base64", strings.Repeat("A", base64.StdEncoding.EncodedLen(4096)+4), base64.StdEncoding.EncodeToString([]byte(strings.Repeat(" ", 4097)))} {
140 broken := cfg
141 broken.KeysBase64 = value
142 requireCredentialsRefused(t, &broken, configOrigin)
143 }
144}
145
146func TestCredentialsKeyBoundsAndP256Validation(t *testing.T) {
147 cfg, _, pub := homeConfigFixture(t)
148 for _, encodedKey := range []string{"", "private-invalid-public-key", base64.StdEncoding.EncodeToString(pub) + "\n", base64.StdEncoding.EncodeToString(pub[:64]), base64.StdEncoding.EncodeToString(append(pub, 0)), base64.StdEncoding.EncodeToString(make([]byte, 65)), strings.Repeat("A", 10000)} {
149 raw, _ := json.Marshal(map[string]string{"key": encodedKey})
150 broken := cfg
151 broken.KeysBase64 = base64.StdEncoding.EncodeToString(raw)
152 requireCredentialsRefused(t, &broken, configOrigin)
153 }
154 keys := map[string]string{}
155 for i := 0; i < 8; i++ {
156 keys[fmt.Sprintf("key-%d", i)] = base64.StdEncoding.EncodeToString(pub)
157 }
158 raw, _ := json.Marshal(keys)
159 cfg.KeysBase64 = base64.StdEncoding.EncodeToString(raw)
160 if _, err := DecodeCredentials(&cfg, configOrigin); err != nil {
161 t.Fatal("eight valid keys refused", err)
162 }
163 keys["key-nine"] = base64.StdEncoding.EncodeToString(pub)
164 raw, _ = json.Marshal(keys)
165 cfg.KeysBase64 = base64.StdEncoding.EncodeToString(raw)
166 requireCredentialsRefused(t, &cfg, configOrigin)
167}
168
169func TestEnvironmentCredentialsRefusePartialAndPreserveLiteralValues(t *testing.T) {
170 for _, suffix := range []string{"ISSUER", "CONSUMER_ID", "KEYS_BASE64", "DEK_BASE64"} {
171 t.Setenv("CONNECT_TEST_HOME_"+suffix, "")
172 }
173 if CredentialsFromEnv("CONNECT_TEST") != nil {
174 t.Fatal("empty env enabled Home")
175 }
176 t.Setenv("CONNECT_TEST_HOME_ISSUER", " https://id.example.invalid")
177 if c := CredentialsFromEnv("CONNECT_TEST"); c == nil || c.Issuer != " https://id.example.invalid" {
178 t.Fatal("raw configuration normalized")
179 }
180 if _, err := LoadEnv("CONNECT_TEST", configOrigin); err != ErrConfig {
181 t.Fatal("partial env accepted", err)
182 }
183 valid, _, _ := homeConfigFixture(t)
184 t.Setenv("CONNECT_TEST_HOME_ISSUER", valid.Issuer)
185 t.Setenv("CONNECT_TEST_HOME_CONSUMER_ID", valid.ConsumerID)
186 t.Setenv("CONNECT_TEST_HOME_KEYS_BASE64", valid.KeysBase64)
187 t.Setenv("CONNECT_TEST_HOME_DEK_BASE64", valid.DEKBase64)
188 if _, err := LoadEnv("CONNECT_TEST", configOrigin); err != nil {
189 t.Fatal(err)
190 }
191}
192
193// TestCredentialsDirectoryOriginIsOptionalAndStrict is spec shared-chrome
194// §6.2: apps need the hub directory's origin to link their switcher to
195// it. It is optional (no switcher without it), and when set it must be a
196// bare https origin by Home's own originHost rules, never the issuer.
197func TestCredentialsDirectoryOriginIsOptionalAndStrict(t *testing.T) {
198 cfg, _, _ := homeConfigFixture(t)
199 settings, err := DecodeCredentials(&cfg, configOrigin)
200 if err != nil || settings.Directory != "" {
201 t.Fatal("an absent directory origin changed the configuration", err)
202 }
203 cfg.Directory = "https://oficina.works"
204 settings, err = DecodeCredentials(&cfg, configOrigin)
205 if err != nil || settings.Directory != "https://oficina.works" {
206 t.Fatal("valid directory origin refused", err)
207 }
208 for _, bad := range []string{"http://oficina.works", "https://oficina.works/", "https://Oficina.works", "https://user@oficina.works", "https://oficina.works:443", "https://oficina.works?x", "https://127.0.0.1", "https://localhost", "https://-bad.example", "oficina.works", "https://id.oficina.works", " https://oficina.works"} {
209 c := cfg
210 c.Directory = bad
211 requireCredentialsRefused(t, &c, configOrigin)
212 }
213 t.Setenv("CAL_HOME_ISSUER", cfg.Issuer)
214 t.Setenv("CAL_HOME_CONSUMER_ID", cfg.ConsumerID)
215 t.Setenv("CAL_HOME_KEYS_BASE64", cfg.KeysBase64)
216 t.Setenv("CAL_HOME_DEK_BASE64", cfg.DEKBase64)
217 t.Setenv("HOME_DIRECTORY_ORIGIN", "https://oficina.works")
218 if got := CredentialsFromEnv("CAL"); got == nil || got.Directory != "https://oficina.works" {
219 t.Fatal("HOME_DIRECTORY_ORIGIN not read", got)
220 }
221 for _, k := range []string{"CAL_HOME_ISSUER", "CAL_HOME_CONSUMER_ID", "CAL_HOME_KEYS_BASE64", "CAL_HOME_DEK_BASE64"} {
222 t.Setenv(k, "")
223 }
224 if got := CredentialsFromEnv("CAL"); got != nil {
225 t.Fatal("a directory origin alone switched Home on")
226 }
227}
228