Skip to content

Commit 23f572d

Browse files
minus7suttod
andcommitted
Implement PASERK serialization
Implement serialization and deserialization of v2/3/4 local/secret/public keys as well as key identifiers (lid/sid/pid). Co-authored-by: Daniel SUTTO <[email protected]>
1 parent be67f28 commit 23f572d

File tree

7 files changed

+361
-0
lines changed

7 files changed

+361
-0
lines changed

keys.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package paseto
2+
3+
// KeyType indicates if key is symmetric, private or public
4+
type KeyType string
5+
6+
// KeyVersion indicates the token version the key may be used for
7+
type KeyVersion int
8+
9+
const (
10+
// Symmetric key used for local tokens
11+
KeyTypeLocal KeyType = "local"
12+
// Asymmetric secret key used for public tokens
13+
KeyTypeSecret KeyType = "secret"
14+
// Asymmetric public key used for public tokens
15+
KeyTypePublic KeyType = "public"
16+
)

paserk/keys.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package paserk
2+
3+
import (
4+
"aidanwoods.dev/go-paseto"
5+
)
6+
7+
type Key interface {
8+
ExportBytes() []byte
9+
Type() paseto.KeyType
10+
Version() paseto.KeyVersion
11+
}
12+
13+
var _ Key = &paseto.V2SymmetricKey{}
14+
var _ Key = &paseto.V2AsymmetricSecretKey{}
15+
var _ Key = &paseto.V2AsymmetricPublicKey{}
16+
var _ Key = &paseto.V3SymmetricKey{}
17+
var _ Key = &paseto.V3AsymmetricSecretKey{}
18+
var _ Key = &paseto.V3AsymmetricPublicKey{}
19+
var _ Key = &paseto.V4SymmetricKey{}
20+
var _ Key = &paseto.V4AsymmetricSecretKey{}
21+
var _ Key = &paseto.V4AsymmetricPublicKey{}

