From 6ad5b48d8a90b5059dd1d08648da5f9f075bac57 Mon Sep 17 00:00:00 2001 From: Benjamin Bengfort Date: Wed, 6 Nov 2024 09:57:40 -0500 Subject: [PATCH] Sunrise Verification Token --- pkg/sunrise/errors.go | 11 ++++ pkg/sunrise/sunrise.go | 1 + pkg/sunrise/verify.go | 97 +++++++++++++++++++++++++++++++++ pkg/sunrise/verify_test.go | 107 +++++++++++++++++++++++++++++++++++++ 4 files changed, 216 insertions(+) create mode 100644 pkg/sunrise/errors.go create mode 100644 pkg/sunrise/sunrise.go create mode 100644 pkg/sunrise/verify.go create mode 100644 pkg/sunrise/verify_test.go diff --git a/pkg/sunrise/errors.go b/pkg/sunrise/errors.go new file mode 100644 index 00000000..9beaa2c0 --- /dev/null +++ b/pkg/sunrise/errors.go @@ -0,0 +1,11 @@ +package sunrise + +import "errors" + +var ( + ErrDecode = errors.New("sunrise: could not decode token") + ErrSize = errors.New("sunrise: invalid size for token") + ErrInvalidEnvelopeID = errors.New("invalid sunrise token: no envelope id") + ErrInvalidExpiration = errors.New("invalid sunrise token: no expiration timestamp") + ErrInvalidNonce = errors.New("invalid sunrise token: incorrect nonce") +) diff --git a/pkg/sunrise/sunrise.go b/pkg/sunrise/sunrise.go new file mode 100644 index 00000000..f3be5878 --- /dev/null +++ b/pkg/sunrise/sunrise.go @@ -0,0 +1 @@ +package sunrise diff --git a/pkg/sunrise/verify.go b/pkg/sunrise/verify.go new file mode 100644 index 00000000..836a865e --- /dev/null +++ b/pkg/sunrise/verify.go @@ -0,0 +1,97 @@ +package sunrise + +import ( + "bytes" + "crypto/rand" + "encoding/binary" + "errors" + "fmt" + "time" + + "github.com/google/uuid" +) + +const ( + nonceLength = 64 + keyLength = 64 + minTokenLength = 16 + nonceLength + 1 + maxTokenLength = 16 + nonceLength + binary.MaxVarintLen64 +) + +type Token struct { + EnvelopeID uuid.UUID + Expiration time.Time + nonce []byte +} + +func NewToken(envelopeID uuid.UUID, expiration time.Time) *Token { + token := &Token{ + EnvelopeID: envelopeID, + Expiration: expiration, + nonce: make([]byte, nonceLength), + } + + if _, err := rand.Read(token.nonce); err != nil { + panic(fmt.Errorf("no crypto random generator available: %w", err)) + } + + return token +} + +func (t *Token) MarshalBinary() ([]byte, error) { + if err := t.Validate(); err != nil { + return nil, err + } + + data := make([]byte, maxTokenLength) + copy(data[:16], t.EnvelopeID[:]) + + i := binary.PutVarint(data[16:], t.Expiration.UnixNano()) + copy(data[16+i:], t.nonce) + + return data[:16+i+nonceLength], nil +} + +func (t *Token) UnmarshalBinary(data []byte) error { + if len(data) > maxTokenLength || len(data) < minTokenLength { + return ErrSize + } + + // Parse envelope ID + t.EnvelopeID = uuid.UUID(data[:16]) + + // Parse expiration time + exp, i := binary.Varint(data[16:]) + if i <= 0 { + return ErrDecode + } + t.Expiration = time.Unix(0, exp) + + // Read the nonce data + t.nonce = data[16+i:] + + // Validate the binary data + return t.Validate() +} + +func (t *Token) Validate() (err error) { + if t.EnvelopeID == uuid.Nil { + err = errors.Join(err, ErrInvalidEnvelopeID) + } + + if t.Expiration.IsZero() { + err = errors.Join(err, ErrInvalidExpiration) + } + + if len(t.nonce) != nonceLength { + err = errors.Join(err, ErrInvalidNonce) + } + + return err +} + +func (t *Token) Equal(o *Token) bool { + return bytes.Equal(t.EnvelopeID[:], o.EnvelopeID[:]) && + t.Expiration.Equal(o.Expiration) && + bytes.Equal(t.nonce, o.nonce) +} diff --git a/pkg/sunrise/verify_test.go b/pkg/sunrise/verify_test.go new file mode 100644 index 00000000..868fa08f --- /dev/null +++ b/pkg/sunrise/verify_test.go @@ -0,0 +1,107 @@ +package sunrise_test + +import ( + "bytes" + "testing" + "time" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" + "github.com/trisacrypto/envoy/pkg/sunrise" +) + +func TestTokenBinary(t *testing.T) { + t.Run("Valid", func(t *testing.T) { + testCases := []*sunrise.Token{ + sunrise.NewToken(uuid.MustParse("24035c84-ff3d-4da2-aef7-8683d9c00978"), time.Date(1994, 12, 20, 15, 21, 1, 3213, time.UTC)), + sunrise.NewToken(uuid.New(), time.Now()), + sunrise.NewToken(uuid.New(), time.Now().Add(312391*time.Hour)), + } + + for i, token := range testCases { + data, err := token.MarshalBinary() + require.NotNil(t, data, "test case %d returned nil data", i) + require.NoError(t, err, "test case %d errored on marshall", i) + + cmpt := &sunrise.Token{} + err = cmpt.UnmarshalBinary(data) + require.NoError(t, err, "test case %d errored on unmarshall", i) + + require.True(t, token.Equal(cmpt), "deserialization mismatch for test case %d", i) + } + }) + + t.Run("BadMarshal", func(t *testing.T) { + testCases := []struct { + token *sunrise.Token + err error + }{ + { + sunrise.NewToken(uuid.Nil, time.Now()), + sunrise.ErrInvalidEnvelopeID, + }, + { + sunrise.NewToken(uuid.New(), time.Time{}), + sunrise.ErrInvalidExpiration, + }, + } + + for i, tc := range testCases { + data, err := tc.token.MarshalBinary() + require.Nil(t, data, "test case %d returned non-nil data", i) + require.ErrorIs(t, err, tc.err, "test case %d return the wrong error", i) + } + }) + + t.Run("BadUnmarshal", func(t *testing.T) { + testCases := []struct { + data []byte + err error + }{ + { + nil, + sunrise.ErrSize, + }, + { + []byte{}, + sunrise.ErrSize, + }, + { + []byte{0x1, 0x2, 0x3, 0x4, 0xf, 0xfe}, + sunrise.ErrSize, + }, + { + bytes.Repeat([]byte{0x1, 0x2, 0x3, 0x4, 0xf, 0xfe}, 64), + sunrise.ErrSize, + }, + { + []byte{ + 0x22, 0x1, 0x33, 0x41, 0xd3, 0x7a, 0x12, 0xc2, 0xab, 0x41, 0x0, 0xfc, 0xe1, 0x7b, 0x7d, 0x15, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0x22, 0x1, 0x33, 0x41, 0xd3, 0x7a, 0x12, 0xc2, 0xab, 0x41, 0x0, 0xfc, 0xe1, 0x7b, 0x7d, 0x15, + 0x22, 0x1, 0x33, 0x41, 0xd3, 0x7a, 0x12, 0xc2, 0xab, 0x41, 0x0, 0xfc, 0xe1, 0x7b, 0x7d, 0x15, + 0x22, 0x1, 0x33, 0x41, 0xd3, 0x7a, 0x12, 0xc2, 0xab, 0x41, 0x0, 0xfc, 0xe1, 0x7b, 0x7d, 0x15, + 0x22, 0x1, 0x33, 0x41, 0xd3, 0x7a, 0x12, 0xc2, 0xab, 0x41, 0x0, 0xfc, 0xe1, 0x7b, 0x7d, 0x15, + }, + sunrise.ErrDecode, + }, + { + []byte{ + 0x22, 0x1, 0x33, 0x41, 0xd3, 0x7a, 0x12, 0xc2, 0xab, 0x41, 0x0, 0xfc, 0xe1, 0x7b, 0x7d, 0x15, + 0xff, 0x00, 0xff, + 0x22, 0x1, 0x33, 0x41, 0xd3, 0x7a, 0x12, 0xc2, 0xab, 0x41, 0x0, 0xfc, 0xe1, 0x7b, 0x7d, 0x15, + 0x22, 0x1, 0x33, 0x41, 0xd3, 0x7a, 0x12, 0xc2, 0xab, 0x41, 0x0, 0xfc, 0xe1, 0x7b, 0x7d, 0x15, + 0x22, 0x1, 0x33, 0x41, 0xd3, 0x7a, 0x12, 0xc2, 0xab, 0x41, 0x0, 0xfc, 0xe1, 0x7b, 0x7d, 0x15, + 0x22, 0x1, 0x33, 0x41, 0xd3, 0x7a, 0x12, 0xc2, 0xab, 0x41, 0x0, 0xfc, 0xe1, 0x7b, 0x7d, 0x15, + }, + sunrise.ErrInvalidNonce, + }, + } + + for i, tc := range testCases { + token := &sunrise.Token{} + err := token.UnmarshalBinary(tc.data) + require.ErrorIs(t, err, tc.err, "test case %d return the wrong error", i) + } + }) +}