diff options
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/paseto/message.go | 17 | ||||
| -rw-r--r-- | lib/paseto/message_test.go | 89 | ||||
| -rw-r--r-- | lib/paseto/paseto.go | 4 | ||||
| -rw-r--r-- | lib/paseto/payload.go | 16 | ||||
| -rw-r--r-- | lib/paseto/payload_test.go | 117 | ||||
| -rw-r--r-- | lib/paseto/v2/example_public_mode_test.go | 2 |
6 files changed, 202 insertions, 43 deletions
diff --git a/lib/paseto/message.go b/lib/paseto/message.go index ef84a84a..f4383df2 100644 --- a/lib/paseto/message.go +++ b/lib/paseto/message.go @@ -14,19 +14,20 @@ import ( // List of error messages for [Message.Unpack]. var ( - ErrTokenHeader = errors.New(`invalid token header`) - ErrTokenFooter = errors.New(`invalid token footer`) - ErrTokenSize = errors.New(`invalid token payload size`) + ErrTokenHeader = errors.New(`invalid token header`) + ErrTokenPayload = errors.New(`invalid token payload`) + ErrTokenFooter = errors.New(`invalid token footer`) + ErrTokenSize = errors.New(`invalid token payload size`) ) // Message defines the payload be signed and verified by sender/receiver. type Message struct { // The following fields are filled after Unpack and MUST not be used // directly. - RawPayload []byte - RawFooter []byte - Sig []byte - PAE []byte + RawPayload []byte `json:"-"` + RawFooter []byte `json:"-"` + Sig []byte `json:"-"` + PAE []byte `json:"-"` // The Payload and optional Footer to be packed and signed into token. Footer Footer @@ -73,7 +74,7 @@ func (msg *Message) Unpack(header, token string, implicit []byte) (err error) { // Step 4: Decodes the payload. paysig, err := base64.RawURLEncoding.DecodeString(token) if err != nil { - return fmt.Errorf(`%s: %w`, logp, err) + return fmt.Errorf(`%s: %w: %w`, logp, ErrTokenPayload, err) } lenpaysig := len(paysig) if lenpaysig <= ed25519.SignatureSize { diff --git a/lib/paseto/message_test.go b/lib/paseto/message_test.go new file mode 100644 index 00000000..4dd63755 --- /dev/null +++ b/lib/paseto/message_test.go @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2026 Shulhan <ms@kilabit.info> + +package paseto + +import ( + "encoding/json" + "testing" + "testing/synctest" + + "git.sr.ht/~shulhan/pakakeh.go/lib/test" +) + +func TestNewMessage(t *testing.T) { + synctest.Test(t, func(t *testing.T) { + sender := Peer{ + ID: `sender`, + } + receiver := Peer{ + ID: `receiver`, + } + msg := NewMessage(sender, receiver, `test`) + + gotmsg, err := json.MarshalIndent(msg, ``, ` `) + if err != nil { + t.Fatal(err) + } + + expmsg := `{ + "Footer": { + "peer_id": "sender" + }, + "Payload": { + "data": null, + "iss": "sender", + "sub": "test", + "aud": "receiver", + "exp": 946684860, + "nbf": 946684800, + "iat": 946684800 + } +}` + test.Assert(t, `NewMessage`, expmsg, string(gotmsg)) + }) +} + +func TestMessage_Unpack(t *testing.T) { + listcase := []struct { + desc string + token string + expError string + expMsg Message + }{{ + desc: `ErrTokenHeader`, + token: `v5.public.xxx.xxx`, + expError: `Unpack: invalid token header: want v4.public.`, + }, { + desc: `ErrTokenFooter`, + token: `v4.public.xxx.+++`, + expError: `Unpack: invalid token footer: illegal base64 data at input byte 0`, + }, { + desc: `ErrTokenPayload`, + token: `v4.public.WvLTlMrX9NpYDQlEIFlnDB==.xxx`, + expError: `Unpack: invalid token payload: illegal base64 data at input byte 22`, + }, { + desc: `ErrTokenSize`, + token: `v4.public.WvLTlMrX9NpYDQlEIFlnDB.xxx`, + expError: `Unpack: invalid token payload size 16`, + }, { + desc: `Valid`, + token: `v4.public.eyJkYXRhIjp7ImVtYWlsIjoiaGVsbG9AZXhhbXBsZS5jb20ifSwiaXNzIjoiam9obkBleGFtcGxlLmNvbSIsInN1YiI6ImhlbGxvIiwiYXVkIjoiamFuZUBleGFtcGxlLm9yZyIsImV4cCI6MTc3NDg1OTg4OSwibmJmIjoxNzc0ODU5ODI5LCJpYXQiOjE3NzQ4NTk4Mjl9eGHMkOlYiVchKl1GqQwSMyTC8eWCpXhNJHGHq_MEJNqlKur-kPpHqY-hWHoh4I1RaBq68MN_TNveqwPIbk9iBQ.eyJkYXRhIjp7InF1b3RlIjoiTGl2ZSB0byBlYXQifSwicGVlcl9pZCI6ImpvaG5AZXhhbXBsZS5jb20ifQ`, + expMsg: Message{ + RawPayload: []byte(`{"data":{"email":"hello@example.com"},"iss":"john@example.com","sub":"hello","aud":"jane@example.org","exp":1774859889,"nbf":1774859829,"iat":1774859829}`), + RawFooter: []byte(`{"data":{"quote":"Live to eat"},"peer_id":"john@example.com"}`), + Sig: []byte{0x78, 0x61, 0xcc, 0x90, 0xe9, 0x58, 0x89, 0x57, 0x21, 0x2a, 0x5d, 0x46, 0xa9, 0xc, 0x12, 0x33, 0x24, 0xc2, 0xf1, 0xe5, 0x82, 0xa5, 0x78, 0x4d, 0x24, 0x71, 0x87, 0xab, 0xf3, 0x4, 0x24, 0xda, 0xa5, 0x2a, 0xea, 0xfe, 0x90, 0xfa, 0x47, 0xa9, 0x8f, 0xa1, 0x58, 0x7a, 0x21, 0xe0, 0x8d, 0x51, 0x68, 0x1a, 0xba, 0xf0, 0xc3, 0x7f, 0x4c, 0xdb, 0xde, 0xab, 0x3, 0xc8, 0x6e, 0x4f, 0x62, 0x5}, + PAE: []byte{0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xa, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x76, 0x34, 0x2e, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x2e, 0x99, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x7b, 0x22, 0x64, 0x61, 0x74, 0x61, 0x22, 0x3a, 0x7b, 0x22, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x22, 0x3a, 0x22, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x40, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x22, 0x7d, 0x2c, 0x22, 0x69, 0x73, 0x73, 0x22, 0x3a, 0x22, 0x6a, 0x6f, 0x68, 0x6e, 0x40, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x22, 0x2c, 0x22, 0x73, 0x75, 0x62, 0x22, 0x3a, 0x22, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x22, 0x2c, 0x22, 0x61, 0x75, 0x64, 0x22, 0x3a, 0x22, 0x6a, 0x61, 0x6e, 0x65, 0x40, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x6f, 0x72, 0x67, 0x22, 0x2c, 0x22, 0x65, 0x78, 0x70, 0x22, 0x3a, 0x31, 0x37, 0x37, 0x34, 0x38, 0x35, 0x39, 0x38, 0x38, 0x39, 0x2c, 0x22, 0x6e, 0x62, 0x66, 0x22, 0x3a, 0x31, 0x37, 0x37, 0x34, 0x38, 0x35, 0x39, 0x38, 0x32, 0x39, 0x2c, 0x22, 0x69, 0x61, 0x74, 0x22, 0x3a, 0x31, 0x37, 0x37, 0x34, 0x38, 0x35, 0x39, 0x38, 0x32, 0x39, 0x7d, 0x3d, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x7b, 0x22, 0x64, 0x61, 0x74, 0x61, 0x22, 0x3a, 0x7b, 0x22, 0x71, 0x75, 0x6f, 0x74, 0x65, 0x22, 0x3a, 0x22, 0x4c, 0x69, 0x76, 0x65, 0x20, 0x74, 0x6f, 0x20, 0x65, 0x61, 0x74, 0x22, 0x7d, 0x2c, 0x22, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x22, 0x3a, 0x22, 0x6a, 0x6f, 0x68, 0x6e, 0x40, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x22, 0x7d, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, + }, + }} + + for _, tc := range listcase { + var msg Message + err := msg.Unpack(`v4.public.`, tc.token, nil) + if err != nil { + test.Assert(t, tc.desc, tc.expError, err.Error()) + continue + } + test.Assert(t, tc.desc, tc.expMsg, msg) + } +} diff --git a/lib/paseto/paseto.go b/lib/paseto/paseto.go index 9268a383..d5b3b569 100644 --- a/lib/paseto/paseto.go +++ b/lib/paseto/paseto.go @@ -5,9 +5,9 @@ // of Platform-Agnostic SEcurity TOkens (PASETO) version 2 and version 4 as // defined in [paseto-v2] and [paseto-v4]. // -// The paseto version 2 is available under sub packet +// The paseto version 2 is available under sub package // [git.sr.ht/~shulhan/pakakeh.go/lib/paseto/v2] and the paseto version 4 -// is available under sub packet +// is available under sub package // [git.sr.ht/~shulhan/pakakeh.go/lib/paseto/v4]. // // [paseto-v2]: https://github.com/paseto-standard/paseto-spec/blob/master/docs/01-Protocol-Versions/Version2.md diff --git a/lib/paseto/payload.go b/lib/paseto/payload.go index a659943a..b5b458bd 100644 --- a/lib/paseto/payload.go +++ b/lib/paseto/payload.go @@ -19,7 +19,7 @@ var ( ErrPayloadSubject = errors.New(`unknown subject`) ErrPayloadAudience = errors.New(`invalid audience`) ErrPayloadExpired = errors.New(`expired`) - ErrPayloadNotBefore = errors.New(`payload cannot be used yet`) + ErrPayloadNotBefore = errors.New(`token cannot be used yet`) ) // Payload represents the data and claims. @@ -84,11 +84,13 @@ func (pload *Payload) Validate(recvID string, sender Peer) (err error) { if len(sender.AllowedSubjects) != 0 { _, ok := sender.AllowedSubjects[pload.Subject] if !ok { - return fmt.Errorf(`%s: %w`, logp, ErrPayloadSubject) + return fmt.Errorf(`%s: %w: %s`, logp, + ErrPayloadSubject, pload.Subject) } } if len(recvID) != 0 && pload.Audience != recvID { - return fmt.Errorf(`%s: %w`, logp, ErrPayloadAudience) + return fmt.Errorf(`%s: %w: %s`, logp, ErrPayloadAudience, + pload.Audience) } if pload.ExpiredAt != 0 { diff := pload.ExpiredAt - now @@ -97,7 +99,9 @@ func (pload *Payload) Validate(recvID string, sender Peer) (err error) { diff *= -1 } if diff > DriftSeconds { - return fmt.Errorf(`%s: %w`, logp, ErrPayloadExpired) + return fmt.Errorf(`%s: %w: exp is %s`, logp, + ErrPayloadExpired, + time.Unix(pload.ExpiredAt, 0).UTC()) } } if pload.NotBefore != 0 { @@ -106,7 +110,9 @@ func (pload *Payload) Validate(recvID string, sender Peer) (err error) { diff *= -1 } if diff > DriftSeconds { - return fmt.Errorf(`%s: %w`, logp, ErrPayloadNotBefore) + return fmt.Errorf(`%s: %w: nbf is %s`, logp, + ErrPayloadNotBefore, + time.Unix(pload.NotBefore, 0).UTC()) } } return nil diff --git a/lib/paseto/payload_test.go b/lib/paseto/payload_test.go index 75f18ba4..e24eb208 100644 --- a/lib/paseto/payload_test.go +++ b/lib/paseto/payload_test.go @@ -5,40 +5,103 @@ package paseto import ( "testing" + "testing/synctest" "time" "git.sr.ht/~shulhan/pakakeh.go/lib/test" ) func TestPayload_Validate(t *testing.T) { - now := time.Now().UTC().Unix() - peer := Peer{} + synctest.Test(t, func(t *testing.T) { + now := time.Now().UTC().Unix() + drifted1s := now + 1 + drifted2x := now + (DriftSeconds * 2) - issued1sAgo := now - 1 - issued6sAgo := now - 6 + listCase := []struct { + desc string + receiverID string + expErr string + sender Peer + pload Payload + }{{ + desc: `ErrPayloadIssuer`, + sender: Peer{ + ID: `me`, + }, + pload: Payload{ + Issuer: `other`, + }, + expErr: `payload: unknown issuer`, + }, { + desc: `ErrPayloadSubject`, + sender: Peer{ + ID: `sender`, + AllowedSubjects: map[string]struct{}{ + `create`: struct{}{}, + `update`: struct{}{}, + }, + }, + pload: Payload{ + Issuer: `sender`, + Subject: `delete`, + }, + expErr: `payload: unknown subject: delete`, + }, { + desc: `ErrPayloadAudience`, + sender: Peer{ + ID: `sender`, + AllowedSubjects: map[string]struct{}{ + `create`: struct{}{}, + `update`: struct{}{}, + }, + }, + receiverID: `receiver`, + pload: Payload{ + Issuer: `sender`, + Audience: `other`, + Subject: `create`, + }, + expErr: `payload: invalid audience: other`, + }, { + desc: `ErrPayloadExpired`, + pload: Payload{ + ExpiredAt: now, + }, + expErr: `payload: expired: exp is 2000-01-01 00:00:00 +0000 UTC`, + }, { + desc: `NotBefore drifted 1s`, + pload: Payload{ + NotBefore: drifted1s, + }, + }, { + desc: `ErrPayloadNotBefore`, + pload: Payload{ + NotBefore: drifted2x, + }, + expErr: `payload: token cannot be used yet: nbf is 2000-01-01 00:00:10 +0000 UTC`, + }, { + desc: `OK`, + sender: Peer{ + ID: `sender`, + AllowedSubjects: map[string]struct{}{ + `create`: struct{}{}, + `update`: struct{}{}, + }, + }, + receiverID: `receiver`, + pload: Payload{ + Issuer: `sender`, + Audience: `receiver`, + Subject: `create`, + }, + }} - listCase := []struct { - desc string - pload *Payload - expErr string - }{{ - desc: `With IssuedAt less than current time`, - pload: &Payload{ - IssuedAt: issued1sAgo, - }, - }, { - desc: `With IssuedAt greater than drift`, - pload: &Payload{ - IssuedAt: issued6sAgo, - }, - expErr: `payload: issued-at is after current time`, - }} - - for _, tc := range listCase { - err := tc.pload.Validate(``, peer) - if err != nil { - test.Assert(t, tc.desc, tc.expErr, err.Error()) - continue + for _, tc := range listCase { + err := tc.pload.Validate(tc.receiverID, tc.sender) + if err != nil { + test.Assert(t, tc.desc, tc.expErr, err.Error()) + continue + } } - } + }) } diff --git a/lib/paseto/v2/example_public_mode_test.go b/lib/paseto/v2/example_public_mode_test.go index e91f56a2..967321f9 100644 --- a/lib/paseto/v2/example_public_mode_test.go +++ b/lib/paseto/v2/example_public_mode_test.go @@ -95,5 +95,5 @@ func ExamplePublicMode() { // Output: // Received data: hello receiver // Received footer: {Data:map[FOOTER:HERE] PeerID:sender} - // payload: unknown subject + // payload: unknown subject: unknown-subject } |
