Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "test-vectors"]
path = test-vectors
url = https://github.com/paseto-standard/test-vectors.git
28 changes: 25 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ key := paseto.NewV4SymmetricKey() // don't share this!!
encrypted := token.V4Encrypt(key, nil)
```

Or sign it (this allows recievers to verify it without sharing secrets):
Or sign it (this allows receivers to verify it without sharing secrets):
```go

secretKey := paseto.NewV4AsymmetricSecretKey() // don't share this!!!
Expand All @@ -66,7 +66,12 @@ publicKey := secretKey.Public() // DO share this one
signed := token.V4Sign(secretKey, nil)
```

To handle a recieved token, let's use an example from Paseto's test vectors:
When handing out the public key, you may want to encode it using [PASERK](https://github.com/paseto-standard/paserk), so it can't be misinterpreted as key for a different type or version:
```go
println(paserk.SerializeKey(publicKey))
```

To handle a received token, let's use an example from Paseto's test vectors:

The Paseto token is as follows
```
Expand All @@ -78,10 +83,17 @@ And the public key, given in hex is:
1eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2
```

Alternatively, the same public key as PASERK:
```
k4.public.Hrnbu7wEfAP9cGBOAHHwmH4Wsot1ciXBHwBBXQ4gsaI
```

Importing a public key, and then verifying a token:

```go
publicKey, err := paseto.NewV4AsymmetricPublicKeyFromHex("1eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2") // this wil fail if given key in an invalid format
// or when the public key is given as PASERK:
publicKey, err := paserk.DeserializeKey[paseto.V4AsymmetricPublicKey]("k4.public.Hrnbu7wEfAP9cGBOAHHwmH4Wsot1ciXBHwBBXQ4gsaI")
signed := "v4.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAyMi0wMS0wMVQwMDowMDowMCswMDowMCJ9v3Jt8mx_TdM2ceTGoqwrh4yDFn0XsHvvV_D0DtwQxVrJEBMl0F2caAdgnpKlt4p7xBnx1HcO-SPo8FPp214HDw.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9"

