envelope

package
v0.99999.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 14, 2022 License: MIT Imports: 12 Imported by: 1

Documentation

Overview

Package envelope replaces the handler package to provide utilities for encrypting and decrypting trisa.SecureEnvelopes as well as sealing and unsealing them. SecureEnvelopes are the unit of transfer in a TRISA transaction and are used to securely exchange sensitive compliance information. Security is provided through two forms of cryptography: symmetric cryptography to encrypt and sign the TRISA payload and asymmetric cryptography to seal the keys and secrets of the envelopes so that only the recipient can open it.

SecureEnvelopes have a lot of terminology, the first paragraph was loaded with it! Some of these terms are defined below in relation to SecureEnvelopes:

- Symmetric cryptography: encryption that requires both the sender and receiver to have the same secret keys. The encryption and digital signature of the payload are symmetric, and the encryption key and HMAC secret are stored on the envelope to ensure that both counterparties have the secrets required to decrypt the payload.

- Asymmetric cryptography: also referred to as public key cryptography, this type of cryptography relies on two keys: a public and a private key. When data is encrypted with the public key, only the private key can be used to decrypt the data. In the case of SecureEnvelopes, the encryption key and HMAC secret are encrypted using the public key of the recipient.

- Encrypt/Decrypt: in relation to a SecureEnvelope, this refers to the symmetric cryptography performed on the payload; these terms are used in contrast to Seal/Unseal. An envelope's payload is referred to as "clear" before encryption and after decryption.

- Sign/Verify: in relation to a SecureEnvelope, this refers to the digital signature or HMAC of the encrypted payload. A digital signature ensures that the cryptographic contents of the payload have not been tampered with and provides the counterparty non-repudiation to affirm that the payload was received and not tampered with.

- Seal/Unseal: in relation to a SecureEnvelope, this refers to the asymmetric cryptography performed on the encryption key and hmac secret; these terms are used in contrast to Encrypt/Decrypt.

This package provides a wrapper, Envelope that is used to create and open SecureEnvelopes. The envelope workflow is as follows; a new envelope is in the clear, it is then encrypted and is referred to as "unsealed", then sealed with the public key of the receipient. When opening an envelope with a private key, the envelope becomes unsealed, then is decrypted in the clear.

For more details about how to work with envelopes, see the example code.

Example (Create)
package main

import (
	"crypto/rand"
	"crypto/rsa"
	"crypto/x509"
	"encoding/pem"
	"fmt"
	"log"
	"os"

	"github.com/trisacrypto/trisa/pkg/ivms101"

	api "github.com/trisacrypto/trisa/pkg/trisa/api/v1beta1"

	generic "github.com/trisacrypto/trisa/pkg/trisa/data/generic/v1beta1"
	"github.com/trisacrypto/trisa/pkg/trisa/envelope"
	"google.golang.org/protobuf/encoding/protojson"
	"google.golang.org/protobuf/proto"
	"google.golang.org/protobuf/types/known/anypb"
)

func main() {
	// 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")

	// Envelopes transition through the following states: clear --> unsealed --> sealed.
	// First create a new envelope in the clear state with the public key of the
	// recipient that will eventually be used to seal the envelope.
	env, _ := envelope.New(payload, envelope.WithRSAPublicKey(&key.PublicKey))

	// Marshal the payload, generate random encryption and hmac secrets, and encrypt
	// the payload, creating a new envelope in the unsealed state.
	env, reject, err := env.Encrypt()

	// Two types of errors are returned from Encrypt and 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)
		}
	}

	// Seal the envelope by encrypting the encryption key and hmac secret on the secure
	// envelope with the public key of the recipient passed in at the first step.
	// Handle the reject and err errors as above.
	env, reject, err = env.Seal()

	// Fetch the secure envelope and send it.
	msg := env.Proto()
	log.Printf("sending secure envelope with id %s", msg.Id)
}

const expectedEnvelopeId = "2b3b4c95-0a78-4f2a-a9fa-041970f97144"

var (
	loadpb = protojson.UnmarshalOptions{
		AllowPartial:   false,
		DiscardUnknown: false,
	}
	dumppb = protojson.MarshalOptions{
		Multiline:       true,
		Indent:          "  ",
		AllowPartial:    true,
		UseProtoNames:   true,
		UseEnumNumbers:  false,
		EmitUnpopulated: true,
	}
)

// Helper method to load a payload fixture, generating it if it hasn't been yet
func loadPayloadFixture(path string) (payload *api.Payload, err error) {
	payload = &api.Payload{}
	if err = loadFixture(path, payload, true); err != nil {
		return nil, err
	}
	return payload, nil
}

