package connect import ( "bytes" "crypto/sha256" "encoding/base64" "encoding/hex" "encoding/json" "fmt" "strings" "testing" assertioncrypto "amadan.net/rastrillo/rastrillo/crypto" ) const configOrigin = "https://blue.docs.oficina.works" func homeConfigFixture(t *testing.T) (Credentials, string, []byte) { t.Helper() key, err := assertioncrypto.Generate() if err != nil { t.Fatal(err) } public := key.SignPub() sum := sha256.Sum256(public) id := hex.EncodeToString(sum[:]) manifest, err := json.Marshal(map[string]string{id: base64.StdEncoding.EncodeToString(public)}) if err != nil { t.Fatal(err) } 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 } func requireCredentialsRefused(t *testing.T, cfg *Credentials, origin string) { t.Helper() settings, err := DecodeCredentials(cfg, origin) if err != ErrConfig || settings != nil { t.Fatalf("configuration acceptance: settings present=%v, error=%v", settings != nil, err) } if err.Error() != "home connect: invalid configuration" { t.Fatal("configuration error leaked a supplied value") } } func TestCredentialsStandaloneAndValidPublicManifest(t *testing.T) { settings, err := DecodeCredentials(nil, "") if err != nil || settings != nil { t.Fatal("nil configuration changed standalone behavior") } cfg, id, pub := homeConfigFixture(t) settings, err = DecodeCredentials(&cfg, configOrigin) if err != nil || settings == nil { t.Fatal("actual Home key manifest refused", err) } if settings.Issuer != cfg.Issuer || settings.ConsumerID != cfg.ConsumerID || !bytes.Equal(settings.Keys[id], pub) || !bytes.Equal(settings.DEK, bytes.Repeat([]byte{17}, 32)) { t.Fatal("decoded configuration changed values") } // Encoded JSON may contain ordinary JSON whitespace. The outer base64 must // remain canonical; requiring canonical JSON would reject Home's own shape. raw, _ := base64.StdEncoding.DecodeString(cfg.KeysBase64) cfg.KeysBase64 = base64.StdEncoding.EncodeToString(append(append([]byte(" \n"), raw...), []byte("\t ")...)) if _, err := DecodeCredentials(&cfg, configOrigin); err != nil { t.Fatal("JSON whitespace refused", err) } } func TestCredentialsRejectsPartialConfiguration(t *testing.T) { cfg, _, _ := homeConfigFixture(t) for _, field := range []string{"all", "issuer", "consumer", "keys", "dek"} { t.Run(field, func(t *testing.T) { broken := cfg switch field { case "all": broken = Credentials{} case "issuer": broken.Issuer = "" case "consumer": broken.ConsumerID = "" case "keys": broken.KeysBase64 = "" case "dek": broken.DEKBase64 = "" } requireCredentialsRefused(t, &broken, configOrigin) }) } } func TestCredentialsUsesExactCanonicalAssertionOrigins(t *testing.T) { cfg, _, _ := homeConfigFixture(t) 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"} { t.Run(bad, func(t *testing.T) { broken := cfg broken.Issuer = bad requireCredentialsRefused(t, &broken, configOrigin) requireCredentialsRefused(t, &cfg, bad) }) } requireCredentialsRefused(t, &cfg, cfg.Issuer) } func TestCredentialsConsumerIDGrammar(t *testing.T) { cfg, _, _ := homeConfigFixture(t) for _, id := range []string{"", strings.Repeat("x", 65), "docs/one", "docs.one", "docs one", " docs", "docs\n", "dócs", "private?value"} { cfg.ConsumerID = id requireCredentialsRefused(t, &cfg, configOrigin) } cfg.ConsumerID = strings.Repeat("A", 64) if _, err := DecodeCredentials(&cfg, configOrigin); err != nil { t.Fatal("64-byte valid consumer refused", err) } } func TestCredentialsRejectsNoncanonicalOrWrongSizeDEK(t *testing.T) { cfg, _, _ := homeConfigFixture(t) 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)} { broken := cfg broken.DEKBase64 = value requireCredentialsRefused(t, &broken, configOrigin) } } func TestCredentialsRejectsAmbiguousKeyManifests(t *testing.T) { cfg, id, pub := homeConfigFixture(t) value := base64.StdEncoding.EncodeToString(pub) quotedID, _ := json.Marshal(id) quotedValue, _ := json.Marshal(value) pair := string(quotedID) + ":" + string(quotedValue) 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\"}"} { t.Run(fmt.Sprintf("shape-%x", sha256.Sum256([]byte(raw))), func(t *testing.T) { broken := cfg broken.KeysBase64 = base64.StdEncoding.EncodeToString([]byte(raw)) requireCredentialsRefused(t, &broken, configOrigin) }) } // Equivalent escaped names are duplicates too, not distinct trusted keys. broken := cfg broken.KeysBase64 = base64.StdEncoding.EncodeToString([]byte(`{"k":"` + value + `","\u006b":"` + value + `"}`)) requireCredentialsRefused(t, &broken, configOrigin) 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)))} { broken := cfg broken.KeysBase64 = value requireCredentialsRefused(t, &broken, configOrigin) } } func TestCredentialsKeyBoundsAndP256Validation(t *testing.T) { cfg, _, pub := homeConfigFixture(t) 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)} { raw, _ := json.Marshal(map[string]string{"key": encodedKey}) broken := cfg broken.KeysBase64 = base64.StdEncoding.EncodeToString(raw) requireCredentialsRefused(t, &broken, configOrigin) } keys := map[string]string{} for i := 0; i < 8; i++ { keys[fmt.Sprintf("key-%d", i)] = base64.StdEncoding.EncodeToString(pub) } raw, _ := json.Marshal(keys) cfg.KeysBase64 = base64.StdEncoding.EncodeToString(raw) if _, err := DecodeCredentials(&cfg, configOrigin); err != nil { t.Fatal("eight valid keys refused", err) } keys["key-nine"] = base64.StdEncoding.EncodeToString(pub) raw, _ = json.Marshal(keys) cfg.KeysBase64 = base64.StdEncoding.EncodeToString(raw) requireCredentialsRefused(t, &cfg, configOrigin) } func TestEnvironmentCredentialsRefusePartialAndPreserveLiteralValues(t *testing.T) { for _, suffix := range []string{"ISSUER", "CONSUMER_ID", "KEYS_BASE64", "DEK_BASE64"} { t.Setenv("CONNECT_TEST_HOME_"+suffix, "") } if CredentialsFromEnv("CONNECT_TEST") != nil { t.Fatal("empty env enabled Home") } t.Setenv("CONNECT_TEST_HOME_ISSUER", " https://id.example.invalid") if c := CredentialsFromEnv("CONNECT_TEST"); c == nil || c.Issuer != " https://id.example.invalid" { t.Fatal("raw configuration normalized") } if _, err := LoadEnv("CONNECT_TEST", configOrigin); err != ErrConfig { t.Fatal("partial env accepted", err) } valid, _, _ := homeConfigFixture(t) t.Setenv("CONNECT_TEST_HOME_ISSUER", valid.Issuer) t.Setenv("CONNECT_TEST_HOME_CONSUMER_ID", valid.ConsumerID) t.Setenv("CONNECT_TEST_HOME_KEYS_BASE64", valid.KeysBase64) t.Setenv("CONNECT_TEST_HOME_DEK_BASE64", valid.DEKBase64) if _, err := LoadEnv("CONNECT_TEST", configOrigin); err != nil { t.Fatal(err) } } // TestCredentialsDirectoryOriginIsOptionalAndStrict is spec shared-chrome // §6.2: apps need the hub directory's origin to link their switcher to // it. It is optional (no switcher without it), and when set it must be a // bare https origin by Home's own originHost rules, never the issuer. func TestCredentialsDirectoryOriginIsOptionalAndStrict(t *testing.T) { cfg, _, _ := homeConfigFixture(t) settings, err := DecodeCredentials(&cfg, configOrigin) if err != nil || settings.Directory != "" { t.Fatal("an absent directory origin changed the configuration", err) } cfg.Directory = "https://oficina.works" settings, err = DecodeCredentials(&cfg, configOrigin) if err != nil || settings.Directory != "https://oficina.works" { t.Fatal("valid directory origin refused", err) } 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"} { c := cfg c.Directory = bad requireCredentialsRefused(t, &c, configOrigin) } t.Setenv("CAL_HOME_ISSUER", cfg.Issuer) t.Setenv("CAL_HOME_CONSUMER_ID", cfg.ConsumerID) t.Setenv("CAL_HOME_KEYS_BASE64", cfg.KeysBase64) t.Setenv("CAL_HOME_DEK_BASE64", cfg.DEKBase64) t.Setenv("HOME_DIRECTORY_ORIGIN", "https://oficina.works") if got := CredentialsFromEnv("CAL"); got == nil || got.Directory != "https://oficina.works" { t.Fatal("HOME_DIRECTORY_ORIGIN not read", got) } for _, k := range []string{"CAL_HOME_ISSUER", "CAL_HOME_CONSUMER_ID", "CAL_HOME_KEYS_BASE64", "CAL_HOME_DEK_BASE64"} { t.Setenv(k, "") } if got := CredentialsFromEnv("CAL"); got != nil { t.Fatal("a directory origin alone switched Home on") } }