paserk/paserk.go

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
package paserk
2+
3+
import (
4+
"crypto/sha512"
5+
"encoding/base64"
6+
"errors"
7+
"fmt"
8+
"golang.org/x/crypto/blake2b"
9+
"hash"
10+
"strconv"
11+
"strings"
12+
13+
"aidanwoods.dev/go-paseto"
14+
)
15+
16+
// type of PASERK token
17+
type PaserkType string
18+
19+
const (
20+
// Unique Identifier for a separate PASERK for local PASETOs
21+
PaserkTypeLid PaserkType = "lid"
22+
// Symmetric key for local tokens
23+
PaserkTypeLocal PaserkType = "local"
24+
// Symmetric key wrapped using asymmetric encryption
25+
PaserkTypeSeal PaserkType = "seal"
26+
// Symmetric key wrapped by another symmetric key
27+
PaserkTypeLocalWrap PaserkType = "local-wrap"
28+
// Symmetric key wrapped using password-based encryption
29+
PaserkTypeLocalPw PaserkType = "local-pw"
30+
// Unique Identifier for a separate PASERK for public PASETOs. (Secret Key)
31+
PaserkTypeSid PaserkType = "sid"
32+
// Public key for verifying public tokens
33+
PaserkTypePublic PaserkType = "public"
34+
// Unique Identifier for a separate PASERK for public PASETOs. (Public Key)
35+
PaserkTypePid PaserkType = "pid"
36+
// Secret key for signing public tokens
37+
PaserkTypeSecret PaserkType = "secret"
38+
// Asymmetric secret key wrapped by another symmetric key
39+
PaserkTypeSecretWrap PaserkType = "secret-wrap"
40+
// Asymmetric secret key wrapped using password-based encryption
41+
PaserkTypeSecretPw PaserkType = "secret-pw"
42+
)
43+
44+
func parsePaserkType(s string) (PaserkType, error) {
45+
switch PaserkType(s) {
46+
case PaserkTypeLid:
47+
return PaserkTypeLid, nil
48+
case PaserkTypeLocal:
49+
return PaserkTypeLocal, nil
50+
case PaserkTypeSeal:
51+
return PaserkTypeSeal, nil
52+
case PaserkTypeLocalWrap:
53+
return PaserkTypeLocalWrap, nil
54+
case PaserkTypeLocalPw:
55+
return PaserkTypeLocalPw, nil
56+
case PaserkTypeSid:
57+
return PaserkTypeSid, nil
58+
case PaserkTypePublic:
59+
return PaserkTypePublic, nil
60+
case PaserkTypePid:
61+
return PaserkTypePid, nil
62+
case PaserkTypeSecret:
63+
return PaserkTypeSecret, nil
64+
case PaserkTypeSecretWrap:
65+
return PaserkTypeSecretWrap, nil
66+
case PaserkTypeSecretPw:
67+
return PaserkTypeSecretPw, nil
68+
default:
69+
return "", errors.New("invalid PASERK type")
70+
}
71+
}
72+
73+
func (paserkType PaserkType) supportsKeyType(typ paseto.KeyType) bool {
74+
if paserkType == PaserkTypeLocal && typ == paseto.KeyTypeLocal {
75+
return true
76+
} else if paserkType == PaserkTypeSecret && typ == paseto.KeyTypeSecret {
77+
return true
78+
} else if paserkType == PaserkTypePublic && typ == paseto.KeyTypePublic {
79+
return true
80+
} else {
81+
return false
82+
}
83+
}
84+
85+
// SerializeKey exports a local/secret/public key as a PASERK token
86+
func SerializeKey(k Key) (string, error) {
87+
var paserkType PaserkType
88+
switch k.Type() {
89+
case paseto.KeyTypeLocal:
90+
paserkType = PaserkTypeLocal
91+
case paseto.KeyTypeSecret:
92+
paserkType = PaserkTypeSecret
93+
case paseto.KeyTypePublic:
94+
paserkType = PaserkTypePublic
95+
default:
96+
return "", errors.New("invalid key type")
97+
}
98+
header := "k" + strconv.Itoa(int(k.Version())) + "." + string(paserkType) + "."
99+
data := base64.RawURLEncoding.EncodeToString(k.ExportBytes())
100+
return header + data, nil
101+
}
102+
103+
// SerializeKeyID exports a local/secret/public key's identity as a lid/sid/pid PASERK token
104+
func SerializeKeyID(k Key) (string, error) {
105+
var paserkType PaserkType
106+
switch k.Type() {
107+
case paseto.KeyTypeLocal:
108+
paserkType = PaserkTypeLid
109+
case paseto.KeyTypeSecret:
110+
paserkType = PaserkTypeSid
111+
case paseto.KeyTypePublic:
112+
paserkType = PaserkTypePid
113+
default:
114+
return "", errors.New("invalid key type")
115+
}
116+
var h hash.Hash
117+
switch k.Version() {
118+
case 1, 3:
119+
h = sha512.New384()
120+
case 2, 4:
121+
h, _ = blake2b.New(33, nil)
122+
default:
123+
return "", errors.New("invalid key version")
124+
}
125+
header := "k" + strconv.Itoa(int(k.Version())) + "." + string(paserkType) + "."
126+
h.Write([]byte(header))
127+
s, err := SerializeKey(k)
128+
if err != nil {
129+
return "", err
130+
}
131+
h.Write([]byte(s))
132+
data := base64.RawURLEncoding.EncodeToString(h.Sum(nil)[:33])
133+
return header + data, nil
134+
}
135+
136+
type KnownKeyTypes interface {
137+
paseto.V2SymmetricKey |
138+
paseto.V2AsymmetricSecretKey |
139+
paseto.V2AsymmetricPublicKey |
140+
paseto.V3SymmetricKey |
141+
paseto.V3AsymmetricSecretKey |
142+
paseto.V3AsymmetricPublicKey |
143+
paseto.V4SymmetricKey |
144+
paseto.V4AsymmetricSecretKey |
145+
paseto.V4AsymmetricPublicKey
146+
Key
147+
}
148+
149+
// DeserializeKey constructs a local/secret/public key of type T from a given
150+
// PASERK token, given that it matches type and version of T
151+
func DeserializeKey[T KnownKeyTypes](paserkStr string) (T, error) {
152+
var t T
153+
frags := strings.Split(paserkStr, ".")
154+
if len(frags) != 3 {
155+
return t, fmt.Errorf("invalid PASERK token: %s", paserkStr)
156+
}
157+
if len(frags[0]) != 2 || frags[0][0] != 'k' {
158+
return t, fmt.Errorf("invalid PASERK version field")
159+
}
160+
version, err := strconv.Atoi(frags[0][1:])
161+
if err != nil {
162+
return t, fmt.Errorf("invalid PASERK version number")
163+
}
164+
typ, err := parsePaserkType(frags[1])
165+
if err != nil {
166+
return t, err
167+
}
168+
data, err := base64.RawURLEncoding.DecodeString(frags[2])
169+
if err != nil {
170+
return t, fmt.Errorf("cannot decode data part of PASERK token: %w", err)
171+
}
172+
173+
if t.Version() != paseto.KeyVersion(version) || !typ.supportsKeyType(t.Type()) {
174+
return t, fmt.Errorf("cannot decode PASERK token of type 'k%d.%s', expected 'k%d.%s'", version, typ, t.Version(), t.Type())
175+
}
176+
177+
var key Key
178+
switch version {
179+
case 2:
180+
switch typ {
181+
case PaserkTypeLocal:
182+
key, err = paseto.V2SymmetricKeyFromBytes(data)
183+
case PaserkTypeSecret:
184+
key, err = paseto.NewV2AsymmetricSecretKeyFromBytes(data)
185+
case PaserkTypePublic:
186+
key, err = paseto.NewV2AsymmetricPublicKeyFromBytes(data)
187+
}
188+
case 3:
189+
switch typ {
190+
case PaserkTypeLocal:
191+
key, err = paseto.V3SymmetricKeyFromBytes(data)
192+
case PaserkTypeSecret:
193+
key, err = paseto.NewV3AsymmetricSecretKeyFromBytes(data)
194+
case PaserkTypePublic:
195+
key, err = paseto.NewV3AsymmetricPublicKeyFromBytes(data)
196+
}
197+
case 4:
198+
switch typ {
199+
case PaserkTypeLocal:
200+
key, err = paseto.V4SymmetricKeyFromBytes(data)
201+
case PaserkTypeSecret:
202+
key, err = paseto.NewV4AsymmetricSecretKeyFromBytes(data)
203+
case PaserkTypePublic:
204+
key, err = paseto.NewV4AsymmetricPublicKeyFromBytes(data)
205+
}
206+
}
207+
if err != nil {
208+
return t, fmt.Errorf("can't construct key from data part of paserk token: %w", err)
209+
}
210+
if key == nil {
211+
return t, fmt.Errorf("deserializing of key of type %T is not implemented", t)
212+
}
213+
return key.(T), nil
214+
}