// Helper method to load a fixture from JSON
func loadFixture(path string, m proto.Message, check bool) (err error) {

	if check {
		if _, err = os.Stat(path); os.IsNotExist(err) {
			if err = generateFixtures(); err != nil {
				return err
			}
		}
	}

	var data []byte
	if data, err = os.ReadFile(path); err != nil {
		return err
	}

	if err = loadpb.Unmarshal(data, m); err != nil {
		return err
	}
	return nil
}

// Helper method to generate secure envelopes from the payload fixtures
func generateFixtures() (err error) {

	var (
		payload        *api.Payload
		pendingPayload *api.Payload
	)

	identity := &ivms101.IdentityPayload{}
	if err = loadFixture("testdata/payload/identity.json", identity, false); err != nil {
		return fmt.Errorf("could not unmarshal identity payload: %v", err)
	}

	pending := &generic.Pending{}
	if err = loadFixture("testdata/payload/pending.json", pending, false); err != nil {
		return fmt.Errorf("could not read pending payload: %v", err)
	}

	transaction := &generic.Transaction{}
	if err = loadFixture("testdata/payload/transaction.json", transaction, false); err != nil {
		return fmt.Errorf("could not read transaction payload: %v", err)
	}

	payload = &api.Payload{
		SentAt:     "2022-01-27T08:21:43Z",
		ReceivedAt: "2022-01-30T16:28:39Z",
	}
	if payload.Identity, err = anypb.New(identity); err != nil {
		return fmt.Errorf("could not create identity payload: %v", err)
	}
	if payload.Transaction, err = anypb.New(transaction); err != nil {
		return fmt.Errorf("could not create transaction payload: %v", err)
	}

	pendingPayload = &api.Payload{
		Identity: payload.Identity,
		SentAt:   payload.SentAt,
	}
	if pendingPayload.Transaction, err = anypb.New(pending); err != nil {
		return fmt.Errorf("could not create pending payload: %v", err)
	}

	if err = dumpFixture("testdata/payload.json", payload); err != nil {
		return fmt.Errorf("could not marshal payload: %v", err)
	}

	if err = dumpFixture("testdata/pending_payload.json", pendingPayload); err != nil {
		return fmt.Errorf("could not marshal pending payload: %v", err)
	}

	env := &api.SecureEnvelope{
		Id:        expectedEnvelopeId,
		Timestamp: "2022-01-27T08:21:43Z",
		Error: &api.Error{
			Code:    api.Error_COMPLIANCE_CHECK_FAIL,
			Message: "specified account has been frozen temporarily",
		},
	}

	if err = dumpFixture("testdata/error_envelope.json", env); err != nil {
		return fmt.Errorf("could not marshal error only envelope: %v", err)
	}

	var handler *envelope.Envelope
	if handler, err = envelope.New(payload); err != nil {
		return err
	}

	if handler, _, err = handler.Encrypt(); err != nil {
		return err
	}

	if err = dumpFixture("testdata/unsealed_envelope.json", handler.Proto()); err != nil {
		return fmt.Errorf("could not marshal unsealed envelope: %v", err)
	}

	key, err := rsa.GenerateKey(rand.Reader, 4096)
	if err != nil {
		return fmt.Errorf("could not generate RSA key fixture")
	}
	if err = dumpPrivateKey("testdata/sealing_key.pem", key); err != nil {
		return err
	}

	if env, _, err = envelope.Seal(pendingPayload, envelope.WithRSAPublicKey(&key.PublicKey)); err != nil {
		return err
	}
	if err = dumpFixture("testdata/sealed_envelope.json", env); err != nil {
		return fmt.Errorf("could not marshal sealed envelope: %v", err)
	}
	return nil
}

func dumpFixture(path string, m proto.Message) (err error) {
	var data []byte
	if data, err = dumppb.Marshal(m); err != nil {
		return err
	}
	return os.WriteFile(path, data, 0644)
}

func dumpPrivateKey(path string, key *rsa.PrivateKey) (err error) {
	var data []byte
	if data, err = x509.MarshalPKCS8PrivateKey(key); err != nil {
		return err
	}

	block := pem.EncodeToMemory(&pem.Block{
		Type:  "RSA PRIVATE KEY",
		Bytes: data,
	})

	return os.WriteFile(path, block, 0600)
}

func loadPrivateKey(path string) (key *rsa.PrivateKey, err error) {
	var data []byte
	if data, err = os.ReadFile(path); err != nil {
		return nil, err
	}

	block, _ := pem.Decode(data)
	if block == nil {
		return nil, fmt.Errorf("could not decode PEM data")
	}

	var keyt interface{}
	if keyt, err = x509.ParsePKCS8PrivateKey(block.Bytes); err != nil {
		return nil, err
	}

	return keyt.(*rsa.PrivateKey), nil
}
Output:

Example (Parse)
package main

