From 9157aba9862f4e8b8296c14577706160168a15d1 Mon Sep 17 00:00:00 2001 From: Benjamin Bengfort Date: Mon, 22 Jul 2024 14:16:35 -0500 Subject: [PATCH] Envelope Trees and Bug Fixes (#168) --- pkg/trisa/envelope/envelope.go | 226 ++++++++---------- pkg/trisa/envelope/envelope_test.go | 195 ++++----------- pkg/trisa/envelope/helpers.go | 144 +++++++++++ pkg/trisa/envelope/helpers_test.go | 208 ++++++++++++++++ pkg/trisa/envelope/options.go | 8 +- .../envelope/testdata/alice.vaspbot.net.pem | 120 ++++++++++ 6 files changed, 611 insertions(+), 290 deletions(-) create mode 100644 pkg/trisa/envelope/helpers.go create mode 100644 pkg/trisa/envelope/helpers_test.go create mode 100644 pkg/trisa/envelope/testdata/alice.vaspbot.net.pem diff --git a/pkg/trisa/envelope/envelope.go b/pkg/trisa/envelope/envelope.go index 8ac9120..1295005 100644 --- a/pkg/trisa/envelope/envelope.go +++ b/pkg/trisa/envelope/envelope.go @@ -55,128 +55,6 @@ import ( "google.golang.org/protobuf/proto" ) -// Seal an envelope using the public signing key of the TRISA peer (must be supplied via -// the WithSealingKey or WithRSAPublicKey options). A secure envelope is created by -// marshaling the payload, encrypting it, then sealing the envelope by encrypting the -// encryption key and hmac secret with the public key of the recipient. This method -// returns two types of errors: a rejection error can be returned to the sender to -// indicate that the TRISA protocol failed, otherwise an error is returned for the user -// to handle. This method is a convenience one-liner, for more control of the sealing -// process or to manage intermediate steps, use the Envelope wrapper directly. -func Seal(payload *api.Payload, opts ...Option) (_ *api.SecureEnvelope, reject *api.Error, err error) { - var env *Envelope - if env, err = New(payload, opts...); err != nil { - return nil, nil, err - } - - // Validate the payload before encrypting - if err = env.ValidatePayload(); err != nil { - return nil, nil, err - } - - // Create a new AES-GCM crypto handler if one is not supplied on the envelope. This - // generates a random encryption key and hmac secret on a per-envelope basis, - // helping to prevent statistical cryptographic attacks. - if env.crypto == nil { - if env.crypto, err = aesgcm.New(nil, nil); err != nil { - return nil, nil, err - } - } - - if reject, err = env.encrypt(payload); reject != nil || err != nil { - if reject != nil { - msg, _ := env.Reject(reject) - return msg.Proto(), reject, err - } - return nil, nil, err - } - - if reject, err = env.sealEnvelope(); reject != nil || err != nil { - if reject != nil { - msg, _ := env.Reject(reject) - return msg.Proto(), reject, err - } - return nil, nil, err - } - - return env.Proto(), nil, nil -} - -// Open a secure envelope using the private key that is paired with the public key that -// was used to seal the envelope (must be supplied via the WithSealingKey or -// WithRSAPrivateKey options). This method decrypts the encryption key and hmac secret, -// decrypts and verifies the payload HMAC signature, then unmarshals the payload and -// verifies its contents. This method returns two types of errors: a rejection error -// that can be returned to the sender to indicate that the TRISA protocol failed, -// otherwise an error is returned for the user to handle. This method is a convenience -// one-liner, for more control of the open envelope process or to manage intermediate -// steps, use the Envelope wrapper directly. -func Open(msg *api.SecureEnvelope, opts ...Option) (payload *api.Payload, reject *api.Error, err error) { - var env *Envelope - if env, err = Wrap(msg, opts...); err != nil { - return nil, nil, err - } - - // A rejection here would be related to a sealing key failure - if reject, err = env.unsealEnvelope(); reject != nil || err != nil { - return nil, reject, err - } - - // A rejection here is related to the decryption, verification, and parsing the payload - if reject, err = env.decrypt(); reject != nil || err != nil { - return nil, reject, err - } - - if payload, err = env.Payload(); err != nil { - return nil, nil, err - } - return payload, nil, nil -} - -// Reject returns a new rejection error to send to the counterparty -func Reject(reject *api.Error, opts ...Option) (_ *api.SecureEnvelope, err error) { - var env *Envelope - if env, err = New(nil, opts...); err != nil { - return nil, err - } - - // Add the error to the envelope and validate - env.msg.Error = reject - - // Determine the transfer state from the rejection - if reject.Retry { - env.msg.TransferState = api.TransferRepair - } else { - env.msg.TransferState = api.TransferRejected - } - - // Validate the message and the error - if err = env.ValidateMessage(); err != nil { - return nil, err - } - - return env.Proto(), nil -} - -// Check returns any error on the specified envelope as well as a bool that indicates -// if the envelope is in an error state (even if the envelope contains a payload). -func Check(msg *api.SecureEnvelope) (_ *api.Error, iserr bool) { - env := &Envelope{msg: msg} - return env.Error(), env.IsError() -} - -// Status returns the state the secure envelope is currently in. -func Status(msg *api.SecureEnvelope) State { - env := &Envelope{msg: msg} - return env.State() -} - -// Timestamp returns the parsed timestamp from the secure envelope. -func Timestamp(msg *api.SecureEnvelope) (time.Time, error) { - env := &Envelope{msg: msg} - return env.Timestamp() -} - // Envelope is a wrapper for a trisa.SecureEnvelope that adds cryptographic // functionality to the protocol buffer payload. An envelope can be in one of three // states: clear, unsealed, and sealed -- referring to the cryptographic status of the @@ -190,6 +68,7 @@ type Envelope struct { payload *api.Payload crypto crypto.Crypto seal crypto.Cipher + parent *Envelope } //=========================================================================== @@ -268,14 +147,57 @@ func WrapError(reject *api.Error, opts ...Option) (env *Envelope, err error) { return env, nil } -// Validate is a one-liner for Wrap(msg).ValidateMessage() and can be used to ensure -// that a secure envelope has been correctly initialized and can be processed. -func Validate(msg *api.SecureEnvelope) (err error) { - var env *Envelope - if env, err = Wrap(msg); err != nil { - return err +// Create an envelope with an encrypted and sealed payload using the public signing key +// of the TRISA peer (supplied via the WithSealingKey or WithRSAPublicKey options). +// The returned envelope has a parent chain that contains the encryption transformations +// at each step so that you can validate that the payload has been constructed correctly. +func Seal(payload *api.Payload, opts ...Option) (env *Envelope, reject *api.Error, err error) { + if env, err = New(payload, opts...); err != nil { + return nil, nil, err + } + + if env, reject, err = env.Encrypt(); err != nil { + if reject != nil { + env, _ = env.Reject(reject) + } + return env, reject, err + } + + if env, reject, err = env.Seal(); err != nil { + if reject != nil { + env, _ = env.Reject(reject) + } + return env, reject, err + } + + return env, nil, nil +} + +// Open a secure envelope using the private key that is paired with the public key used +// to seal the envelope (provided using the WithUnsealingKey or WithRSAPrivateKey +// options). The returned envelope has a partent chain that contains the decryption +// transformations at each step so tha tyou can validate that the payload has been +// constructed correctly. +func Open(msg *api.SecureEnvelope, opts ...Option) (env *Envelope, reject *api.Error, err error) { + if env, err = Wrap(msg, opts...); err != nil { + return nil, nil, err + } + + if env, reject, err = env.Unseal(); err != nil { + if reject != nil { + env, _ = env.Reject(reject) + } + return env, reject, err + } + + if env, reject, err = env.Decrypt(); err != nil { + if reject != nil { + env, _ = env.Reject(reject) + } + return env, reject, err } - return env.ValidateMessage() + + return env, nil, nil } //=========================================================================== @@ -300,6 +222,7 @@ func (e *Envelope) Reject(reject *api.Error, opts ...Option) (env *Envelope, err PublicKeySignature: "", TransferState: api.TransferStateUnspecified, }, + parent: e, } if reject.Retry { @@ -348,6 +271,7 @@ func (e *Envelope) Update(payload *api.Payload, opts ...Option) (env *Envelope, crypto: e.crypto, seal: e.seal, payload: payload, + parent: e, } // Apply the options @@ -393,6 +317,7 @@ func (e *Envelope) Encrypt(opts ...Option) (env *Envelope, reject *api.Error, er }, crypto: e.crypto, seal: e.seal, + parent: e, } // Apply the options @@ -443,7 +368,7 @@ func (e *Envelope) encrypt(payload *api.Payload) (_ *api.Error, err error) { return nil, fmt.Errorf("could not sign payload data: %s", err) } - // Populate metadata on envelope + // Populate metadata on envelope and reset public key signature since its not present e.msg.EncryptionKey = e.crypto.EncryptionKey() e.msg.HmacSecret = e.crypto.HMACSecret() e.msg.EncryptionAlgorithm = e.crypto.EncryptionAlgorithm() @@ -484,6 +409,7 @@ func (e *Envelope) Decrypt(opts ...Option) (env *Envelope, reject *api.Error, er }, crypto: e.crypto, seal: e.seal, + parent: e, } // Apply the options @@ -587,6 +513,7 @@ func (e *Envelope) Seal(opts ...Option) (env *Envelope, reject *api.Error, err e }, crypto: e.crypto, seal: e.seal, + parent: e, } // Apply the options @@ -660,6 +587,7 @@ func (e *Envelope) Unseal(opts ...Option) (env *Envelope, reject *api.Error, err }, crypto: e.crypto, seal: e.seal, + parent: e, } // Apply the options @@ -690,9 +618,10 @@ func (e *Envelope) unsealEnvelope() (reject *api.Error, err error) { return api.Errorf(api.InvalidKey, "could not unseal HMAC secret").WithRetry(), err } - // Mark the envelope as unsealed + // Mark the envelope as unsealed and remove the public key signature e.msg.Sealed = false e.msg.PublicKeySignature = "" + return nil, nil } @@ -773,6 +702,41 @@ func (e *Envelope) Sealer() crypto.Cipher { return e.seal } +// Returns the parent envelope for envelope sequence chain lookups. +func (e *Envelope) Parent() *Envelope { + return e.parent +} + +// Finds the public key signature by looking up the envelope tree until a non-zero +// public key signature is available. Returns empty string if none exists. This is +// useful when you receive an envelope and fully decrypt it, but need to refer back to +// the public key signature that was used in the original sealed envelope. +func (e *Envelope) FindPublicKeySignature() string { + switch { + case e.msg.PublicKeySignature != "": + return e.msg.PublicKeySignature + case e.parent != nil: + return e.parent.FindPublicKeySignature() + default: + return "" + } +} + +// Find payload returns the nearest payload by looking up the envelope tree until a +// non-nil payload is available. Returns nil if none exists. This is useful when you +// have a payload that is encrypted and sealed, and want to refer back to the original +// payload without keeping track of all of the original envelopes. +func (e *Envelope) FindPayload() *api.Payload { + switch { + case e.payload != nil: + return e.payload + case e.parent != nil: + return e.parent.FindPayload() + default: + return nil + } +} + //=========================================================================== // Envelope Validation //=========================================================================== diff --git a/pkg/trisa/envelope/envelope_test.go b/pkg/trisa/envelope/envelope_test.go index c941df0..752c9e7 100644 --- a/pkg/trisa/envelope/envelope_test.go +++ b/pkg/trisa/envelope/envelope_test.go @@ -18,41 +18,13 @@ import ( "github.com/trisacrypto/trisa/pkg/trisa/crypto/aesgcm" generic "github.com/trisacrypto/trisa/pkg/trisa/data/generic/v1beta1" "github.com/trisacrypto/trisa/pkg/trisa/envelope" + "github.com/trisacrypto/trisa/pkg/trisa/keys" + "github.com/trisacrypto/trisa/pkg/trust" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/anypb" ) -func ExampleSeal() { - // Create compliance payload to send to counterparty. Use key exchange or GDS to - // fetch the public sealing key of the recipient. See the testdata fixtures for - // example data. Note: we're loading an RSA private key and extracting its public - // key for example and testing purposes. - payload, _ := loadPayloadFixture("testdata/payload.json") - key, _ := loadPrivateKey("testdata/sealing_key.pem") - - // Seal the payload: encrypting and digitally signing the marshaled protocol buffers - // with a randomly generated encryption key and HMAC secret, then encrypting those - // secrets with the public key of the recipient. - msg, reject, err := envelope.Seal(payload, envelope.WithRSAPublicKey(&key.PublicKey)) - - // Two types errors may be returned from envelope.Seal - if err != nil { - if reject != nil { - // If both err and reject are non-nil, then a TRISA protocol error occurred - // and the rejection error can be sent back to the originator if you're - // sealing the envelope in response to a transfer request - log.Println(reject.String()) - } else { - // Otherwise log the error and handle with user-specific code - log.Fatal(err) - } - } - - // Otherwise send the secure envelope to the recipient - log.Printf("sending secure envelope with id %s", msg.Id) -} - func Example_create() { // Create compliance payload to send to counterparty. Use key exchange or GDS to // fetch the public sealing key of the recipient. See the testdata fixtures for @@ -93,38 +65,6 @@ func Example_create() { log.Printf("sending secure envelope with id %s", msg.Id) } -func ExampleOpen() { - // Receive a sealed secure envelope from the counterparty. Ensure you have the - // private key paired with the public key identified by the public key signature on - // the secure envelope in order to unseal and decrypt the payload. See testdata - // fixtures for example data. Note: we're loading an RSA private key used in other - // examples for demonstration and testing purposes. - msg, _ := loadEnvelopeFixture("testdata/sealed_envelope.json") - key, _ := loadPrivateKey("testdata/sealing_key.pem") - - // Open the secure envelope, unsealing the encryption key and hmac secret with the - // supplied private key, then decrypting, verifying, and unmarshaling the payload - // using those secrets. - payload, reject, err := envelope.Open(msg, envelope.WithRSAPrivateKey(key)) - - // Two types errors may be returned from envelope.Open - if err != nil { - if reject != nil { - // If both err and reject are non-nil, then a TRISA protocol error occurred - // and the rejection error can be sent back to the originator if you're - // opening the envelope in response to a transfer request - out, _ := envelope.Reject(reject, envelope.WithEnvelopeID(msg.Id)) - log.Printf("sending TRISA rejection for envelope %s: %s", out.Id, reject) - } else { - // Otherwise log the error and handle with user-specific code - log.Fatal(err) - } - } - - // Handle the payload with your interal compliance processing mechanism. - log.Printf("received payload sent at %s", payload.SentAt) -} - func Example_parse() { // Receive a sealed secure envelope from the counterparty. Ensure you have the // private key paired with the public key identified by the public key signature on @@ -178,18 +118,21 @@ func TestSendEnvelopeWorkflow(t *testing.T) { env, err := envelope.New(payload, envelope.WithRSAPublicKey(&key.PublicKey)) require.NoError(t, err, "could not create envelope with no payload and no options") require.Equal(t, envelope.Clear, env.State(), "expected clear state not %q", env.State()) + require.Nil(t, env.Parent(), "expected new envelope parent to be nil") eenv, reject, err := env.Encrypt() require.NoError(t, err, "could not encrypt envelope") require.Nil(t, reject, "expected no API error returned from encryption") require.NotSame(t, env, eenv, "Encrypt should return a clone of the original envelope") require.Equal(t, envelope.Unsealed, eenv.State(), "expected unsealed state not %q", eenv.State()) + require.Same(t, env, eenv.Parent(), "expected encrypted env parent to be the original env") senv, reject, err := eenv.Seal() require.NoError(t, err, "could not seal envelope") require.Nil(t, reject, "expected no API error returned from sealing") require.NotSame(t, eenv, senv, "Seal should return a clone of the original envelope") require.Equal(t, envelope.Sealed, senv.State(), "expected sealed state not %q", senv.State()) + require.Same(t, eenv, senv.Parent(), "expected sealed env parent to be encrypted env") // Fetch the message and check that it is ready to send msg := senv.Proto() @@ -220,6 +163,7 @@ func TestRecvEnvelopeWorkflow(t *testing.T) { require.NoError(t, err, "could not wrap the envelope") require.NoError(t, senv.ValidateMessage(), "secure envelope fixture is invalid") require.Equal(t, envelope.Sealed, senv.State(), "expected sealed state not %q", senv.State()) + require.Nil(t, senv.Parent(), "expected wrapped envelope parent to be nil") // Unseal the envelope eenv, reject, err := senv.Unseal() @@ -227,6 +171,7 @@ func TestRecvEnvelopeWorkflow(t *testing.T) { require.Nil(t, reject, "a rejection error was unexpectedly returned") require.NotSame(t, senv, eenv, "Unseal should return a clone of the original envelope") require.Equal(t, envelope.Unsealed, eenv.State(), "expected unsealed state not %q", eenv.State()) + require.Same(t, senv, eenv.Parent(), "expected unsealed envelope parent to be sealed envelope") // Decrypt the envelope env, reject, err := eenv.Decrypt() @@ -236,6 +181,7 @@ func TestRecvEnvelopeWorkflow(t *testing.T) { require.Equal(t, envelope.Clear, env.State(), "expected clear state not %q", eenv.State()) require.NotNil(t, env.Crypto(), "decrypted envelopes should maintain crytpo context") require.NotNil(t, env.Sealer(), "decrypted envelopes should maintain sealer context") + require.Same(t, eenv, env.Parent(), "expected decrypted envelope parent to be unsealed envelope") // Get the payload from the envelope payload, err := env.Payload() @@ -280,43 +226,27 @@ func TestRecvEnvelopeWorkflow(t *testing.T) { require.Equal(t, msg.PublicKeySignature, out.PublicKeySignature, "public key signature mismatch") } -func TestOneLiners(t *testing.T) { - payload, err := loadPayloadFixture("testdata/pending_payload.json") - require.NoError(t, err, "could not load pending payload") +func TestSealAndOpen(t *testing.T) { + payload, err := loadPayloadFixture("testdata/payload.json") + require.NoError(t, err, "could not load payload") - key, err := loadPrivateKey("testdata/sealing_key.pem") - require.NoError(t, err, "could not load sealing key") + certs, err := loadCerts("testdata/alice.vaspbot.net.pem") + require.NoError(t, err, "could not load certificates from disk") - // Create an envelope from the payload and the key - msg, reject, err := envelope.Seal(payload, envelope.WithRSAPublicKey(&key.PublicKey)) + env, reject, err := envelope.Seal(payload, envelope.WithSealingKey(certs)) require.NoError(t, err, "could not seal envelope") - require.Nil(t, reject, "unexpected rejection error") - - // Ensure the msg is valid - require.NotEmpty(t, msg.Id, "no envelope id on the message") - require.NotEmpty(t, msg.Payload, "no payload on the message") - require.NotEmpty(t, msg.EncryptionKey, "no encryption key on the message") - require.NotEmpty(t, msg.EncryptionAlgorithm, "no encryption algorithm on the message") - require.NotEmpty(t, msg.Hmac, "no hmac signature on the message") - require.NotEmpty(t, msg.HmacSecret, "no hmac secret on the message") - require.NotEmpty(t, msg.HmacAlgorithm, "no hmac algorithm on the message") - require.Empty(t, msg.Error, "unexpected error on the message") - require.NotEmpty(t, msg.Timestamp, "no timestamp on the message") - require.True(t, msg.Sealed, "message not marked as sealed") - require.NotEmpty(t, msg.PublicKeySignature, "no public key signature on the message") - - // Serialize and Deserialize the message - data, err := proto.Marshal(msg) - require.NoError(t, err, "could not marshal outgoing message") - - in := &api.SecureEnvelope{} - require.NoError(t, proto.Unmarshal(data, in), "could not unmarshal incoming message") - - // Open the envelope with the private key - decryptedPayload, reject, err := envelope.Open(in, envelope.WithRSAPrivateKey(key)) + require.Nil(t, reject, "no rejection should have been returned on seal") + require.Equal(t, envelope.Sealed, env.State()) + + msg, reject, err := envelope.Open(env.Proto(), envelope.WithUnsealingKey(certs)) require.NoError(t, err, "could not open envelope") - require.Nil(t, reject, "unexpected rejection error") - require.True(t, proto.Equal(payload, decryptedPayload), "payloads do not match") + require.Nil(t, reject, "no rejection should have been returned on open") + require.Equal(t, envelope.Clear, msg.State()) + + decrypted, err := msg.Payload() + require.NoError(t, err, "could not fetch payload") + + require.True(t, proto.Equal(payload, decrypted)) } func TestEnvelopeAccessors(t *testing.T) { @@ -388,65 +318,6 @@ func TestEnvelopeAccessors(t *testing.T) { require.True(t, ts.Equal(actualTS), "timestamp did not match expected timestamp") } -func TestCheck(t *testing.T) { - emsg, err := loadEnvelopeFixture("testdata/error_envelope.json") - require.NoError(t, err, "could not load error envelope fixture") - - terr, iserr := envelope.Check(emsg) - require.True(t, iserr, "expected error envelope to return iserr true") - require.Equal(t, api.ComplianceCheckFail, terr.Code) - require.Equal(t, "specified account has been frozen temporarily", terr.Message) - require.False(t, terr.Retry) - - for _, path := range []string{"testdata/unsealed_envelope.json", "testdata/sealed_envelope.json"} { - msg, err := loadEnvelopeFixture(path) - require.NoError(t, err, "could not load %s", path) - - terr, iserr = envelope.Check(msg) - require.False(t, iserr) - require.Nil(t, terr) - } -} - -func TestStatus(t *testing.T) { - testCases := []struct { - path string - state envelope.State - }{ - {"testdata/error_envelope.json", envelope.Error}, - {"testdata/unsealed_envelope.json", envelope.Unsealed}, - {"testdata/sealed_envelope.json", envelope.Sealed}, - } - - for i, tc := range testCases { - msg, err := loadEnvelopeFixture(tc.path) - require.NoError(t, err, "could not load fixture from %s", tc.path) - - state := envelope.Status(msg) - require.Equal(t, tc.state, state, "test case %d expected %s got %s", i+1, tc.state, state) - } -} - -func TestTimestamp(t *testing.T) { - testCases := []struct { - path string - expected time.Time - }{ - {"testdata/error_envelope.json", time.Time(time.Date(2022, time.January, 27, 8, 21, 43, 0, time.UTC))}, - {"testdata/unsealed_envelope.json", time.Date(2022, time.March, 29, 14, 16, 27, 453444000, time.UTC)}, - {"testdata/sealed_envelope.json", time.Date(2022, time.March, 29, 14, 16, 29, 755212000, time.UTC)}, - } - - for i, tc := range testCases { - msg, err := loadEnvelopeFixture(tc.path) - require.NoError(t, err, "could not load fixture from %s", tc.path) - - ts, err := envelope.Timestamp(msg) - require.NoError(t, err, "timestamp parsing error on test case %d", i) - require.True(t, tc.expected.Equal(ts), "timestamp mismatch on test case %d", i) - } -} - func TestWrapError(t *testing.T) { t.Run("Valid", func(t *testing.T) { testCases := []struct { @@ -753,7 +624,7 @@ func generateFixtures() (err error) { return err } - if env, _, err = envelope.Seal(pendingPayload, envelope.WithRSAPublicKey(&key.PublicKey)); err != nil { + if env, _, err = envelope.SealPayload(pendingPayload, envelope.WithRSAPublicKey(&key.PublicKey)); err != nil { return err } if err = dumpFixture("testdata/sealed_envelope.json", env); err != nil { @@ -802,3 +673,17 @@ func loadPrivateKey(path string) (key *rsa.PrivateKey, err error) { return keyt.(*rsa.PrivateKey), nil } + +func loadCerts(path string) (_ keys.Key, err error) { + var sz *trust.Serializer + if sz, err = trust.NewSerializer(false); err != nil { + return nil, err + } + + var certs *trust.Provider + if certs, err = sz.ReadFile(path); err != nil { + return nil, err + } + + return keys.FromProvider(certs) +} diff --git a/pkg/trisa/envelope/helpers.go b/pkg/trisa/envelope/helpers.go new file mode 100644 index 0000000..5829b05 --- /dev/null +++ b/pkg/trisa/envelope/helpers.go @@ -0,0 +1,144 @@ +package envelope + +import ( + "time" + + api "github.com/trisacrypto/trisa/pkg/trisa/api/v1beta1" + "github.com/trisacrypto/trisa/pkg/trisa/crypto/aesgcm" +) + +//=========================================================================== +// Quick One-Line Helper Functions +//=========================================================================== + +// SealPayload an envelope using the public signing key of the TRISA peer (must be +// supplied via the WithSealingKey or WithRSAPublicKey options). A secure envelope is +// created by marshaling the payload, encrypting it, then sealing the envelope by +// encrypting the encryption key and hmac secret with the public key of the recipient. +// This method returns two types of errors: a rejection error can be returned to the +// sender to indicate that the TRISA protocol failed, otherwise an error is returned for +// the user to handle. This method is a convenience one-liner, for more control of the +// sealing process or to manage intermediate steps, use the Envelope wrapper directly. +func SealPayload(payload *api.Payload, opts ...Option) (_ *api.SecureEnvelope, reject *api.Error, err error) { + var env *Envelope + if env, err = New(payload, opts...); err != nil { + return nil, nil, err + } + + // Validate the payload before encrypting + if err = env.ValidatePayload(); err != nil { + return nil, nil, err + } + + // Create a new AES-GCM crypto handler if one is not supplied on the envelope. This + // generates a random encryption key and hmac secret on a per-envelope basis, + // helping to prevent statistical cryptographic attacks. + if env.crypto == nil { + if env.crypto, err = aesgcm.New(nil, nil); err != nil { + return nil, nil, err + } + } + + if reject, err = env.encrypt(payload); reject != nil || err != nil { + if reject != nil { + msg, _ := env.Reject(reject) + return msg.Proto(), reject, err + } + return nil, nil, err + } + + if reject, err = env.sealEnvelope(); reject != nil || err != nil { + if reject != nil { + msg, _ := env.Reject(reject) + return msg.Proto(), reject, err + } + return nil, nil, err + } + + return env.Proto(), nil, nil +} + +// OpenPayload a secure envelope using the private key that is paired with the public +// key that was used to seal the envelope (must be supplied via the WithUnsealingKey or +// WithRSAPrivateKey options). This method decrypts the encryption key and hmac secret, +// decrypts and verifies the payload HMAC signature, then unmarshals the payload and +// verifies its contents. This method returns two types of errors: a rejection error +// that can be returned to the sender to indicate that the TRISA protocol failed, +// otherwise an error is returned for the user to handle. This method is a convenience +// one-liner, for more control of the open envelope process or to manage intermediate +// steps, use the Envelope wrapper directly. +func OpenPayload(msg *api.SecureEnvelope, opts ...Option) (payload *api.Payload, reject *api.Error, err error) { + var env *Envelope + if env, err = Wrap(msg, opts...); err != nil { + return nil, nil, err + } + + // A rejection here would be related to a sealing key failure + if reject, err = env.unsealEnvelope(); reject != nil || err != nil { + return nil, reject, err + } + + // A rejection here is related to the decryption, verification, and parsing the payload + if reject, err = env.decrypt(); reject != nil || err != nil { + return nil, reject, err + } + + if payload, err = env.Payload(); err != nil { + return nil, nil, err + } + return payload, nil, nil +} + +// Reject returns a new rejection error to send to the counterparty +func Reject(reject *api.Error, opts ...Option) (_ *api.SecureEnvelope, err error) { + var env *Envelope + if env, err = New(nil, opts...); err != nil { + return nil, err + } + + // Add the error to the envelope and validate + env.msg.Error = reject + + // Determine the transfer state from the rejection + if reject.Retry { + env.msg.TransferState = api.TransferRepair + } else { + env.msg.TransferState = api.TransferRejected + } + + // Validate the message and the error + if err = env.ValidateMessage(); err != nil { + return nil, err + } + + return env.Proto(), nil +} + +// Check returns any error on the specified envelope as well as a bool that indicates +// if the envelope is in an error state (even if the envelope contains a payload). +func Check(msg *api.SecureEnvelope) (_ *api.Error, iserr bool) { + env := &Envelope{msg: msg} + return env.Error(), env.IsError() +} + +// Status returns the state the secure envelope is currently in. +func Status(msg *api.SecureEnvelope) State { + env := &Envelope{msg: msg} + return env.State() +} + +// Timestamp returns the parsed timestamp from the secure envelope. +func Timestamp(msg *api.SecureEnvelope) (time.Time, error) { + env := &Envelope{msg: msg} + return env.Timestamp() +} + +// Validate is a one-liner for Wrap(msg).ValidateMessage() and can be used to ensure +// that a secure envelope has been correctly initialized and can be processed. +func Validate(msg *api.SecureEnvelope) (err error) { + var env *Envelope + if env, err = Wrap(msg); err != nil { + return err + } + return env.ValidateMessage() +} diff --git a/pkg/trisa/envelope/helpers_test.go b/pkg/trisa/envelope/helpers_test.go new file mode 100644 index 0000000..e9b0b1a --- /dev/null +++ b/pkg/trisa/envelope/helpers_test.go @@ -0,0 +1,208 @@ +package envelope_test + +import ( + "log" + "testing" + "time" + + "github.com/stretchr/testify/require" + api "github.com/trisacrypto/trisa/pkg/trisa/api/v1beta1" + "github.com/trisacrypto/trisa/pkg/trisa/envelope" + "google.golang.org/protobuf/proto" +) + +func ExampleSealPayload() { + // Create compliance payload to send to counterparty. Use key exchange or GDS to + // fetch the public sealing key of the recipient. See the testdata fixtures for + // example data. Note: we're loading an RSA private key and extracting its public + // key for example and testing purposes. + payload, _ := loadPayloadFixture("testdata/payload.json") + key, _ := loadPrivateKey("testdata/sealing_key.pem") + + // Seal the payload: encrypting and digitally signing the marshaled protocol buffers + // with a randomly generated encryption key and HMAC secret, then encrypting those + // secrets with the public key of the recipient. + msg, reject, err := envelope.SealPayload(payload, envelope.WithRSAPublicKey(&key.PublicKey)) + + // Two types errors may be returned from envelope.Seal + if err != nil { + if reject != nil { + // If both err and reject are non-nil, then a TRISA protocol error occurred + // and the rejection error can be sent back to the originator if you're + // sealing the envelope in response to a transfer request + log.Println(reject.String()) + } else { + // Otherwise log the error and handle with user-specific code + log.Fatal(err) + } + } + + // Otherwise send the secure envelope to the recipient + log.Printf("sending secure envelope with id %s", msg.Id) +} + +func ExampleOpenPayload() { + // Receive a sealed secure envelope from the counterparty. Ensure you have the + // private key paired with the public key identified by the public key signature on + // the secure envelope in order to unseal and decrypt the payload. See testdata + // fixtures for example data. Note: we're loading an RSA private key used in other + // examples for demonstration and testing purposes. + msg, _ := loadEnvelopeFixture("testdata/sealed_envelope.json") + key, _ := loadPrivateKey("testdata/sealing_key.pem") + + // Open the secure envelope, unsealing the encryption key and hmac secret with the + // supplied private key, then decrypting, verifying, and unmarshaling the payload + // using those secrets. + payload, reject, err := envelope.OpenPayload(msg, envelope.WithRSAPrivateKey(key)) + + // Two types errors may be returned from envelope.Open + if err != nil { + if reject != nil { + // If both err and reject are non-nil, then a TRISA protocol error occurred + // and the rejection error can be sent back to the originator if you're + // opening the envelope in response to a transfer request + out, _ := envelope.Reject(reject, envelope.WithEnvelopeID(msg.Id)) + log.Printf("sending TRISA rejection for envelope %s: %s", out.Id, reject) + } else { + // Otherwise log the error and handle with user-specific code + log.Fatal(err) + } + } + + // Handle the payload with your interal compliance processing mechanism. + log.Printf("received payload sent at %s", payload.SentAt) +} + +func TestOneLiners(t *testing.T) { + payload, err := loadPayloadFixture("testdata/pending_payload.json") + require.NoError(t, err, "could not load pending payload") + + key, err := loadPrivateKey("testdata/sealing_key.pem") + require.NoError(t, err, "could not load sealing key") + + // Create an envelope from the payload and the key + msg, reject, err := envelope.SealPayload(payload, envelope.WithRSAPublicKey(&key.PublicKey)) + require.NoError(t, err, "could not seal envelope") + require.Nil(t, reject, "unexpected rejection error") + + // Ensure the msg is valid + require.NotEmpty(t, msg.Id, "no envelope id on the message") + require.NotEmpty(t, msg.Payload, "no payload on the message") + require.NotEmpty(t, msg.EncryptionKey, "no encryption key on the message") + require.NotEmpty(t, msg.EncryptionAlgorithm, "no encryption algorithm on the message") + require.NotEmpty(t, msg.Hmac, "no hmac signature on the message") + require.NotEmpty(t, msg.HmacSecret, "no hmac secret on the message") + require.NotEmpty(t, msg.HmacAlgorithm, "no hmac algorithm on the message") + require.Empty(t, msg.Error, "unexpected error on the message") + require.NotEmpty(t, msg.Timestamp, "no timestamp on the message") + require.True(t, msg.Sealed, "message not marked as sealed") + require.NotEmpty(t, msg.PublicKeySignature, "no public key signature on the message") + + // Serialize and Deserialize the message + data, err := proto.Marshal(msg) + require.NoError(t, err, "could not marshal outgoing message") + + in := &api.SecureEnvelope{} + require.NoError(t, proto.Unmarshal(data, in), "could not unmarshal incoming message") + + // Open the envelope with the private key + decryptedPayload, reject, err := envelope.OpenPayload(in, envelope.WithRSAPrivateKey(key)) + require.NoError(t, err, "could not open envelope") + require.Nil(t, reject, "unexpected rejection error") + require.True(t, proto.Equal(payload, decryptedPayload), "payloads do not match") +} + +func TestReject(t *testing.T) { + envelopeID := "63c763bc-f820-4a76-b64f-15587ec84a13" + + t.Run("Repair", func(t *testing.T) { + err := &api.Error{ + Code: api.Error_EXCEEDED_TRADING_VOLUME, + Message: "our system is overloaded, please try again later", + Retry: true, + } + + msg, e := envelope.Reject(err, envelope.WithEnvelopeID(envelopeID)) + require.NoError(t, e, "expected no error creating a transfer state") + + require.Equal(t, envelopeID, msg.Id, "envelope does not have correct id") + }) + + t.Run("Reject", func(t *testing.T) { + err := &api.Error{ + Code: api.Error_UNKNOWN_ORIGINATOR, + Message: "the originator specified does not have an account with us", + Retry: false, + } + + msg, e := envelope.Reject(err, envelope.WithEnvelopeID(envelopeID)) + require.NoError(t, e, "expected no error creating a transfer state") + + require.Equal(t, envelopeID, msg.Id, "envelope does not have correct id") + }) + + t.Run("Invalid", func(t *testing.T) { + msg, e := envelope.Reject(&api.Error{}, envelope.WithEnvelopeID(envelopeID)) + require.ErrorIs(t, e, envelope.ErrNoMessageData) + require.Nil(t, msg) + }) +} + +func TestCheck(t *testing.T) { + emsg, err := loadEnvelopeFixture("testdata/error_envelope.json") + require.NoError(t, err, "could not load error envelope fixture") + + terr, iserr := envelope.Check(emsg) + require.True(t, iserr, "expected error envelope to return iserr true") + require.Equal(t, api.ComplianceCheckFail, terr.Code) + require.Equal(t, "specified account has been frozen temporarily", terr.Message) + require.False(t, terr.Retry) + + for _, path := range []string{"testdata/unsealed_envelope.json", "testdata/sealed_envelope.json"} { + msg, err := loadEnvelopeFixture(path) + require.NoError(t, err, "could not load %s", path) + + terr, iserr = envelope.Check(msg) + require.False(t, iserr) + require.Nil(t, terr) + } +} + +func TestStatus(t *testing.T) { + testCases := []struct { + path string + state envelope.State + }{ + {"testdata/error_envelope.json", envelope.Error}, + {"testdata/unsealed_envelope.json", envelope.Unsealed}, + {"testdata/sealed_envelope.json", envelope.Sealed}, + } + + for i, tc := range testCases { + msg, err := loadEnvelopeFixture(tc.path) + require.NoError(t, err, "could not load fixture from %s", tc.path) + + state := envelope.Status(msg) + require.Equal(t, tc.state, state, "test case %d expected %s got %s", i+1, tc.state, state) + } +} + +func TestTimestamp(t *testing.T) { + testCases := []struct { + path string + expected time.Time + }{ + {"testdata/error_envelope.json", time.Time(time.Date(2022, time.January, 27, 8, 21, 43, 0, time.UTC))}, + {"testdata/unsealed_envelope.json", time.Date(2022, time.March, 29, 14, 16, 27, 453444000, time.UTC)}, + {"testdata/sealed_envelope.json", time.Date(2022, time.March, 29, 14, 16, 29, 755212000, time.UTC)}, + } + + for i, tc := range testCases { + msg, err := loadEnvelopeFixture(tc.path) + require.NoError(t, err, "could not load fixture from %s", tc.path) + + ts, err := envelope.Timestamp(msg) + require.NoError(t, err, "timestamp parsing error on test case %d", i) + require.True(t, tc.expected.Equal(ts), "timestamp mismatch on test case %d", i) + } +} diff --git a/pkg/trisa/envelope/options.go b/pkg/trisa/envelope/options.go index 0eba646..88b4f05 100644 --- a/pkg/trisa/envelope/options.go +++ b/pkg/trisa/envelope/options.go @@ -71,9 +71,9 @@ func WithSealingKey(key interface{}) Option { if ikey, ok := key.(keys.PublicKey); ok { var err error if key, err = ikey.SealingKey(); err != nil { - return WithSealingKey(key) + return errorOption(err) } - return errorOption(err) + return WithSealingKey(key) } return func(e *Envelope) (err error) { @@ -94,9 +94,9 @@ func WithUnsealingKey(key interface{}) Option { if ikey, ok := key.(keys.PrivateKey); ok { var err error if key, err = ikey.UnsealingKey(); err != nil { - return WithUnsealingKey(key) + return errorOption(err) } - return errorOption(err) + return WithUnsealingKey(key) } return func(e *Envelope) (err error) { diff --git a/pkg/trisa/envelope/testdata/alice.vaspbot.net.pem b/pkg/trisa/envelope/testdata/alice.vaspbot.net.pem new file mode 100644 index 0000000..479461c --- /dev/null +++ b/pkg/trisa/envelope/testdata/alice.vaspbot.net.pem @@ -0,0 +1,120 @@ +-----BEGIN CERTIFICATE----- +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 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +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 +-----END CERTIFICATE----- +-----BEGIN PRIVATE KEY----- +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 +fUEUQwvHKHfotwZpvkPUrBJ3btmKkQ== +-----END PRIVATE KEY----- \ No newline at end of file