diff --git a/pkg/trisa/crypto/aesgcm/aesgcm.go b/pkg/trisa/crypto/aesgcm/aesgcm.go index d0c56721..46561000 100644 --- a/pkg/trisa/crypto/aesgcm/aesgcm.go +++ b/pkg/trisa/crypto/aesgcm/aesgcm.go @@ -6,12 +6,19 @@ import ( "crypto/cipher" "crypto/hmac" "crypto/sha256" - "errors" "fmt" "github.com/trisacrypto/trisa/pkg/trisa/crypto" ) +const ( + Algorithm = "AES-GCM" + SignatureAlgorithm = "HMAC-SHA256" + Algorithm256 = "AES256-GCM" + Algorithm192 = "AES192-GCM" + Algorithm128 = "AES128-GCM" +) + // AESGCM implements the crypto.Crypto interface using the AES-GCM algorithm for // symmetric-key encryption. This algorithm is widely adopted for it's performance and // throughput rates for state-of-the-art high-speed communication on inexpensive @@ -29,7 +36,7 @@ type AESGCM struct { func New(encryptionKey, hmacSecret []byte) (_ *AESGCM, err error) { if len(encryptionKey) == 0 { if encryptionKey, err = crypto.Random(32); err != nil { - return nil, fmt.Errorf("could not generate encryption key: %s", err) + return nil, fmt.Errorf("could not generate encryption key: %w", err) } } @@ -66,7 +73,7 @@ func (c *AESGCM) Encrypt(plaintext []byte) (ciphertext []byte, err error) { // Decrypt a message using the struct key, extracting the nonce from the end. func (c *AESGCM) Decrypt(ciphertext []byte) (plaintext []byte, err error) { if len(ciphertext) < 12 { - return nil, errors.New("empty cipher text") + return nil, crypto.ErrMissingCiphertext } data := ciphertext[:len(ciphertext)-12] @@ -84,7 +91,7 @@ func (c *AESGCM) Decrypt(ciphertext []byte) (plaintext []byte, err error) { plaintext, err = aesgcm.Open(nil, nonce, data, nil) if err != nil { - return nil, fmt.Errorf("could not decrypt ciphertext: %s", err) + return nil, fmt.Errorf("could not decrypt ciphertext: %w", err) } return plaintext, nil } @@ -93,20 +100,20 @@ func (c *AESGCM) Decrypt(ciphertext []byte) (plaintext []byte, err error) { func (c *AESGCM) EncryptionAlgorithm() string { switch len(c.key) { case 32: - return "AES256-GCM" + return Algorithm256 case 24: - return "AES192-GCM" + return Algorithm192 case 16: - return "AES128-GCM" + return Algorithm128 default: - return "AES-GCM" + return Algorithm } } // Sign the specified data (ususally the ciphertext) using the struct secret. func (c *AESGCM) Sign(data []byte) (signature []byte, err error) { if len(data) == 0 { - return nil, errors.New("cannot sign empty data") + return nil, crypto.ErrCannotSignEmpty } hm := hmac.New(sha256.New, c.secret) @@ -120,7 +127,7 @@ func (c *AESGCM) Verify(data, signature []byte) (err error) { hm.Write(data) if !bytes.Equal(signature, hm.Sum(nil)) { - return errors.New("hmac signature mismatch") + return crypto.ErrHMACSignatureMismatch } return nil @@ -128,7 +135,7 @@ func (c *AESGCM) Verify(data, signature []byte) (err error) { // SignatureAlgorithm returns the name of the hmac_algorithm for adding to the Transaction. func (c *AESGCM) SignatureAlgorithm() string { - return "HMAC-SHA256" + return SignatureAlgorithm } // EncryptionKey is a read-only getter. diff --git a/pkg/trisa/crypto/errors.go b/pkg/trisa/crypto/errors.go new file mode 100644 index 00000000..ecacd444 --- /dev/null +++ b/pkg/trisa/crypto/errors.go @@ -0,0 +1,10 @@ +package crypto + +import "errors" + +var ( + ErrCannotSignEmpty = errors.New("cannot sign empty data") + ErrMissingCiphertext = errors.New("empty cipher text") + ErrHMACSignatureMismatch = errors.New("hmac signature mismatch") + ErrPrivateKeyRequired = errors.New("private key required for decryption") +) diff --git a/pkg/trisa/crypto/rsaoeap/rsaoeap.go b/pkg/trisa/crypto/rsaoeap/rsaoeap.go index 276c1dfc..8aac3199 100644 --- a/pkg/trisa/crypto/rsaoeap/rsaoeap.go +++ b/pkg/trisa/crypto/rsaoeap/rsaoeap.go @@ -4,12 +4,14 @@ import ( "crypto/rand" "crypto/rsa" "crypto/sha512" - "errors" "fmt" + "github.com/trisacrypto/trisa/pkg/trisa/crypto" "github.com/trisacrypto/trisa/pkg/trisa/keys/signature" ) +const Algorithm = "RSA-OAEP-SHA512" + // RSA implements the crypto.Cipher interface using RSA public/private key algorithm // as specified in PKCS #1. Messages are encrypted with the public key and can only be // decrypted using the private key. RSA objects must have a public key but the private @@ -47,7 +49,7 @@ func (c *RSA) Encrypt(plaintext []byte) (ciphertext []byte, err error) { // Decrypt the message using the private key. func (c *RSA) Decrypt(ciphertext []byte) (plaintext []byte, err error) { if c.priv == nil { - return nil, errors.New("private key required for decryption") + return nil, crypto.ErrPrivateKeyRequired } hash := sha512.New() @@ -60,7 +62,7 @@ func (c *RSA) Decrypt(ciphertext []byte) (plaintext []byte, err error) { // EncryptionAlgorithm returns the name of the algorithm for adding to the Transaction. func (c *RSA) EncryptionAlgorithm() string { - return "RSA-OAEP-SHA512" + return Algorithm } // PublicKeySignature implements KeyIdentifier by computing a base64 encoded SHA-256 diff --git a/pkg/trisa/envelope/envelope.go b/pkg/trisa/envelope/envelope.go index 95803902..289e6eaf 100644 --- a/pkg/trisa/envelope/envelope.go +++ b/pkg/trisa/envelope/envelope.go @@ -44,6 +44,7 @@ For more details about how to work with envelopes, see the example code. package envelope import ( + "errors" "fmt" "time" @@ -892,3 +893,34 @@ func (e *Envelope) ValidateError() error { return nil } + +// ValidateHMAC checks if the HMAC signature is valid with respect to the HMAC secret +// and encrypted payload. This is generally used for non-repudiation purposes. +func (e *Envelope) ValidateHMAC() (valid bool, err error) { + // The payload is required to validate the HMAC signature + if len(e.msg.Payload) == 0 { + return false, ErrNoPayload + } + + // An HMAC signature is required for validating the HMAC signature! + if len(e.msg.Hmac) == 0 { + return false, ErrNoHMACInfo + } + + // The cryptography mechanism must have been created, either from encryption or + // decryption. We do not check the state in case crypto has been added by the user. + if e.crypto == nil { + return false, ErrCannotVerify + } + + // Validate the HMAC signature + if err = e.crypto.Verify(e.msg.Payload, e.msg.Hmac); err != nil { + if errors.Is(err, crypto.ErrHMACSignatureMismatch) { + return false, nil + } + return false, err + } + + // HMAC signature is valid + return true, nil +} diff --git a/pkg/trisa/envelope/envelope_test.go b/pkg/trisa/envelope/envelope_test.go index bdc0353a..c941df03 100644 --- a/pkg/trisa/envelope/envelope_test.go +++ b/pkg/trisa/envelope/envelope_test.go @@ -15,6 +15,7 @@ import ( "github.com/stretchr/testify/require" "github.com/trisacrypto/trisa/pkg/ivms101" api "github.com/trisacrypto/trisa/pkg/trisa/api/v1beta1" + "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" "google.golang.org/protobuf/encoding/protojson" @@ -498,7 +499,110 @@ func TestWrapError(t *testing.T) { require.ErrorIs(t, err, tc.expected, "expected validation error on test case %d", i) } }) +} + +func TestValidateHMAC(t *testing.T) { + crypto, err := aesgcm.New(nil, nil) + require.NoError(t, err, "could not create cryptographic handler") + + payload, err := loadPayloadFixture("testdata/payload.json") + require.NoError(t, err, "could not load payload") + + t.Run("Valid", func(t *testing.T) { + env, err := envelope.New(payload, envelope.WithCrypto(crypto)) + require.NoError(t, err, "could not create envelope") + + env, _, err = env.Encrypt() + require.NoError(t, err, "could not encrypt envelope") + valid, err := env.ValidateHMAC() + require.NoError(t, err, "could not validate hmac signature") + require.True(t, valid, "expected hmac signature to be valid") + }) + + t.Run("PayloadRequired", func(t *testing.T) { + env, err := envelope.New(nil, envelope.WithCrypto(crypto)) + require.NoError(t, err, "could not create envelope") + + valid, err := env.ValidateHMAC() + require.False(t, valid) + require.ErrorIs(t, err, envelope.ErrNoPayload) + }) + + t.Run("HMACInfoRequired", func(t *testing.T) { + // Encrypt the payload + data, err := proto.Marshal(payload) + require.NoError(t, err, "could not marshal payload data") + + ciphertext, err := crypto.Encrypt(data) + require.NoError(t, err, "could not encrypt payload data") + + // Create a secure envelope without the HMAC info + se := &api.SecureEnvelope{ + Id: uuid.NewString(), + Payload: ciphertext, + Timestamp: time.Now().UTC().Format(time.RFC3339), + } + env, err := envelope.Wrap(se, envelope.WithCrypto(crypto)) + require.NoError(t, err, "could not create envelope") + + valid, err := env.ValidateHMAC() + require.False(t, valid) + require.ErrorIs(t, err, envelope.ErrNoHMACInfo) + }) + + t.Run("CryptoRequired", func(t *testing.T) { + // Encrypt the payload + data, err := proto.Marshal(payload) + require.NoError(t, err, "could not marshal payload data") + + ciphertext, err := crypto.Encrypt(data) + require.NoError(t, err, "could not encrypt payload data") + + // Create a secure envelope without the HMAC info + se := &api.SecureEnvelope{ + Id: uuid.NewString(), + Payload: ciphertext, + EncryptionKey: crypto.EncryptionKey(), + EncryptionAlgorithm: crypto.EncryptionAlgorithm(), + HmacSecret: crypto.HMACSecret(), + HmacAlgorithm: crypto.SignatureAlgorithm(), + Timestamp: time.Now().UTC().Format(time.RFC3339), + } + + se.Hmac, err = crypto.Sign([]byte("this is a standin for the data that would be in the real payload")) + require.NoError(t, err, "could not sign the message") + + env, err := envelope.Wrap(se) + require.NoError(t, err, "could not create envelope") + + valid, err := env.ValidateHMAC() + require.False(t, valid) + require.ErrorIs(t, err, envelope.ErrCannotVerify) + }) + + t.Run("InvalidSignature", func(t *testing.T) { + // Create a secure envelope with the HMAC info + se := &api.SecureEnvelope{ + Id: uuid.NewString(), + Payload: []byte("this is definitely not going to be valid ciphertext"), + EncryptionKey: crypto.EncryptionKey(), + EncryptionAlgorithm: crypto.EncryptionAlgorithm(), + HmacSecret: crypto.HMACSecret(), + HmacAlgorithm: crypto.SignatureAlgorithm(), + Timestamp: time.Now().UTC().Format(time.RFC3339), + } + + se.Hmac, err = crypto.Sign([]byte("this is a standin for the data that would be in the real payload")) + require.NoError(t, err, "could not sign the message") + + env, err := envelope.Wrap(se, envelope.WithCrypto(crypto)) + require.NoError(t, err, "could not create envelope") + + valid, err := env.ValidateHMAC() + require.False(t, valid) + require.NoError(t, err) + }) } const ( diff --git a/pkg/trisa/envelope/errors.go b/pkg/trisa/envelope/errors.go index 64ad4fbc..96a026a3 100644 --- a/pkg/trisa/envelope/errors.go +++ b/pkg/trisa/envelope/errors.go @@ -11,7 +11,7 @@ var ( 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") + ErrNoPayload = errors.New("invalid payload: payload has not been decrypted or is missing") 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") @@ -24,4 +24,5 @@ var ( 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") + ErrCannotVerify = errors.New("cannot verify hmac: no cryptographic handler available") )