import (
	"crypto/rand"
	"crypto/rsa"
	"crypto/x509"
	"encoding/pem"
	"fmt"
	"log"
	"os"

	"github.com/trisacrypto/trisa/pkg/ivms101"

	api "github.com/trisacrypto/trisa/pkg/trisa/api/v1beta1"

	generic "github.com/trisacrypto/trisa/pkg/trisa/data/generic/v1beta1"
	"github.com/trisacrypto/trisa/pkg/trisa/envelope"
	"google.golang.org/protobuf/encoding/protojson"
	"google.golang.org/protobuf/proto"
	"google.golang.org/protobuf/types/known/anypb"
)

func main() {
	// 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")

	// Envelopes transition through the following states: sealed --> unsealed --> clear.
	// First wrap the incoming envelope in the sealed state.
	env, _ := envelope.Wrap(msg)

	// Unseal the envelope using the private key loaded above; this decrypts the
	// encryption key and hmac secret using asymmetric encryption and returns a new
	// unsealed envelope.
	env, reject, err := env.Unseal(envelope.WithRSAPrivateKey(key))

	// Two types of errors are returned from Unseal and Decrypt
	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
			// unsealing the envelope in response to a transfer request.
			out, _ := env.Reject(reject)
			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)
		}
	}

	// Decrypt the envelope using the unsealed secrets, verify the HMAC signature, then
	// unmarshal and verify the payload into new envelope in the clear state.
	// Handle the reject and err errors as above.
	env, reject, err = env.Decrypt()

	// Handle the payload with your interal compliance processing mechanism.
	payload, _ := env.Payload()
	log.Printf("received payload sent at %s", payload.SentAt)
}

const expectedEnvelopeId = "2b3b4c95-0a78-4f2a-a9fa-041970f97144"

var (
	loadpb = protojson.UnmarshalOptions{
		AllowPartial:   false,
		DiscardUnknown: false,
	}
	dumppb = protojson.MarshalOptions{
		Multiline:       true,
		Indent:          "  ",
		AllowPartial:    true,
		UseProtoNames:   true,
		UseEnumNumbers:  false,
		EmitUnpopulated: true,
	}
)

// Helper method to load a secure envelope fixture, generating the fixtures from the
// payloads if they have not yet been generated.
func loadEnvelopeFixture(path string) (msg *api.SecureEnvelope, err error) {
	msg = &api.SecureEnvelope{}
	if err = loadFixture(path, msg, true); err != nil {
		return nil, err
	}
	return msg, nil
}

// Helper method to load a fixture from JSON
func loadFixture(path string, m proto.Message, check bool) (err error) {

	if check {
		if _, err = os.Stat(path); os.IsNotExist(err) {
			if err = generateFixtures(); err != nil {
				return err
			}
		}
	}

	var data []byte
	if data, err = os.ReadFile(path); err != nil {
		return err
	}

	if err = loadpb.Unmarshal(data, m); err != nil {
		return err
	}
	return nil
}

// Helper method to generate secure envelopes from the payload fixtures
func generateFixtures() (err error) {

	var (
		payload        *api.Payload
		pendingPayload *api.Payload
	)

	identity := &ivms101.IdentityPayload{}
	if err = loadFixture("testdata/payload/identity.json", identity, false); err != nil {
		return fmt.Errorf("could not unmarshal identity payload: %v", err)
	}

	pending := &generic.Pending{}
	if err = loadFixture("testdata/payload/pending.json", pending, false); err != nil {
		return fmt.Errorf("could not read pending payload: %v", err)
	}

	transaction := &generic.Transaction{}
	if err = loadFixture("testdata/payload/transaction.json", transaction, false); err != nil {
		return fmt.Errorf("could not read transaction payload: %v", err)
	}

	payload = &api.Payload{
		SentAt:     "2022-01-27T08:21:43Z",
		ReceivedAt: "2022-01-30T16:28:39Z",
	}
	if payload.Identity, err = anypb.New(identity); err != nil {
		return fmt.Errorf("could not create identity payload: %v", err)
	}
	if payload.Transaction, err = anypb.New(transaction); err != nil {
		return fmt.Errorf("could not create transaction payload: %v", err)
	}

	pendingPayload = &api.Payload{
		Identity: payload.Identity,
		SentAt:   payload.SentAt,
	}
	if pendingPayload.Transaction, err = anypb.New(pending); err != nil {
		return fmt.Errorf("could not create pending payload: %v", err)
	}

	if err = dumpFixture("testdata/payload.json", payload); err != nil {
		return fmt.Errorf("could not marshal payload: %v", err)
	}

	if err = dumpFixture("testdata/pending_payload.json", pendingPayload); err != nil {
		return fmt.Errorf("could not marshal pending payload: %v", err)
	}

	env := &api.SecureEnvelope{
		Id:        expectedEnvelopeId,
		Timestamp: "2022-01-27T08:21:43Z",
		Error: &api.Error{
			Code:    api.Error_COMPLIANCE_CHECK_FAIL,
			Message: "specified account has been frozen temporarily",
		},
	}

	if err = dumpFixture("testdata/error_envelope.json", env); err != nil {
		return fmt.Errorf("could not marshal error only envelope: %v", err)
	}

	var handler *envelope.Envelope
	if handler, err = envelope.New(payload); err != nil {
		return err
	}

	if handler, _, err = handler.Encrypt(); err != nil {
		return err
	}

	if err = dumpFixture("testdata/unsealed_envelope.json", handler.Proto()); err != nil {
		return fmt.Errorf("could not marshal unsealed envelope: %v", err)
	}

	key, err := rsa.GenerateKey(rand.Reader, 4096)
	if err != nil {
		return fmt.Errorf("could not generate RSA key fixture")
	}
	if err = dumpPrivateKey("testdata/sealing_key.pem", key); err != nil {
		return err
	}

	if env, _, err = envelope.Seal(pendingPayload, envelope.WithRSAPublicKey(&key.PublicKey)); err != nil {
		return err
	}
	if err = dumpFixture("testdata/sealed_envelope.json", env); err != nil {
		return fmt.Errorf("could not marshal sealed envelope: %v", err)
	}
	return nil
}

