From 5b24c4eb05b2b52c4177d5f41cba30cb68495c8c Mon Sep 17 00:00:00 2001 From: Joel Lee Date: Wed, 13 Mar 2024 08:51:33 +0800 Subject: [PATCH] feat: HTTP Hook - Add custom envconfig decoding for HTTP Hook Secrets (#1467) ## What kind of change does this PR introduce? We represent HTTP Hooks as a `|` separated list, similar to what we do with required password characters. Asymmetric keys are separated by `:` like: `v1a,whpk_mypublickey|whsk_mysecretkey:v1,whsec_mysymettrickey` We opt for `:` and `|` as they are not part of the base64 alphabet which we use to store secrets. Co-authored-by: joel --- internal/conf/configuration.go | 24 +++++++++--- internal/conf/configuration_test.go | 57 +++++++++++++++++++++++++++-- 2 files changed, 73 insertions(+), 8 deletions(-) diff --git a/internal/conf/configuration.go b/internal/conf/configuration.go index b3a5b9613..d23598795 100644 --- a/internal/conf/configuration.go +++ b/internal/conf/configuration.go @@ -28,7 +28,7 @@ var postgresNamesRegexp = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]{0,62}$`) // So this 4 * Math.ceil(24/3) = 32 and 4 * Math.ceil(64/3) = 88 for symmetric secrets // Since Ed25519 key is 32 bytes so we have 4 * Math.ceil(32/3) = 44 var symmetricSecretFormat = regexp.MustCompile(`^v1,whsec_[A-Za-z0-9+/=]{32,88}`) -var asymmetricSecretFormat = regexp.MustCompile(`^v1a,whpk_[A-Za-z0-9+/=]{44,};whsk_[A-Za-z0-9+/=]{44,}$`) +var asymmetricSecretFormat = regexp.MustCompile(`^v1a,whpk_[A-Za-z0-9+/=]{44,}:whsk_[A-Za-z0-9+/=]{44,}$`) // Time is used to represent timestamps in the configuration, as envconfig has // trouble parsing empty strings, due to time.Time.UnmarshalText(). @@ -452,11 +452,25 @@ type HookConfiguration struct { CustomSMSProvider ExtensibilityPointConfiguration `json:"custom_sms_provider" split_words:"true"` } +type HTTPHookSecrets []string + +func (h *HTTPHookSecrets) Decode(value string) error { + parts := strings.Split(value, "|") + for _, part := range parts { + if part != "" { + *h = append(*h, part) + } + } + + return nil +} + type ExtensibilityPointConfiguration struct { - URI string `json:"uri"` - Enabled bool `json:"enabled"` - HookName string `json:"hook_name"` - HTTPHookSecrets []string `json:"secrets"` + URI string `json:"uri"` + Enabled bool `json:"enabled"` + HookName string `json:"hook_name"` + // We use | as a separator for keys and : as a separator for keys within a keypair. For instance: v1,whsec_test|v1a,whpk_myother:v1a,whsk_testkey|v1,whsec_secret3 + HTTPHookSecrets []string `json:"secrets" envconfig:"secrets"` } func (h *HookConfiguration) Validate() error { diff --git a/internal/conf/configuration_test.go b/internal/conf/configuration_test.go index e802b8f43..f881857bf 100644 --- a/internal/conf/configuration_test.go +++ b/internal/conf/configuration_test.go @@ -98,6 +98,57 @@ func TestPasswordRequiredCharactersDecode(t *testing.T) { } } +func TestHTTPHookSecretsDecode(t *testing.T) { + examples := []struct { + Value string + Result []string + }{ + { + Value: "v1,whsec_secret1|v1a,whpk_secrets:whsk_secret2|v1,whsec_secret3", + Result: []string{"v1,whsec_secret1", "v1a,whpk_secrets:whsk_secret2", "v1,whsec_secret3"}, + }, + { + Value: "v1,whsec_singlesecret", + Result: []string{"v1,whsec_singlesecret"}, + }, + { + Value: " ", + Result: []string{" "}, + }, + { + Value: "", + Result: nil, + }, + { + Value: "|a|b|c", + Result: []string{ + "a", + "b", + "c", + }, + }, + { + Value: "||||", + Result: nil, + }, + { + Value: "::", + Result: []string{"::"}, + }, + { + Value: "secret1::secret3", + Result: []string{"secret1::secret3"}, + }, + } + + for i, example := range examples { + var into HTTPHookSecrets + + require.NoError(t, into.Decode(example.Value), "Example %d failed with error", i) + require.Equal(t, []string(into), example.Result, "Example %d got unexpected result", i) + } +} + func TestValidateExtensibilityPointURI(t *testing.T) { cases := []struct { desc string @@ -138,11 +189,11 @@ func TestValidateExtensibilityPointSecrets(t *testing.T) { }{ // Positive test cases {desc: "Valid Symmetric Secret", secret: []string{"v1,whsec_NDYzODhlNTY0ZGI1OWZjYTU2NjMwN2FhYzM3YzBkMWQ0NzVjNWRkNTJmZDU0MGNhYTAzMjVjNjQzMzE3Mjk2Zg====="}, expectError: false}, - {desc: "Valid Asymmetric Secret", secret: []string{"v1a,whpk_NDYzODhlNTY0ZGI1OWZjYTU2NjMwN2FhYzM3YzBkMWQ0NzVjNWRkNTJmZDU0MGNhYTAzMjVjNjQzMzE3Mjk2Zg==;whsk_abc889a6b1160015025064f108a48d6aba1c7c95fa8e304b4d225e8ae0121511"}, expectError: false}, - {desc: "Valid Mix of Symmetric and asymmetric Secret", secret: []string{"v1,whsec_2b49264c90fd15db3bb0e05f4e1547b9c183eb06d585be8a", "v1a,whpk_46388e564db59fca566307aac37c0d1d475c5dd52fd540caa0325c643317296f;whsk_YWJjODg5YTZiMTE2MDAxNTAyNTA2NGYxMDhhNDhkNmFiYTFjN2M5NWZhOGUzMDRiNGQyMjVlOGFlMDEyMTUxMSI="}, expectError: false}, + {desc: "Valid Asymmetric Secret", secret: []string{"v1a,whpk_NDYzODhlNTY0ZGI1OWZjYTU2NjMwN2FhYzM3YzBkMWQ0NzVjNWRkNTJmZDU0MGNhYTAzMjVjNjQzMzE3Mjk2Zg==:whsk_abc889a6b1160015025064f108a48d6aba1c7c95fa8e304b4d225e8ae0121511"}, expectError: false}, + {desc: "Valid Mix of Symmetric and asymmetric Secret", secret: []string{"v1,whsec_2b49264c90fd15db3bb0e05f4e1547b9c183eb06d585be8a", "v1a,whpk_46388e564db59fca566307aac37c0d1d475c5dd52fd540caa0325c643317296f:whsk_YWJjODg5YTZiMTE2MDAxNTAyNTA2NGYxMDhhNDhkNmFiYTFjN2M5NWZhOGUzMDRiNGQyMjVlOGFlMDEyMTUxMSI="}, expectError: false}, // Negative test cases - {desc: "Invalid Asymmetric Secret", secret: []string{"v1a,john;jill", "jill"}, expectError: true}, + {desc: "Invalid Asymmetric Secret", secret: []string{"v1a,john:jill", "jill"}, expectError: true}, {desc: "Invalid Symmetric Secret", secret: []string{"tommy"}, expectError: true}, } for _, tc := range cases {