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

Implement RFC7797 #419

Merged
merged 6 commits into from Jul 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/lint.yml
Expand Up @@ -8,7 +8,7 @@ jobs:
- uses: actions/checkout@v2
- uses: golangci/golangci-lint-action@v2
with:
version: v1.39.0
version: v1.41.1
- name: Run go vet
run: |
go vet ./...
1 change: 1 addition & 0 deletions .golangci.yml
Expand Up @@ -36,6 +36,7 @@ linters:
- nestif
- nlreturn
- paralleltest
- tagliatelle
- testpackage
- thelper
- wrapcheck
Expand Down
6 changes: 6 additions & 0 deletions Changes
@@ -1,6 +1,12 @@
Changes
=======

v1.2.5 (UNRELEASED)
[New features]
* Implement RFC7797. The value of the header field `b64` changes
how the payload is treated in JWS
* Implement detached payloads for JWS

v1.2.4 15 Jul 2021
[Bug fixes]
* We had the same off-by-one in another place and jumped the gun on
Expand Down
3 changes: 2 additions & 1 deletion jws/README.md
@@ -1,12 +1,13 @@
# github.com/lestrrat-go/jwx/jws [![Go Reference](https://pkg.go.dev/badge/github.com/lestrrat-go/jwx/jws.svg)](https://pkg.go.dev/github.com/lestrrat-go/jwx/jws)