func dumpFixture(path string, m proto.Message) (err error) {
	var data []byte
	if data, err = dumppb.Marshal(m); err != nil {
		return err
	}
	return os.WriteFile(path, data, 0644)
}

func dumpPrivateKey(path string, key *rsa.PrivateKey) (err error) {
	var data []byte
	if data, err = x509.MarshalPKCS8PrivateKey(key); err != nil {
		return err
	}

	block := pem.EncodeToMemory(&pem.Block{
		Type:  "RSA PRIVATE KEY",
		Bytes: data,
	})

	return os.WriteFile(path, block, 0600)
}

func loadPrivateKey(path string) (key *rsa.PrivateKey, err error) {
	var data []byte
	if data, err = os.ReadFile(path); err != nil {
		return nil, err
	}

	block, _ := pem.Decode(data)
	if block == nil {
		return nil, fmt.Errorf("could not decode PEM data")
	}

	var keyt interface{}
	if keyt, err = x509.ParsePKCS8PrivateKey(block.Bytes); err != nil {
		return nil, err
	}

	return keyt.(*rsa.PrivateKey), nil
}
Output:

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	ErrNoMessage                = errors.New("invalid envelope: no wrapped message")
	ErrNoEnvelopeId             = errors.New("invalid envelope: no envelope id")
	ErrNoTimestamp              = errors.New("invalid envelope: no ordering timestamp")
	ErrNoMessageData            = errors.New("invalid envelope: must contain either error or payload")
	ErrNoEncryptionInfo         = errors.New("invalid envelope: missing encryption key or algorithm")
	ErrNoHMACInfo               = errors.New("invalid envelope: missing hmac signature, secret, or algorithm")
	ErrNoPayload                = errors.New("invalid payload: payload has not been decrypted")
	ErrNoIdentityPayload        = errors.New("invalid payload: payload does not contain identity data")
	ErrNoTransactionPayload     = errors.New("invalid payload: payload does not contain transaction data")
	ErrNoSentAtPayload          = errors.New("invalid payload: sent at timestamp is missing")
	ErrInvalidSentAtPayload     = errors.New("invalid payload: could not parse sent at timestamp in RFC3339 format")
	ErrInvalidReceivedatPayload = errors.New("invalid payload: could not parse received at timestamp in RFC3339 format")
	ErrCannotEncrypt            = errors.New("cannot encrypt envelope: no cryptographic handler available")
	ErrCannotSeal               = errors.New("cannot seal envelope: no public key cryptographic handler available")
	ErrCannotUnseal             = errors.New("cannot unseal envelope: no private key cryptographic handler available")
	ErrNoOrderingTimesamp       = errors.New("missing ordering timestamp on secure envelope")
	ErrInvalidOrderingTimestamp = errors.New("could not parse ordering timestamp on secure envelope as RFC3339 timestamp")
)

Functions

func Check added in v0.3.5

func Check(msg *api.SecureEnvelope) (_ *api.Error, iserr bool)

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 Open

func Open(msg *api.SecureEnvelope, opts ...Option) (payload *api.Payload, reject *api.Error, err error)

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.

Example
package main

import (
	"crypto/rand"
	"crypto/rsa"
	"crypto/x509"
	"encoding/pem"
	"fmt"
	"log"
	"os"

	"github.com/trisacrypto/trisa/pkg/ivms101"

	api "github.com/trisacrypto/trisa/pkg/trisa/api/v1beta1"

	generic "github.com/trisacrypto/trisa/pkg/trisa/data/generic/v1beta1"
	"github.com/trisacrypto/trisa/pkg/trisa/envelope"
	"google.golang.org/protobuf/encoding/protojson"
	"google.golang.org/protobuf/proto"
	"google.golang.org/protobuf/types/known/anypb"
)

