Skip to content

Commit

Permalink
Adds HeaderSHA256 predicate (#1905)
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     4675351               237.2 ns/op             0 B/op          0 allocs/op
```

Signed-off-by: Alexander Yastrebov <[email protected]>
  • Loading branch information
AlexanderYastrebov authored Dec 8, 2022
1 parent 9fea7d6 commit c9260a4
Show file tree
Hide file tree
Showing 5 changed files with 251 additions and 0 deletions.
72 changes: 72 additions & 0 deletions docs/reference/predicates.md
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,78 @@ JWTPayloadAllKVRegexp("iss", "^https://")
JWTPayloadAnyKVRegexp("iss", "^https://")
```

### HeaderSHA256

Matches if SHA-256 hash of the header value (known as [pre-shared key](https://en.wikipedia.org/wiki/Pre-shared_key) or 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")`.

The secret value must be kept secret, must be used by a single client and must be rotated periodically.
See below how to generate random secret value using [OpenSSL](https://www.openssl.org/docs/man1.1.1/man1/rand.html).

Parameters:

* header name (string)
* one or more hex-encoded SHA-256 hashes of the matching header values (string)

Secure secret value example:
```sh
#
# 1. Generate cryptographically secure pseudo random secret header value:
# - length of at least 32 bytes (the size of the SHA-256 output)
# - encode as -base64 or -hex to get ASCII text value
#
SECRET=$(openssl rand -base64 32)
echo $SECRET
3YchPsliGjBXvyl/ncLWEI8/loKGrj/VNM4garxWEmA=

#
# 2. Get SHA-256 hash of the secret header value to use as HeaderSHA256 argument:
# - use echo -n to not output the trailing newline
#
echo -n $SECRET | sha256sum
a6131ba920df753c8109500cc11818f7192336d06532f6fa13009c2e4f6e1841 -
```
```
// 3. Configure route to match hash of the secret value
HeaderSHA256(
"X-Secret",
"a6131ba920df753c8109500cc11818f7192336d06532f6fa13009c2e4f6e1841"
) -> inlineContent("ok\n") -> <shunt>
```
```sh
# 4. Test secret value
curl -H "X-Secret: $SECRET" http://localhost:9090
```

Secret rotation example:
```
// 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 access authentication](https://en.wikipedia.org/wiki/Basic_access_authentication) example:
```
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
74 changes: 74 additions & 0 deletions predicates/auth/headersha256.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package auth

import (
"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 [][sha256.Size]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 [][sha256.Size]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, *(*[sha256.Size]byte)(hash)) // https://github.com/golang/go/issues/46505
}

return &headerSha256Predicate{name, hashes}, nil
}

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

valueHash := sha256.Sum256([]byte(value))

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

return false
}
103 changes: 103 additions & 0 deletions predicates/auth/headersha256_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
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", "00112233"},
},
{
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 @@ -33,6 +33,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 @@ -1733,6 +1733,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 c9260a4

Please sign in to comment.