aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--lib/paseto/message.go77
-rw-r--r--lib/paseto/v4/public_mode.go173
-rw-r--r--lib/paseto/v4/public_mode_example_test.go115
3 files changed, 321 insertions, 44 deletions
diff --git a/lib/paseto/message.go b/lib/paseto/message.go
index 47718f9e..e9dca297 100644
--- a/lib/paseto/message.go
+++ b/lib/paseto/message.go
@@ -3,8 +3,83 @@
package paseto
+import (
+ "crypto/ed25519"
+ "encoding/base64"
+ "fmt"
+ "strings"
+ "time"
+)
+
// Message defines the payload be signed and verified by sender/receiver.
type Message struct {
- Payload Payload
+ // The following fields are filled after Unpack and MUST not be used
+ // directly.
+ RawPayload []byte
+ RawFooter []byte
+ Sig []byte
+ PAE []byte
+
+ // The Payload and optional Footer to be packed and signed into token.
Footer Footer
+ Payload Payload
+}
+
+// NewMessage returns [Message] where all fields except [Payload.Data] and
+// [Footer.Data] are set using send, receiver, subject, and current time.
+func NewMessage(sender, receiver Peer, subject string) (msg *Message) {
+ now := time.Now().UTC().Unix()
+ msg = &Message{
+ Payload: Payload{
+ Issuer: sender.ID,
+ Audience: receiver.ID,
+ Subject: subject,
+ IssuedAt: now,
+ NotBefore: now,
+ ExpiredAt: now + DefaultTTL,
+ },
+ Footer: Footer{
+ PeerID: sender.ID,
+ },
+ }
+ return msg
+}
+
+// Unpack returns the decoded token into a [Message].
+func (msg *Message) Unpack(header, token string, implicit []byte) (err error) {
+ logp := `Unpack`
+
+ // Step 3: verify the header and unpack the footer if it exists.
+ token, found := strings.CutPrefix(token, header)
+ if !found {
+ return fmt.Errorf(`%s: invalid header, want %s`, logp, header)
+ }
+ token, footerb64, found := strings.Cut(token, `.`)
+ if found {
+ msg.RawFooter, err = base64.RawURLEncoding.DecodeString(footerb64)
+ if err != nil {
+ return fmt.Errorf(`%s: invalid footer: %w`, logp, err)
+ }
+ }
+
+ // Step 4: Decodes the payload.
+ paysig, err := base64.RawURLEncoding.DecodeString(token)
+ if err != nil {
+ return fmt.Errorf(`%s: %w`, logp, err)
+ }
+ lenpaysig := len(paysig)
+ if lenpaysig <= ed25519.SignatureSize {
+ return fmt.Errorf(`%s: invalid payload size %d`, logp, lenpaysig)
+ }
+ msg.RawPayload = paysig[:lenpaysig-64]
+ msg.Sig = paysig[lenpaysig-64:]
+
+ // Step 5: Generate PAE.
+ msg.PAE, err = PreAuthEncode([]byte(header), msg.RawPayload,
+ msg.RawFooter, implicit)
+ if err != nil {
+ return fmt.Errorf(`%s: %w`, logp, err)
+ }
+
+ return nil
}
diff --git a/lib/paseto/v4/public_mode.go b/lib/paseto/v4/public_mode.go
index 3ea901eb..605be124 100644
--- a/lib/paseto/v4/public_mode.go
+++ b/lib/paseto/v4/public_mode.go
@@ -7,53 +7,126 @@ import (
"bytes"
"crypto/ed25519"
"encoding/base64"
+ "encoding/json"
+ "errors"
"fmt"
"slices"
- "strings"
+ "sync"
"git.sr.ht/~shulhan/pakakeh.go/lib/paseto"
)
const publicHeader = `v4.public.`
+// List of errors for [PublicMode.Unpack] and [PublicMode.Verify].
+var (
+ ErrSignature = errors.New(`invalid signature`)
+)
+
// PublicMode contains ed25519 private and public key for signing and
// verifying message.
type PublicMode struct {
- priv ed25519.PrivateKey
- pub ed25519.PublicKey
+ peers map[string]paseto.Peer
+
+ PrivateKey ed25519.PrivateKey
+ PublicKey ed25519.PublicKey
+
+ sync.Mutex
}
// NewPublicMode returns new instance of public mode from ed25519 seeds.
func NewPublicMode(seed []byte) (pmode *PublicMode) {
- pmode = &PublicMode{}
- pmode.priv = ed25519.NewKeyFromSeed(seed)
- pmode.pub = pmode.priv.Public().(ed25519.PublicKey)
+ pmode = &PublicMode{
+ peers: make(map[string]paseto.Peer),
+ }
+ pmode.PrivateKey = ed25519.NewKeyFromSeed(seed)
+ pmode.PublicKey = pmode.PrivateKey.Public().(ed25519.PublicKey)
return pmode
}
-// Sign returns the public token that has been signing with private key.
-// The token contains msg and optional footer.
-func (pmode *PublicMode) Sign(msg, footer, implicit []byte) (token string, err error) {
+// AddPeer adds the peer p to list of known peers for verifying incoming
+// token.
+// The only required fields in [paseto.Peer] is ID and Public.
+func (pmode *PublicMode) AddPeer(peer paseto.Peer) (err error) {
+ if len(peer.ID) == 0 {
+ return errors.New(`empty peer ID`)
+ }
+ if len(peer.Public) == 0 {
+ return errors.New(`empty public key`)
+ }
+ pmode.Lock()
+ pmode.peers[peer.ID] = peer
+ pmode.Unlock()
+ return nil
+}
+
+// GetPeer returns the Peer and true if ID found in list of peers.
+func (pmode *PublicMode) GetPeer(id string) (peer paseto.Peer, ok bool) {
+ pmode.Lock()
+ peer, ok = pmode.peers[id]
+ pmode.Unlock()
+ return peer, ok
+}
+
+// Pack returns the signed [paseto.Message] as public token.
+// The token can be verified and decoded into Message again using
+// [PublicMode.Unpack].
+func (pmode *PublicMode) Pack(msg paseto.Message, implicit []byte) (
+ token string, err error,
+) {
+ logp := `Pack`
+
+ msg.RawPayload, err = json.Marshal(msg.Payload)
+ if err != nil {
+ return ``, fmt.Errorf(`%s: %w`, logp, err)
+ }
+ msg.RawFooter, err = json.Marshal(msg.Footer)
+ if err != nil {
+ return ``, fmt.Errorf(`%s: %w`, logp, err)
+ }
+ token, err = pmode.Sign(msg.RawPayload, msg.RawFooter, implicit)
+ if err != nil {
+ return ``, fmt.Errorf(`%s: %w`, logp, err)
+ }
+ return token, nil
+}
+
+// RemovePeer removes the peer from list of peers based on its ID.
+func (pmode *PublicMode) RemovePeer(id string) {
+ pmode.Lock()
+ delete(pmode.peers, id)
+ pmode.Unlock()
+}
+
+// Sign returns the public token that has been signed with private key.
+// The token contains header, payload, signature, and optional footer.
+//
+// The token can be verified to get the payload and footer again using
+// [PublicMode.Verify].
+func (pmode *PublicMode) Sign(payload, footer, implicit []byte) (
+ token string, err error,
+) {
logp := `Sign`
// Step 3: pack header, message, footer, and implicit.
- pae, err := paseto.PreAuthEncode([]byte(publicHeader), msg, footer, implicit)
+ pae, err := paseto.PreAuthEncode([]byte(publicHeader),
+ payload, footer, implicit)
if err != nil {
return ``, fmt.Errorf(`%s: %w`, logp, err)
}
// Step 4: Sign pae.
- sig := ed25519.Sign(pmode.priv, pae)
+ sig := ed25519.Sign(pmode.PrivateKey, pae)
// Step 5: Pack all into token,
var buf bytes.Buffer
buf.WriteString(publicHeader)
- payload := slices.Concat(msg, sig)
- n := base64.RawURLEncoding.EncodedLen(len(payload))
+ paysig := slices.Concat(payload, sig)
+ n := base64.RawURLEncoding.EncodedLen(len(paysig))
b64 := make([]byte, n)
- base64.RawURLEncoding.Encode(b64, payload)
+ base64.RawURLEncoding.Encode(b64, paysig)
buf.Write(b64)
if len(footer) != 0 {
@@ -66,47 +139,61 @@ func (pmode *PublicMode) Sign(msg, footer, implicit []byte) (token string, err e
return buf.String(), nil
}
-// Verify returns the msg, with optional footer data, inside the token only if
-// its signature is valid.
-func (pmode *PublicMode) Verify(token string, implicit []byte) (msg, footer []byte, err error) {
- logp := `Verify`
+// Unpack verifies the token and returns it as [paseto.Message].
+// The token is verified using public key from peer ID in the footer, that
+// should has been added previously using [PublicMode.AddPeer].
+//
+// Note that, the [paseto.Payload] is not validated yet.
+// It is up to the user to validate and handle the error.
+func (pmode *PublicMode) Unpack(token string, implicit []byte, msg *paseto.Message) (
+ err error,
+) {
+ logp := `Unpack`
- // Step 3: verify the header and unpack the footer if it exists.
- token, found := strings.CutPrefix(token, publicHeader)
- if !found {
- return nil, nil, fmt.Errorf(`%s: invalid header, want %s`, logp, publicHeader)
- }
- token, footerb64, found := strings.Cut(token, `.`)
- if found {
- footer, err = base64.RawURLEncoding.DecodeString(footerb64)
- if err != nil {
- return nil, nil, fmt.Errorf(`%s: invalid footer: %w`, logp, err)
- }
+ err = msg.Unpack(publicHeader, token, implicit)
+ if err != nil {
+ return fmt.Errorf(`%s: %w`, logp, err)
}
- // Step 4: Decodes the payload.
- payload, err := base64.RawURLEncoding.DecodeString(token)
+ err = json.Unmarshal(msg.RawFooter, &msg.Footer)
if err != nil {
- return nil, nil, fmt.Errorf(`%s: %w`, logp, err)
+ return fmt.Errorf(`%s: %w`, logp, err)
}
- lenpayload := len(payload)
- if lenpayload <= ed25519.SignatureSize {
- return nil, nil, fmt.Errorf(`%s: invalid payload size, want %d got %d`,
- logp, ed25519.SignatureSize, len(payload))
+ sender, ok := pmode.GetPeer(msg.Footer.PeerID)
+ if !ok {
+ return fmt.Errorf(`%s: unknown peer ID %s`, logp, msg.Footer.PeerID)
}
- msg = payload[:lenpayload-64]
- sig := payload[lenpayload-64:]
- // Step 5: Generate PAE.
- pae, err := paseto.PreAuthEncode([]byte(publicHeader), msg, footer, implicit)
+ if !ed25519.Verify(sender.Public, msg.PAE, msg.Sig) {
+ return fmt.Errorf(`%s: %w`, logp, ErrSignature)
+ }
+
+ err = json.Unmarshal(msg.RawPayload, &msg.Payload)
+ if err != nil {
+ return fmt.Errorf(`%s: %w`, logp, err)
+ }
+
+ return nil
+}
+
+// Verify returns the payload and optional footer inside the token only if its
+// signature is valid.
+// The token is verified using the public key belong to PublicMode.
+func (pmode *PublicMode) Verify(token string, implicit []byte) (
+ payload, footer []byte, err error,
+) {
+ logp := `Verify`
+ msg := &paseto.Message{}
+
+ err = msg.Unpack(publicHeader, token, implicit)
if err != nil {
return nil, nil, fmt.Errorf(`%s: %w`, logp, err)
}
// Step 6: Verify the signature.
- if !ed25519.Verify(pmode.pub, pae, sig) {
- return nil, nil, fmt.Errorf(`%s: invalid message signature`, logp)
+ if !ed25519.Verify(pmode.PublicKey, msg.PAE, msg.Sig) {
+ return nil, nil, fmt.Errorf(`%s: %w`, logp, ErrSignature)
}
- return msg, footer, nil
+ return msg.RawPayload, msg.RawFooter, nil
}
diff --git a/lib/paseto/v4/public_mode_example_test.go b/lib/paseto/v4/public_mode_example_test.go
index 38c3df0f..7d2ee53a 100644
--- a/lib/paseto/v4/public_mode_example_test.go
+++ b/lib/paseto/v4/public_mode_example_test.go
@@ -4,9 +4,14 @@
package pasetov4
import (
+ "encoding/base64"
"encoding/hex"
+ "encoding/json"
"fmt"
"log"
+ "strings"
+
+ "git.sr.ht/~shulhan/pakakeh.go/lib/paseto"
)
func ExamplePublicMode() {
@@ -35,3 +40,113 @@ func ExamplePublicMode() {
// {"data":"signed message!"}
// {"kid":1000}
}
+
+type DummyData struct {
+ Email string `json:"email"`
+}
+type DummyFooterData struct {
+ Quote string `json:"quote"`
+}
+
+func ExamplePublicMode_Pack() {
+ createPeer := func(id, secret string) (peer paseto.Peer, pmode *PublicMode) {
+ seed, err := hex.DecodeString(secret)
+ if err != nil {
+ log.Fatal(err)
+ }
+ pmode = NewPublicMode(seed)
+ peer = paseto.Peer{
+ ID: id,
+ Private: pmode.PrivateKey,
+ Public: pmode.PublicKey,
+ }
+ return peer, pmode
+ }
+ sender, senderpm := createPeer(
+ `john@example.com`,
+ `b4cbfb43df4ce210727d953e4a713307fa19bb7d9f85041438d9e11b942a3774`,
+ )
+ receiver, _ := createPeer(
+ `jane@example.org`,
+ `2222222222222222222222222222222222222222222222222222222222222222`,
+ )
+
+ sendMsg := paseto.NewMessage(sender, receiver, `hello`)
+ sendMsg.Payload.Data = DummyData{
+ Email: `hello@example.com`,
+ }
+ sendMsg.Footer.Data = DummyFooterData{
+ Quote: `Live to eat`,
+ }
+
+ token, err := senderpm.Pack(*sendMsg, nil)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ pieces := strings.Split(token, `.`)
+ fmt.Printf("Header: %s.%s\n", pieces[0], pieces[1])
+
+ paysig, _ := base64.RawURLEncoding.DecodeString(pieces[2])
+ payload := paysig[:len(paysig)-64]
+ pdata := paseto.Payload{
+ Data: &DummyData{},
+ }
+ _ = json.Unmarshal(payload, &pdata)
+ fmt.Printf("Payload.Data: %+v\n", pdata.Data)
+
+ footer, _ := base64.RawURLEncoding.DecodeString(pieces[3])
+ fmt.Printf("Footer: %s", footer)
+
+ // Output:
+ // Header: v4.public
+ // Payload.Data: &{Email:hello@example.com}
+ // Footer: {"data":{"quote":"Live to eat"},"peer_id":"john@example.com"}
+}
+
+func ExamplePublicMode_Unpack() {
+ createPeer := func(id, secret string) (peer paseto.Peer, pmode *PublicMode) {
+ seed, err := hex.DecodeString(secret)
+ if err != nil {
+ log.Fatal(err)
+ }
+ pmode = NewPublicMode(seed)
+ peer = paseto.Peer{
+ ID: id,
+ Private: pmode.PrivateKey,
+ Public: pmode.PublicKey,
+ }
+ return peer, pmode
+ }
+ sender, _ := createPeer(
+ `john@example.com`,
+ `b4cbfb43df4ce210727d953e4a713307fa19bb7d9f85041438d9e11b942a3774`,
+ )
+ _, recvpm := createPeer(
+ `jane@example.org`,
+ `2222222222222222222222222222222222222222222222222222222222222222`,
+ )
+ recvpm.AddPeer(sender)
+
+ token := `v4.public.eyJpc3MiOiJqb2huQGV4YW1wbGUuY29tIiwic3ViIjoiaGVsbG8iLCJhdWQiOiJqYW5lQGV4YW1wbGUub3JnIiwiZXhwIjoxNzc0ODE5NzYzLCJuYmYiOjE3NzQ4MTk3MDMsImlhdCI6MTc3NDgxOTcwMywiZGF0YSI6eyJlbWFpbCI6ImhlbGxvQGV4YW1wbGUuY29tIn19Ui741xeDUhQE2w31smKyAJ_d-fhMH20SRLkIY3lDbIBfj_tq7es9-H2wQR3q6EZZRPrlAPxEug712IEgomTnBA.eyJkYXRhIjp7InF1b3RlIjoiTGlmZSBpcyBmb3IgZWF0aW5nIn0sInBlZXJfaWQiOiJqb2huQGV4YW1wbGUuY29tIn0`
+ pdata := DummyData{}
+ fdata := DummyFooterData{}
+ recvMsg := paseto.Message{
+ Payload: paseto.Payload{
+ Data: &pdata,
+ },
+ Footer: paseto.Footer{
+ Data: &fdata,
+ },
+ }
+ err := recvpm.Unpack(token, nil, &recvMsg)
+ if err != nil {
+ log.Fatal(err)
+ }
+ fmt.Printf("%+v\n", recvMsg.Payload.Data)
+ fmt.Printf("%+v\n", recvMsg.Footer.Data)
+
+ // Output:
+ // &{Email:hello@example.com}
+ // &{Quote:Life is for eating}
+}