func main() {
	// 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)
}

const expectedEnvelopeId = "2b3b4c95-0a78-4f2a-a9fa-041970f97144"

var (
	loadpb = protojson.UnmarshalOptions{
		AllowPartial:   false,
		DiscardUnknown: false,
	}
	dumppb = protojson.MarshalOptions{
		Multiline:       true,
		Indent:          "  ",
		AllowPartial:    true,
		UseProtoNames:   true,
		UseEnumNumbers:  false,
		EmitUnpopulated: true,
	}
)

// Helper method to load a secure envelope fixture, generating the fixtures from the
// payloads if they have not yet been generated.
func loadEnvelopeFixture(path string) (msg *api.SecureEnvelope, err error) {
	msg = &api.SecureEnvelope{}
	if err = loadFixture(path, msg, true); err != nil {
		return nil, err
	}
	return msg, nil
}

// Helper method to load a fixture from JSON
func loadFixture(path string, m proto.Message, check bool) (err error) {

	if check {
		if _, err = os.Stat(path); os.IsNotExist(err) {
			if err = generateFixtures(); err != nil {
				return err
			}
		}
	}

	var data []byte
	if data, err = os.ReadFile(path); err != nil {
		return err
	}

	if err = loadpb.Unmarshal(data, m); err != nil {
		return err
	}
	return nil
}

// Helper method to generate secure envelopes from the payload fixtures
func generateFixtures() (err error) {

	var (
		payload        *api.Payload
		pendingPayload *api.Payload
	)

	identity := &ivms101.IdentityPayload{}
	if err = loadFixture("testdata/payload/identity.json", identity, false); err != nil {
		return fmt.Errorf("could not unmarshal identity payload: %v", err)
	}

	pending := &generic.Pending{}
	if err = loadFixture("testdata/payload/pending.json", pending, false); err != nil {
		return fmt.Errorf("could not read pending payload: %v", err)
	}

	transaction := &generic.Transaction{}
	if err = loadFixture("testdata/payload/transaction.json", transaction, false); err != nil {
		return fmt.Errorf("could not read transaction payload: %v", err)
	}

	payload = &api.Payload{
		SentAt:     "2022-01-27T08:21:43Z",
		ReceivedAt: "2022-01-30T16:28:39Z",
	}
	if payload.Identity, err = anypb.New(identity); err != nil {
		return fmt.Errorf("could not create identity payload: %v", err)
	}
	if payload.Transaction, err = anypb.New(transaction); err != nil {
		return fmt.Errorf("could not create transaction payload: %v", err)
	}

	pendingPayload = &api.Payload{
		Identity: payload.Identity,
		SentAt:   payload.SentAt,
	}
	if pendingPayload.Transaction, err = anypb.New(pending); err != nil {
		return fmt.Errorf("could not create pending payload: %v", err)
	}

	if err = dumpFixture("testdata/payload.json", payload); err != nil {
		return fmt.Errorf("could not marshal payload: %v", err)
	}

	if err = dumpFixture("testdata/pending_payload.json", pendingPayload); err != nil {
		return fmt.Errorf("could not marshal pending payload: %v", err)
	}

	env := &api.SecureEnvelope{
		Id:        expectedEnvelopeId,
		Timestamp: "2022-01-27T08:21:43Z",
		Error: &api.Error{
			Code:    api.Error_COMPLIANCE_CHECK_FAIL,
			Message: "specified account has been frozen temporarily",
		},
	}

	if err = dumpFixture("testdata/error_envelope.json", env); err != nil {
		return fmt.Errorf("could not marshal error only envelope: %v", err)
	}

	var handler *envelope.Envelope
	if handler, err = envelope.New(payload); err != nil {
		return err
	}

	if handler, _, err = handler.Encrypt(); err != nil {
		return err
	}

	if err = dumpFixture("testdata/unsealed_envelope.json", handler.Proto()); err != nil {
		return fmt.Errorf("could not marshal unsealed envelope: %v", err)
	}

	key, err := rsa.GenerateKey(rand.Reader, 4096)
	if err != nil {
		return fmt.Errorf("could not generate RSA key fixture")
	}
	if err = dumpPrivateKey("testdata/sealing_key.pem", key); err != nil {
		return err
	}

	if env, _, err = envelope.Seal(pendingPayload, envelope.WithRSAPublicKey(&key.PublicKey)); err != nil {
		return err
	}
	if err = dumpFixture("testdata/sealed_envelope.json", env); err != nil {
		return fmt.Errorf("could not marshal sealed envelope: %v", err)
	}
	return nil
}

func dumpFixture(path string, m proto.Message) (err error) {
	var data []byte
	if data, err = dumppb.Marshal(m); err != nil {
		return err
	}
	return os.WriteFile(path, data, 0644)
}

