Skip to content

Commit

Permalink
Merge pull request #1 from Kachit/signature
Browse files Browse the repository at this point in the history
Signature
  • Loading branch information
Kachit committed Dec 6, 2022
2 parents c076fe8 + 41bb2ea commit 63985b8
Show file tree
Hide file tree
Showing 7 changed files with 219 additions and 5 deletions.
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,3 +212,35 @@ fmt.Println(result.Code)
fmt.Println(result.Message)
fmt.Println((*result.Data).ID)
```

### Verify webhook signature
```go
requestPayload := `
{
"id": 226,
"request_amount": 10,
"request_currency": "USD",
"account_amount": 737.9934,
"account_currency": "UGX",
"transaction_fee": 21.4018,
"total_credit": 716.5916,
"customer_charged": false,
"provider_id": "mtn_ug",
"merchant_reference": "76859aae-f148-48c5-9901-2e474cf19b71",
"internal_reference": "DUSUPAY405GZM1G5JXGA71IK",
"transaction_status": "COMPLETED",
"transaction_type": "collection",
"message": "Transaction Completed Successfully"
}
`
requestUri := "https://www.sample-url.com/callback"
signature := "value from 'dusupay-signature' http header"

var webhook dusupay.CollectionWebhook
_ = json.Unmarshal(requestPayload, &webhook)

rawBytes, _ := ioutil.ReadFile("path/to/dusupay-public-key.pem")

