Skip to content

Commit 6fc60b3

Browse files
authored
Add traffic-mesh signature decoder. (#173)
1 parent d865f09 commit 6fc60b3

File tree

4 files changed

+155
-0
lines changed

4 files changed

+155
-0
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ require (
1010
github.com/bugsnag/bugsnag-go v1.5.3
1111
github.com/bugsnag/panicwrap v1.2.0 // indirect
1212
github.com/confluentinc/confluent-kafka-go v1.4.2
13+
github.com/dgrijalva/jwt-go v3.2.0+incompatible
1314
github.com/go-chi/chi v4.0.2+incompatible
1415
github.com/gofrs/uuid v3.2.0+incompatible // indirect
1516
github.com/google/go-cmp v0.4.1 // indirect

go.sum

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
6969
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
7070
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
7171
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
72+
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
7273
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
7374
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
7475
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=

trafficmesh/signature.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package trafficmesh
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"net/http"
7+
"net/url"
8+
9+
"github.com/dgrijalva/jwt-go"
10+
)
11+
12+
const signatureHeader = "X-NF-Mesh-Signature"
13+
14+
// SignatureDecoder decodes a signed traffic-mesh header.
15+
type SignatureDecoder struct {
16+
secret string
17+
}
18+
19+
// SignaturePayload represents the fields in a traffic-mesh signature.
20+
type SignaturePayload struct {
21+
jwt.StandardClaims
22+
SiteID string `json:"sid,omitempty"`
23+
DeployID string `json:"did,omitempty"`
24+
AccountID string `json:"aid,omitempty"`
25+
URL string `json:"url,omitempty"`
26+
Remapped bool `json:"remapped,omitempty"`
27+
}
28+
29+
// NewSignatureDecoder constructs a new SignatureDecoder. When secret is an empty string,
30+
// DecodeSignature is a no-op.
31+
func NewSignatureDecoder(secret string) *SignatureDecoder {
32+
return &SignatureDecoder{
33+
secret: secret,
34+
}
35+
}
36+
37+
// DecodeSignature decodes a traffic-mesh signature. When either the secret or the header is an
38+
// empty string, this method returns a nil payload and nil error.
39+
func (d *SignatureDecoder) DecodeSignature(req *http.Request) (*SignaturePayload, error) {
40+
if d.secret == "" || req.Header.Get(signatureHeader) == "" {
41+
return nil, nil
42+
}
43+
44+
payload := new(SignaturePayload)
45+
token, err := jwt.ParseWithClaims(req.Header.Get(signatureHeader), payload, func(token *jwt.Token) (interface{}, error) {
46+
alg, ok := token.Method.(*jwt.SigningMethodHMAC)
47+
if !ok || alg != jwt.SigningMethodHS256 {
48+
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
49+
}
50+
return []byte(d.secret), nil
51+
})
52+
if err != nil {
53+
return nil, fmt.Errorf("failed to decode traffic mesh signature: %w", err)
54+
}
55+
if !token.Valid {
56+
return nil, errors.New("invalid token")
57+
}
58+
59+
payloadURL, err := url.Parse(payload.URL)
60+
if err != nil {
61+
return nil, err
62+
}
63+
if payloadURL.Host != req.Host {
64+
return nil, fmt.Errorf("token host %s doesn't match request host: %s", payloadURL.Host, req.Host)
65+
}
66+
if payloadURI, reqURI := payloadURL.RequestURI(), req.URL.RequestURI(); payloadURI != reqURI {
67+
return nil, fmt.Errorf("token uri %s doesn't match request uri: %s", payloadURI, reqURI)
68+
}
69+
70+
return payload, nil
71+
}

trafficmesh/signature_test.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package trafficmesh
2+
3+
import (
4+
"net/http"
5+
"net/http/httptest"
6+
"testing"
7+
8+
"github.com/dgrijalva/jwt-go"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func TestSignatureDecoder(t *testing.T) {
13+
const url = "https://example.org/index.html"
14+
15+
var makeReq = func() *http.Request {
16+
req := httptest.NewRequest(http.MethodGet, "https://192.0.2.1/index.html", nil)
17+
req.Host = "example.org"
18+
return req
19+
}
20+
21+
var addToken = func(req *http.Request, secret, url string) {
22+
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
23+
"sid": "1",
24+
"did": "2",
25+
"url": url,
26+
"remapped": true,
27+
})
28+
sig, err := token.SignedString([]byte(secret))
29+
require.NoError(t, err)
30+
req.Header.Set(signatureHeader, sig)
31+
}
32+
33+
dec := NewSignatureDecoder("secret")
34+
35+
t.Run("no token", func(t *testing.T) {
36+
req := makeReq()
37+
payload, err := dec.DecodeSignature(req)
38+
require.NoError(t, err)
39+
require.Nil(t, payload)
40+
})
41+
42+
t.Run("valid token", func(t *testing.T) {
43+
req := makeReq()
44+
addToken(req, "secret", url)
45+
46+
payload, err := dec.DecodeSignature(req)
47+
require.NoError(t, err)
48+
49+
require.Equal(t, payload.SiteID, "1")
50+
require.Equal(t, payload.DeployID, "2")
51+
})
52+
53+
t.Run("wrong secret", func(t *testing.T) {
54+
req := makeReq()
55+
addToken(req, "wrong-secret", url)
56+
57+
_, err := dec.DecodeSignature(req)
58+
require.Error(t, err)
59+
})
60+
61+
t.Run("wrong path", func(t *testing.T) {
62+
req := makeReq()
63+
addToken(req, "secret", "http://example.org/stuff")
64+
_, err := dec.DecodeSignature(req)
65+
require.Error(t, err)
66+
})
67+
68+
t.Run("wrong host", func(t *testing.T) {
69+
req := makeReq()
70+
addToken(req, "secret", "http://example.net/index.html")
71+
_, err := dec.DecodeSignature(req)
72+
require.Error(t, err)
73+
})
74+
75+
t.Run("remapped flag", func(t *testing.T) {
76+
req := makeReq()
77+
addToken(req, "secret", url)
78+
payload, err := dec.DecodeSignature(req)
79+
require.NoError(t, err)
80+
require.True(t, payload.Remapped)
81+
})
82+
}

0 commit comments

Comments
 (0)