func dumpPrivateKey(path string, key *rsa.PrivateKey) (err error) {
	var data []byte
	if data, err = x509.MarshalPKCS8PrivateKey(key); err != nil {
		return err
	}

	block := pem.EncodeToMemory(&pem.Block{
		Type:  "RSA PRIVATE KEY",
		Bytes: data,
	})

	return os.WriteFile(path, block, 0600)
}

func loadPrivateKey(path string) (key *rsa.PrivateKey, err error) {
	var data []byte
	if data, err = os.ReadFile(path); err != nil {
		return nil, err
	}

	block, _ := pem.Decode(data)
	if block == nil {
		return nil, fmt.Errorf("could not decode PEM data")
	}

	var keyt interface{}
	if keyt, err = x509.ParsePKCS8PrivateKey(block.Bytes); err != nil {
		return nil, err
	}

	return keyt.(*rsa.PrivateKey), nil
}
Output:

func Reject

func Reject(reject *api.Error, opts ...Option) (_ *api.SecureEnvelope, err error)

Reject returns a new rejection error to send to the counterparty

func Seal

func Seal(payload *api.Payload, opts ...Option) (_ *api.SecureEnvelope, reject *api.Error, err error)

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.

Example
package main

import (
	"crypto/rand"
	"crypto/rsa"
	"crypto/x509"
	"encoding/pem"
	"fmt"
	"log"
	"os"

	"github.com/trisacrypto/trisa/pkg/ivms101"

	api "github.com/trisacrypto/trisa/pkg/trisa/api/v1beta1"

	generic "github.com/trisacrypto/trisa/pkg/trisa/data/generic/v1beta1"
	"github.com/trisacrypto/trisa/pkg/trisa/envelope"
	"google.golang.org/protobuf/encoding/protojson"
	"google.golang.org/protobuf/proto"
	"google.golang.org/protobuf/types/known/anypb"
)

func main() {
	// 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)
}

const expectedEnvelopeId = "2b3b4c95-0a78-4f2a-a9fa-041970f97144"

var (
	loadpb = protojson.UnmarshalOptions{
		AllowPartial:   false,
		DiscardUnknown: false,
	}
	dumppb = protojson.MarshalOptions{
		Multiline:       true,
		Indent:          "  ",
		AllowPartial:    true,
		UseProtoNames:   true,
		UseEnumNumbers:  false,
		EmitUnpopulated: true,
	}
)

// Helper method to load a payload fixture, generating it if it hasn't been yet
func loadPayloadFixture(path string) (payload *api.Payload, err error) {
	payload = &api.Payload{}
	if err = loadFixture(path, payload, true); err != nil {
		return nil, err
	}
	return payload, nil
}

// Helper method to load a fixture from JSON
func loadFixture(path string, m proto.Message, check bool) (err error) {

	if check {
		if _, err = os.Stat(path); os.IsNotExist(err) {
			if err = generateFixtures(); err != nil {
				return err
			}
		}
	}

	var data []byte
	if data, err = os.ReadFile(path); err != nil {
		return err
	}

	if err = loadpb.Unmarshal(data, m); err != nil {
		return err
	}
	return nil
}

// Helper method to generate secure envelopes from the payload fixtures
func generateFixtures() (err error) {

	var (
		payload        *api.Payload
		pendingPayload *api.Payload
	)

	identity := &ivms101.IdentityPayload{}
	if err = loadFixture("testdata/payload/identity.json", identity, false); err != nil {
		return fmt.Errorf("could not unmarshal identity payload: %v", err)
	}

	pending := &generic.Pending{}
	if err = loadFixture("testdata/payload/pending.json", pending, false); err != nil {
		return fmt.Errorf("could not read pending payload: %v", err)
	}

	transaction := &generic.Transaction{}
	if err = loadFixture("testdata/payload/transaction.json", transaction, false); err != nil {
		return fmt.Errorf("could not read transaction payload: %v", err)
	}

	payload = &api.Payload{
		SentAt:     "2022-01-27T08:21:43Z",
		ReceivedAt: "2022-01-30T16:28:39Z",
	}
	if payload.Identity, err = anypb.New(identity); err != nil {
		return fmt.Errorf("could not create identity payload: %v", err)
	}
	if payload.Transaction, err = anypb.New(transaction); err != nil {
		return fmt.Errorf("could not create transaction payload: %v", err)
	}

	pendingPayload = &api.Payload{
		Identity: payload.Identity,
		SentAt:   payload.SentAt,
	}
	if pendingPayload.Transaction, err = anypb.New(pending); err != nil {
		return fmt.Errorf("could not create pending payload: %v", err)
	}

	if err = dumpFixture("testdata/payload.json", payload); err != nil {
		return fmt.Errorf("could not marshal payload: %v", err)
	}

	if err = dumpFixture("testdata/pending_payload.json", pendingPayload); err != nil {
		return fmt.Errorf("could not marshal pending payload: %v", err)
	}

	env := &api.SecureEnvelope{
		Id:        expectedEnvelopeId,
		Timestamp: "2022-01-27T08:21:43Z",
		Error: &api.Error{
			Code:    api.Error_COMPLIANCE_CHECK_FAIL,
			Message: "specified account has been frozen temporarily",
		},
	}

	if err = dumpFixture("testdata/error_envelope.json", env); err != nil {
		return fmt.Errorf("could not marshal error only envelope: %v", err)
	}

	var handler *envelope.Envelope
	if handler, err = envelope.New(payload); err != nil {
		return err
	}

	if handler, _, err = handler.Encrypt(); err != nil {
		return err
	}

	if err = dumpFixture("testdata/unsealed_envelope.json", handler.Proto()); err != nil {
		return fmt.Errorf("could not marshal unsealed envelope: %v", err)
	}

	key, err := rsa.GenerateKey(rand.Reader, 4096)
	if err != nil {
		return fmt.Errorf("could not generate RSA key fixture")
	}
	if err = dumpPrivateKey("testdata/sealing_key.pem", key); err != nil {
		return err
	}

	if env, _, err = envelope.Seal(pendingPayload, envelope.WithRSAPublicKey(&key.PublicKey)); err != nil {
		return err
	}
	if err = dumpFixture("testdata/sealed_envelope.json", env); err != nil {
		return fmt.Errorf("could not marshal sealed envelope: %v", err)
	}
	return nil
}

