aboutsummaryrefslogtreecommitdiff
path: root/lib/paseto
diff options
context:
space:
mode:
Diffstat (limited to 'lib/paseto')
-rw-r--r--lib/paseto/message.go17
-rw-r--r--lib/paseto/message_test.go89
-rw-r--r--lib/paseto/paseto.go4
-rw-r--r--lib/paseto/payload.go16
-rw-r--r--lib/paseto/payload_test.go117
-rw-r--r--lib/paseto/v2/example_public_mode_test.go2
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
}