aboutsummaryrefslogtreecommitdiff
path: root/lib/paseto
diff options
context:
space:
mode:
authorShulhan <m.shulhan@gmail.com>2020-09-09 17:01:03 +0700
committerShulhan <m.shulhan@gmail.com>2020-09-09 17:01:03 +0700
commit50e094f7dccfa4e06c3c9d26265f03a74af003b7 (patch)
tree1f5b5fabef19a90be9c835017fc26592b200fbfc /lib/paseto
parent18ab9aad4c3a99822b9dfdc37a05dae12f4fb05d (diff)
downloadpakakeh.go-50e094f7dccfa4e06c3c9d26265f03a74af003b7.tar.xz
paseto: implement strict JSON token validation
* The Issuer field must equal to peer.ID * The Audience field must equal to our.ID, * If peer.AllowedSubjects is not empty, the Subject value must be in one of them, * The current time must be after IssuedAt field, * The current time must after NotBefore "nbf" field, * The current time must before ExpiredAt field.
Diffstat (limited to 'lib/paseto')
-rw-r--r--lib/paseto/example_public_mode_test.go45
-rw-r--r--lib/paseto/json_footer.go2
-rw-r--r--lib/paseto/json_token.go44
-rw-r--r--lib/paseto/key.go27
-rw-r--r--lib/paseto/keys.go40
-rw-r--r--lib/paseto/paseto.go65
-rw-r--r--lib/paseto/public_mode.go67
7 files changed, 223 insertions, 67 deletions
diff --git a/lib/paseto/example_public_mode_test.go b/lib/paseto/example_public_mode_test.go
index a5004a2e..6868a8d1 100644
--- a/lib/paseto/example_public_mode_test.go
+++ b/lib/paseto/example_public_mode_test.go
@@ -12,35 +12,42 @@ import (
)
func ExamplePublicMode() {
+ subjectMessage := "message"
+
senderSK, _ := hex.DecodeString("e9ae9c7eae2fce6fd6727b5ca8df0fbc0aa60a5ffb354d4fdee1729e4e1463688d2160a4dc71a9a697d6ad6424da3f9dd18a259cdd51b0ae2b521e998b82d36e")
senderPK, _ := hex.DecodeString("8d2160a4dc71a9a697d6ad6424da3f9dd18a259cdd51b0ae2b521e998b82d36e")
senderKey := Key{
- id: "sender",
- private: ed25519.PrivateKey(senderSK),
- public: ed25519.PublicKey(senderPK),
+ ID: "sender",
+ Private: ed25519.PrivateKey(senderSK),
+ Public: ed25519.PublicKey(senderPK),
+ AllowedSubjects: map[string]struct{}{
+ subjectMessage: struct{}{},
+ },
}
receiverSK, _ := hex.DecodeString("4983da648bff1fd3e1892df9c56370215aa640829a5cab02d6616b115fa0bc5707c22e74ab9b181f8d87bdf03cf88476ec4c35e5517e173f236592f6695d59f5")
receiverPK, _ := hex.DecodeString("07c22e74ab9b181f8d87bdf03cf88476ec4c35e5517e173f236592f6695d59f5")
receiverKey := Key{
- id: "receiver",
- private: ed25519.PrivateKey(receiverSK),
- public: ed25519.PublicKey(receiverPK),
+ ID: "receiver",
+ Private: ed25519.PrivateKey(receiverSK),
+ Public: ed25519.PublicKey(receiverPK),
}
//
// In the sender part, we register the sender key and the public key
// of receiver in the list of peers.
//
- senderPeers := map[string]ed25519.PublicKey{
- receiverKey.id: receiverKey.public,
- }
- sender := NewPublicMode(senderKey, senderPeers)
+ sender := NewPublicMode(senderKey)
+ sender.AddPeer(receiverKey)
- addFooter := map[string]interface{}{
+ footer := map[string]interface{}{
"FOOTER": "HERE",
}
- token, err := sender.Pack([]byte("hello receiver"), addFooter)
+ token, err := sender.Pack(receiverKey.ID, subjectMessage, []byte("hello receiver"), footer)
+ if err != nil {
+ log.Fatal(err)
+ }
+ invalidToken, err := sender.Pack(receiverKey.ID, "unknown-subject", []byte("hello receiver"), footer)
if err != nil {
log.Fatal(err)
}
@@ -52,10 +59,8 @@ func ExamplePublicMode() {
// In the receiver part, we register the receiver key and the public key
// of sender in the list of peers.
//
- receiverPeers := map[string]ed25519.PublicKey{
- senderKey.id: senderKey.public,
- }
- receiver := NewPublicMode(receiverKey, receiverPeers)
+ receiver := NewPublicMode(receiverKey)
+ receiver.AddPeer(senderKey)
// receiver receive the token from sender and unpack it ...
gotData, gotFooter, err := receiver.Unpack(token)
@@ -65,7 +70,15 @@ func ExamplePublicMode() {
fmt.Printf("Received data: %s\n", gotData)
fmt.Printf("Received footer: %+v\n", gotFooter)
+
+ // receiver receive invalid token from sender and unpack it ...
+ gotData, gotFooter, err = receiver.Unpack(invalidToken)
+ if err != nil {
+ fmt.Println(err)
+ }
+
// Output:
// Received data: hello receiver
// Received footer: map[FOOTER:HERE]
+ // token subject "unknown-subject" is not allowed for key "sender"
}
diff --git a/lib/paseto/json_footer.go b/lib/paseto/json_footer.go
index 6a720051..ef6b28fc 100644
--- a/lib/paseto/json_footer.go
+++ b/lib/paseto/json_footer.go
@@ -6,5 +6,5 @@ package paseto
type JSONFooter struct {
KID string `json:"kid"`
- Data map[string]interface{} `json:"data"`
+ Data map[string]interface{} `json:"data,omitempty"`
}
diff --git a/lib/paseto/json_token.go b/lib/paseto/json_token.go
index 272a1ecf..3958e00f 100644
--- a/lib/paseto/json_token.go
+++ b/lib/paseto/json_token.go
@@ -25,19 +25,51 @@ type JSONToken struct {
}
//
-// Validate the ExpiredAt and NotBefore time fields.
+// Validate the JSON token fields,
//
-func (jtoken *JSONToken) Validate() (err error) {
+// * The Issuer must equaal to peer.ID
+// * The Audience must equal to received ID,
+// * If peer.AllowedSubjects is not empty, the Subject value must be in
+// one of them,
+// * The current time must be after IssuedAt field,
+// * The current time must after NotBefore "nbf" field,
+// * The current time must before ExpiredAt field.
+//
+// If one of the above condition is not passed, it will return an error.
+//
+func (jtoken *JSONToken) Validate(audience string, peer Key) (err error) {
now := time.Now()
- if jtoken.ExpiredAt != nil {
- if now.After(*jtoken.ExpiredAt) {
- return fmt.Errorf("token is expired")
+ if jtoken.Issuer != peer.ID {
+ return fmt.Errorf("expecting issuer %q, got %q", peer.ID,
+ jtoken.Issuer)
+ }
+ if len(peer.AllowedSubjects) != 0 {
+ _, isAllowed := peer.AllowedSubjects[jtoken.Subject]
+ if !isAllowed {
+ return fmt.Errorf("token subject %q is not allowed for key %q",
+ jtoken.Subject, peer.ID)
+ }
+ }
+ if len(audience) != 0 {
+ if jtoken.Audience != audience {
+ return fmt.Errorf("expecting audience %q, got %q",
+ audience, jtoken.Audience)
+ }
+ }
+ if jtoken.IssuedAt != nil {
+ if now.Equal(*jtoken.IssuedAt) || now.Before(*jtoken.IssuedAt) {
+ return fmt.Errorf("token issued at before current time")
}
}
if jtoken.NotBefore != nil {
- if now.Before(*jtoken.NotBefore) {
+ if now.Equal(*jtoken.NotBefore) || now.Before(*jtoken.NotBefore) {
return fmt.Errorf("token is too early")
}
}
+ if jtoken.ExpiredAt != nil {
+ if now.Equal(*jtoken.ExpiredAt) || now.After(*jtoken.ExpiredAt) {
+ return fmt.Errorf("token is expired")
+ }
+ }
return nil
}
diff --git a/lib/paseto/key.go b/lib/paseto/key.go
index 48a5eaf4..850f720d 100644
--- a/lib/paseto/key.go
+++ b/lib/paseto/key.go
@@ -7,18 +7,19 @@ package paseto
import "crypto/ed25519"
type Key struct {
- id string
- private ed25519.PrivateKey
- public ed25519.PublicKey
-}
+ // ID is a unique key ID.
+ ID string
+
+ // PrivateKey for signing public token.
+ Private ed25519.PrivateKey
+
+ // PublicKey for verifying public token.
+ Public ed25519.PublicKey
-//
-// NewKey create new Key from hex encoded strings.
-//
-func NewKey(id string, private ed25519.PrivateKey, public ed25519.PublicKey) Key {
- return Key{
- id: id,
- private: private,
- public: public,
- }
+ // AllowedSubjects contains list of subject that are allowed in the
+ // token's claim "sub" to be signed by this public key.
+ // This field is used by receiver to check the claim "sub" and compare
+ // it with this list.
+ // Empty list means allowing all subjects.
+ AllowedSubjects map[string]struct{}
}
diff --git a/lib/paseto/keys.go b/lib/paseto/keys.go
new file mode 100644
index 00000000..a9b5a244
--- /dev/null
+++ b/lib/paseto/keys.go
@@ -0,0 +1,40 @@
+// Copyright 2020, Shulhan <ms@kilabit.info>. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package paseto
+
+import "sync"
+
+//
+// keys contains and maintains list of public Keys and its configuration.
+//
+type keys struct {
+ sync.Mutex
+ v map[string]Key
+}
+
+func newKeys() *keys {
+ return &keys{
+ v: make(map[string]Key),
+ }
+}
+
+func (p *keys) upsert(k Key) {
+ p.Lock()
+ p.v[k.ID] = k
+ p.Unlock()
+}
+
+func (p *keys) get(id string) (k Key, ok bool) {
+ p.Lock()
+ k, ok = p.v[id]
+ p.Unlock()
+ return
+}
+
+func (p *keys) delete(id string) {
+ p.Lock()
+ delete(p.v, id)
+ p.Unlock()
+}
diff --git a/lib/paseto/paseto.go b/lib/paseto/paseto.go
index 3025d71a..1aa99533 100644
--- a/lib/paseto/paseto.go
+++ b/lib/paseto/paseto.go
@@ -3,9 +3,9 @@
// license that can be found in the LICENSE file.
//
-// Package paseto provide a simple, ready to use, implementation of
-// Platform-Agnostic SEcurity TOkens (PASETOs) v2 as defined in draft of RFC
-// 01 [1].
+// Package paseto provide a simple, ready to use, opinionated implementation
+// of Platform-Agnostic SEcurity TOkens (PASETOs) v2 as defined in draft of
+// RFC 01 [1].
//
// Limitation
//
@@ -21,24 +21,33 @@
// The public mode focus on signing and verifing data, everything else is
// handled and filled automatically.
//
-// For example, when generating token for signing, the user data is stored
-// using key "data" inside the JSON token, encoded using base64.
-// The Issuer will be set to the Key's ID, the expiration date is set to
-// current time plus TTL.
-// The footer will always generated using JSONFooter with KID (Key-ID) set to
-// the Key's ID.
-// Additional footer data can be added on the Data field.
+// Steps for sender when generating new token, the Pack() method,
+//
+// * Prepare the JSON token claims, set
+// ** Issuer "iss" to PublicMode.our.ID
+// ** Subject "sub" to subject value from parameter
+// ** Audience "aud" to audience value from parameter
+// ** IssuedAt to current time
+// ** NotBefore to current time
+// ** ExpiredAt to current time + 60 seconds
+// ** Data field to base64 encoded of data value from parameter
+// * Prepare the JSON footer, set
+// ** Key ID "kid" to PublicMode.our.ID
//
-// When verifying token, the key ID is read from footer and verified using one
-// of the public key registered previously.
+// The user's claims data is stored using key "data" inside the JSON token,
+// encoded using base64 (with padding).
+// Additional footer data can be added on the Data field.
//
// Overall, the following JSONToken and JSONFooter is generated for each
// token,
//
// JSONToken:{
// "iss": <Key.ID>,
+// "sub": <Subject parameter>,
+// "aud": <Audience parameter>
// "exp": <time.Now() + TTL>,
// "iat": <time.Now()>,
+// "nbf": <time.Now()>,
// "data": <base64.StdEncoding.EncodeToString(userData)>,
// }
// JSONFooter:{
@@ -46,6 +55,38 @@
// "data": {}
// }
//
+// On the receiver side, they will have list of registered peers Key (include
+// ID, public Key, and list of allowed subject).
+//
+// PublicMode:{
+// peers: map[Key.ID]Key{
+// Public: <ed25519.PublicKey>,
+// AllowedSubjects: map[string]struct{}{
+// "/api/x": struct{}{},
+// "/api/y:read": struct{}{},
+// "/api/z:write": struct{}{},
+// ...
+// },
+// },
+// }
+//
+// Step for receiver to process the token, the Unpack() method,
+//
+// * Decode the token footer
+// * Get the registered public key based on "kid" value in token footer
+// ** If no peers key exist matched with "kid" value, reject the token
+// * Verify the token using the peer public key
+// ** If verification failed, reject the token
+// * Validate the token
+// ** The Issuer must equal to peer ID
+// ** The Audience must equal to receiver ID
+// ** If the peer AllowedSubjects is not empty, the Subject must be in
+// one of them
+// ** The current time must be after IssuedAt
+// ** The current time must be after NotBefore
+// ** The current time must be before ExpiredAt
+// ** If one of the above condition is not passed, it will return an error.
+//
// References
//
// [1] https://github.com/paragonie/paseto/blob/master/docs/RFC/draft-paragon-paseto-rfc-01.txt
diff --git a/lib/paseto/public_mode.go b/lib/paseto/public_mode.go
index 41c4ef34..77244146 100644
--- a/lib/paseto/public_mode.go
+++ b/lib/paseto/public_mode.go
@@ -5,7 +5,6 @@
package paseto
import (
- "crypto/ed25519"
"encoding/base64"
"encoding/json"
"fmt"
@@ -13,42 +12,72 @@ import (
"time"
)
+//
+// DefaultTTL define the time-to-live of each token, by setting ExpiredAt to
+// current time + DefaultTTL.
+// If you want longer token, increase this value before using Pack().
+//
var DefaultTTL = 60 * time.Second
//
// PublicMode implement the PASETO public mode to signing and verifying data
// using private key and one or more shared public keys.
+// The PublicMode contains list of peer public keys for verifying the incoming
+// token.
//
type PublicMode struct {
our Key
- peers map[string]ed25519.PublicKey
+ peers *keys
}
//
// NewPublicMode create new PublicMode with our private key for signing
-// outgoing token and list of peer public keys for verifying the incoming
-// token.
+// outgoing token.
//
-func NewPublicMode(our Key, peers map[string]ed25519.PublicKey) (auth *PublicMode) {
+func NewPublicMode(our Key) (auth *PublicMode) {
auth = &PublicMode{
our: our,
- peers: peers,
+ peers: newKeys(),
}
-
return auth
}
//
+// AddPeer add a key to list of known peers for verifying incoming token.
+// The Key.Public
+//
+func (auth *PublicMode) AddPeer(k Key) (err error) {
+ if len(k.ID) == 0 {
+ return fmt.Errorf("empty key ID")
+ }
+ if len(k.Public) == 0 {
+ return fmt.Errorf("empty public key")
+ }
+ auth.peers.upsert(k)
+ return nil
+}
+
+//
+// RemovePeer remove peer's key from list.
+//
+func (auth *PublicMode) RemovePeer(id string) {
+ auth.peers.delete(id)
+}
+
+//
// Pack the data into token.
//
-func (auth *PublicMode) Pack(data []byte, addFooter map[string]interface{}) (
+func (auth *PublicMode) Pack(audience, subject string, data []byte, footer map[string]interface{}) (
token string, err error,
) {
now := time.Now()
expiredAt := now.Add(DefaultTTL)
jsonToken := JSONToken{
- Issuer: auth.our.id,
+ Issuer: auth.our.ID,
+ Subject: subject,
+ Audience: audience,
IssuedAt: &now,
+ NotBefore: &now,
ExpiredAt: &expiredAt,
Data: base64.StdEncoding.EncodeToString(data),
}
@@ -59,22 +88,22 @@ func (auth *PublicMode) Pack(data []byte, addFooter map[string]interface{}) (
}
jsonFooter := JSONFooter{
- KID: auth.our.id,
- Data: addFooter,
+ KID: auth.our.ID,
+ Data: footer,
}
- footer, err := json.Marshal(&jsonFooter)
+ rawfooter, err := json.Marshal(&jsonFooter)
if err != nil {
return "", err
}
- return Sign(auth.our.private, msg, footer)
+ return Sign(auth.our.Private, msg, rawfooter)
}
//
// Unpack the token to get the JSONToken and the data.
//
-func (auth *PublicMode) Unpack(token string) (data []byte, addFooter map[string]interface{}, err error) {
+func (auth *PublicMode) Unpack(token string) (data []byte, footer map[string]interface{}, err error) {
pieces := strings.Split(token, ".")
if len(pieces) != 4 {
return nil, nil, fmt.Errorf("invalid token format")
@@ -86,17 +115,17 @@ func (auth *PublicMode) Unpack(token string) (data []byte, addFooter map[string]
return nil, nil, fmt.Errorf("expecting public mode, got " + pieces[1])
}
- footer, err := base64.RawURLEncoding.DecodeString(pieces[3])
+ rawfooter, err := base64.RawURLEncoding.DecodeString(pieces[3])
if err != nil {
return nil, nil, err
}
jsonFooter := &JSONFooter{}
- err = json.Unmarshal(footer, jsonFooter)
+ err = json.Unmarshal(rawfooter, jsonFooter)
if err != nil {
return nil, nil, err
}
- peerKey, ok := auth.peers[jsonFooter.KID]
+ peerKey, ok := auth.peers.get(jsonFooter.KID)
if !ok {
return nil, nil, fmt.Errorf("unknown peer key ID %s", jsonFooter.KID)
}
@@ -106,7 +135,7 @@ func (auth *PublicMode) Unpack(token string) (data []byte, addFooter map[string]
return nil, nil, err
}
- msg, err := Verify(peerKey, msgSig, footer)
+ msg, err := Verify(peerKey.Public, msgSig, rawfooter)
if err != nil {
return nil, nil, err
}
@@ -117,7 +146,7 @@ func (auth *PublicMode) Unpack(token string) (data []byte, addFooter map[string]
return nil, nil, err
}
- err = jtoken.Validate()
+ err = jtoken.Validate(auth.our.ID, peerKey)
if err != nil {
return nil, nil, err
}