Skip to content

Commit

Permalink
Support ECDSA.
Browse files Browse the repository at this point in the history
  • Loading branch information
pascaldekloe committed Aug 24, 2018
1 parent bd14524 commit b6dacb2
Show file tree
Hide file tree
Showing 11 changed files with 456 additions and 70 deletions.
40 changes: 24 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ A JSON Web Token (JWT) library for the Go programming language.

The API enforces secure use by design. Unsigned tokens are
[rejected](https://godoc.org/github.com/pascaldekloe/jwt#ErrUnsecured)
and there is no support for (ECDSA) encryption—use wire encryption instead.
With less than 500 lines of code and no third party dependencies, the
and there is no support for encryption—use wire encryption instead.
With about 500 lines of code and no third party dependencies, the
implementation maintains full unit test coverage.

This is free and unencumbered software released into the
Expand All @@ -17,10 +17,12 @@ This is free and unencumbered software released into the
## Get Started

The package comes with functions to verify
[[HMACCheck](https://godoc.org/github.com/pascaldekloe/jwt#HMACCheck),
[[ECDSACheck](https://godoc.org/github.com/pascaldekloe/jwt#ECDSACheck),
[HMACCheck](https://godoc.org/github.com/pascaldekloe/jwt#HMACCheck),
[RSACheck](https://godoc.org/github.com/pascaldekloe/jwt#RSACheck)]
and issue
[[HMACSign](https://godoc.org/github.com/pascaldekloe/jwt#Claims.HMACSign),
[[ECDSASign](https://godoc.org/github.com/pascaldekloe/jwt#Claims.ECDSASign),
[HMACSign](https://godoc.org/github.com/pascaldekloe/jwt#Claims.HMACSign),
[RSASign](https://godoc.org/github.com/pascaldekloe/jwt#Claims.RSASign)]
claims.

Expand Down Expand Up @@ -75,18 +77,24 @@ Optionally one can use the claims object in the service handlers as shown in the
### Performance on a Mac Pro (late 2013)

```
BenchmarkHMACSign/HS256-12 500000 3398 ns/op
BenchmarkHMACSign/HS384-12 500000 3940 ns/op
BenchmarkHMACSign/HS512-12 300000 4032 ns/op
BenchmarkHMACCheck/HS256-12 200000 6853 ns/op
BenchmarkHMACCheck/HS384-12 200000 7495 ns/op
BenchmarkHMACCheck/HS512-12 200000 7603 ns/op
BenchmarkRSASign/1024-bit-12 3000 422137 ns/op
BenchmarkRSASign/2048-bit-12 1000 2094975 ns/op
BenchmarkRSASign/4096-bit-12 100 12902447 ns/op
BenchmarkRSACheck/1024-bit-12 50000 34042 ns/op
BenchmarkRSACheck/2048-bit-12 20000 73650 ns/op
BenchmarkRSACheck/4096-bit-12 10000 203782 ns/op
BenchmarkECDSASign/ES256-12 50000 38114 ns/op
BenchmarkECDSASign/ES384-12 300 4279447 ns/op
BenchmarkECDSASign/ES512-12 200 8064569 ns/op
BenchmarkECDSACheck/ES256-12 10000 105350 ns/op
BenchmarkECDSACheck/ES384-12 200 8331596 ns/op
BenchmarkECDSACheck/ES512-12 100 16024017 ns/op
BenchmarkHMACSign/HS256-12 500000 3498 ns/op
BenchmarkHMACSign/HS384-12 300000 4071 ns/op
BenchmarkHMACSign/HS512-12 300000 4144 ns/op
BenchmarkHMACCheck/HS256-12 200000 6834 ns/op
BenchmarkHMACCheck/HS384-12 200000 7543 ns/op
BenchmarkHMACCheck/HS512-12 200000 7622 ns/op
BenchmarkRSASign/1024-bit-12 3000 424131 ns/op
BenchmarkRSASign/2048-bit-12 1000 2102947 ns/op
BenchmarkRSASign/4096-bit-12 100 12877484 ns/op
BenchmarkRSACheck/1024-bit-12 50000 32982 ns/op
BenchmarkRSACheck/2048-bit-12 20000 73431 ns/op
BenchmarkRSACheck/4096-bit-12 10000 201450 ns/op
```

[![JWT.io](https://jwt.io/img/badge.svg)](https://jwt.io/)
66 changes: 66 additions & 0 deletions bench_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,72 @@ var benchClaims = &Claims{
},
}

func BenchmarkECDSASign(b *testing.B) {
b.Run(ES256, func(b *testing.B) {
for i := 0; i < b.N; i++ {
_, err := benchClaims.ECDSASign(ES256, testKeyEC256)
if err != nil {
b.Fatal(err)
}
}
})
b.Run(ES384, func(b *testing.B) {
for i := 0; i < b.N; i++ {
_, err := benchClaims.ECDSASign(ES384, testKeyEC384)
if err != nil {
b.Fatal(err)
}
}
})
b.Run(ES512, func(b *testing.B) {
for i := 0; i < b.N; i++ {
_, err := benchClaims.ECDSASign(ES512, testKeyEC521)
if err != nil {
b.Fatal(err)
}
}
})
}

func BenchmarkECDSACheck(b *testing.B) {
b.Run(ES256, func(b *testing.B) {
token, err := benchClaims.ECDSASign(ES256, testKeyEC256)
if err != nil {
b.Fatal(err)
}
for i := 0; i < b.N; i++ {
_, err := ECDSACheck(token, &testKeyEC256.PublicKey)
if err != nil {
b.Fatal(err)
}
}
})
b.Run(ES384, func(b *testing.B) {
token, err := benchClaims.ECDSASign(ES384, testKeyEC384)
if err != nil {
b.Fatal(err)
}
for i := 0; i < b.N; i++ {
_, err := ECDSACheck(token, &testKeyEC384.PublicKey)
if err != nil {
b.Fatal(err)
}
}
})
b.Run(ES512, func(b *testing.B) {
token, err := benchClaims.ECDSASign(ES512, testKeyEC521)
if err != nil {
b.Fatal(err)
}
for i := 0; i < b.N; i++ {
_, err := ECDSACheck(token, &testKeyEC521.PublicKey)
if err != nil {
b.Fatal(err)
}
}
})
}

func BenchmarkHMACSign(b *testing.B) {
// 512-bit key
secret := make([]byte, 64)
Expand Down
46 changes: 40 additions & 6 deletions check.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ package jwt
import (
"bytes"
"crypto"
"crypto/ecdsa"
"crypto/hmac"
"crypto/rsa"
_ "crypto/sha256" // link binary
_ "crypto/sha512" // link binary
"encoding/json"
"errors"
"math/big"
)

// ErrSigMiss means the signature check failed.
Expand All @@ -19,6 +21,38 @@ var ErrUnsecured = errors.New("jwt: unsecured—no signature")

var errPart = errors.New("jwt: missing base64 part")

// ECDSACheck parses a JWT and returns the claims set if, and only if, the
// signature checks out. Note that this excludes unsecured JWTs [ErrUnsecured].
// When the algorithm is not in ECDSAAlgs then the error is ErrAlgUnk.
// See Valid to complete the verification.
func ECDSACheck(jwt []byte, key *ecdsa.PublicKey) (*Claims, error) {
firstDot, lastDot, buf, err := scan(jwt)
if err != nil {
return nil, err
}

// create signature
hash, err := selectHash(ECDSAAlgs, jwt[:firstDot], buf)
if err != nil {
return nil, err
}
digest := hash.New()
digest.Write(jwt[:lastDot])

// verify signature
n, err := encoding.Decode(buf, jwt[lastDot+1:])
if err != nil {
return nil, errors.New("jwt: malformed signature: " + err.Error())
}
r := big.NewInt(0).SetBytes(buf[:n/2])
s := big.NewInt(0).SetBytes(buf[n/2 : n])
if !ecdsa.Verify(key, digest.Sum(buf[:0]), r, s) {
return nil, ErrSigMiss
}

return parseClaims(jwt[firstDot+1:lastDot], buf)
}

// HMACCheck parses a JWT and returns the claims set if, and only if, the
// signature checks out. Note that this excludes unsecured JWTs [ErrUnsecured].
// When the algorithm is not in HMACAlgs then the error is ErrAlgUnk.
Expand All @@ -34,15 +68,15 @@ func HMACCheck(jwt, secret []byte) (*Claims, error) {
if err != nil {
return nil, err
}
mac := hmac.New(hash.New, secret)
mac.Write(jwt[:lastDot])
digest := hmac.New(hash.New, secret)
digest.Write(jwt[:lastDot])

// verify signature
n, err := encoding.Decode(buf, jwt[lastDot+1:])
if err != nil {
return nil, errors.New("jwt: malformed signature: " + err.Error())
}
if !hmac.Equal(buf[:n], mac.Sum(buf[n:n])) {
if !hmac.Equal(buf[:n], digest.Sum(buf[n:n])) {
return nil, ErrSigMiss
}

Expand All @@ -64,15 +98,15 @@ func RSACheck(jwt []byte, key *rsa.PublicKey) (*Claims, error) {
if err != nil {
return nil, err
}
h := hash.New()
h.Write(jwt[:lastDot])
digest := hash.New()
digest.Write(jwt[:lastDot])

// verify signature
n, err := encoding.Decode(buf, jwt[lastDot+1:])
if err != nil {
return nil, errors.New("jwt: malformed signature: " + err.Error())
}
if err := rsa.VerifyPKCS1v15(key, hash, h.Sum(buf[n:n]), buf[:n]); err != nil {
if err := rsa.VerifyPKCS1v15(key, hash, digest.Sum(buf[n:n]), buf[:n]); err != nil {
return nil, ErrSigMiss
}

Expand Down
63 changes: 58 additions & 5 deletions check_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package jwt
import (
"bytes"
"crypto"
"crypto/ecdsa"
_ "crypto/md5" // link
"crypto/rsa"
"encoding/json"
Expand All @@ -13,6 +14,42 @@ import (
"github.com/pascaldekloe/goe/verify"
)

var goldenECDSAs = []struct {
key *ecdsa.PublicKey
token string
claims string
}{
0: {
key: &testKeyEC256.PublicKey,
token: "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJjdHVudCIsImF1ZCI6ImJvYXJkIn0.AnYB6w3Zh7MBYE9uLE8Hp693DHf-1Xm_WiXl-ZTAIabuO1ER4O38T5PPkducsHPZ4NCPLqh2bprlRJGnE_s5IA",
claims: `{"iss":"ctunt","aud":"board"}`,
},
1: {
key: &testKeyEC384.PublicKey,
token: "eyJhbGciOiJFUzM4NCJ9.e30.1WD7DU260TLDzwiJQa-ri7FBnXlRsOzEpTKDmMt51dzqDiYguVch7VqNLTVkHCb4oJ-LDJ8-PGaeoo4jcNkGQjGg1HUiHWEZNyUUPRbxnzTKOWD1Z3VAlPDgnhXp1i8t",
claims: `{}`,
},
2: {
key: &testKeyEC521.PublicKey,
token: "eyJhbGciOiJFUzUxMiJ9.eyJzdWIiOiJha3JpZWdlciIsInByZWZpeCI6IkRyLiJ9.APhisjBsvFDWLojTWUP7uyEiilIOU4KYVEgqFr5GdJbd5ucuejztFUvzRZq8njo2s0jLqwMN6H0IhG9YHDMRKTgQAbEbOT_13tN6Xs4sTtxefuf_jlJTfTLtg9_2A22iGYgSDBTzWpunC-Ofuq4XegptS2NuC6XGTFu41DbQX6EmEb-7",
claims: `{"sub":"akrieger","prefix":"Dr."}`,
},
}

func TestECDSACheck(t *testing.T) {
for i, gold := range goldenECDSAs {
claims, err := ECDSACheck([]byte(gold.token), gold.key)
if err != nil {
t.Errorf("%d: check error: %s", i, err)
continue
}
if !bytes.Equal([]byte(claims.Raw), []byte(gold.claims)) {
t.Errorf("%d: got claims JSON %q, want %q", i, claims.Raw, gold.claims)
continue
}
}
}

var goldenHMACs = []struct {
secret []byte
token string
Expand All @@ -25,8 +62,8 @@ var goldenHMACs = []struct {
claims: &Claims{
Raw: json.RawMessage([]byte("{\"iss\":\"joe\",\r\n \"exp\":1300819380,\r\n \"http://example.com/is_root\":true}")),
Set: map[string]interface{}{
"iss": "joe",
"exp": 1300819380.0,
"iss": "joe",
"exp": 1300819380.0,
"http://example.com/is_root": true,
},
Registered: Registered{
Expand Down Expand Up @@ -100,7 +137,11 @@ func TestRSACheck(t *testing.T) {
}

func TestCheckMiss(t *testing.T) {
_, err := HMACCheck([]byte(goldenHMACs[0].token), nil)
_, err := ECDSACheck([]byte(goldenECDSAs[0].token), &testKeyEC521.PublicKey)
if err != ErrSigMiss {
t.Errorf("ECDSA check got error %v, want %v", err, ErrSigMiss)
}
_, err = HMACCheck([]byte(goldenHMACs[0].token), nil)
if err != ErrSigMiss {
t.Errorf("HMAC check got error %v, want %v", err, ErrSigMiss)
}
Expand All @@ -111,7 +152,11 @@ func TestCheckMiss(t *testing.T) {
}

func TestCheckAlgWrong(t *testing.T) {
_, err := HMACCheck([]byte(goldenRSAs[0].token), nil)
_, err := ECDSACheck([]byte(goldenRSAs[0].token), nil)
if err != ErrAlgUnk {
t.Errorf("RSA alg for ECDSA got error %v, want %v", err, ErrAlgUnk)
}
_, err = HMACCheck([]byte(goldenRSAs[0].token), nil)
if err != ErrAlgUnk {
t.Errorf("RSA alg for HMAC got error %v, want %v", err, ErrAlgUnk)
}
Expand Down Expand Up @@ -151,7 +196,11 @@ func TestCheckHashNotLinked(t *testing.T) {

func TestCheckIncomplete(t *testing.T) {
// header only
_, err := RSACheck([]byte("eyJhbGciOiJub25lIn0"), &testKeyRSA1024.PublicKey)
_, err := ECDSACheck([]byte("eyJhbGciOiJFUzI1NiJ9"), &testKeyEC256.PublicKey)
if err != errPart {
t.Errorf("one base64 chunk got error %v, want %v", err, errPart)
}
_, err = RSACheck([]byte("eyJhbGciOiJub25lIn0"), &testKeyRSA1024.PublicKey)
if err != errPart {
t.Errorf("one base64 chunk got error %v, want %v", err, errPart)
}
Expand Down Expand Up @@ -183,6 +232,10 @@ func TestCheckBrokenBase64(t *testing.T) {
}

want = "jwt: malformed signature: "
_, err = ECDSACheck([]byte("eyJhbGciOiJFUzI1NiJ9.e30.*"), nil)
if err == nil || !strings.HasPrefix(err.Error(), want) {
t.Errorf("corrupt base64 in ECDSA signature got error %v, want %s…", err, want)
}
_, err = HMACCheck([]byte("eyJhbGciOiJIUzI1NiJ9.e30.*"), nil)
if err == nil || !strings.HasPrefix(err.Error(), want) {
t.Errorf("corrupt base64 in HMAC signature got error %v, want %s…", err, want)
Expand Down
Loading

0 comments on commit b6dacb2

Please sign in to comment.