Skip to content

Commit

Permalink
Add Support for Signature Validation via HTTP Header
Browse files Browse the repository at this point in the history
* Implement HTTP PKI annotator
* Implement HTTP request parser
* Implement related unit tests
* Made VerifySignature and DeriveHash public and updated their uses

Fix #25

Signed-off-by: husseinfakharany <fakharany.hussein@gmail.com>
  • Loading branch information
husseinfakharany authored and tsconn23 committed Feb 18, 2022
1 parent 4119b48 commit 1519dec
Show file tree
Hide file tree
Showing 17 changed files with 671 additions and 24 deletions.
6 changes: 3 additions & 3 deletions internal/annotators/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import (
"io/ioutil"
)

func deriveHash(hash contracts.HashType, data []byte) string {
func DeriveHash(hash contracts.HashType, data []byte) string {
var h hashprovider.Provider
switch hash {
case contracts.MD5Hash:
Expand All @@ -41,7 +41,7 @@ func deriveHash(hash contracts.HashType, data []byte) string {
return h.Derive(data)
}

func signAnnotation(key config.KeyInfo, a contracts.Annotation) (string, error) {
func SignAnnotation(key config.KeyInfo, a contracts.Annotation) (string, error) {
var s signprovider.Provider
switch key.Type {
case contracts.KeyEd25519:
Expand All @@ -64,7 +64,7 @@ func signAnnotation(key config.KeyInfo, a contracts.Annotation) (string, error)
return signed, nil
}

func verifySignature(key config.KeyInfo, src contracts.Annotation) (bool, error) {
func VerifySignature(key config.KeyInfo, src contracts.Annotation) (bool, error) {
var s signprovider.Provider
switch key.Type {
case contracts.KeyEd25519:
Expand Down
4 changes: 2 additions & 2 deletions internal/annotators/base_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func TestDeriveHash(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := deriveHash(tt.hashType, tt.input)
result := DeriveHash(tt.hashType, tt.input)
assert.Equal(t, tt.output, result)
})
}
Expand Down Expand Up @@ -80,7 +80,7 @@ func TestSignAnnotation(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := signAnnotation(tt.cfg, tt.annotation)
result, err := SignAnnotation(tt.cfg, tt.annotation)
test.CheckError(err, tt.expectError, tt.name, t)

if err == nil {
Expand Down
40 changes: 40 additions & 0 deletions internal/annotators/http/constants.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*******************************************************************************
* Copyright 2022 Dell Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*******************************************************************************/

package http

type specialtyComponent string

const (
method specialtyComponent = "@method"
authority specialtyComponent = "@authority"
scheme specialtyComponent = "@scheme"
requestTarget specialtyComponent = "@request-target"
path specialtyComponent = "@path"
query specialtyComponent = "@query"
queryParams specialtyComponent = "@query-params"
)

const (
contentLength string = "Content-Length"
contentType string = "Content-Type"
testRequest string = "testRequest"
)

func (s specialtyComponent) Validate() bool {
if s == method || s == authority || s == scheme || s == requestTarget || s == path || s == query || s == queryParams {
return true
}
return false
}
141 changes: 141 additions & 0 deletions internal/annotators/http/parser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/*******************************************************************************
* Copyright 2022 Dell Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*******************************************************************************/

package http

import (
"bytes"
"fmt"
"net/http"
"strings"
)

type parseResult struct {
Seed string
Signature string
Keyid string
Algorithm string
}

func RemoveExtraSpaces(s string) string {
return strings.Join(strings.Fields(s), " ")
}

func parseRequest(r *http.Request) (parseResult, error) {
//Signature Inputs extraction
signatureInput := r.Header.Get("Signature-Input")
signature := r.Header.Get("Signature")

signatureInputList := strings.SplitN(signatureInput, ";", 2)

signatureInputHeader := strings.Fields(signatureInputList[0])
signatureInputTail := signatureInputList[1]

var keyid, algorithm string

signatureInputParsedTail := strings.Split(signatureInputTail, ";")
for _, s := range signatureInputParsedTail {

if strings.Contains(s, "alg") {
raw := strings.Split(s, "=")[1]
algorithm = strings.Trim(raw, "\"")
}

if strings.Contains(s, "keyid") {
raw := strings.Split(s, "=")[1]
keyid = strings.Trim(raw, "\"")
}
}

signatureInputFields := make(map[string][]string)

parsedSignatureInput := ""
var s parseResult

for _, field := range signatureInputHeader {
//remove double quotes from the field to access it directly in the header map
key := field[1 : len(field)-1]
if key[0:1] == "@" {
switch specialtyComponent(key) {
case method:
signatureInputFields[key] = []string{r.Method}
case authority:
signatureInputFields[key] = []string{r.Host}
case scheme:
protool := r.Proto
scheme := strings.ToLower(strings.Split(protool, "/")[0])
signatureInputFields[key] = []string{scheme}
case requestTarget:
signatureInputFields[key] = []string{r.RequestURI}
case path:
signatureInputFields[key] = []string{r.URL.Path}
case query:
var query string = "?"
query += r.URL.RawQuery
signatureInputFields[key] = []string{query}
case queryParams:
rawQueryParams := strings.Split(r.URL.RawQuery, "&")
var queryParams []string
for _, rawQueryParam := range rawQueryParams {
if rawQueryParam != "" {
parameter := strings.Split(rawQueryParam, "=")
name := parameter[0]
value := parameter[1]
b := new(bytes.Buffer)
fmt.Fprintf(b, ";name=\"%s\": %s", name, value)
queryParams = append(queryParams, b.String())
}
}
signatureInputFields[key] = queryParams
default:
return s, fmt.Errorf("Unhandled Specialty Component %s", key)
}
} else {
fieldValues := r.Header.Values(key)

if len(fieldValues) == 0 {
return s, fmt.Errorf("Unhandled Specialty Component %s", key)
} else if len(fieldValues) == 1 {
value := RemoveExtraSpaces(r.Header.Get(key))
signatureInputFields[key] = []string{value}

} else {

value := ""
for i := 0; i < len(fieldValues); i++ {
value += fieldValues[i]
if i != (len(fieldValues) - 1) {
value += ", "
}
}
value = RemoveExtraSpaces(value)
signatureInputFields[key] = []string{value}
}
}
// Construct final output string
keyValues := signatureInputFields[key]
if len(keyValues) == 1 {
parsedSignatureInput += ("\"" + key + "\" " + keyValues[0] + "\n")
} else {
for _, v := range keyValues {
parsedSignatureInput += ("\"" + key + "\"" + v + "\n")
}
}
}

parsedSignatureInput = fmt.Sprintf("%s;%s", parsedSignatureInput, signatureInputTail)
s = parseResult{Seed: parsedSignatureInput, Signature: signature, Keyid: keyid, Algorithm: algorithm}

return s, nil
}
103 changes: 103 additions & 0 deletions internal/annotators/http/parser_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*******************************************************************************
* Copyright 2022 Dell Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*******************************************************************************/

package http

import (
"encoding/json"
"io/ioutil"
"net/http/httptest"
"testing"

"github.com/project-alvarium/alvarium-sdk-go/pkg/config"
"github.com/stretchr/testify/assert"
)

func TestHttpPkiAnnotator_RequestParser(t *testing.T) {
b, err := ioutil.ReadFile("./test/config.json")
if err != nil {
t.Fatalf(err.Error())
}

var cfg config.SdkInfo
err = json.Unmarshal(b, &cfg)
if err != nil {
t.Fatalf(err.Error())
}

base := httptest.NewRequest("POST", "/foo?var1=&var2=2", nil)

base.Header.Set("Host", "example.com")
base.Header.Set("Date", "Tue, 20 Apr 2021 02:07:55 GMT")
base.Header.Set("Content-Type", "application/json")
base.Header.Set("Content-Length", "18")
base.Header.Set("Signature", "whatever")

seedTests := []struct {
name string
signatureInput string
expectedSeed string
expectError bool
}{
{"testing integeration of all Signature-Input fields",
"\"date\" \"@method\" \"@path\" \"@authority\" \"content-type\" \"content-length\" \"@query-params\" \"@query\";created=1644758607;keyid=\"public.key\";alg=\"ed25519\";",
"\"date\" Tue, 20 Apr 2021 02:07:55 GMT\n\"@method\" POST\n\"@path\" /foo\n\"@authority\" example.com\n\"content-type\" application/json\n\"content-length\" 18\n\"@query-params\";name=\"var1\": \n\"@query-params\";name=\"var2\": 2\n\"@query\" ?var1=&var2=2\n;created=1644758607;keyid=\"public.key\";alg=\"ed25519\";", false},

{"testing @method", "\"@method\";", "\"@method\" POST\n;", false},
{"testing @authority", "\"@authority\";", "\"@authority\" example.com\n;", false},

{"testing @scheme", "\"@scheme\";", "\"@scheme\" http\n;", false},
{"testing @request-target", "\"@request-target\";", "\"@request-target\" /foo?var1=&var2=2\n;", false},

{"testing @path", "\"@path\";", "\"@path\" /foo\n;", false},

{"testing @query", "\"@query\";", "\"@query\" ?var1=&var2=2\n;", false},
{"testing @query-params", "\"@query-params\";", "\"@query-params\";name=\"var1\": \n\"@query-params\";name=\"var2\": 2\n;", false},

{"testing non-existant derived component", "\"@x-test\";", "", true},
{"testing non-existant header field", "\"x-test\";", "", true},
}

for _, tt := range seedTests {

req := base.Clone(base.Context())
req.Header.Set("Signature-Input", tt.signatureInput)

t.Run(tt.name, func(t *testing.T) {
signatureInfo, err := parseRequest(req)
if tt.expectError {
assert.Error(t, err)
} else {
assert.Equal(t, tt.expectedSeed, signatureInfo.Seed)
}
})

}

req := base.Clone(base.Context())
req.Header.Set("Signature-Input", "\"@query\";created=1644758607;keyid=\"public.key\";alg=\"ed25519\";")
parsed, err := parseRequest(req)

t.Run("testing signature", func(t *testing.T) {
assert.Equal(t, "whatever", parsed.Signature)
})

t.Run("testing keyid", func(t *testing.T) {
assert.Equal(t, "public.key", parsed.Keyid)
})

t.Run("testing algorithm", func(t *testing.T) {
assert.Equal(t, "ed25519", parsed.Algorithm)
})
}
Loading

0 comments on commit 1519dec

Please sign in to comment.