Package jws implements JWS as described in [RFC7515](https://tools.ietf.org/html/rfc7515)
Package jws implements JWS as described in [RFC7515](https://tools.ietf.org/html/rfc7515) and [RFC7797](https://tools.ietf.org/html/rfc7797)

* Parse and generate compact or JSON serializations
* Sign and verify arbitrary payload
* Use any of the keys supported in [github.com/lestrrat-go/jwx/jwk](../jwk)
* Add arbitrary fields in the JWS object
* Ability to add/replace existing signature methods
* Respect "b64" settings for RFC7797

Examples are located in the examples directory ([jws_example_test.go](../examples/jws_example_test.go))

Expand Down
1 change: 1 addition & 0 deletions jws/interface.go
Expand Up @@ -51,6 +51,7 @@ import (
type Message struct {
payload []byte
signatures []*Signature
b64 bool // true if payload should be base64 encoded
}

type Signature struct {
Expand Down
65 changes: 57 additions & 8 deletions jws/jws.go
Expand Up @@ -79,6 +79,11 @@ func (s *payloadSigner) PublicHeader() Headers {
// the type of key you provided, otherwise an error is returned.
//
// If you would like to pass custom headers, use the WithHeaders option.
//
// If the headers contain "b64" field, then the boolean value for the field
// is respected when creating the compact serialization form. That is,
// if you specify a header with `{"b64": false}`, then the payload is
// not base64 encoded.
func Sign(payload []byte, alg jwa.SignatureAlgorithm, key interface{}, options ...SignOption) ([]byte, error) {
var hdrs Headers
for _, o := range options {
Expand Down Expand Up @@ -163,11 +168,14 @@ func SignMulti(payload []byte, options ...Option) ([]byte, error) {
// use `Parse` function to get `Message` object.
func Verify(buf []byte, alg jwa.SignatureAlgorithm, key interface{}, options ...VerifyOption) ([]byte, error) {
var dst *Message
var detachedPayload []byte
//nolint:forcetypeassert
for _, option := range options {
switch option.Ident() {
case identMessage{}:
dst = option.Value().(*Message)
case identDetachedPayload{}:
detachedPayload = option.Value().([]byte)
}
}

Expand All @@ -177,9 +185,9 @@ func Verify(buf []byte, alg jwa.SignatureAlgorithm, key interface{}, options ...
}

if buf[0] == '{' {
return verifyJSON(buf, alg, key, dst)
return verifyJSON(buf, alg, key, dst, detachedPayload)
}
return verifyCompact(buf, alg, key, dst)
return verifyCompact(buf, alg, key, dst, detachedPayload)
}

// VerifySet uses keys store in a jwk.Set to verify the payload in `buf`.
Expand Down Expand Up @@ -217,7 +225,7 @@ func VerifySet(buf []byte, set jwk.Set) ([]byte, error) {
return nil, errors.New(`failed to verify message with any of the keys in the jwk.Set object`)
}

func verifyJSON(signed []byte, alg jwa.SignatureAlgorithm, key interface{}, dst *Message) ([]byte, error) {
func verifyJSON(signed []byte, alg jwa.SignatureAlgorithm, key interface{}, dst *Message, detachedPayload []byte) ([]byte, error) {
verifier, err := NewVerifier(alg)
if err != nil {
return nil, errors.Wrap(err, "failed to create verifier")
Expand All @@ -228,8 +236,21 @@ func verifyJSON(signed []byte, alg jwa.SignatureAlgorithm, key interface{}, dst
return nil, errors.Wrap(err, `failed to unmarshal JSON message`)
}

if len(m.payload) != 0 && detachedPayload != nil {
return nil, errors.New(`can't specify detached payload for JWS with payload`)
}

if detachedPayload != nil {
m.payload = detachedPayload
}

// Pre-compute the base64 encoded version of payload
payload := base64.EncodeToString(m.payload)
var payload string
if m.b64 {
payload = base64.EncodeToString(m.payload)
} else {
payload = string(m.payload)
}

buf := pool.GetBytesBuffer()
defer pool.ReleaseBytesBuffer(buf)
Expand Down Expand Up @@ -263,7 +284,23 @@ func verifyJSON(signed []byte, alg jwa.SignatureAlgorithm, key interface{}, dst
return nil, errors.New(`could not verify with any of the signatures`)
}

func verifyCompact(signed []byte, alg jwa.SignatureAlgorithm, key interface{}, dst *Message) ([]byte, error) {
// get the value of b64 header field.
// If the field does not exist, returns true (default)
// Otherwise return the value specified by the header field.
func getB64Value(hdr Headers) bool {
b64raw, ok := hdr.Get("b64")
if !ok {
return true // default
}

b64, ok := b64raw.(bool) // default
if !ok {
return false
}
return b64
}

func verifyCompact(signed []byte, alg jwa.SignatureAlgorithm, key interface{}, dst *Message, detachedPayload []byte) ([]byte, error) {
protected, payload, signature, err := SplitCompact(signed)
if err != nil {
return nil, errors.Wrap(err, `failed extract from compact serialization format`)
Expand All @@ -279,6 +316,9 @@ func verifyCompact(signed []byte, alg jwa.SignatureAlgorithm, key interface{}, d

verifyBuf.Write(protected)
verifyBuf.WriteByte('.')
if len(payload) == 0 && detachedPayload != nil {
payload = detachedPayload
}
verifyBuf.Write(payload)

decodedSignature, err := base64.Decode(signature)
Expand All @@ -303,13 +343,22 @@ func verifyCompact(signed []byte, alg jwa.SignatureAlgorithm, key interface{}, d
}
}
}

if err := verifier.Verify(verifyBuf.Bytes(), decodedSignature, key); err != nil {
return nil, errors.Wrap(err, `failed to verify message`)
}

decodedPayload, err := base64.Decode(payload)
if err != nil {
return nil, errors.Wrap(err, `message verified, failed to decode payload`)
var decodedPayload []byte
if !getB64Value(hdr) { // it's not base64 encode
decodedPayload = payload
}

if decodedPayload == nil {
v, err := base64.Decode(payload)
if err != nil {
return nil, errors.Wrap(err, `message verified, failed to decode payload`)
}
decodedPayload = v
}

if dst != nil {
Expand Down
115 changes: 115 additions & 0 deletions jws/jws_test.go
Expand Up @@ -1137,3 +1137,118 @@ func TestWithMessage(t *testing.T) {
return
}
}

func TestRFC7797(t *testing.T) {
const keysrc = `{"kty":"oct",
"k":"AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow"
}`
detached := []byte(`$.02`)

key, err := jwk.ParseKey([]byte(keysrc))
if !assert.NoError(t, err, `jwk.Parse should succeed`) {
return
}

t.Run("Invalid payload when b64 = false", func(t *testing.T) {
const payload = `$.02`
hdrs := jws.NewHeaders()
hdrs.Set("b64", false)
hdrs.Set("crit", "b64")

_, err := jws.Sign([]byte(payload), jwa.HS256, key, jws.WithHeaders(hdrs))
if !assert.Error(t, err, `jws.Sign should fail`) {
return
}
})
t.Run("Roundtrip", func(t *testing.T) {
const payload = `hell0w0r1d`
hdrs := jws.NewHeaders()
hdrs.Set("b64", false)
hdrs.Set("crit", "b64")

signed, err := jws.Sign([]byte(payload), jwa.HS256, key, jws.WithHeaders(hdrs))
if !assert.NoError(t, err, `jws.Sign should succeed`) {
return
}

verified, err := jws.Verify(signed, jwa.HS256, key)
if !assert.NoError(t, err, `jws.Verify should succeed`) {
return
}

if !assert.Equal(t, []byte(payload), verified, `payload should match`) {
return
}
})

t.Run("Verify", func(t *testing.T) {
testcases := []struct {
Name string
Input []byte
VerifyOptions []jws.VerifyOption
Error bool
}{
{
Name: "JSON format",
Input: []byte(`{
"protected": "eyJhbGciOiJIUzI1NiIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19",
"payload": "$.02",
"signature": "A5dxf2s96_n5FLueVuW1Z_vh161FwXZC4YLPff6dmDY"
}`),
},
{
Name: "JSON format (detached payload)",
VerifyOptions: []jws.VerifyOption{
jws.WithDetachedPayload(detached),
},
Input: []byte(`{
"protected": "eyJhbGciOiJIUzI1NiIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19",
"signature": "A5dxf2s96_n5FLueVuW1Z_vh161FwXZC4YLPff6dmDY"
}`),
},
{
Name: "JSON Format (b64 does not match)",
Error: true,
Input: []byte(`{
"signatures": [
{
"protected": "eyJhbGciOiJIUzI1NiIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19",
"signature": "A5dxf2s96_n5FLueVuW1Z_vh161FwXZC4YLPff6dmDY"
},
{
"protected": "eyJhbGciOiJIUzI1NiIsImI2NCI6dHJ1ZSwiY3JpdCI6WyJiNjQiXX0",
"signature": "6BjugbC8MfrT_yy5WxWVFZrEHVPDtpdsV9u-wbzQDV8"
}
],
"payload":"$.02"
}`),
},
{
Name: "Compact (detached payload)",
Input: []byte(`eyJhbGciOiJIUzI1NiIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..A5dxf2s96_n5FLueVuW1Z_vh161FwXZC4YLPff6dmDY`),
VerifyOptions: []jws.VerifyOption{
jws.WithDetachedPayload(detached),
},
},
}

for _, tc := range testcases {
tc := tc
t.Run(tc.Name, func(t *testing.T) {
payload, err := jws.Verify(tc.Input, jwa.HS256, key, tc.VerifyOptions...)
if tc.Error {
if !assert.Error(t, err, `jws.Verify should fail`) {
return
}
} else {
if !assert.NoError(t, err, `jws.Verify should succeed`) {
return
}
if !assert.Equal(t, detached, payload, `payload should match`) {
return
}
}
})
}
})
}