-
-
Notifications
You must be signed in to change notification settings - Fork 22
Implement PASERK serialization #44
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
minus7
wants to merge
3
commits into
aidantwoods:main
Choose a base branch
from
minus7:paserk
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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{} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| package paserk | ||
|
|
||
| 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) | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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 generateto embed all the test vectors into the repo and run the tests.There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 generateso that it's easy to update as the test vectors change for future versions, though that's a nice-to-have.There was a problem hiding this comment.
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-2which doesn't test PASERK itself. Instead, it seems to be meant to assert that aV4SymmetricKeycan'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.