Skip to content

Commit

Permalink
Implement RFC7797 (#419)
Browse files Browse the repository at this point in the history
* Implement RFC7797

(TODO: return error if b64 = false and payload contains '.')

* remove unused

* style tweaks

* upgrade golangci-lint

accidentally added directives for newer golangci-lint. might as well
upgrade it then

* check for b64 = false and payload contain '.'

* Update Changes
  • Loading branch information
lestrrat committed Jul 28, 2021
1 parent 90b0d09 commit 9204f79
Show file tree
Hide file tree
Showing 9 changed files with 228 additions and 25 deletions.
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
}
}
})
}
})
}

0 comments on commit 9204f79

Please sign in to comment.