Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 24 additions & 13 deletions scripts/verify_opslevel_webhook_signature/go/signature.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
51 changes: 26 additions & 25 deletions scripts/verify_opslevel_webhook_signature/go/signature_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -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)
}
Expand All @@ -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)
}
Expand All @@ -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)
}
Expand All @@ -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)
Expand All @@ -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)
Expand Down