func dumpFixture(path string, m proto.Message) (err error) {
	var data []byte
	if data, err = dumppb.Marshal(m); err != nil {
		return err
	}
	return os.WriteFile(path, data, 0644)
}

func dumpPrivateKey(path string, key *rsa.PrivateKey) (err error) {
	var data []byte
	if data, err = x509.MarshalPKCS8PrivateKey(key); err != nil {
		return err
	}

	block := pem.EncodeToMemory(&pem.Block{
		Type:  "RSA PRIVATE KEY",
		Bytes: data,
	})

	return os.WriteFile(path, block, 0600)
}

func loadPrivateKey(path string) (key *rsa.PrivateKey, err error) {
	var data []byte
	if data, err = os.ReadFile(path); err != nil {
		return nil, err
	}

	block, _ := pem.Decode(data)
	if block == nil {
		return nil, fmt.Errorf("could not decode PEM data")
	}

	var keyt interface{}
	if keyt, err = x509.ParsePKCS8PrivateKey(block.Bytes); err != nil {
		return nil, err
	}

	return keyt.(*rsa.PrivateKey), nil
}
Output:

func Validate

func Validate(msg *api.SecureEnvelope) (err error)

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.

Types

type Envelope

type Envelope struct {
	// contains filtered or unexported fields
}

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 wrapped secure envelope. For example, a clear envelope can have its payload directly read, but if an envelope is unsealed, then it must be decrypted before the payload can be parsed into specific data structures. Similarly, an unsealed envelope can be sealed in preparation for sending to a recipient, or remain unsealed for secure long term data storage.

func New

func New(payload *api.Payload, opts ...Option) (env *Envelope, err error)

New creates a new Envelope from scratch with the associated payload and options. New should be used to initialize a secure envelope to send to a recipient, usually before the first transfer of an information exchange, Open is used thereafter.

func Wrap

func Wrap(msg *api.SecureEnvelope, opts ...Option) (env *Envelope, err error)

Wrap initializes an Envelope from an incoming secure envelope without modifying the original envelope. The Envelope can then be inspected and managed using cryptographic and accessor functions.

func (*Envelope) Crypto

func (e *Envelope) Crypto() crypto.Crypto

Crypto returns the cryptographic method used to encrypt/decrypt the payload.

func (*Envelope) Decrypt

func (e *Envelope) Decrypt(opts ...Option) (env *Envelope, reject *api.Error, err error)

The original envelope is not modified, the secure envelope is cloned.

func (*Envelope) Encrypt

func (e *Envelope) Encrypt(opts ...Option) (env *Envelope, reject *api.Error, err error)

Encrypt the envelope by marshaling the payload, encrypting, and digitally signing it. If the original envelope does not have a crypto method (either by the user supplying one via options or from an incoming secure envelope) then a new AESGCM crypto is created with a random encryption key and hmac secret. Encrypt returns a new unsealed envelope that maintains the original crypto and cipher mechanisms. Two types of errors may be returned: a rejection error is intended to be returned to the sender due to some protocol-specific issue, otherwise an error is returned if the user has made some error that requires rehandling of the envelope. The original envelope is not modified, the secure envelope is cloned.

func (*Envelope) Error

func (e *Envelope) Error() *api.Error