paserk/paserk_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package paserk
2+
3+
import (
4+
"aidanwoods.dev/go-paseto"
5+
"github.com/stretchr/testify/require"
6+
"testing"
7+
)
8+
9+
func TestSerializeKey(t *testing.T) {
10+
k, _ := paseto.NewV4AsymmetricPublicKeyFromHex("0000000000000000000000000000000000000000000000000000000000000000")
11+
s, err := SerializeKey(k)
12+
require.NoError(t, err)
13+
require.Equal(t, "k4.public.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", s)
14+
}
15+
16+
func TestDeserializeKey(t *testing.T) {
17+
k, err := DeserializeKey[paseto.V4AsymmetricPublicKey]("k4.public.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")
18+
require.NoError(t, err)
19+
require.Equal(t, "0000000000000000000000000000000000000000000000000000000000000000", k.ExportHex())
20+
}
21+
22+
func TestDeserializeKeyFailure(t *testing.T) {
23+
_, err := DeserializeKey[paseto.V4AsymmetricPublicKey]("kx.secret.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")
24+
require.Error(t, err)
25+
require.Equal(t, "invalid PASERK version number", err.Error())
26+
27+
_, err = DeserializeKey[paseto.V4AsymmetricPublicKey]("k4.something.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")
28+
require.Error(t, err)
29+
require.Equal(t, "invalid PASERK type", err.Error())
30+
31+
_, err = DeserializeKey[paseto.V4AsymmetricPublicKey]("k4.secret.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")
32+
require.Error(t, err)
33+
require.Equal(t, "cannot decode PASERK token of type 'k4.secret', expected 'k4.public'", err.Error())
34+
35+
_, err = DeserializeKey[paseto.V4AsymmetricPublicKey]("k2.public.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")
36+
require.Error(t, err)
37+
require.Equal(t, "cannot decode PASERK token of type 'k2.public', expected 'k4.public'", err.Error())
38+
}
39+
40+
func TestSerializeKeyID(t *testing.T) {
41+
k, _ := paseto.V4SymmetricKeyFromHex("0000000000000000000000000000000000000000000000000000000000000000")
42+
s, err := SerializeKeyID(k)
43+
require.NoError(t, err)
44+
require.Equal(t, "k4.lid.bqltbNc4JLUAmc9Xtpok-fBuI0dQN5_m3CD9W_nbh559", s)
45+
}

