Compare commits

...

10 Commits

Author SHA1 Message Date
91908f15e7 Adds NewRouter 2015-08-26 14:08:18 +02:00
f64f225b18 Adds NewJWT 2015-08-26 14:08:02 +02:00
336987e806 Adds a jwt middleware
It is not certain that it is final, as the jwt is just an interface{}
2015-08-18 19:46:15 +02:00
26b7356b79 Adds more unit testing
Some basic security check for tempering of JWS.
2015-08-18 19:45:41 +02:00
abe3cbab1f Adds unit testing for jws 2015-08-18 18:34:23 +02:00
00c5853b45 Adds JWS encode and decode
It also adds interfaces Signer and HS256 and RS256 implementation.
2015-08-18 16:11:56 +02:00
4eb3bdaa13 Adds an internal library for JWS / JWT encoding 2015-08-17 13:51:27 +02:00
dc107d934d Lints error_formatter.go 2015-08-13 12:05:01 +02:00
4785e489d5 Adds recovery test 2015-08-13 10:43:01 +02:00
5a9b19fcb1 Removes unuseful stub code 2015-08-13 10:17:20 +02:00
16 changed files with 1154 additions and 16 deletions

View File

@@ -6,4 +6,4 @@ build:
check:
go test -coverprofile=cover.out -covermode=count
go vet
# golint
golint

View File

