From 21b6849602a66c4f25693aa6ec15bc68d5b7a1a5 Mon Sep 17 00:00:00 2001 From: Daniel SUTTO <28776655+suttod@users.noreply.github.com> Date: Wed, 1 Jun 2022 12:56:21 +0200 Subject: [PATCH 1/5] Export V4AsymmetricPublicKey to paserk k4.public --- keys.go | 72 ++++++++++++++ paserk.go | 150 +++++++++++++++++++++++++++++ test-vectors/PASERK/k4.public.json | 30 ++++++ v4_keys.go | 22 +++++ vectors_test.go | 32 ++++++ 5 files changed, 306 insertions(+) create mode 100644 keys.go create mode 100644 paserk.go create mode 100644 test-vectors/PASERK/k4.public.json diff --git a/keys.go b/keys.go new file mode 100644 index 0000000..c0fce6c --- /dev/null +++ b/keys.go @@ -0,0 +1,72 @@ +package paseto + +// keyPurpose indicates if key is symmetric, private or public +type keyPurpose int + +// keyVersion indicates the token version the key may be used for +type KeyVersion int + +const ( + // Invalid key version + KeyVersionInvalid KeyVersion = 0 + // Key used for V1 tokens + KeyVersionV1 KeyVersion = 1 + // Key used for V2 tokens + KeyVersionV2 KeyVersion = 2 + // Key used for V3 tokens + KeyVersionV3 KeyVersion = 3 + // Key used for V4 tokens + KeyVersionV4 KeyVersion = 4 + + // Symmetric key used for local tokens + keyPurposeLocal keyPurpose = 1 + // Asymmetric secret key used for public tokens + keyPurposeSecret keyPurpose = 2 + // Asymmetric public key used for public tokens + keyPurposePublic keyPurpose = 3 +) + +type Key interface { + // Export raw key data as hex string + ExportHex() string + // Export raw key data as byte array + ExportBytes() []byte + // Export key as paserk token of the given token type + ExportPaserk(PaserkType) (string, error) + // Returns purpose of key + getPurpose() keyPurpose + // Returns the version of the paseto tokens the key may be used for + getVersion() KeyVersion +} + +// Convert key version to PASERK header string +func KeyVersionToString(version KeyVersion) string { + switch version { + case KeyVersionV1: + return "k1" + case KeyVersionV2: + return "k2" + case KeyVersionV3: + return "k3" + case KeyVersionV4: + return "k4" + default: + return "" + } +} + +// Parse key version from PASERK header string (eg. "k2") +func ParseKeyVersionFromString(versionStr string) KeyVersion { + switch versionStr { + case "k1": + return KeyVersionV1 + case "k2": + return KeyVersionV2 + case "k3": + return KeyVersionV3 + case "k4": + return KeyVersionV4 + default: + return KeyVersionInvalid + } +} diff --git a/paserk.go b/paserk.go new file mode 100644 index 0000000..1e1c3e1 --- /dev/null +++ b/paserk.go @@ -0,0 +1,150 @@ +package paseto + +import "fmt" + +// type of PASERK token +type PaserkType int + +// Error indicating the given paserk import/export method is not yet implemented on the key type. +type NotImplementedError struct { + keyTypeStr string + paserkTypeStr string +} + +// Error indicating the given paserk type is not valid for the key type. +type InvalidPaserkTypeError struct { + keyTypeStr string + paserkTypeStr string +} + +const ( + // Invalid Paserk token type + PaserkTypeInvalid PaserkType = 0 + // Unique Identifier for a separate PASERK for local PASETOs + PaserkTypeLid PaserkType = 1 + // Symmetric key for local tokens + PaserkTypeLocal PaserkType = 2 + // Symmetric key wrapped using asymmetric encryption + PaserkTypeSeal PaserkType = 3 + // Symmetric key wrapped by another symmetric key + PaserkTypeLocalWrap PaserkType = 4 + // Symmetric key wrapped using password-based encryption + PaserkTypeLocalPw PaserkType = 5 + // Unique Identifier for a separate PASERK for public PASETOs. (Secret Key) + PaserkTypeSid PaserkType = 6 + // Public key for verifying public tokens + PaserkTypePublic PaserkType = 7 + // Unique Identifier for a separate PASERK for public PASETOs. (Public Key) + PaserkTypePid PaserkType = 8 + // Secret key for signing public tokens + PaserkTypeSecret PaserkType = 9 + // Asymmetric secret key wrapped by another symmetric key + PaserkTypeSecretWrap PaserkType = 10 + // Asymmetric secret key wrapped using password-based encryption + PaserkTypeSecretPw PaserkType = 11 +) + +// Convert token type to PASERK header string `type` +func PaserkTypeToString(paserkType PaserkType) string { + switch paserkType { + case PaserkTypeLid: + return "lid" + case PaserkTypeLocal: + return "local" + case PaserkTypeSeal: + return "seal" + case PaserkTypeLocalWrap: + return "local-wrap" + case PaserkTypeLocalPw: + return "local-pw" + case PaserkTypeSid: + return "sid" + case PaserkTypePublic: + return "public" + case PaserkTypePid: + return "pid" + case PaserkTypeSecret: + return "secret" + case PaserkTypeSecretWrap: + return "secret-wrap" + case PaserkTypeSecretPw: + return "secret-pw" + default: + return "" + } +} + +// Parse token type from string value of `type` field of PASERK token +func PaserkTypeFromString(typeStr string) PaserkType { + switch typeStr { + case "lid": + return PaserkTypeLid + case "local": + return PaserkTypeLocal + case "seal": + return PaserkTypeSeal + case "local-wrap": + return PaserkTypeLocalWrap + case "local-pw": + return PaserkTypeLocalPw + case "sid": + return PaserkTypeSid + case "public": + return PaserkTypePublic + case "pid": + return PaserkTypePid + case "secret": + return PaserkTypeSecret + case "secret-wrap": + return PaserkTypeSecretWrap + case "secret-pw": + return PaserkTypeSecretPw + default: + return PaserkTypeInvalid + } +} + +// Checks if the representation (paserk token type) is available for the key +func (paserkType PaserkType) isAvailableForKey(key Key) bool { + switch key.getPurpose() { + case keyPurposeLocal: + switch paserkType { + case PaserkTypeLid, + PaserkTypeLocal, + PaserkTypeSeal, + PaserkTypeLocalWrap, + PaserkTypeLocalPw: + return true + default: + return false + } + case keyPurposePublic: + switch paserkType { + case PaserkTypePid, + PaserkTypePublic: + return true + default: + return false + } + case keyPurposeSecret: + switch paserkType { + case PaserkTypeSid, + PaserkTypeSecret, + PaserkTypeSecretWrap, + PaserkTypeSecretPw: + return true + default: + return false + } + default: + return false + } +} + +func (e NotImplementedError) Error() string { + return fmt.Sprintf("PASERK type %s is not yet implemented on key type %s", e.paserkTypeStr, e.keyTypeStr) +} + +func (e InvalidPaserkTypeError) Error() string { + return fmt.Sprintf("PASERK type %s is invalid for key type %s", e.paserkTypeStr, e.keyTypeStr) +} diff --git a/test-vectors/PASERK/k4.public.json b/test-vectors/PASERK/k4.public.json new file mode 100644 index 0000000..c060a63 --- /dev/null +++ b/test-vectors/PASERK/k4.public.json @@ -0,0 +1,30 @@ +{ + "name": "PASERK k4.public Test Vectors", + "tests": [ + { + "name": "k4.public-1", + "expect-fail": false, + "key": "0000000000000000000000000000000000000000000000000000000000000000", + "paserk": "k4.public.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + }, + { + "name": "k4.public-2", + "expect-fail": false, + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "paserk": "k4.public.cHFyc3R1dnd4eXp7fH1-f4CBgoOEhYaHiImKi4yNjo8" + }, + { + "name": "k4.public-3", + "expect-fail": false, + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e90", + "paserk": "k4.public.cHFyc3R1dnd4eXp7fH1-f4CBgoOEhYaHiImKi4yNjpA" + }, + { + "name": "k4.public-fail-1", + "expect-fail": true, + "key": "02707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f", + "paserk": null, + "comment": "Implementations MUST NOT accept a PASERK of the wrong version." + } + ] +} \ No newline at end of file diff --git a/v4_keys.go b/v4_keys.go index edd3487..87ec61b 100644 --- a/v4_keys.go +++ b/v4_keys.go @@ -2,7 +2,9 @@ package paseto import ( "crypto/ed25519" + "encoding/base64" "encoding/hex" + "fmt" "aidanwoods.dev/go-paseto/internal/hashing" "aidanwoods.dev/go-paseto/internal/random" @@ -48,6 +50,26 @@ func (k V4AsymmetricPublicKey) ExportBytes() []byte { return k.material } +// ExportPaserk export a V4AsymmetricPublicKey to a paserk token of type paserkType +func (k V4AsymmetricPublicKey) ExportPaserk(paserkType PaserkType) (string, error) { + if !paserkType.isAvailableForKey(&k) { + return "", InvalidPaserkTypeError{fmt.Sprintf("%T", k), PaserkTypeToString(paserkType)} + } + if paserkType != PaserkTypePublic { + return "", NotImplementedError{fmt.Sprintf("%T", k), PaserkTypeToString(paserkType)} + } + header := KeyVersionToString(k.getVersion()) + "." + PaserkTypeToString(paserkType) + "." + data := base64.RawURLEncoding.EncodeToString(k.ExportBytes()) + return header + data, nil +} + +func (k *V4AsymmetricPublicKey) getVersion() KeyVersion { + return KeyVersionV4 +} +func (k *V4AsymmetricPublicKey) getPurpose() keyPurpose { + return keyPurposePublic +} + // V4AsymmetricSecretKey v4 public private key type V4AsymmetricSecretKey struct { material ed25519.PrivateKey diff --git a/vectors_test.go b/vectors_test.go index 70ee28e..5d2bf6c 100644 --- a/vectors_test.go +++ b/vectors_test.go @@ -27,6 +27,8 @@ type TestVector struct { Footer string ExpectFail bool `json:"expect-fail"` ImplicitAssertation string `json:"implicit-assertion"` + Paserk string + Comment string } func TestV2(t *testing.T) { @@ -299,3 +301,33 @@ func TestV4(t *testing.T) { }) } } + +func TestPaserkV4Public(t *testing.T) { + data, err := os.ReadFile("test-vectors/PASERK/k4.public.json") + require.NoError(t, err) + + var tests TestVectors + err = json.Unmarshal(data, &tests) + require.NoError(t, err) + + for _, test := range tests.Tests { + t.Run(test.Name, func(t *testing.T) { + + k, err := paseto.NewV4AsymmetricPublicKeyFromHex(test.Key) + if test.ExpectFail { + require.Error(t, err) + return + } + require.NoError(t, err) + + token, err := k.ExportPaserk(paseto.PaserkTypePublic) + if test.ExpectFail { + require.Error(t, err) + return + } + require.NoError(t, err) + + require.Equal(t, test.Paserk, token) + }) + } +} From 5935d375864de130351e45f6825a69a9eb25bf4e Mon Sep 17 00:00:00 2001 From: Daniel SUTTO <28776655+suttod@users.noreply.github.com> Date: Wed, 1 Jun 2022 21:28:08 +0200 Subject: [PATCH 2/5] refact: rename function for better convention --- keys.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keys.go b/keys.go index c0fce6c..4bb8372 100644 --- a/keys.go +++ b/keys.go @@ -56,7 +56,7 @@ func KeyVersionToString(version KeyVersion) string { } // Parse key version from PASERK header string (eg. "k2") -func ParseKeyVersionFromString(versionStr string) KeyVersion { +func KeyVersionFromString(versionStr string) KeyVersion { switch versionStr { case "k1": return KeyVersionV1 From 6d343b529b353cfe6dc466773dd34d531a3e8c23 Mon Sep 17 00:00:00 2001 From: Daniel SUTTO <28776655+suttod@users.noreply.github.com> Date: Wed, 1 Jun 2022 21:30:16 +0200 Subject: [PATCH 3/5] Parse v4.public paserk to V4AsymmetricPublicKey --- paserk.go | 37 ++++++++++++++++++++++++++++++++++++- vectors_test.go | 15 +++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/paserk.go b/paserk.go index 1e1c3e1..ec726cd 100644 --- a/paserk.go +++ b/paserk.go @@ -1,6 +1,12 @@ package paseto -import "fmt" +import ( + "encoding/base64" + "fmt" + "strings" + + "github.com/pkg/errors" +) // type of PASERK token type PaserkType int @@ -148,3 +154,32 @@ func (e NotImplementedError) Error() string { func (e InvalidPaserkTypeError) Error() string { return fmt.Sprintf("PASERK type %s is invalid for key type %s", e.paserkTypeStr, e.keyTypeStr) } + +func ParsePaserkRaw(paserkStr string) (Key, error) { + frags := strings.Split(paserkStr, ".") + if len(frags) != 3 { + return nil, fmt.Errorf("Invalid PASERK token: %s", paserkStr) + } + tokenVersion := KeyVersionFromString(frags[0]) + typ := PaserkTypeFromString(frags[1]) + data, err := base64.RawURLEncoding.DecodeString(frags[2]) + if err != nil { + return nil, errors.Wrap(err, "can't decode data part of pasrk key") + } + + switch tokenVersion { + case KeyVersionV4: + switch typ { + case PaserkTypePublic: + key, err := NewV4AsymmetricPublicKeyFromBytes(data) + if err != nil { + return nil, errors.Wrap(err, "can't construct key from data part of paserk key") + } + return &key, nil + default: + return nil, NotImplementedError{frags[0], frags[1]} + } + default: + return nil, NotImplementedError{frags[0], frags[1]} + } +} diff --git a/vectors_test.go b/vectors_test.go index 5d2bf6c..638e644 100644 --- a/vectors_test.go +++ b/vectors_test.go @@ -330,4 +330,19 @@ func TestPaserkV4Public(t *testing.T) { require.Equal(t, test.Paserk, token) }) } + + for _, test := range tests.Tests { + t.Run(test.Name+"-reverse", func(t *testing.T) { + + k, err := paseto.ParsePaserkRaw(test.Paserk) + if test.ExpectFail { + require.Error(t, err) + return + } + require.NoError(t, err) + + v4key := k.(*paseto.V4AsymmetricPublicKey) + require.Equal(t, test.Key, v4key.ExportHex()) + }) + } } From e7fab38023fa878934ff84717bbe1ec544708516 Mon Sep 17 00:00:00 2001 From: Daniel SUTTO <28776655+suttod@users.noreply.github.com> Date: Wed, 1 Jun 2022 22:01:40 +0200 Subject: [PATCH 4/5] refact: move ExportPaserk to paserk.go Reason: Multpile export methods (eg. wrap) require different inputs, so that different signatures would be needed. Exporting algorithm (Operation) is independent from key version, so it is better to be implemented as single function instead of receiver --- keys.go | 2 -- paserk.go | 27 +++++++++++++++++++++++++++ v4_keys.go | 15 --------------- vectors_test.go | 2 +- 4 files changed, 28 insertions(+), 18 deletions(-) diff --git a/keys.go b/keys.go index 4bb8372..03cf8ca 100644 --- a/keys.go +++ b/keys.go @@ -31,8 +31,6 @@ type Key interface { ExportHex() string // Export raw key data as byte array ExportBytes() []byte - // Export key as paserk token of the given token type - ExportPaserk(PaserkType) (string, error) // Returns purpose of key getPurpose() keyPurpose // Returns the version of the paseto tokens the key may be used for diff --git a/paserk.go b/paserk.go index ec726cd..2bfbb3e 100644 --- a/paserk.go +++ b/paserk.go @@ -155,6 +155,33 @@ func (e InvalidPaserkTypeError) Error() string { return fmt.Sprintf("PASERK type %s is invalid for key type %s", e.paserkTypeStr, e.keyTypeStr) } +// ExportPaserk export a V4AsymmetricPublicKey to a paserk token of type paserkType +func ExportPaserkRaw(k Key) (string, error) { + var paserkType PaserkType + switch k.getPurpose() { + case keyPurposeLocal: + paserkType = PaserkTypeLocal + break + case keyPurposeSecret: + paserkType = PaserkTypeSecret + break + case keyPurposePublic: + paserkType = PaserkTypePublic + break + default: + return "", errors.New("invalid key purpose") + } + if !paserkType.isAvailableForKey(k) { + return "", InvalidPaserkTypeError{fmt.Sprintf("%T", k), PaserkTypeToString(paserkType)} + } + if paserkType != PaserkTypePublic { + return "", NotImplementedError{fmt.Sprintf("%T", k), PaserkTypeToString(paserkType)} + } + header := KeyVersionToString(k.getVersion()) + "." + PaserkTypeToString(paserkType) + "." + data := base64.RawURLEncoding.EncodeToString(k.ExportBytes()) + return header + data, nil +} + func ParsePaserkRaw(paserkStr string) (Key, error) { frags := strings.Split(paserkStr, ".") if len(frags) != 3 { diff --git a/v4_keys.go b/v4_keys.go index 87ec61b..21bfb88 100644 --- a/v4_keys.go +++ b/v4_keys.go @@ -2,9 +2,7 @@ package paseto import ( "crypto/ed25519" - "encoding/base64" "encoding/hex" - "fmt" "aidanwoods.dev/go-paseto/internal/hashing" "aidanwoods.dev/go-paseto/internal/random" @@ -50,19 +48,6 @@ func (k V4AsymmetricPublicKey) ExportBytes() []byte { return k.material } -// ExportPaserk export a V4AsymmetricPublicKey to a paserk token of type paserkType -func (k V4AsymmetricPublicKey) ExportPaserk(paserkType PaserkType) (string, error) { - if !paserkType.isAvailableForKey(&k) { - return "", InvalidPaserkTypeError{fmt.Sprintf("%T", k), PaserkTypeToString(paserkType)} - } - if paserkType != PaserkTypePublic { - return "", NotImplementedError{fmt.Sprintf("%T", k), PaserkTypeToString(paserkType)} - } - header := KeyVersionToString(k.getVersion()) + "." + PaserkTypeToString(paserkType) + "." - data := base64.RawURLEncoding.EncodeToString(k.ExportBytes()) - return header + data, nil -} - func (k *V4AsymmetricPublicKey) getVersion() KeyVersion { return KeyVersionV4 } diff --git a/vectors_test.go b/vectors_test.go index 638e644..2ea4ba5 100644 --- a/vectors_test.go +++ b/vectors_test.go @@ -320,7 +320,7 @@ func TestPaserkV4Public(t *testing.T) { } require.NoError(t, err) - token, err := k.ExportPaserk(paseto.PaserkTypePublic) + token, err := paseto.ExportPaserkRaw(&k) if test.ExpectFail { require.Error(t, err) return From 66e376bcf42ffd35bfbc8bbcef8833c20b45835f Mon Sep 17 00:00:00 2001 From: Daniel SUTTO <28776655+suttod@users.noreply.github.com> Date: Fri, 3 Jun 2022 09:19:29 +0200 Subject: [PATCH 5/5] refact: remove redundat break --- paserk.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/paserk.go b/paserk.go index 2bfbb3e..5ba01ba 100644 --- a/paserk.go +++ b/paserk.go @@ -161,13 +161,10 @@ func ExportPaserkRaw(k Key) (string, error) { switch k.getPurpose() { case keyPurposeLocal: paserkType = PaserkTypeLocal - break case keyPurposeSecret: paserkType = PaserkTypeSecret - break case keyPurposePublic: paserkType = PaserkTypePublic - break default: return "", errors.New("invalid key purpose") }