Error returns the TRISA rejection error on the envelope if it exists

func (*Envelope) ID

func (e *Envelope) ID() string

ID returns the envelope ID

func (*Envelope) Payload

func (e *Envelope) Payload() (_ *api.Payload, err error)

Payload returns the parsed trisa.Payload protocol buffer if available. If the envelope is not decrypted then an error is returned.

func (*Envelope) Proto

func (e *Envelope) Proto() *api.SecureEnvelope

Proto returns the trisa.SecureEnvelope protocol buffer.

func (*Envelope) Reject

func (e *Envelope) Reject(reject *api.Error, opts ...Option) (env *Envelope, err error)

Reject returns a new secure envelope that contains a TRISA rejection error but no payload. The original envelope is not modified, the secure envelope is cloned.

func (*Envelope) Seal

func (e *Envelope) Seal(opts ...Option) (env *Envelope, reject *api.Error, err error)

Seal the envelope using public key cryptography so that the envelope can only be decrypted by the recipient. This method encrypts the encryption key and hmac secret using the supplied public key, marking the secure envelope as sealed and updates the signature of the public key used to seal the secure envelope. Two types of errors may be returned from this method: a rejection error used to communicate to the sender that something went wrong and they should resend the envelope or an error that the user should handle in their own code base. The original envelope is not modified, the secure envelope is cloned.

func (*Envelope) Sealer

func (e *Envelope) Sealer() crypto.Cipher

Sealer returns the cryptographic cipher method used to seal/unseal the envelope.

func (*Envelope) State

func (e *Envelope) State() State

State returns the current state of the envelope.

func (*Envelope) Timestamp

func (e *Envelope) Timestamp() (ts time.Time, err error)

Timestamp returns the ordering timestamp of the secure envelope. If the timestamp is not on the envelope or it cannot be parsed, an error is returned.

func (*Envelope) Unseal

func (e *Envelope) Unseal(opts ...Option) (env *Envelope, reject *api.Error, err error)

Unseal the envelope using public key cryptography so that the envelope can be opened and decrypted. This method requires a sealing private key and will return an error if one is not available. If the envelope is not able to be opened because the secure envelope contains an unknown or improper state a rejection error is returned to communicate back to the sender that the envelope could not be unsealed. The original envelope is not modified, the secure envelope is cloned.

func (*Envelope) Update

func (e *Envelope) Update(payload *api.Payload, opts ...Option) (env *Envelope, err error)

Update the envelope with a new payload maintaining the original crypto method. This is useful to prepare a response to the user, for example updating the ReceivedAt timestamp in the payload then re-encrypting the secure envelope to send back to the originator. Most often, this method is also used with the WithSealingKey option so that the envelope workflow for sealing an envelope can be applied completely. The original envelope is not modified, the secure envelope is cloned.

func (*Envelope) ValidateMessage

func (e *Envelope) ValidateMessage() error

ValidateMessage returns an error if the secure envelope does not have the required fields to send.

func (*Envelope) ValidatePayload

func (e *Envelope) ValidatePayload() error

ValidatePayload returns an error if the payload is not ready to be encrypted. TODO: should we parse the types of the payload to ensure they're TRISA types?

type Option

type Option func(e *Envelope) error

func FromEnvelope

func FromEnvelope(env *Envelope) Option

func WithAESGCM

func WithAESGCM(encryptionKey []byte, hmacSecret []byte) Option

func WithCrypto

func WithCrypto(crypto crypto.Crypto) Option

func WithEnvelopeID

func WithEnvelopeID(id string) Option

func WithRSAPrivateKey

func WithRSAPrivateKey(key *rsa.PrivateKey) Option

func WithRSAPublicKey

func WithRSAPublicKey(key *rsa.PublicKey) Option

func WithSeal

func WithSeal(seal crypto.Cipher) Option

func WithSealingKey

func WithSealingKey(key interface{}) Option

func WithTimestamp

func WithTimestamp(ts time.Time) Option

func WithUnsealingKey

func WithUnsealingKey(key interface{}) Option

type State

type State uint16
const (
	Unknown       State = iota
	Clear               // The envelope has been decrypted and the payload is available on it
	Unsealed            // The envelope is unsealed and can be decrypted without any other information
	Sealed              // The envelope is sealed and must be unsealed with a private key or it is ready to send
	Error               // The envelope does not contain a payload but does contain an error field
	ClearError          // The envelope contains both a decrypted payload and an error
	UnsealedError       // The envelope contains both an error and a payload and is unsealed
	SealedError         // The envelope contains both an error and a payload and is sealed
	Corrupted           // The envelope is in an invalid state and cannot be moved into a correct state
)

func Status added in v0.3.5

func Status(msg *api.SecureEnvelope) State

Status returns the state the secure envelope is currently in.

func (State) String

func (s State) String() string

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL