| 1 | package docs |
| 2 | |
| 3 | import ( |
| 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 | |
| 16 | const configOrigin = "https://blue.docs.oficina.works" |
| 17 | |
| 18 | func homeConfigFixture(t *testing.T) (HomeConfig, 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 HomeConfig{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 | } |
| 33 | func requireHomeConfigRefused(t *testing.T, cfg *HomeConfig, origin string) { |
| 34 | t.Helper() |
| 35 | settings, err := validatedHomeConfig(cfg, origin) |
| 36 | if err != errHomeConfig || settings != nil { |
| 37 | t.Fatalf("configuration acceptance: settings present=%v, error=%v", settings != nil, err) |
| 38 | } |
| 39 | if err.Error() != "docs: invalid Home configuration" { |
| 40 | t.Fatal("configuration error leaked a supplied value") |
| 41 | } |
| 42 | } |
| 43 | |
| 44 | func TestHomeConfigStandaloneAndValidPublicManifest(t *testing.T) { |
| 45 | settings, err := validatedHomeConfig(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 = validatedHomeConfig(&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 := validatedHomeConfig(&cfg, configOrigin); err != nil { |
| 62 | t.Fatal("JSON whitespace refused", err) |
| 63 | } |
| 64 | } |
| 65 | |
| 66 | func TestHomeConfigRejectsPartialConfiguration(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 = HomeConfig{} |
| 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 | requireHomeConfigRefused(t, &broken, configOrigin) |
| 84 | }) |
| 85 | } |
| 86 | } |
| 87 | |
| 88 | func TestHomeConfigUsesExactCanonicalAssertionOrigins(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 | requireHomeConfigRefused(t, &broken, configOrigin) |
| 95 | requireHomeConfigRefused(t, &cfg, bad) |
| 96 | }) |
| 97 | } |
| 98 | requireHomeConfigRefused(t, &cfg, cfg.Issuer) |
| 99 | } |
| 100 | |
| 101 | func TestHomeConfigConsumerIDGrammar(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 | requireHomeConfigRefused(t, &cfg, configOrigin) |
| 106 | } |
| 107 | cfg.ConsumerID = strings.Repeat("A", 64) |
| 108 | if _, err := validatedHomeConfig(&cfg, configOrigin); err != nil { |
| 109 | t.Fatal("64-byte valid consumer refused", err) |
| 110 | } |
| 111 | } |
| 112 | |
| 113 | func TestHomeConfigRejectsNoncanonicalOrWrongSizeDEK(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 | requireHomeConfigRefused(t, &broken, configOrigin) |
| 119 | } |
| 120 | } |
| 121 | |
| 122 | func TestHomeConfigRejectsAmbiguousKeyManifests(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 | requireHomeConfigRefused(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 | requireHomeConfigRefused(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 | requireHomeConfigRefused(t, &broken, configOrigin) |
| 143 | } |
| 144 | } |
| 145 | |
| 146 | func TestHomeConfigKeyBoundsAndP256Validation(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 | requireHomeConfigRefused(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 := validatedHomeConfig(&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 | requireHomeConfigRefused(t, &cfg, configOrigin) |
| 167 | } |
| 168 | |