parser := paseto.NewParserWithoutExpiryCheck() // only used because this example token has expired, use NewParser() (which checks expiry by default)
Expand All @@ -99,6 +111,8 @@ require.Equal(t,
require.NoError(t, err)
```



# Supported Claims Validators
The following validators are supported:

Expand Down Expand Up @@ -163,10 +177,18 @@ Version 3 is fully supported.
## Version 2
Version 2 is fully supported.

# Supported PASERK Types

Key serialization and deserialization (`local`, `secret`, `public`) is supported, so is creating key identifiers (`lid`, `sid`, `pid`).

Wrapped/encrypted keys are **not** supported (`seal`, `local-wrap`, `local-pw`, `secret-wrap`, `secret-pw`).

See the [PASERK documentation](https://github.com/paseto-standard/paserk) for what these mean.

# Supported Go Versions
Only [officially supported](https://go.dev/doc/devel/release#policy) versions of Go will be
supported by Go Paseto. Versions of Go which have recently gone out of support may continue to work
with this library for some time, however this is not guarenteed and should not be relied on.

When support for an out of date version of Go is dropped, this will be done as part of a minor
version bump.
version bump.
16 changes: 16 additions & 0 deletions keys.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package paseto

// KeyType indicates if key is symmetric, private or public
type KeyType string

// KeyVersion indicates the token version the key may be used for
type KeyVersion int

const (
// Symmetric key used for local tokens
KeyTypeLocal KeyType = "local"
// Asymmetric secret key used for public tokens
KeyTypeSecret KeyType = "secret"
// Asymmetric public key used for public tokens
KeyTypePublic KeyType = "public"
)
21 changes: 21 additions & 0 deletions paserk/keys.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package paserk

import (
"aidanwoods.dev/go-paseto/v2"
)

type Key interface {
ExportBytes() []byte
Type() paseto.KeyType
Version() paseto.KeyVersion
}

var _ Key = &paseto.V2SymmetricKey{}
var _ Key = &paseto.V2AsymmetricSecretKey{}
var _ Key = &paseto.V2AsymmetricPublicKey{}
var _ Key = &paseto.V3SymmetricKey{}
var _ Key = &paseto.V3AsymmetricSecretKey{}
var _ Key = &paseto.V3AsymmetricPublicKey{}
var _ Key = &paseto.V4SymmetricKey{}
var _ Key = &paseto.V4AsymmetricSecretKey{}
var _ Key = &paseto.V4AsymmetricPublicKey{}
204 changes: 204 additions & 0 deletions paserk/paserk.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
package paserk

import (
"crypto/sha512"
"encoding/base64"
"errors"
"fmt"
"golang.org/x/crypto/blake2b"
"hash"
"strconv"
"strings"

"aidanwoods.dev/go-paseto/v2"
)

// PaserkType reflects the type field of a PASERK
type PaserkType string

const (
// Unique Identifier for a separate PASERK for local PASETOs
PaserkTypeLid PaserkType = "lid"
// Symmetric key for local tokens
PaserkTypeLocal PaserkType = "local"
// Symmetric key wrapped using asymmetric encryption
PaserkTypeSeal PaserkType = "seal"
// Symmetric key wrapped by another symmetric key
PaserkTypeLocalWrap PaserkType = "local-wrap"
// Symmetric key wrapped using password-based encryption
PaserkTypeLocalPw PaserkType = "local-pw"
// Unique Identifier for a separate PASERK for public PASETOs. (Secret Key)
PaserkTypeSid PaserkType = "sid"
// Public key for verifying public tokens
PaserkTypePublic PaserkType = "public"
// Unique Identifier for a separate PASERK for public PASETOs. (Public Key)
PaserkTypePid PaserkType = "pid"
// Secret key for signing public tokens
PaserkTypeSecret PaserkType = "secret"
// Asymmetric secret key wrapped by another symmetric key
PaserkTypeSecretWrap PaserkType = "secret-wrap"
// Asymmetric secret key wrapped using password-based encryption
PaserkTypeSecretPw PaserkType = "secret-pw"
)

func parsePaserkType(s string) (PaserkType, error) {
switch PaserkType(s) {
case PaserkTypeLid:
return PaserkTypeLid, nil
case PaserkTypeLocal:
return PaserkTypeLocal, nil
case PaserkTypeSeal:
return PaserkTypeSeal, nil
case PaserkTypeLocalWrap:
return PaserkTypeLocalWrap, nil
case PaserkTypeLocalPw:
return PaserkTypeLocalPw, nil
case PaserkTypeSid:
return PaserkTypeSid, nil
case PaserkTypePublic:
return PaserkTypePublic, nil
case PaserkTypePid:
return PaserkTypePid, nil
case PaserkTypeSecret:
return PaserkTypeSecret, nil
case PaserkTypeSecretWrap:
return PaserkTypeSecretWrap, nil
case PaserkTypeSecretPw:
return PaserkTypeSecretPw, nil
default:
return "", errors.New("invalid PASERK type")
}
}

func (paserkType PaserkType) supportsKeyType(kt paseto.KeyType) bool {
switch paserkType {
case PaserkTypeLocal:
return kt == paseto.KeyTypeLocal
case PaserkTypeSecret:
return kt == paseto.KeyTypeSecret
case PaserkTypePublic:
return kt == paseto.KeyTypePublic
default:
return false
}
}

// SerializeKey exports a local/secret/public key as a PASERK
func SerializeKey(k Key) (string, error) {
var paserkType PaserkType
switch k.Type() {
case paseto.KeyTypeLocal:
paserkType = PaserkTypeLocal
case paseto.KeyTypeSecret:
paserkType = PaserkTypeSecret
case paseto.KeyTypePublic:
paserkType = PaserkTypePublic
default:
return "", errors.New("invalid key type")
}
header := "k" + strconv.Itoa(int(k.Version())) + "." + string(paserkType) + "."
data := base64.RawURLEncoding.EncodeToString(k.ExportBytes())
return header + data, nil
}

// SerializeKeyID exports a local/secret/public key's identity as a lid/sid/pid PASERK
func SerializeKeyID(k Key) (string, error) {
var paserkType PaserkType
switch k.Type() {
case paseto.KeyTypeLocal:
paserkType = PaserkTypeLid
case paseto.KeyTypeSecret:
paserkType = PaserkTypeSid
case paseto.KeyTypePublic:
paserkType = PaserkTypePid
default:
return "", errors.New("invalid key type")
}
var h hash.Hash
switch k.Version() {
case 1, 3:
h = sha512.New384()
case 2, 4:
h, _ = blake2b.New(33, nil)
default:
return "", errors.New("invalid key version")
}
header := "k" + strconv.Itoa(int(k.Version())) + "." + string(paserkType) + "."
h.Write([]byte(header))
s, err := SerializeKey(k)
if err != nil {
return "", err
}
h.Write([]byte(s))
data := base64.RawURLEncoding.EncodeToString(h.Sum(nil)[:33])
return header + data, nil
}

// DeserializeKey constructs a local/secret/public key of type T from a given
// PASERK, given that it matches type and version of T
func DeserializeKey[T Key](paserkStr string) (T, error) {
var t T
frags := strings.Split(paserkStr, ".")
if len(frags) != 3 {
return t, fmt.Errorf("invalid PASERK: %s", paserkStr)
}
if len(frags[0]) != 2 || frags[0][0] != 'k' {
return t, fmt.Errorf("invalid PASERK version field")
}
version, err := strconv.Atoi(frags[0][1:])
if err != nil {
return t, fmt.Errorf("invalid PASERK version number")
}
typ, err := parsePaserkType(frags[1])
if err != nil {
return t, err
}
data, err := base64.RawURLEncoding.DecodeString(frags[2])
if err != nil {
return t, fmt.Errorf("cannot decode data part of PASERK: %w", err)
}

if t.Version() != paseto.KeyVersion(version) || !typ.supportsKeyType(t.Type()) {
return t, fmt.Errorf("cannot decode PASERK of type 'k%d.%s', expected 'k%d.%s'", version, typ, t.Version(), t.Type())
}

var key Key
switch version {
case 2:
switch typ {
case PaserkTypeLocal:
key, err = paseto.V2SymmetricKeyFromBytes(data)
case PaserkTypeSecret:
key, err = paseto.NewV2AsymmetricSecretKeyFromBytes(data)
case PaserkTypePublic:
key, err = paseto.NewV2AsymmetricPublicKeyFromBytes(data)
}
case 3:
switch typ {
case PaserkTypeLocal:
key, err = paseto.V3SymmetricKeyFromBytes(data)
case PaserkTypeSecret:
key, err = paseto.NewV3AsymmetricSecretKeyFromBytes(data)
case PaserkTypePublic:
key, err = paseto.NewV3AsymmetricPublicKeyFromBytes(data)
}
case 4:
switch typ {
case PaserkTypeLocal:
key, err = paseto.V4SymmetricKeyFromBytes(data)
case PaserkTypeSecret:
key, err = paseto.NewV4AsymmetricSecretKeyFromBytes(data)
case PaserkTypePublic:
key, err = paseto.NewV4AsymmetricPublicKeyFromBytes(data)
}
default:
return t, fmt.Errorf("unsupported PASERK version %d", version)
}
if err != nil {
return t, fmt.Errorf("can't construct key from data part of PASERK: %w", err)
}
if key == nil {
return t, fmt.Errorf("deserializing of key of type %T is not implemented", t)
}
return key.(T), nil
}
45 changes: 45 additions & 0 deletions paserk/paserk_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package paserk
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My only comment is it might be better to run the official test suite here.

You could use go generate to embed all the test vectors into the repo and run the tests.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could copy the JSON file into the repo like it's done for the PASETO test vectors and run those additionally

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep that would make a lot of sense. I only suggest go generate so that it's easy to update as the test vectors change for future versions, though that's a nice-to-have.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added them now.
Some of the test cases make no sense imo, like k2.secret-fail-2 which doesn't test PASERK itself. Instead, it seems to be meant to assert that a V4SymmetricKey can't be serialized as e.g. k1.local. Since the target type is inferred from the key itself, and it is tested whether a key serializes to the correct PASERK type by the other tests, I think it's fine that the test just does nothing (it hits the expected fail branch since the PASERK in the test data is empty). Same applies for the other expected-fail tests that are meant to test if a cut short key will serialize.


import (
"aidanwoods.dev/go-paseto/v2"
"github.com/stretchr/testify/require"
"testing"
)

func TestSerializeKey(t *testing.T) {
k, _ := paseto.NewV4AsymmetricPublicKeyFromHex("0000000000000000000000000000000000000000000000000000000000000000")
s, err := SerializeKey(k)
require.NoError(t, err)
require.Equal(t, "k4.public.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", s)
}

func TestDeserializeKey(t *testing.T) {
k, err := DeserializeKey[paseto.V4AsymmetricPublicKey]("k4.public.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")
require.NoError(t, err)
require.Equal(t, "0000000000000000000000000000000000000000000000000000000000000000", k.ExportHex())
}

func TestDeserializeKeyFailure(t *testing.T) {
_, err := DeserializeKey[paseto.V4AsymmetricPublicKey]("kx.secret.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")
require.Error(t, err)
require.Equal(t, "invalid PASERK version number", err.Error())

_, err = DeserializeKey[paseto.V4AsymmetricPublicKey]("k4.something.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")
require.Error(t, err)
require.Equal(t, "invalid PASERK type", err.Error())

_, err = DeserializeKey[paseto.V4AsymmetricPublicKey]("k4.secret.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")
require.Error(t, err)
require.Equal(t, "cannot decode PASERK of type 'k4.secret', expected 'k4.public'", err.Error())

_, err = DeserializeKey[paseto.V4AsymmetricPublicKey]("k2.public.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")
require.Error(t, err)
require.Equal(t, "cannot decode PASERK of type 'k2.public', expected 'k4.public'", err.Error())
}

func TestSerializeKeyID(t *testing.T) {
k, _ := paseto.V4SymmetricKeyFromHex("0000000000000000000000000000000000000000000000000000000000000000")
s, err := SerializeKeyID(k)
require.NoError(t, err)
require.Equal(t, "k4.lid.bqltbNc4JLUAmc9Xtpok-fBuI0dQN5_m3CD9W_nbh559", s)
}
Loading