Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generation and parsing of jwt supports string type keys #157

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 26 additions & 12 deletions hmac.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,14 @@ func (m *SigningMethodHMAC) Alg() string {
// Verify implements token verification for the SigningMethod. Returns nil if the signature is valid.
func (m *SigningMethodHMAC) Verify(signingString, signature string, key interface{}) error {
// Verify the key is the right type
keyBytes, ok := key.([]byte)
if !ok {
var keyBytes []byte

switch k := key.(type) {
case string:
keyBytes = StringToBytes(k)
case []byte:
keyBytes = k
default:
return ErrInvalidKeyType
}

Expand Down Expand Up @@ -78,18 +84,26 @@ func (m *SigningMethodHMAC) Verify(signingString, signature string, key interfac
}

// Sign implements token signing for the SigningMethod.
// Key must be []byte
// Key must be []byte, string
func (m *SigningMethodHMAC) Sign(signingString string, key interface{}) (string, error) {
if keyBytes, ok := key.([]byte); ok {
if !m.Hash.Available() {
return "", ErrHashUnavailable
}

hasher := hmac.New(m.Hash.New, keyBytes)
hasher.Write([]byte(signingString))
var keyBytes []byte

switch k := key.(type) {
case string:
keyBytes = StringToBytes(k)
case []byte:
keyBytes = k
default:
return "", ErrInvalidKeyType
}

return EncodeSegment(hasher.Sum(nil)), nil
if !m.Hash.Available() {
return "", ErrHashUnavailable
}

return "", ErrInvalidKeyType
hasher := hmac.New(m.Hash.New, keyBytes)
hasher.Write([]byte(signingString))

return EncodeSegment(hasher.Sum(nil)), nil

}
30 changes: 18 additions & 12 deletions hmac_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,17 +48,21 @@ var hmacTestData = []struct {
// Sample data from http://tools.ietf.org/html/draft-jones-json-web-signature-04#appendix-A.1
var hmacTestKey, _ = ioutil.ReadFile("test/hmacTestKey")

var hmacTestKeyString = string(hmacTestKey)

func TestHMACVerify(t *testing.T) {
for _, data := range hmacTestData {
parts := strings.Split(data.tokenString, ".")

method := jwt.GetSigningMethod(data.alg)
err := method.Verify(strings.Join(parts[0:2], "."), parts[2], hmacTestKey)
if data.valid && err != nil {
t.Errorf("[%v] Error while verifying key: %v", data.name, err)
}
if !data.valid && err == nil {
t.Errorf("[%v] Invalid key passed validation", data.name)
for _, hmacTestKey := range []interface{}{hmacTestKey, hmacTestKeyString} {
err := method.Verify(strings.Join(parts[0:2], "."), parts[2], hmacTestKey)
if data.valid && err != nil {
t.Errorf("[%v] Error while verifying key: %v", data.name, err)
}
if !data.valid && err == nil {
t.Errorf("[%v] Invalid key passed validation", data.name)
}
}
}
}
Expand All @@ -68,12 +72,14 @@ func TestHMACSign(t *testing.T) {
if data.valid {
parts := strings.Split(data.tokenString, ".")
method := jwt.GetSigningMethod(data.alg)
sig, err := method.Sign(strings.Join(parts[0:2], "."), hmacTestKey)
if err != nil {
t.Errorf("[%v] Error signing token: %v", data.name, err)
}
if sig != parts[2] {
t.Errorf("[%v] Incorrect signature.\nwas:\n%v\nexpecting:\n%v", data.name, sig, parts[2])
for _, hmacTestKey := range []interface{}{hmacTestKey, hmacTestKeyString} {
sig, err := method.Sign(strings.Join(parts[0:2], "."), hmacTestKey)
if err != nil {
t.Errorf("[%v] Error signing token: %v", data.name, err)
}
if sig != parts[2] {
t.Errorf("[%v] Incorrect signature.\nwas:\n%v\nexpecting:\n%v", data.name, sig, parts[2])
}
}
}
}
Expand Down
15 changes: 15 additions & 0 deletions string_to_bytes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package jwt

import (
"reflect"
"unsafe"
)

func StringToBytes(s string) (b []byte) {
bh := (*reflect.SliceHeader)(unsafe.Pointer(&b))
Copy link
Member

@mfridman mfridman Jan 24, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for opening a PR.

There has to be an extremely good rationale for introducing the unsafe package backed with benchmark results.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your answer.
I'm in China and it's night time. I can add benchmark examples tomorrow.
First of all, the memory layout of string and []byte is similar.

// reflect.StringHeader

type StringHeader struct {
        Data uintptr
        Len  int
}
/*
┌────────────────┐
│                │
│                │
│  Data(uintptr) │
│                │
│  8byte         │
│                │
├────────────────┤
│                │
│                │
│  Len(int)      │
│                │
│  8byte         │
│                │
└────────────────┘
 */
// reflect.SliceHeader
type SliceHeader struct {
        Data uintptr
        Len  int
        Cap  int
}

/*
┌───────────────────┐
│                   │
│   Data(uintptr)   │
│                   │
│   8byte           │
│                   │
│                   │
├───────────────────┤
│                   │
│   Len(int)        │
│                   │
│   8byte           │
│                   │
│                   │
├───────────────────┤
│                   │
│   Cap(int)        │
│                   │
│   8byte           │
│                   │
│                   │
└───────────────────┘
 */

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

package test

import (
	"reflect"
	"testing"
	"unsafe"
)

func StringToBytes(s string) (b []byte) {
	bh := (*reflect.SliceHeader)(unsafe.Pointer(&b))
	sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
	bh.Data = sh.Data
	bh.Len = sh.Len
	bh.Cap = sh.Len
	return b
}

func BenchmarkStringToBytes_Std(t *testing.B) {
	s := "hello world"
	for i := 0; i < t.N; i++ {
		b := []byte(s)
		_ = b
	}
}

func BenchmarkStringToBytes(t *testing.B) {
	s := "hello world"
	for i := 0; i < t.N; i++ {
		b := StringToBytes(s)
		_ = b
	}
}
goos: darwin
goarch: amd64
pkg: test
cpu: Intel(R) Core(TM) i7-1068NG7 CPU @ 2.30GHz
BenchmarkStringToBytes_Std-8   	296108143	         3.970 ns/op	       0 B/op	       0 allocs/op
BenchmarkStringToBytes-8       	1000000000	         0.2813 ns/op	       0 B/op	       0 allocs/op
PASS
ok  	test	2.305s

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added benchmark data @mfridman

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be a bit biased, but I do not seem a good reason here to include unsafe code, despite the performance gain. If you want faster performance, why not just use []byte?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be a bit biased, but I do not seem a good reason here to include unsafe code, despite the performance gain. If you want faster performance, why not just use []byte?

Using StringToBytes or [] byte to realize the type conversion from string to [] byte is not the key point. This PR is to make the sign and verify interface support string type

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Putting aside performance, if the caller has a string it is trivial to pass []byte("string"). I general I wish this wasn't an interface{} but we can't break the contract in the current version.

Do we gain anything by passing that type conversion to this library? Maybe? I'm trying to understand which problem we're solving.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ran into this same issue of expecting that Sign would accept a string type and ended up opening #245 without noticing this PR..

Do we gain anything by passing that type conversion to this library? Maybe? I'm trying to understand which problem we're solving.

It is friendlier to callers of this code.
Sure it's simple for the caller to wrap it but the same can be said for adding this directly into this package.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the conversion were to happen in jwt, I agree that it should not rely on unsafe and would recommend following the approach here #245

It looks like gin and fasthttp do this conversion:
https://github.com/gin-gonic/gin/blob/ee4de846a894e9049321e809d69f4343f62d2862/internal/bytesconv/bytesconv.go#L11-L24
https://github.com/valyala/fasthttp/blob/404c8a896896943715f8fb3906a8d054fae17d3e/bytesconv.go#L320-L343

heres an thread on golang-nuts about this and go team members directly recommending against it:
https://groups.google.com/g/Golang-Nuts/c/ENgbUzYvCuU/m/90yGx7GUAgAJ

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is your key a string in the first place? It shouldn’t be. The hmac package expects a byte slice, so that is why we only accept a byte slice. Having the key as a string for me is not a good design choice since this is not a „password“ and does not necessarily be human readible but a random byte slice.

sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
bh.Data = sh.Data
bh.Len = sh.Len
bh.Cap = sh.Len
return b
}