diff --git a/scripts/verify_opslevel_webhook_signature/go/signature.go b/scripts/verify_opslevel_webhook_signature/go/signature.go index 16d7058..9673aef 100644 --- a/scripts/verify_opslevel_webhook_signature/go/signature.go +++ b/scripts/verify_opslevel_webhook_signature/go/signature.go @@ -10,40 +10,51 @@ import ( "strings" ) +const ErrorMissingHeaderPattern = "missing header '%s'" + const ( - HeaderSignatureCanonical = "X-Opslevel-Signature" - HeaderSignature = "X-OpsLevel-Signature" - HeaderTimingCanonical = "X-Opslevel-Timing" - HeaderTiming = "X-OpsLevel-Timing" + HeaderSignature = "X-OpsLevel-Signature" + HeaderTiming = "X-OpsLevel-Timing" + HeaderActionUUID = "X-OpsLevel-Action-Uuid" ) // Search for OpsLevel signature func GetSignatureFromHeader(headers http.Header) (string, error) { - if headers[HeaderSignatureCanonical] == nil { - return "", fmt.Errorf("missing header '%s'", HeaderSignatureCanonical) + signature := headers.Get(HeaderSignature) + + if signature == "" { + return "", fmt.Errorf(ErrorMissingHeaderPattern, HeaderSignature) } - return headers[HeaderSignatureCanonical][0], nil + + return signature, nil } // Build the content to be signed func BuildContent(headers http.Header, additionalHeadersToKeep []string, body []byte) (string, error) { - if headers[HeaderTimingCanonical] == nil { - return "", fmt.Errorf("missing header '%s'", HeaderTimingCanonical) + if headers.Get(HeaderTiming) == "" { + return "", fmt.Errorf(ErrorMissingHeaderPattern, HeaderTiming) } // Build the headers for signature verification - keys := append([]string{HeaderTiming}, additionalHeadersToKeep...) // Make sure we always have X-Opslevel-Timing in there + keys := []string{HeaderTiming} // Make sure we always have X-Opslevel-Timing in there + // Adds X-OpsLevel-Action-Uuid from asynchronous action requests + if headers.Get(HeaderActionUUID) != "" { + keys = append(keys, HeaderActionUUID) + } + for _, additionalHeader := range additionalHeadersToKeep { + keys = append(keys, additionalHeader) + } sort.Strings(keys) // Create the header content portion - headerContent := []string{} + headerContent := make([]string, 0, len(keys)) for _, k := range keys { - headerContent = append(headerContent, k+":"+headers[http.CanonicalHeaderKey(k)][0]) + headerContent = append(headerContent, k+":"+headers.Get(k)) } h := strings.Join(headerContent, ",") // Build content - return fmt.Sprintf("%s+%s", h, string(body[:])), nil + return fmt.Sprintf("%s+%s", h, string(body)), nil } // Verify that the content match the hmacSig. diff --git a/scripts/verify_opslevel_webhook_signature/go/signature_test.go b/scripts/verify_opslevel_webhook_signature/go/signature_test.go index 8955f90..05faaa6 100644 --- a/scripts/verify_opslevel_webhook_signature/go/signature_test.go +++ b/scripts/verify_opslevel_webhook_signature/go/signature_test.go @@ -13,19 +13,19 @@ func TestGetSignatureFromHeader(t *testing.T) { t.Error("expecting error") } - headers[HeaderSignatureCanonical] = []string{"sha256=ab238ca1f60b94dbf50e7b237baf0dc93f02e4ff21736d549f9625d5300f962b"} + headers.Set(HeaderSignature, "sha256=ab238ca1f60b94dbf50e7b237baf0dc93f02e4ff21736d549f9625d5300f962b") hmacSig, _ := GetSignatureFromHeader(headers) - if hmacSig != headers[HeaderSignatureCanonical][0] { - t.Errorf("received sig is different, expected: '%s', received: '%s'", headers[HeaderSignatureCanonical][0], hmacSig) + if hmacSig != headers.Get(HeaderSignature) { + t.Errorf("received sig is different, expected: '%s', received: '%s'", headers.Get(HeaderSignature), hmacSig) } } func TestGetContent(t *testing.T) { headers := make(http.Header) - headers[HeaderTimingCanonical] = []string{"1726164245"} - headers[HeaderSignatureCanonical] = []string{"sha256=ab238ca1f60b94dbf50e7b237baf0dc93f02e4ff21736d549f9625d5300f962b"} + headers.Set(HeaderTiming, "1726164245") + headers.Set(HeaderSignature, "sha256=ab238ca1f60b94dbf50e7b237baf0dc93f02e4ff21736d549f9625d5300f962b") content, err := BuildContent(headers, nil, []byte{}) - expectedContent := fmt.Sprintf("%s:%s+", HeaderTiming, headers[HeaderTimingCanonical][0]) + expectedContent := fmt.Sprintf("%s:%s+", HeaderTiming, headers.Get(HeaderTiming)) if content != expectedContent { t.Errorf("content ('%s') is not what is expected ('%s')", content, expectedContent) } @@ -37,13 +37,14 @@ func TestGetContent(t *testing.T) { func TestGetContentMultiHeader(t *testing.T) { headers := make(http.Header) - headers[HeaderTimingCanonical] = []string{"1726164245"} - headers[HeaderSignatureCanonical] = []string{"sha256=ab238ca1f60b94dbf50e7b237baf0dc93f02e4ff21736d549f9625d5300f962b"} - headers["Anotherheader-Signature"] = []string{"somevalue"} + headers.Set(HeaderTiming, "1726164245") + headers.Set(HeaderSignature, "sha256=ab238ca1f60b94dbf50e7b237baf0dc93f02e4ff21736d549f9625d5300f962b") + headers.Set(HeaderActionUUID, "baddecaf-cafe-badd-ecaf-123456789012") + headers.Set("Anotherheader-Signature", "somevalue") content, err := BuildContent(headers, []string{"Anotherheader-Signature"}, []byte{}) - expectedContent := fmt.Sprintf("Anotherheader-Signature:%s,%s:%s+", headers["Anotherheader-Signature"][0], HeaderTiming, headers[HeaderTimingCanonical][0]) + expectedContent := fmt.Sprintf("Anotherheader-Signature:%s,%s:%s,%s:%s+", headers.Get("Anotherheader-Signature"), HeaderActionUUID, headers.Get(HeaderActionUUID), HeaderTiming, headers.Get(HeaderTiming)) if content != expectedContent { t.Errorf("content ('%s') is not what is expected ('%s')", content, expectedContent) } @@ -55,13 +56,13 @@ func TestGetContentMultiHeader(t *testing.T) { func TestGetContentMultiHeaderWeirdLowercase(t *testing.T) { headers := make(http.Header) - headers[HeaderTimingCanonical] = []string{"1726164245"} - headers[HeaderSignatureCanonical] = []string{"sha256=ab238ca1f60b94dbf50e7b237baf0dc93f02e4ff21736d549f9625d5300f962b"} - headers["Anotherheader-Signature"] = []string{"somevalue"} + headers.Set(HeaderTiming, "1726164245") + headers.Set(HeaderSignature, "sha256=ab238ca1f60b94dbf50e7b237baf0dc93f02e4ff21736d549f9625d5300f962b") + headers.Set("Anotherheader-Signature", "somevalue") content, err := BuildContent(headers, []string{"anotherheader-Signature"}, []byte{}) - expectedContent := fmt.Sprintf("X-OpsLevel-Timing:%s,anotherheader-Signature:%s+", headers[HeaderTimingCanonical][0], headers["Anotherheader-Signature"][0]) + expectedContent := fmt.Sprintf("X-OpsLevel-Timing:%s,anotherheader-Signature:%s+", headers.Get(HeaderTiming), headers.Get("Anotherheader-Signature")) if content != expectedContent { t.Errorf("content ('%s') is not what is expected ('%s')", content, expectedContent) } @@ -73,14 +74,14 @@ func TestGetContentMultiHeaderWeirdLowercase(t *testing.T) { func TestGetContentMultiHeaderMissing(t *testing.T) { headers := make(http.Header) - headers[HeaderTimingCanonical] = []string{"1726164245"} - headers[HeaderSignatureCanonical] = []string{"sha256=ab238ca1f60b94dbf50e7b237baf0dc93f02e4ff21736d549f9625d5300f962b"} - headers["Anotherheader-Signature"] = []string{"somevalue"} + headers.Set(HeaderTiming, "1726164245") + headers.Set(HeaderSignature, "sha256=ab238ca1f60b94dbf50e7b237baf0dc93f02e4ff21736d549f9625d5300f962b") + headers.Set("Anotherheader-Signature", "somevalue") // Test non-canonical header content, err := BuildContent(headers, nil, []byte{}) - expectedContent := fmt.Sprintf("X-OpsLevel-Timing:%s+", headers[HeaderTimingCanonical][0]) + expectedContent := fmt.Sprintf("X-OpsLevel-Timing:%s+", headers.Get(HeaderTiming)) if content != expectedContent { t.Errorf("content ('%s') is not what is expected ('%s')", content, expectedContent) } @@ -92,8 +93,8 @@ func TestGetContentMultiHeaderMissing(t *testing.T) { func TestVerify(t *testing.T) { headers := make(http.Header) - headers[HeaderTimingCanonical] = []string{"1726164245"} - headers[HeaderSignatureCanonical] = []string{"sha256=89649e9d66e0f48c8a6e67fc12197d68dbcb91710391555fcf3a59d8757bf63b"} + headers.Set(HeaderTiming, "1726164245") + headers.Set(HeaderSignature, "sha256=89649e9d66e0f48c8a6e67fc12197d68dbcb91710391555fcf3a59d8757bf63b") content, err := BuildContent(headers, nil, []byte{}) if err != nil { t.Fatalf("there should be no error on GetContent: %s", err) @@ -103,24 +104,24 @@ func TestVerify(t *testing.T) { t.Fatalf("there should be no error on GetSignatureFromHeader: %s", err) } computedSig, _ := Verify(content, hmacSig, "somesecrethere") - if computedSig != headers[HeaderSignatureCanonical][0] { - t.Errorf("computed signature should be equal to header signature, expected: '%s', received: '%s'", computedSig, headers[HeaderSignatureCanonical][0]) + if computedSig != headers.Get(HeaderSignature) { + t.Errorf("computed signature should be equal to header signature, expected: '%s', received: '%s'", computedSig, headers.Get(HeaderSignature)) } } func TestGetContentErrors(t *testing.T) { headers := make(http.Header) - headers[HeaderSignatureCanonical] = []string{"sha256=66eec7a940647ad571944363d4e044e6b39a687c1df8b0808f86b8a4ea085d6e"} + headers.Set(HeaderSignature, "sha256=66eec7a940647ad571944363d4e044e6b39a687c1df8b0808f86b8a4ea085d6e") content, err := BuildContent(headers, nil, []byte{}) if err == nil { - t.Errorf("should have received '%s' missing header errors", HeaderTimingCanonical) + t.Errorf("should have received '%s' missing header errors", HeaderTiming) } if content != "" { t.Errorf("content should be '\"\"', got '%s'", content) } - headers[HeaderTimingCanonical] = []string{"1726164245"} + headers.Set(HeaderTiming, "1726164245") _, err = BuildContent(headers, nil, []byte{}) if err != nil { t.Errorf("should not return any error: %s", err)