Skip to content

Commit

Permalink
Adds HeaderSHA256 predicate
Browse files Browse the repository at this point in the history
`HeaderSHA256` predicate matches SHA-256 hash of the configured header value.

```
$ go test ./predicates/auth/ -run NONE -bench BenchmarkHeaderSHA256Match -benchmem
goos: linux
goarch: amd64
pkg: github.com/zalando/skipper/predicates/auth
cpu: Intel(R) Core(TM) i5-8350U CPU @ 1.70GHz
BenchmarkHeaderSHA256Match-8     3489882               340.6 ns/op            32 B/op          1 allocs/op
```

Signed-off-by: Alexander Yastrebov <[email protected]>
  • Loading branch information
AlexanderYastrebov committed Nov 22, 2021
1 parent 1e26fcd commit bb9d8ec
Show file tree
Hide file tree
Showing 5 changed files with 225 additions and 0 deletions.
47 changes: 47 additions & 0 deletions docs/reference/predicates.md
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,53 @@ JWTPayloadAllKVRegexp("iss", "^https://")
JWTPayloadAnyKVRegexp("iss", "^https://")
```

### HeaderSHA256

Matches if SHA-256 hash of the header value (secret) equals to any of the configured hash values.
Several hash values could be used to match multiple secrets e.g. during secret rotation.

Hash values only hide secrets from parties that have access to the source of Skipper routes.
Authentication strength depends on the strength of the secret value so e.g.
`HeaderSHA256("X-Secret", "2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b")`
is not stronger than just `Header("X-Secret", "secret")`.

Parameters:

* header name (string)
* one or more hex-encoded SHA-256 hash values of [pre-shared keys](https://en.wikipedia.org/wiki/Pre-shared_key) (string)

Examples:

```
// Pre-shared key
HeaderSHA256(
"X-Secret",
"2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b" // SHA256("secret")
) -> inlineContent("ok\n") -> <shunt>
```

```
// To rotate secret:
// * add new secret - both old and new secrets match during rotation
// * update client to use new secret
// * remove old secret
HeaderSHA256(
"X-Secret",
"cba06b5736faf67e54b07b561eae94395e774c517a7d910a54369e1263ccfbd4", // SHA256("old")
"11507a0e2f5e69d5dfa40a62a1bd7b6ee57e6bcd85c67c9b8431b36fff21c437" // SHA256("new")
) -> inlineContent("ok\n") -> <shunt>
```

```
// Basic Auth
anon: * -> setResponseHeader("WWW-Authenticate", `Basic realm="foo", charset="UTF-8"`) -> status(401) -> <shunt>;
auth: HeaderSHA256(
"Authorization",
"caae07e42ed8d231a58edcde95782b0feb67186172c18c89894ce4c2174df137", // SHA256("Basic " + BASE64("test:123£"))
"157da8472590f0ce0a7c651bd79aecb5cc582944fcf76cbabada915d333deee8" // SHA256("Basic " + BASE64("Aladdin:open sesame"))
) -> inlineContent("ok\n") -> <shunt>;
```

## Interval

An interval implements custom predicates to match routes only during some period of time.
Expand Down
76 changes: 76 additions & 0 deletions predicates/auth/headersha256.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package auth

import (
"bytes"
"crypto/sha256"
"encoding/hex"
"net/http"

"github.com/zalando/skipper/predicates"
"github.com/zalando/skipper/routing"
)

type headerSha256Spec struct{}

type headerSha256Predicate struct {
name string
hashes [][]byte
}

// NewHeaderSHA256 creates a predicate specification, whose instances match SHA-256 hash of the header value.
//
// The HeaderSHA256 predicate requires the header name and one or more hex-encoded SHA-256 hash values of the matching header.
func NewHeaderSHA256() routing.PredicateSpec { return &headerSha256Spec{} }

func (*headerSha256Spec) Name() string {
return predicates.HeaderSHA256Name
}

// Create a predicate instance matching SHA256 hash of the header value
func (*headerSha256Spec) Create(args []interface{}) (routing.Predicate, error) {
if len(args) < 2 {
return nil, predicates.ErrInvalidPredicateParameters
}

name, ok := args[0].(string)
if !ok {
return nil, predicates.ErrInvalidPredicateParameters
}

var hashes [][]byte
for _, arg := range args[1:] {
hexHash, ok := arg.(string)
if !ok {
return nil, predicates.ErrInvalidPredicateParameters
}
hash, err := hex.DecodeString(hexHash)
if err != nil {
return nil, err
}
if len(hash) != sha256.Size {
return nil, predicates.ErrInvalidPredicateParameters
}
hashes = append(hashes, hash)
}

return &headerSha256Predicate{name, hashes}, nil
}

func (p *headerSha256Predicate) Match(r *http.Request) bool {
value := r.Header.Get(p.name)
if value == "" {
return false
}

h := sha256.New()
h.Write([]byte(value))
valueHash := h.Sum(nil)

for _, hash := range p.hashes {
if bytes.Equal(valueHash, hash) {
return true
}
}

return false
}
100 changes: 100 additions & 0 deletions predicates/auth/headersha256_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package auth

import (
"net/http"
"testing"
)

func TestHeaderSHA256Args(t *testing.T) {
s := NewHeaderSHA256()
for _, tc := range []struct {
args []interface{}
}{
{
args: []interface{}{},
},
{
args: []interface{}{"X-Secret", "xyz"},
},
{
args: []interface{}{"X-Secret", "00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF", "AA"},
},
} {
if _, err := s.Create(tc.args); err == nil {
t.Errorf("expected error for arguments: %v", tc.args)
}
}
}

func TestHeaderSHA256Match(t *testing.T) {
s := NewHeaderSHA256()
p, err := s.Create([]interface{}{
"X-Secret",
"2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b", // "secret"
"5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8", // "password"
})
if err != nil {
t.Fatal(err)
}
for _, tc := range []struct {
header http.Header
match bool
}{
{
header: http.Header{
"X-Test": []string{"foo"},
},
match: false,
},
{
header: http.Header{
"X-Secret": []string{"foo"},
},
match: false,
},
{
header: http.Header{
"X-Secret": []string{"SECRET"},
},
match: false,
},
{
header: http.Header{
"X-Secret": []string{"PASSWORD"},
},
match: false,
},
{
header: http.Header{
"X-Secret": []string{"secret"},
},
match: true,
},
{
header: http.Header{
"X-Secret": []string{"password"},
},
match: true,
},
} {
if p.Match(&http.Request{Header: tc.header}) != tc.match {
t.Errorf("expected match: %v", tc.match)
}
}
}

func BenchmarkHeaderSHA256Match(b *testing.B) {
s := NewHeaderSHA256()
p, err := s.Create([]interface{}{"X-Secret", "2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b"})
if err != nil {
b.Fatal(err)
}
r := &http.Request{Header: http.Header{"X-Secret": []string{"secret"}}}
if !p.Match(r) {
b.Fatal("match expected")
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
p.Match(r)
}
}
1 change: 1 addition & 0 deletions predicates/predicates.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const (
JWTPayloadAllKVName = "JWTPayloadAllKV"
JWTPayloadAnyKVRegexpName = "JWTPayloadAnyKVRegexp"
JWTPayloadAllKVRegexpName = "JWTPayloadAllKVRegexp"
HeaderSHA256Name = "HeaderSHA256"
AfterName = "After"
BeforeName = "Before"
BetweenName = "Between"
Expand Down
1 change: 1 addition & 0 deletions skipper.go
Original file line number Diff line number Diff line change
Expand Up @@ -1482,6 +1482,7 @@ func run(o Options, sig chan os.Signal, idleConnsCH chan struct{}) error {
pauth.NewJWTPayloadAnyKV(),
pauth.NewJWTPayloadAllKVRegexp(),
pauth.NewJWTPayloadAnyKVRegexp(),
pauth.NewHeaderSHA256(),
methods.New(),
tee.New(),
forwarded.NewForwardedHost(),
Expand Down

0 comments on commit bb9d8ec

Please sign in to comment.