Skip to content

Commit

Permalink
Expose HMAC validation to user (trisacrypto#162)
Browse files Browse the repository at this point in the history
  • Loading branch information
bbengfort authored May 25, 2024
1 parent 9ba45a2 commit bd2ebe9
Show file tree
Hide file tree
Showing 6 changed files with 171 additions and 15 deletions.
29 changes: 18 additions & 11 deletions pkg/trisa/crypto/aesgcm/aesgcm.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
}
}

Expand Down Expand Up @@ -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]
Expand All @@ -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
}
Expand All @@ -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)
Expand All @@ -120,15 +127,15 @@ 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
}

// 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.
Expand Down
10 changes: 10 additions & 0 deletions pkg/trisa/crypto/errors.go
Original file line number Diff line number Diff line change
@@ -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")
)
8 changes: 5 additions & 3 deletions pkg/trisa/crypto/rsaoeap/rsaoeap.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand All @@ -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
Expand Down
32 changes: 32 additions & 0 deletions pkg/trisa/envelope/envelope.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ For more details about how to work with envelopes, see the example code.
package envelope

import (
"errors"
"fmt"
"time"

Expand Down Expand Up @@ -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
}
104 changes: 104 additions & 0 deletions pkg/trisa/envelope/envelope_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 (
Expand Down
3 changes: 2 additions & 1 deletion pkg/trisa/envelope/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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")
)

0 comments on commit bd2ebe9

Please sign in to comment.