@@ -11,8 +11,13 @@ type narcoKey int
const errorFormatterKey narcoKey = 1
// ErrorFormatter defines a function that can format an HTTP1.1 error
// and is but is Context-aware
type ErrorFomatter func(ctx context.Context, rw http.ResponseWriter, message interface{}, status int)
// Error is formatting a in a http.Response an error, like
// http.Error. However it uses the ErrorFormatter in the current
// context to do so, if one is defined with WithErrorFormatter
func Error(ctx context.Context, rw http.ResponseWriter, message interface{}, status int) {
fmter, ok := ctx.Value(errorFormatterKey).(ErrorFomatter)
if ok == false {
@@ -25,18 +30,25 @@ func Error(ctx context.Context, rw http.ResponseWriter, message interface{}, sta
var narcoDefaultFormatter ErrorFomatter
// TextErrorFormatter is an ErrorFormatter that formats the error by
// just putting plain text
func TextErrorFormatter(ctx context.Context, rw http.ResponseWriter, message interface{}, status int) {
rw.Header()["Content-Type"] = []string{"text/plain; charset=utf-8"}
rw.WriteHeader(status)
fmt.Fprintf(rw, "%s", message)
}
// BasicHTMLErrorFormatter is an ErrorFormatter that formats the error
// in a very basic HTML page (you may want to write your own with a
// choosen template.
func BasicHTMLErrorFormatter(ctx context.Context, rw http.ResponseWriter, message interface{}, status int) {
rw.Header()["Content-Type"] = []string{"text/html; charset=utf-8"}
rw.WriteHeader(status)
fmt.Fprintf(rw, `<!doctype html><html><body><h1>Error %d</h1>%s</body></html>`, status, message)
}
// WithErrorFormatter is returning a context where the user has
// defined an ErrorFormatter that Error() should use.
func WithErrorFormatter(ctx context.Context, fmter ErrorFomatter) context.Context {
return context.WithValue(ctx, errorFormatterKey, fmter)
}

80
jwt.go Normal file
View File

@@ -0,0 +1,80 @@
package narco
import (
"fmt"
"net/http"
"strings"
"github.com/codemodus/chain"
"golang.org/x/net/context"
"ponyo.epfl.ch/gitlab/alexandre.tuleu/narco/jwt"
)
type JWT struct {
signer jwt.Signer
}
const (
jwtKeyGenerator narcoKey = 2
jwtKeyOutput narcoKey = 3
)
func (j *JWT) Wrap() func(chain.Handler) chain.Handler {
return func(other chain.Handler) chain.Handler {
return chain.HandlerFunc(func(ctx context.Context, rw http.ResponseWriter, req *http.Request) {
tokenGenerator, ok := ctx.Value(jwtKeyGenerator).(TokenCreator)
if tokenGenerator == nil || ok == false {
//we did not register a token we ignore all processing
other.ServeHTTPContext(ctx, rw, req)
return
}
// check for the header
tokenStr, ok := req.Header["Authorization"]
if ok == false {
// no Authorization header, we are just not
// authentified, we process down, no token added
other.ServeHTTPContext(ctx, rw, req)
return
}
if len(tokenStr) != 1 || strings.HasPrefix(tokenStr[0], "Bearer ") == false {
Error(ctx, rw, fmt.Errorf("Invalid Authorization HTTP Header %v", tokenStr), http.StatusForbidden)
return
}
tkData := strings.TrimPrefix(tokenStr[0], "Bearer ")
//parse the desired token, with signature checking
token := tokenGenerator()
err := jwt.DecodeJWS([]byte(tkData), token, j.signer)
if err != nil {
Error(ctx, rw, err, http.StatusForbidden)
return
}
// we process down the pipeline, with the token added to
// context
other.ServeHTTPContext(context.WithValue(ctx, jwtKeyOutput, token), rw, req)
})
}
}
func GetJwt(ctx context.Context) interface{} {
return ctx.Value(jwtKeyOutput)
}
//A Token Creator should produce a new pointer to a desired token
//struct
type TokenCreator func() interface{}
// RegisterJwtType should be used to register the token rtyope
// expected in the context. TokenCreator should allocate a new,
// unparsed token.
func RegisterJwtType(ctx context.Context, generator TokenCreator) context.Context {
return context.WithValue(ctx, jwtKeyGenerator, generator)
}
func NewJWT(s jwt.Signer) *JWT {
return &JWT{signer: s}
}

1
jwt/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
cover.out

9
jwt/Makefile Normal file
View File

@@ -0,0 +1,9 @@
all: build check
build:
go build
check:
go test -coverprofile=cover.out -covermode=count
go vet
golint

63
jwt/base64.go Normal file
View File

@@ -0,0 +1,63 @@
package jwt
import (
"encoding/base64"
"fmt"
)
// Base64EncodedBufferLen return the size needed to encode the
// Base64Encode (padding '=' would be added, can be stripped with
// Base64DecodedStrippedLen
func Base64EncodedBufferLen(n int) int {
return base64.URLEncoding.EncodedLen(n)
}
// Base64EncodedStrippedLen return the size of the encoded slice with no
// padding.
func Base64EncodedStrippedLen(n int) int {
eN := Base64EncodedBufferLen(n)
mod := n % 3
if mod == 0 {
return eN
}
return eN - (3 - mod)
}
// Base64DecodedStrippedLen returns the size of the data, without 0
// bit padding, from the stripped length n.
func Base64DecodedStrippedLen(n int) int {
return Base64DecodedLenFromStripped(n) + n - ((n+3)/4)*4
}
// Base64DecodedLenFromStripped returns the size of the data, with 0
// bit padding, from the stripped length n.
func Base64DecodedLenFromStripped(n int) int {
return base64.URLEncoding.DecodedLen(4 * ((n + 3) / 4))
}
// Base64Decode decodes a payload of data as a []byte, from a stripped
// '=' encoded string.
func Base64Decode(dst, src []byte) error {
switch len(src) % 4 {
// in this case, we have to copy the src, in order to allow decode part of the input
case 2:
oldSrc := src
src = make([]byte, len(src)+2)
copy(src, oldSrc)
src[len(src)-2] = '='
src[len(src)-1] = '='
case 3:
oldSrc := src
src = make([]byte, len(src)+1)
copy(src, oldSrc)
src[len(src)-1] = '='
case 1:
return fmt.Errorf("jwt: Invalid base64 string (length:%d %% 4 == 1): '%s'", len(src), src)
}
_, err := base64.URLEncoding.Decode(dst, src)
if err != nil {
return fmt.Errorf("jwt: %s", err)
}
return nil
}

66
jwt/base64_test.go Normal file
View File

@@ -0,0 +1,66 @@
package jwt
import (
"encoding/base64"
"testing"
. "gopkg.in/check.v1"
)
func Test(t *testing.T) {
TestingT(t)
}
type Base64Suite struct{}
var _ = Suite(&Base64Suite{})
type dataAndEncode struct {
data []byte
encoded []byte
}
func (s *Base64Suite) TestEncodeWithNoTrailing(c *C) {
data := []dataAndEncode{
{[]byte{0, 0, 0}, []byte("AAAA")}, //With trailing Should be AAAA
{[]byte{0, 0}, []byte("AAA")}, //With trailing Should be AAA=
{[]byte{0}, []byte("AA")}, //With trailing Should be AA==
}
for _, d := range data {
res := make([]byte, Base64EncodedBufferLen(len(d.data)))
base64.URLEncoding.Encode(res, d.data)
c.Check(res[:Base64EncodedStrippedLen(len(d.data))], DeepEquals, d.encoded)
}
}
func (s *Base64Suite) TestDecodeWithNoTrailing(c *C) {
data := map[string][]byte{
"AAAA": []byte{0, 0, 0},
"AAA": []byte{0, 0},
"AA": []byte{0},
}
for encoded, expected := range data {
res := make([]byte, Base64DecodedLenFromStripped(len(encoded)))
err := Base64Decode(res, []byte(encoded))
if c.Check(err, IsNil) == false {
continue
}
c.Check(res[:Base64DecodedStrippedLen(len(encoded))], DeepEquals, expected)
}
}
func (s *Base64Suite) TestDetectBadFormat(c *C) {
data := map[string]string{
"A": `jwt: Invalid base64 string \(length:[0-9]+ % 4 == 1\): 'A'`,
"ABCD%===": `jwt: illegal base64 data at input byte [0-9]+`,
}
for encoded, errorMatches := range data {
res := make([]byte, Base64DecodedLenFromStripped(len(encoded)))
err := Base64Decode(res, []byte(encoded))
c.Check(err, ErrorMatches, errorMatches)
}
}

147
jwt/jose.go Normal file
View File

@@ -0,0 +1,147 @@
package jwt
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"reflect"
"strings"
)
// JOSE represent a JSON Object Signing and Encryption header as
// defined in RFC 7515.
type JOSE struct {
Algorithm string `json:"alg"`
Type string `json:"typ,omitempty"`
Content string `json:"cty,omitempty"`
Critical []string `json:"crit,omitempty"`
JWKSetURL string `json:"jku,omitempty"`
JSONWebKey string `json:"jwk,omitempty"`
KeyID string `json:"kid,omitempty"`
X509URL string `json:"x5u,omitempty"`
X509CertificateChain string `json:"x5c,omitempty"`
X509ThumbprintSha1 string `json:"x5t,omitempty"`
X509ThumbprintSha256 string `json:"x5t#S256,omitempty"`
//Sets of adfditional headers, private or public
AdditionalHeaders map[string]interface{} `json:"-"`
}
// EncodeJSON encodes a JOSE header in JSON format. Not using
// MarshallJSON tyo avoid loops
func (j *JOSE) EncodeJSON() ([]byte, error) {
data, err := json.Marshal(j)
if err != nil {
return nil, err
}
if len(data) == 0 || data[0] != '{' || data[len(data)-1] != '}' {
return nil, fmt.Errorf("jws: Invalid JSON encoding: %s", data)
}
if len(j.AdditionalHeaders) == 0 {
return data, nil
}
moreHeader, err := json.Marshal(j.AdditionalHeaders)
if err != nil {
return nil, err
}
if len(moreHeader) == 0 || moreHeader[0] != '{' || moreHeader[len(moreHeader)-1] != '}' {
return nil, fmt.Errorf("jws: Invalid JSON encoding: %s", data)
}
data[len(data)-1] = ','
return append(data, moreHeader[1:]...), nil
}
// EncodeBase64 encodes a JOSE into JSON base64 string. will allocate
// buffer.
func (j *JOSE) EncodeBase64() ([]byte, error) {
dec, err := j.EncodeJSON()
if err != nil {
return nil, err
}
enc := make([]byte, Base64EncodedBufferLen(len(dec)))
base64.URLEncoding.Encode(enc, dec)
return enc[:Base64EncodedStrippedLen(len(dec))], nil
}
// DecodeJOSE a JOSE header from a base64 text as defined in the RFC 7515
func DecodeJOSE(data []byte) (*JOSE, error) {
bData := make([]byte, Base64DecodedLenFromStripped(len(data)))
if err := Base64Decode(bData, data); err != nil {
return nil, err
}
j := &JOSE{}
if err := json.NewDecoder(bytes.NewBuffer(bData)).Decode(j); err != nil {
return nil, err
}
return j, nil
}
// Validate validates a JOSE header data. Maybe it should disappear
func (j *JOSE) Validate() error {
if len(j.Algorithm) == 0 {
return fmt.Errorf("jwt: missing 'alg' header")
}
return nil
}
type fieldSetter func(*JOSE, string)
var fieldSetters = make(map[string]fieldSetter)
// UnmarshalJSON is here to satisfy interface json.Unmarshaller. We
// need to provide ou own unmarshaller for the additional header.
func (j *JOSE) UnmarshalJSON(b []byte) error {
raw := make(map[string]interface{})
if err := json.Unmarshal(b, &raw); err != nil {
return err
}
if j.AdditionalHeaders == nil {
j.AdditionalHeaders = make(map[string]interface{}, len(raw))
}
for key, data := range raw {
if fSetter, ok := fieldSetters[key]; ok == true {
strData, ok := data.(string)
if ok == false {
return &json.UnmarshalTypeError{
Value: reflect.TypeOf(data).Kind().String(),
Type: reflect.TypeOf(string("")),
}
}
fSetter(j, strData)
continue
}
j.AdditionalHeaders[key] = data
}
return nil
}
func init() {
tJOSE := reflect.TypeOf(JOSE{})
for i := 0; i < tJOSE.NumField(); i++ {
jField := tJOSE.Field(i)
jTag := jField.Tag.Get("json")
if len(jTag) == 0 || jTag == "-" {
continue
}
jName := strings.Split(jTag, ",")
fieldSetters[jName[0]] = func(j *JOSE, data string) {
reflect.ValueOf(j).Elem().FieldByName(jField.Name).SetString(data)
}
}
}

103
jwt/jose_test.go Normal file
View File

@@ -0,0 +1,103 @@
package jwt
import (
"encoding/base64"
. "gopkg.in/check.v1"
)
type JOSESuite struct{}
var _ = Suite(&JOSESuite{})
func (s *JOSESuite) TestCheckForDataEncoding(c *C) {
data := map[string]string{
`"alg":"none"}`: `json: .*`,
`{alg:"none"}`: `invalid character '.' looking for beginning of object key string`,
`{"alg":1234}`: `json: cannot unmarshal float64 into Go value of type string`,
}
for dec, errMatches := range data {
encoded := make([]byte, Base64EncodedBufferLen(len(dec)))
base64.URLEncoding.Encode(encoded, []byte(dec))
j, err := DecodeJOSE(encoded)
c.Check(j, IsNil)
c.Check(err, ErrorMatches, errMatches)
}
}
func (s *JOSESuite) TestHandleDuplicateField(c *C) {
decoded := `{"alg":"foo","alg":"none"}`
encoded := make([]byte, Base64EncodedBufferLen(len(decoded)))
base64.URLEncoding.Encode(encoded, []byte(decoded))
j, err := DecodeJOSE(encoded)
c.Assert(j, NotNil)
c.Assert(err, IsNil)
c.Assert(j.Algorithm, Equals, "none")
}
func (s *JOSESuite) TestHeaderValidation(c *C) {
data := map[string]string{
`{"alg":""}`: `jwt: missing 'alg' header`,
`{}`: `jwt: missing 'alg' header`,
}
for dec, errMatches := range data {
encoded := make([]byte, Base64EncodedBufferLen(len(dec)))
base64.URLEncoding.Encode(encoded, []byte(dec))
j, err := DecodeJOSE(encoded)
c.Check(j, NotNil)
c.Check(err, IsNil)
c.Check(j.Validate(), ErrorMatches, errMatches)
}
}
func (s *JOSESuite) TestHandlesPrivateHeaders(c *C) {
data := []map[string]interface{}{
map[string]interface{}{
"foo": true,
"bar": 12.34,
"baz": float64(456), //default of json is to parse float64
"blah": "omg",
},
}
for _, headers := range data {
j := &JOSE{
Algorithm: "none",
AdditionalHeaders: make(map[string]interface{}),
}
for n, v := range headers {
j.AdditionalHeaders[n] = v
}
enc, err := j.EncodeBase64()
if c.Check(err, IsNil) == false {
continue
}
res, err := DecodeJOSE(enc)
c.Check(err, IsNil)
c.Assert(res, DeepEquals, j)
}
}
func (s *JOSESuite) TestEncodingIsCompact(c *C) {
j := &JOSE{
Algorithm: "none",
Critical: []string{"exp", "foo"},
}
// it does not print empty fields.
data, err := j.EncodeJSON()
c.Check(err, IsNil)
c.Check(string(data), Equals, `{"alg":"none","crit":["exp","foo"]}`)
b64, err := j.EncodeBase64()
c.Check(err, IsNil)
c.Check(len(b64), Equals, 47)
}

150
jwt/jws.go Normal file
View File

@@ -0,0 +1,150 @@
package jwt
import (
"encoding/base64"
"encoding/json"
"fmt"
)
// EncodeJWS encode a JSON Object with the serialized JWS format as
// specified in RFC 7515
func EncodeJWS(j *JOSE, v interface{}, s Signer) ([]byte, error) {
payload, err := json.Marshal(v)
if err != nil {
return nil, err
}
if s != nil {
j.Algorithm = s.Algorithm()
} else {
j.Algorithm = "none"
}
header, err := j.EncodeJSON()
if err != nil {
return nil, err
}
//Allocate a buffer long enough for all payload
res := make([]byte, Base64EncodedBufferLen(len(header))+Base64EncodedBufferLen(len(payload))+2)
base64.URLEncoding.Encode(res, header)
lengthHeader := Base64EncodedStrippedLen(len(header))
res[lengthHeader] = '.'
base64.URLEncoding.Encode(res[lengthHeader+1:], payload)
fullPayloadLength := Base64EncodedStrippedLen(len(payload)) + 1 + lengthHeader
res[fullPayloadLength] = '.'
if s == nil {
// unprotected jws, not signing it
return res[:fullPayloadLength+1], nil
}
signature, err := s.Sign(res[:fullPayloadLength])
if err != nil {
return nil, err
}
signedRes := make([]byte, fullPayloadLength+1+Base64EncodedBufferLen(len(signature)))
copy(signedRes, res[:fullPayloadLength+1])
base64.URLEncoding.Encode(signedRes[fullPayloadLength+1:], signature)
return signedRes[:fullPayloadLength+1+Base64EncodedStrippedLen(len(signature))], nil
}
// isBase64URLEncoding returns if the byte is a character used for
// base64 URL Encoding
func isBase64URLEncoding(b byte) bool {
if b == '-' || b == '_' {
return true
}
if b < '0' || b > 'z' {
return false
}
if b <= '9' {
return true
}
if b < 'A' {
return false
}
if b <= 'Z' {
return true
}
if b < 'a' {
return false
}
return true
}
// DecodeJWS decode an object encoded with the JWS serialized fromat
// as specified by RFC 7515. to avoid an attack where the unprotected
// JOSE header would contain a modified alg field, the Signer should
// also be specified.
func DecodeJWS(data []byte, v interface{}, s Signer) error {
var headerLength, payloadLength int
for i, c := range data {
if isBase64URLEncoding(c) == true {
continue
}
if c != '.' {
return fmt.Errorf("jws: invalid serialization character %v in '%s'", c, data)
}
if headerLength == 0 {
if i == 0 {
return fmt.Errorf("jws: invalid emtpy JOSE header in %s", data)
}
headerLength = i
continue
}
if payloadLength == 0 {
if i-headerLength == 1 {
return fmt.Errorf("jws: invalid emtpy payload in %s", data)
}
payloadLength = i - headerLength - 1
continue
}
return fmt.Errorf("jws: invalid third '.' in %s", data)
}
signatureLength := len(data) - 2 - headerLength - payloadLength
signature := make([]byte, Base64DecodedLenFromStripped(signatureLength))
signedLength := headerLength + payloadLength + 1
Base64Decode(signature, data[signedLength+1:])
signature = signature[:Base64DecodedStrippedLen(signatureLength)]
if s != nil {
if err := s.Verify(data[:signedLength], signature); err != nil {
return err
}
} else {
if signatureLength != 0 {
return fmt.Errorf("jws: Invalid JWS, got a signature, but none expected")
}
}
//decode jose
jose, err := DecodeJOSE(data[:headerLength])
if err != nil {
return err
}
algo := "none"
if s != nil {
algo = s.Algorithm()
}
if jose.Algorithm != algo {
return fmt.Errorf("jws: Mismatched signing algorithm got %s, expected %s", jose.Algorithm, algo)
}
payload := make([]byte, Base64DecodedLenFromStripped(payloadLength))
Base64Decode(payload, data[headerLength+1:headerLength+1+payloadLength])
payload = payload[:Base64DecodedStrippedLen(payloadLength)]
//data is safe, just need to decode it.
return json.Unmarshal(payload, v)
}

211
jwt/jws_test.go Normal file
View File

@@ -0,0 +1,211 @@
package jwt
import (
"crypto/rsa"
"crypto/x509"
"encoding/json"
"fmt"
"regexp"
"time"
. "gopkg.in/check.v1"
)
type JWSSuite struct {
signers []Signer
hmacKey []byte
rsaKey *rsa.PrivateKey
}
var _ = Suite(&JWSSuite{})
func (s *JWSSuite) SetUpSuite(c *C) {
var err error
s.hmacKey = NewHMACKey(24)
s.rsaKey, err = CachedRSAkey()
c.Assert(err, IsNil)
s.signers = []Signer{
NewHMAC256Signer(s.hmacKey),
NewHMAC384Signer(s.hmacKey),
NewHMAC512Signer(s.hmacKey),
NewRSA256Signer(s.rsaKey),
NewRSA384Signer(s.rsaKey),
NewRSA512Signer(s.rsaKey),
}
}
func (s *JWSSuite) TestCanEncodeSigned(c *C) {
type foo struct {
A string
B int
}
j := &JOSE{}
for _, signer := range s.signers {
decoded := foo{A: "blah", B: 42}
res, err := EncodeJWS(j, decoded, signer)
c.Check(string(res), Matches, `\A[0-9A-Za-z_\-]+\.[0-9A-Za-z_\-]+.[0-9A-Za-z_\-]+\z`)
if c.Check(err, IsNil) == false {
continue
}
redecoded := foo{}
err = DecodeJWS(res, &redecoded, signer)
if c.Check(err, IsNil, Commentf("Algo is: %s", signer.Algorithm())) == true {
c.Check(redecoded, DeepEquals, decoded)
}
}
}
func (s *JWSSuite) TestCanEncodeUnprotected(c *C) {
type foo struct {
A string
B int
}
j := &JOSE{}
decoded := foo{A: "blah", B: 42}
res, err := EncodeJWS(j, decoded, nil)
c.Check(string(res), Matches, `\A[0-9A-Za-z_\-]+\.[0-9A-Za-z_\-]+.\z`)
c.Assert(err, IsNil)
redecoded := foo{}
err = DecodeJWS(res, &redecoded, nil)
c.Assert(err, IsNil)
c.Check(redecoded, DeepEquals, decoded)
}
func (s *JWSSuite) TestAttackerTriesToForgeAToken(c *C) {
type ident struct {
Iat int64
Adm bool
}
//--------------------------------------------------------------------------
// Server side
//--------------------------------------------------------------------------
//we create a token and sign it
validToken, err := EncodeJWS(&JOSE{}, ident{Iat: time.Now().Unix(), Adm: false}, s.signers[0])
c.Assert(err, IsNil)
//--------------------------------------------------------------------------
// Untrusted side
//--------------------------------------------------------------------------
// now an attacker takes our valid token, unmarshall it, and create a new one with no signature
t := ident{}
tokenRx := regexp.MustCompile(`\A([a-zA-Z0-9_\-]+)\.([a-zA-Z0-9_\-]+).([a-zA-Z0-9_\-]+)\z`)
matches := tokenRx.FindSubmatch(validToken)
c.Assert(matches, NotNil)
// extract JOSE
j, err := DecodeJOSE(matches[1])
c.Assert(err, IsNil)
// extract payload
payload := make([]byte, Base64DecodedLenFromStripped(len(matches[2])))
Base64Decode(payload, matches[2])
err = json.Unmarshal(payload, &t)
c.Assert(err, IsNil)
//we forge an admin token
t.Adm = true
//we un-sign the token, by using the none algorithm
forgedToken, err := EncodeJWS(j, t, nil)
//--------------------------------------------------------------------------
// Server side
//--------------------------------------------------------------------------
//we verify the forgedToken, it should fail
received := ident{Adm: false}
// please note that we speficy ecplitely the signer, and therefore
// the algorithm to use.
err = DecodeJWS(forgedToken, &received, s.signers[0])
//we got a wrong signature for our chosen algorithm
c.Assert(err, ErrorMatches, fmt.Sprintf(`jws: .* %s .*`, s.signers[0].Algorithm()))
// of course, our token remain Adm:false
c.Assert(received.Adm, Equals, false)
}
func (s *JWSSuite) TestAttackerTriesToForgeAHMACToken(c *C) {
type ident struct {
Iat int64
Adm bool
}
//--------------------------------------------------------------------------
// Server side
//--------------------------------------------------------------------------
//we create a token and sign it using RSA256
serverSigner := NewRSA256Signer(s.rsaKey)
validToken, err := EncodeJWS(&JOSE{}, ident{Iat: time.Now().Unix(), Adm: false}, serverSigner)
c.Assert(err, IsNil)
//--------------------------------------------------------------------------
// Untrusted side
//--------------------------------------------------------------------------
// now an attacker takes our valid token, unmarshall it, and create a new one with no signature
t := ident{}
tokenRx := regexp.MustCompile(`\A([a-zA-Z0-9_\-]+)\.([a-zA-Z0-9_\-]+).([a-zA-Z0-9_\-]+)\z`)
matches := tokenRx.FindSubmatch(validToken)
c.Assert(matches, NotNil)
// extract JOSE
j, err := DecodeJOSE(matches[1])
c.Assert(err, IsNil)
// extract payload
payload := make([]byte, Base64DecodedLenFromStripped(len(matches[2])))
Base64Decode(payload, matches[2])
err = json.Unmarshal(payload, &t)
c.Assert(err, IsNil)
//we forge an admin token
t.Adm = true
//now here is the trick, we sign an HMAC token using the serverPublicKey
publicKeyAsByte, err := x509.MarshalPKIXPublicKey(s.rsaKey.Public())
c.Assert(err, IsNil, Commentf("While marshalling key"))
//we create a new signing algorithm expecting the server will
//still use the assymetric key on the HMAC.
attackerSigner := NewHMAC256Signer(publicKeyAsByte)
//we un-sign the token, by using another symmetric algorithm, and
//the publickKey
forgedToken, err := EncodeJWS(j, t, attackerSigner)
//--------------------------------------------------------------------------
// Server side
//--------------------------------------------------------------------------
//we verify the forgedToken, it should fail
received := ident{Adm: false}
// please note that we speficy ecplitely the signer, and therefore
// the algorithm to use.
err = DecodeJWS(forgedToken, &received, serverSigner)
//we got a wrong signature for our chosen algorithm, we still are
//using the rsa algorithm
c.Assert(err, ErrorMatches, `crypto/rsa: verification error`)
// of course, our token remain Adm:false
c.Assert(received.Adm, Equals, false)
}

164
jwt/signer.go Normal file
View File

@@ -0,0 +1,164 @@
package jwt
import (
"crypto"
"crypto/hmac"
"crypto/rand"
"crypto/rsa"
"fmt"
)
type hashLinkError struct {
name string
}
func (e *hashLinkError) Error() string {
return fmt.Sprintf("jws: Hash %s is not linked into binary", e.name)
}
// A Signer is able to crypto-sign a JWS object.
type Signer interface {
// Sign returns the signature of a binary payload. It should
// returns the signature in binary format
Sign([]byte) ([]byte, error)
// Verify check if the signed binary paylod correspond to the
// provided binary siganture. It assumes a binary format
// signature.
Verify(signed, signature []byte) error
// Algorithm is returning a string idnetifying the algorithm used
// as defined in RFC 7518.
Algorithm() string
}
// A HMACSigner is a Signer using HMAC based algorithm
type HMACSigner struct {
name string
hash crypto.Hash
key []byte
}
// NewHMAC256Signer returs a HMACSigner using a SHA256 hash.
func NewHMAC256Signer(key []byte) Signer {
return &HMACSigner{
name: "HS256",
hash: crypto.SHA256,
key: key,
}
}
// NewHMAC384Signer returs a HMACSigner using a SHA384 hash.
func NewHMAC384Signer(key []byte) Signer {
return &HMACSigner{
name: "HS384",
hash: crypto.SHA384,
key: key,
}
}
// NewHMAC512Signer returs a HMACSigner using a SHA512 hash.
func NewHMAC512Signer(key []byte) Signer {
return &HMACSigner{
name: "HS512",
hash: crypto.SHA512,
key: key,
}
}
// Algorithm return 'HSXXX' where XXX is either 256|384|512 depending
// on the SHA Hash used.
func (s *HMACSigner) Algorithm() string {
return s.name
}
// Sign compute the binary signature for data.
func (s *HMACSigner) Sign(data []byte) ([]byte, error) {
if s.hash.Available() == false {
return nil, &hashLinkError{s.name}
}
hasher := hmac.New(s.hash.New, s.key)
hasher.Write(data)
return hasher.Sum(nil), nil
}
// Verify returns an error if the binary provided signature does not
// correspond to signed. It return nil if the signature is correct.
func (s *HMACSigner) Verify(signed, signature []byte) error {
expected, err := s.Sign(signed)
if err != nil {
return err
}
if hmac.Equal(signature, expected) == false {
return fmt.Errorf("jws: Invalid %s signature", s.name)
}
return nil
}
// A RSASigner is a Signer using a PKS 1v15 algorithm.
type RSASigner struct {
name string
hash crypto.Hash
key *rsa.PrivateKey
}
// NewRSA256Signer returns a RSASigner using a SHA 256 hash.
func NewRSA256Signer(key *rsa.PrivateKey) Signer {
return &RSASigner{
name: "RS256",
hash: crypto.SHA256,
key: key,
}
}
// NewRSA384Signer returns a RSASigner using a SHA 384 hash.
func NewRSA384Signer(key *rsa.PrivateKey) Signer {
return &RSASigner{
name: "RS384",
hash: crypto.SHA384,
key: key,
}
}
// NewRSA512Signer returns a RSASigner using a SHA 512 hash.
func NewRSA512Signer(key *rsa.PrivateKey) Signer {
return &RSASigner{
name: "RS512",
hash: crypto.SHA512,
key: key,
}
}
// Sign compute the binary signature for data.
func (s *RSASigner) Sign(data []byte) ([]byte, error) {
if s.hash.Available() == false {
return nil, &hashLinkError{s.name}
}
hasher := s.hash.New()
hasher.Write(data)
res, err := rsa.SignPKCS1v15(rand.Reader, s.key, s.hash, hasher.Sum(nil))
if err != nil {
return nil, err
}
return res, nil
}
// Verify returns an error if the binary provided signature does not
// correspond to signed. It return nil if the signature is correct.
func (s *RSASigner) Verify(signed, signature []byte) error {
if s.hash.Available() == false {
return &hashLinkError{s.name}
}
hasher := s.hash.New()
hasher.Write(signed)
return rsa.VerifyPKCS1v15(s.key.Public().(*rsa.PublicKey), s.hash, hasher.Sum(nil), signature)
}
// Algorithm return 'RSXXX' where XXX is either 256|384|512 depending
// on the SHA Hash used.
func (s *RSASigner) Algorithm() string {
return s.name
}

55
jwt/signer_test.go Normal file
View File

@@ -0,0 +1,55 @@
package jwt
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
)
func NewHMACKey(n int) []byte {
res := make([]byte, n)
rand.Reader.Read(res)
return res
}
func CachedRSAkey() (*rsa.PrivateKey, error) {
keyPath := filepath.Join(os.TempDir(), "narco-jwt-test.key")
f, err := os.Open(keyPath)
if err != nil {
if os.IsNotExist(err) == false {
return nil, err
}
// generate a key
log.Printf("Generating a new key")
key, err := rsa.GenerateKey(rand.Reader, 1024)
if err != nil {
return nil, err
}
f, err := os.Create(keyPath)
if err != nil {
log.Printf("Could not cache the generated key: %s", err)
return key, nil
}
data := x509.MarshalPKCS1PrivateKey(key)
_, err = f.Write(data)
if err != nil {
log.Printf("Could not cache the generated key: %s", err)
}
return key, nil
}
keydata, err := ioutil.ReadAll(f)
if err != nil {
return nil, fmt.Errorf("Could not read %s: %s", keyPath, err)
}
return x509.ParsePKCS1PrivateKey(keydata)
}

View File

@@ -23,21 +23,6 @@ func NewLogger() *Logger {
return &Logger{log.New(os.Stdout, "[narco] ", log.LstdFlags)}
}
type FooResponseWriter struct {
header http.Header
}
func (f *FooResponseWriter) Header() http.Header {
return f.header
}
func (f *FooResponseWriter) WriteHeader(status int) {
}
func (f *FooResponseWriter) Write(b []byte) (int, error) {
return len(b), nil
}
func (l *Logger) Wrap() func(chain.Handler) chain.Handler {
return func(other chain.Handler) chain.Handler {
return chain.HandlerFunc(func(ctx context.Context, h http.ResponseWriter, r *http.Request) {

73
recovery_test.go Normal file
View File

@@ -0,0 +1,73 @@
package narco
import (
"bytes"
"log"
"net/http"
"net/http/httptest"
"strings"
. "gopkg.in/check.v1"
)
type RecovererSuite struct {
*NarcoSuite
recov *Recoverer
buffer bytes.Buffer
rec *httptest.ResponseRecorder
req *http.Request
URL string
}
var _ = Suite(&RecovererSuite{NarcoSuite: NewNarcoSuite()})
func (s *RecovererSuite) SetUpSuite(c *C) {
s.recov = NewRecoverer()
s.recov.Logger = log.New(&s.buffer, "[narco] ", log.LstdFlags)
s.chain = s.chain.Append(s.recov.Wrap())
c.Assert(s.chain, NotNil)
}
func (s *RecovererSuite) SetUpTest(c *C) {
s.buffer.Reset()
s.rec = httptest.NewRecorder()
var err error
s.URL = "http://" + httptest.DefaultRemoteAddr + "/"
s.req, err = http.NewRequest("GET", s.URL, nil)
c.Assert(err, IsNil, Commentf("Unexpected error :%s", err))
}
func (s *RecovererSuite) TestRecoversFromPanic(c *C) {
// this should not panic
defer func() {
err := recover()
c.Assert(err, IsNil, Commentf("It should not have panic: %s", err))
}()
s.ServeHTTP(s.rec, s.req, func(rw http.ResponseWriter, req *http.Request) {
panic("foo")
})
lines := strings.Split(s.buffer.String(), "\n")
c.Assert(lines[0], Matches, `\[narco\] .* `+"PANIC: foo")
for i, l := range lines {
if i == 0 {
continue
}
matchString := "foo"
if i == 1 {
matchString = `goroutine [0-9]+ (\[running\])?:`
} else if i%2 == 0 {
//either function call, empty or created by line
matchString = `(.*\(.*\)|created by .*|)`
} else {
//function location, or <autogenerated>
matchString = `\t(.*\.go|\<autogenerated\>):[0-9]+ .*`
}
c.Assert(l, Matches, matchString, Commentf("On line %d", i))
}
}

19
router.go Normal file
View File

@@ -0,0 +1,19 @@
package narco
import (
"net/http"
"github.com/codemodus/chain"
"github.com/julienschmidt/httprouter"
"golang.org/x/net/context"
)
type HandlerFunc func(ctx context.Context, rw http.ResponseWriter, req *http.Request, ps httprouter.Params)
func EndChain(chain chain.Chain, h HandlerFunc) httprouter.Handle {
return func(rw http.ResponseWriter, req *http.Request, ps httprouter.Params) {
chain.EndFn(func(ctx context.Context, rw_ http.ResponseWriter, req_ *http.Request) {
h(ctx, rw_, req_, ps)
}).ServeHTTP(rw, req)
}
}