v2_keys.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,13 @@ func (k V2AsymmetricPublicKey) ExportBytes() []byte {
4747
return k.material
4848
}
4949

50+
func (k V2AsymmetricPublicKey) Version() KeyVersion {
51+
return 2
52+
}
53+
func (k V2AsymmetricPublicKey) Type() KeyType {
54+
return KeyTypePublic
55+
}
56+
5057
// V2AsymmetricSecretKey V2 public private key
5158
type V2AsymmetricSecretKey struct {
5259
material ed25519.PrivateKey
@@ -75,6 +82,13 @@ func (k V2AsymmetricSecretKey) ExportSeedHex() string {
7582
return encoding.HexEncode(k.material.Seed())
7683
}
7784

85+
func (k V2AsymmetricSecretKey) Version() KeyVersion {
86+
return 2
87+
}
88+
func (k V2AsymmetricSecretKey) Type() KeyType {
89+
return KeyTypeSecret
90+
}
91+
7892
// NewV2AsymmetricSecretKey generate a new secret key for use with asymmetric
7993
// cryptography. Don't forget to export the public key for sharing, DO NOT share
8094
// this secret key.
@@ -158,6 +172,13 @@ func (k V2SymmetricKey) ExportBytes() []byte {
158172
return k.material[:]
159173
}
160174

175+
func (k V2SymmetricKey) Version() KeyVersion {
176+
return 2
177+
}
178+
func (k V2SymmetricKey) Type() KeyType {
179+
return KeyTypeLocal
180+
}
181+
161182
// V2SymmetricKeyFromHex constructs a key from hex
162183
func V2SymmetricKeyFromHex(hexEncoded string) (V2SymmetricKey, error) {
163184
var bytes []byte

v3_keys.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,13 @@ func (k V3AsymmetricPublicKey) ExportBytes() []byte {
6060
return k.compressed()
6161
}
6262

63+
func (k V3AsymmetricPublicKey) Version() KeyVersion {
64+
return 3
65+
}
66+
func (k V3AsymmetricPublicKey) Type() KeyType {
67+
return KeyTypePublic
68+
}
69+
6370
// V3AsymmetricSecretKey v3 public private key
6471
type V3AsymmetricSecretKey struct {
6572
material ecdsa.PrivateKey
@@ -80,6 +87,13 @@ func (k V3AsymmetricSecretKey) ExportBytes() []byte {
8087
return k.material.D.Bytes()
8188
}
8289

90+
func (k V3AsymmetricSecretKey) Version() KeyVersion {
91+
return 3
92+
}
93+
func (k V3AsymmetricSecretKey) Type() KeyType {
94+
return KeyTypeSecret
95+
}
96+
8397
// NewV3AsymmetricSecretKey generate a new secret key for use with asymmetric
8498
// cryptography. Don't forget to export the public key for sharing, DO NOT share
8599
// this secret key.
@@ -145,6 +159,14 @@ func (k V3SymmetricKey) ExportBytes() []byte {
145159
return k.material[:]
146160
}
147161

162+
func (k V3SymmetricKey) Version() KeyVersion {
163+
return 3
164+
}
165+
166+
func (k V3SymmetricKey) Type() KeyType {
167+
return KeyTypeLocal
168+
}
169+
148170
// V3SymmetricKeyFromHex constructs a key from hex
149171
func V3SymmetricKeyFromHex(hexEncoded string) (V3SymmetricKey, error) {
150172
var bytes []byte

0 commit comments

Comments
 (0)