validator, _ := dusupay.NewSignatureValidator(rawBytes)
err := validator.ValidateSignature(webhook, requestUri, signature)
```
64 changes: 64 additions & 0 deletions signature.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package dusupay

import (
"bytes"
"crypto"
"crypto/rsa"
"crypto/sha512"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"errors"
"fmt"
)

//IncomingWebhookInterface interface
type IncomingWebhookInterface interface {
BuildPayloadString(url string) string
}

//NewSignatureValidator method
func NewSignatureValidator(publicKeyBytes []byte) (*SignatureValidator, error) {
block, _ := pem.Decode(publicKeyBytes)
if block == nil {
return nil, errors.New("wrong public key data")
}
publicKey, err := parsePublicKey(block.Bytes)
if err != nil {
return nil, err
}
return &SignatureValidator{publicKey}, nil
}

//SignatureValidator struct
type SignatureValidator struct {
publicKey *rsa.PublicKey
}

//ValidateSignature method (see https://docs.dusupay.com/webhooks-and-redirects/webhooks/signature-verification)
func (sv *SignatureValidator) ValidateSignature(webhook IncomingWebhookInterface, webhookUrl string, signature string) error {
messageBytes := bytes.NewBufferString(webhook.BuildPayloadString(webhookUrl))
hash := sha512.New()
hash.Write(messageBytes.Bytes())
digest := hash.Sum(nil)

data, err := base64.StdEncoding.DecodeString(signature)
if err != nil {
return err
}
return rsa.VerifyPKCS1v15(sv.publicKey, crypto.SHA512, digest, data)
}

//parsePublicKey method
func parsePublicKey(rawBytes []byte) (*rsa.PublicKey, error) {
key, err := x509.ParsePKIXPublicKey(rawBytes)
if err != nil {
return nil, fmt.Errorf("parsePublicKey wrong parse PKIX: %v", err)
}
switch pk := key.(type) {
case *rsa.PublicKey:
return pk, nil
default:
return nil, errors.New("parsePublicKey: PublicKey must be of type rsa.PublicKey")
}
}
72 changes: 72 additions & 0 deletions signature_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package dusupay

import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
"io/ioutil"
"testing"
)

type SignatureTestSuite struct {
suite.Suite
}

func (suite *SignatureTestSuite) TestValidateSignatureCollectionWebhookSuccess() {
rawBytes, _ := ioutil.ReadFile("stubs/rsa/public-key.pem")
webhook := &CollectionWebhook{ID: 226, InternalReference: "DUSUPAY405GZM1G5JXGA71IK", TransactionStatus: "COMPLETED"}
validator, _ := NewSignatureValidator(rawBytes)
err := validator.ValidateSignature(webhook, "https://www.sample-url.com/callback", stubSignature)
assert.NoError(suite.T(), err)
}

func (suite *SignatureTestSuite) TestValidateSignatureCollectionWebhookWrongPayload() {
rawBytes, _ := ioutil.ReadFile("stubs/rsa/public-key.pem")
webhook := &CollectionWebhook{ID: 225, InternalReference: "DUSUPAY405GZM1G5JXGA71IK", TransactionStatus: "COMPLETED"}
validator, _ := NewSignatureValidator(rawBytes)
err := validator.ValidateSignature(webhook, "https://www.sample-url.com/callback", stubSignature)
assert.Error(suite.T(), err)
assert.Equal(suite.T(), "crypto/rsa: verification error", err.Error())
}

func (suite *SignatureTestSuite) TestValidateSignatureCollectionWebhookNonBase64Signature() {
rawBytes, _ := ioutil.ReadFile("stubs/rsa/public-key.pem")
webhook := &CollectionWebhook{ID: 225, InternalReference: "DUSUPAY405GZM1G5JXGA71IK", TransactionStatus: "COMPLETED"}
validator, _ := NewSignatureValidator(rawBytes)
err := validator.ValidateSignature(webhook, "https://www.sample-url.com/callback", "qwerty")
assert.Error(suite.T(), err)
assert.Equal(suite.T(), "illegal base64 data at input byte 4", err.Error())
}

func (suite *SignatureTestSuite) TestValidateSignaturePayoutWebhookSuccess() {
rawBytes, _ := ioutil.ReadFile("stubs/rsa/public-key.pem")
webhook := &PayoutWebhook{ID: 226, InternalReference: "DUSUPAY405GZM1G5JXGA71IK", TransactionStatus: "COMPLETED"}
validator, _ := NewSignatureValidator(rawBytes)
err := validator.ValidateSignature(webhook, "https://www.sample-url.com/callback", stubSignature)
assert.NoError(suite.T(), err)
}

func (suite *SignatureTestSuite) TestValidateSignatureRefundWebhookSuccess() {
rawBytes, _ := ioutil.ReadFile("stubs/rsa/public-key.pem")
webhook := &RefundWebhook{ID: 226, InternalReference: "DUSUPAY405GZM1G5JXGA71IK", TransactionStatus: "COMPLETED"}
validator, _ := NewSignatureValidator(rawBytes)
err := validator.ValidateSignature(webhook, "https://www.sample-url.com/callback", stubSignature)
assert.NoError(suite.T(), err)
}

func (suite *SignatureTestSuite) TestParsePublicKeyError() {
pk, err := parsePublicKey([]byte(`foo`))
assert.Error(suite.T(), err)
assert.Contains(suite.T(), err.Error(), "parsePublicKey wrong parse PKIX: asn1: structure error: tags don't match")
assert.Nil(suite.T(), pk)
}

func (suite *SignatureTestSuite) TestNewSignatureValidatorError() {
validator, err := NewSignatureValidator([]byte(`foo`))
assert.Error(suite.T(), err)
assert.Contains(suite.T(), err.Error(), "wrong public key data")
assert.Nil(suite.T(), validator)
}

func TestSignatureTestSuite(t *testing.T) {
suite.Run(t, new(SignatureTestSuite))
}
9 changes: 9 additions & 0 deletions stubs/rsa/public-key.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmiVRA6VUyxaLCj1BC2Ij
ImHSyPxmS7Zvoyima3WJRcm1AtMdibLdWB4NJJFN7CyGjjZ18R7lB1CJjxWNbFIE
pwM/NahyBDYXfYwyzkkiqLIwndSLIBzpLTBUJj0EArLCVHDLAIooYP3DWkSz6yj2
S3WT4NgYAptJP0yFv9rsnfzIMFXGiPUQeVEuoxiUXX/usTg1wWXLVZtL/BLJS98D
Sr7X65gwfEqgCaQOCJWRoUlsjfnNi3uEqSmQisRqrFk5eiOsfo4WaBohhOStu4nU
GKy/G/2JxOTJVaXvyRv8Ejj5mxxyMp0XcjdpSOa6OajxVgrOIRVGnJxwSWLQRpOu
FwIDAQAB
-----END PUBLIC KEY-----
6 changes: 6 additions & 0 deletions stubs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ import (
"strings"
)

const stubSignature = `gYC3u1wUtk6UFpOVvCx+AyCnE3LXkS9Sg74fiRUQxRDDlllPu5vuRUrEbEqq/TEO90fYr76KGAWC6YSo
J9joYwk8RVftDQ1pNhROdfRkXL/yaQbrAvuT2gM2sO+HJhShCBLWbBcfXPOcjGcCedPCHSNFc5bq/Mk/
DszqlFEoH0dUN8hmqXQr673zyFivaKT76CpJTcmn5nvJi8r6IGoOJXb5uN8CdMTXbT6J08OmsbILPlfX
qe8PS/IlvXz11oy5xUaLXt+whhZL8rBrwQUsi9aNVf8Gd5m93D2ls1z03zDSOjSlb26Rvnvk97+XSM13
KuGbYjc3eJ6CUlQuIbTC1A==`

func BuildStubConfig() *Config {
return &Config{
Uri: SandboxAPIUrl,
Expand Down
17 changes: 12 additions & 5 deletions webhooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,6 @@ import (
"net/http"
)

//WebhookInterface interface
type WebhookInterface interface {
GetPayloadString() string
}

//CollectionWebhook struct
type CollectionWebhook struct {
ID int64 `json:"id"`
Expand All @@ -33,6 +28,10 @@ type CollectionWebhook struct {
InstitutionName string `json:"institution_name"`
}

func (cw *CollectionWebhook) BuildPayloadString(url string) string {
return fmt.Sprintf("%d:%s:%s:%s", cw.ID, cw.InternalReference, cw.TransactionStatus, url)
}

//PayoutWebhook struct
type PayoutWebhook struct {
ID int64 `json:"id"`
Expand All @@ -53,6 +52,10 @@ type PayoutWebhook struct {
InstitutionName string `json:"institution_name"`
}

func (pw *PayoutWebhook) BuildPayloadString(url string) string {
return fmt.Sprintf("%d:%s:%s:%s", pw.ID, pw.InternalReference, pw.TransactionStatus, url)
}

//RefundWebhook struct
type RefundWebhook struct {
ID int64 `json:"id"`
Expand All @@ -69,6 +72,10 @@ type RefundWebhook struct {
Message string `json:"message"`
}

func (rw *RefundWebhook) BuildPayloadString(url string) string {
return fmt.Sprintf("%d:%s:%s:%s", rw.ID, rw.InternalReference, rw.TransactionStatus, url)
}

//WebhookResponse struct
type WebhookResponse struct {
ResponseBody
Expand Down
24 changes: 24 additions & 0 deletions webhooks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ func (suite *WebhooksTestSuite) TestCollectionWebhookUnmarshalSuccess() {
assert.Equal(suite.T(), "MTN Mobile Money - Optional", webhook.InstitutionName)
}

func (suite *WebhooksTestSuite) TestCollectionWebhookBuildPayloadString() {
var webhook CollectionWebhook
body, _ := LoadStubResponseData("stubs/webhooks/request/collection-success.json")
_ = json.Unmarshal(body, &webhook)
result := webhook.BuildPayloadString("https://www.sample-url.com/callback")
assert.Equal(suite.T(), "226:DUSUPAY405GZM1G5JXGA71IK:COMPLETED:https://www.sample-url.com/callback", result)
}

func (suite *WebhooksTestSuite) TestPayoutWebhookUnmarshalSuccess() {
var webhook PayoutWebhook
body, _ := LoadStubResponseData("stubs/webhooks/request/payout-success.json")
Expand All @@ -62,6 +70,14 @@ func (suite *WebhooksTestSuite) TestPayoutWebhookUnmarshalSuccess() {
assert.Equal(suite.T(), "MTN Mobile Money - Optional", webhook.InstitutionName)
}

func (suite *WebhooksTestSuite) TestPayoutWebhookBuildPayloadString() {
var webhook PayoutWebhook
body, _ := LoadStubResponseData("stubs/webhooks/request/payout-success.json")
_ = json.Unmarshal(body, &webhook)
result := webhook.BuildPayloadString("https://www.sample-url.com/callback")
assert.Equal(suite.T(), "226:DUSUPAY405GZM1G5JXGA71IK:COMPLETED:https://www.sample-url.com/callback", result)
}

func (suite *WebhooksTestSuite) TestRefundWebhookUnmarshalSuccess() {
var webhook RefundWebhook
body, _ := LoadStubResponseData("stubs/webhooks/request/refund-success.json")
Expand All @@ -81,6 +97,14 @@ func (suite *WebhooksTestSuite) TestRefundWebhookUnmarshalSuccess() {
assert.Equal(suite.T(), "4860610032773134", webhook.AccountNumber)
}

func (suite *WebhooksTestSuite) TestRefundWebhookBuildPayloadString() {
var webhook RefundWebhook
body, _ := LoadStubResponseData("stubs/webhooks/request/refund-success.json")
_ = json.Unmarshal(body, &webhook)
result := webhook.BuildPayloadString("https://www.sample-url.com/callback")
assert.Equal(suite.T(), "65205:RFD-DUSUPAYXYXYXYXYXYXYXYXYX-3486003:COMPLETED:https://www.sample-url.com/callback", result)
}

func TestWebhooksTestSuite(t *testing.T) {
suite.Run(t, new(WebhooksTestSuite))
}
Expand Down

0 comments on commit 63985b8

Please sign in to comment.