From 4ecf00be0c4e24986dfb45b0dca9b4a0aa4ec8fd Mon Sep 17 00:00:00 2001 From: Nicholas Molnar <65710+neekolas@users.noreply.github.com> Date: Thu, 17 Oct 2024 17:22:27 -0700 Subject: [PATCH] Recover signer from payer envelopes (#234) ## tl;dr - Adds method to recover the signer from payer envelopes (https://github.com/xmtp/xmtpd/issues/232) - Refactors domain separation for jwt's and payer envelopes ### AI Assisted Summary Introduced domain separation for payer signatures, and added functionality to recover signer addresses from payer envelopes. ### What changed? - Created a new `constants` package with domain separation labels for JWT and payer signatures. - Updated the JWT signing method to use the new constant for domain separation. - Implemented a `RecoverSigner` method for `PayerEnvelope` to extract the signer's address. - Added utility functions for hashing and signing payer envelopes. - Expanded test coverage for envelope signer recovery. ### Why make this change? This change enhances security and consistency by introducing domain separation for signatures. It also adds the ability to recover signer addresses from payer envelopes, which is crucial for verifying the authenticity of messages and implementing payer-based features in the system. --- pkg/authn/signingMethod.go | 18 ++++++-------- pkg/constants/constants.go | 6 +++++ pkg/envelopes/envelopes_test.go | 43 +++++++++++++++++++++++++++++++++ pkg/envelopes/payer.go | 20 +++++++++++++++ pkg/utils/hash.go | 20 +++++++++++++++ pkg/utils/signature.go | 20 +++++++++++++++ 6 files changed, 116 insertions(+), 11 deletions(-) create mode 100644 pkg/constants/constants.go create mode 100644 pkg/utils/hash.go create mode 100644 pkg/utils/signature.go diff --git a/pkg/authn/signingMethod.go b/pkg/authn/signingMethod.go index 044f124d..2742c5eb 100644 --- a/pkg/authn/signingMethod.go +++ b/pkg/authn/signingMethod.go @@ -7,14 +7,14 @@ import ( ethcrypto "github.com/ethereum/go-ethereum/crypto" "github.com/golang-jwt/jwt/v5" + "github.com/xmtp/xmtpd/pkg/utils" ) const ( - ALGORITHM = "ES256K" - SIG_LENGTH = 65 - R_LENGTH = 32 - S_LENGTH = 32 - DOMAIN_SEPARATION_LABEL = "jwt" + ALGORITHM = "ES256K" + SIG_LENGTH = 65 + R_LENGTH = 32 + S_LENGTH = 32 ) var ( @@ -36,7 +36,7 @@ func (sm *SigningMethodSecp256k1) Verify(signingString string, sig []byte, key i return ErrWrongKeyFormat } - hashedString := hashStringWithDomainSeparation(signingString) + hashedString := utils.HashJWTSignatureInput([]byte(signingString)) if len(sig) != SIG_LENGTH { return ErrBadSignature @@ -58,7 +58,7 @@ func (sm *SigningMethodSecp256k1) Sign(signingString string, key interface{}) ([ return nil, ErrWrongKeyFormat } - hashedString := hashStringWithDomainSeparation(signingString) + hashedString := utils.HashJWTSignatureInput([]byte(signingString)) sig, err := ethcrypto.Sign(hashedString, priv) if err != nil { @@ -76,10 +76,6 @@ func (sm *SigningMethodSecp256k1) Alg() string { return ALGORITHM } -func hashStringWithDomainSeparation(signingString string) []byte { - return ethcrypto.Keccak256([]byte(DOMAIN_SEPARATION_LABEL + signingString)) -} - func init() { method := &SigningMethodSecp256k1{} jwt.RegisterSigningMethod(ALGORITHM, func() jwt.SigningMethod { diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go new file mode 100644 index 00000000..a1e7370a --- /dev/null +++ b/pkg/constants/constants.go @@ -0,0 +1,6 @@ +package constants + +const ( + JWT_DOMAIN_SEPARATION_LABEL = "jwt|" + PAYER_DOMAIN_SEPARATION_LABEL = "payer|" +) diff --git a/pkg/envelopes/envelopes_test.go b/pkg/envelopes/envelopes_test.go index 41b68000..51f059b5 100644 --- a/pkg/envelopes/envelopes_test.go +++ b/pkg/envelopes/envelopes_test.go @@ -3,10 +3,14 @@ package envelopes import ( "testing" + ethcrypto "github.com/ethereum/go-ethereum/crypto" "github.com/stretchr/testify/require" + "github.com/xmtp/xmtpd/pkg/proto/identity/associations" "github.com/xmtp/xmtpd/pkg/proto/xmtpv4/message_api" + "github.com/xmtp/xmtpd/pkg/testutils" envelopeTestUtils "github.com/xmtp/xmtpd/pkg/testutils/envelopes" "github.com/xmtp/xmtpd/pkg/topic" + "github.com/xmtp/xmtpd/pkg/utils" "google.golang.org/protobuf/proto" ) @@ -132,3 +136,42 @@ func TestPayloadType(t *testing.T) { require.False(t, clientEnvelope.TopicMatchesPayload()) } + +func TestRecoverSigner(t *testing.T) { + payerPrivateKey := testutils.RandomPrivateKey(t) + rawPayerEnv := envelopeTestUtils.CreatePayerEnvelope(t) + + payerSignature, err := utils.SignPayerEnvelope( + rawPayerEnv.UnsignedClientEnvelope, + payerPrivateKey, + ) + require.NoError(t, err) + rawPayerEnv.PayerSignature = &associations.RecoverableEcdsaSignature{ + Bytes: payerSignature, + } + + payerEnv, err := NewPayerEnvelope(rawPayerEnv) + require.NoError(t, err) + + signer, err := payerEnv.RecoverSigner() + require.NoError(t, err) + require.Equal(t, ethcrypto.PubkeyToAddress(payerPrivateKey.PublicKey).Hex(), signer.Hex()) + + // Now test with an incorrect signature + wrongPayerSignature, err := utils.SignPayerEnvelope( + testutils.RandomBytes(128), + payerPrivateKey, + ) + require.NoError(t, err) + rawPayerEnv.PayerSignature = &associations.RecoverableEcdsaSignature{ + Bytes: wrongPayerSignature, + } + payerEnv, err = NewPayerEnvelope(rawPayerEnv) + require.NoError(t, err) + + // This will recover an incorrect signer address because the inputs to the signature + // do not match the unsigned client envelope + newSigner, err := payerEnv.RecoverSigner() + require.NoError(t, err) + require.NotEqual(t, signer.Hex(), newSigner.Hex()) +} diff --git a/pkg/envelopes/payer.go b/pkg/envelopes/payer.go index e56d6f7e..81b10bdd 100644 --- a/pkg/envelopes/payer.go +++ b/pkg/envelopes/payer.go @@ -3,7 +3,10 @@ package envelopes import ( "errors" + "github.com/ethereum/go-ethereum/common" + ethcrypto "github.com/ethereum/go-ethereum/crypto" "github.com/xmtp/xmtpd/pkg/proto/xmtpv4/message_api" + "github.com/xmtp/xmtpd/pkg/utils" "google.golang.org/protobuf/proto" ) @@ -35,3 +38,20 @@ func (p *PayerEnvelope) Bytes() ([]byte, error) { } return bytes, nil } + +func (p *PayerEnvelope) RecoverSigner() (*common.Address, error) { + payerSignature := p.proto.PayerSignature + if payerSignature == nil { + return nil, errors.New("payer signature is missing") + } + + hash := utils.HashPayerSignatureInput(p.proto.UnsignedClientEnvelope) + signer, err := ethcrypto.SigToPub(hash, payerSignature.Bytes) + if err != nil { + return nil, err + } + + address := ethcrypto.PubkeyToAddress(*signer) + + return &address, nil +} diff --git a/pkg/utils/hash.go b/pkg/utils/hash.go new file mode 100644 index 00000000..ddac31f9 --- /dev/null +++ b/pkg/utils/hash.go @@ -0,0 +1,20 @@ +package utils + +import ( + ethcrypto "github.com/ethereum/go-ethereum/crypto" + "github.com/xmtp/xmtpd/pkg/constants" +) + +func HashPayerSignatureInput(unsignedClientEnvelope []byte) []byte { + return ethcrypto.Keccak256( + []byte(constants.PAYER_DOMAIN_SEPARATION_LABEL), + unsignedClientEnvelope, + ) +} + +func HashJWTSignatureInput(textToSign []byte) []byte { + return ethcrypto.Keccak256( + []byte(constants.JWT_DOMAIN_SEPARATION_LABEL), + textToSign, + ) +} diff --git a/pkg/utils/signature.go b/pkg/utils/signature.go new file mode 100644 index 00000000..59f8aada --- /dev/null +++ b/pkg/utils/signature.go @@ -0,0 +1,20 @@ +package utils + +import ( + "crypto/ecdsa" + + ethcrypto "github.com/ethereum/go-ethereum/crypto" +) + +func SignPayerEnvelope( + unsignedClientEnvelope []byte, + payerPrivateKey *ecdsa.PrivateKey, +) ([]byte, error) { + hash := HashPayerSignatureInput(unsignedClientEnvelope) + signature, err := ethcrypto.Sign(hash, payerPrivateKey) + if err != nil { + return nil, err + } + + return signature, nil +}