Skip to content

Instantly share code, notes, and snippets.

@amlwwalker
Created November 28, 2019 16:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save amlwwalker/3213e01424364425748446e43542d5b6 to your computer and use it in GitHub Desktop.
Save amlwwalker/3213e01424364425748446e43542d5b6 to your computer and use it in GitHub Desktop.
Validating Nexmo Signatures on incoming webhooks
package nexmo
// Config holds any values required for dispatching SMS messages to Nexmo
type Config struct {
Creds *Credentials
NexmoWorkers int `env:"NEXMO_WORKERS" envDefault:"5"`
NexmoURL string `env:"NEXMO_URL" envDefault:"https://rest.nexmo.com/sms/json"`
NexmoSecretFile string `env:"NEXMO_CONFIG_FILE" envDefault:"/secrets/nexmo/nexmo.json"`
}
// Credentials holds the values required for interacting with Nexmo
type Credentials struct {
Secret string `json:"api_secret"`
Key string `json:"api_key"`
Origin string `json:"origin"`
SignatureSecret string `json:"signature_secret"`
}
type NexmoDeliveryReceipt struct {
ErrorCode string `json:"err-code"`
APIKey string `json:"api-key"`
TimeStamp string `json:"timestamp"`
MessageTimestamp string `json:"message-timestamp"`
MessageID string `json:"messageId"`
MSISDN string `json:"msisdn"`
NetworkCode string `json:"network-code"`
Signature string `json:"sig"`
Nonce string `json:"nonce"`
Price string `json:"price"`
Scts string `json:"scts"`
Status string `json:"status"`
To string `json:"to"`
}
package nexmo
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"sort"
"strings"
"github.com/fatih/structs"
)
// This function checks that the body's signature matches the signature in the body.
// hash method in nexmo is set to SHA256 HMAC
func GenerateIncomingSignature(config *Config, parameters NexmoDeliveryReceipt) string {
m := structs.Map(parameters) // make a map of the keys of the object so we can sort by key
s := structs.New(parameters) // create an indexed struct from the object
var nexmoString string //store the final string
results := make(map[string]string)
for i, _ := range m {
name := s.Field(i) //get the field from the object
value := name.Value().(string)
tagValue := name.Tag("json")
tagValue = strings.Replace(tagValue, "&", "_", -1) //retrieve the name according to the json tags
tagValue = strings.Replace(tagValue, "=", "_", -1) //could be done with a regex but meh.
value = strings.Replace(value, "&", "_", -1)
value = strings.Replace(value, "=", "_", -1) //could be done with a regex but meh.
if tagValue != "-" && tagValue != "sig" {
results[tagValue] = value
}
}
//now convert the map to a new array for sorting
var notSorted []string
for k := range results {
notSorted = append(notSorted, k)
}
//finally, sort
sort.Strings(notSorted)
//and generate the string
for _, k := range notSorted {
nexmoString += "&" + fmt.Sprintf("%s=%v", k, results[k])
}
// Create a new HMAC by defining the hash type and the key (as byte array)
h := hmac.New(sha256.New, []byte(config.Creds.SignatureSecret))
// Write Data to it
h.Write([]byte(nexmoString))
// Get result and encode as hexadecimal string
sha := hex.EncodeToString(h.Sum(nil))
//sha can now be used as the signature if we want to verify our requests to nexmo
return strings.ToUpper(sha) //the result comes back upper case
}
func ValidateRequest(config *Config, receipt NexmoDeliveryReceipt) bool {
validSignature := GenerateIncomingSignature(config, receipt)
fmt.Println(validSignature, receipt.Signature)
return validSignature == receipt.Signature
}
package nexmo
import (
"testing"
)
func TestDeliveryReceiptAuthenticateIncomingRequest(t *testing.T) {
testConfig := &Config{
Creds: &Credentials{
Secret: "NexmoSecret",
Key: "NexmoKey",
Origin: "TestSender",
SignatureSecret: "my_secret_key_for_testing",
},
NexmoWorkers: 5,
NexmoURL: "http://nexmo.example.local/sms",
NexmoSecretFile: "",
}
t.Run("Should generate a SHA256 HASH that matches the expected value. Tests the function directly", func(t *testing.T) {
deliveryReceipt := NexmoDeliveryReceipt{
MessageTimestamp: "2019-11-28 12:30:07",
ErrorCode: "0",
APIKey: "ff4df137",
TimeStamp: "1574944796",
Nonce: "c8b92f47-8f68-445b-a299-fce7035d0b92",
MessageID: "1500000088E2D6F9",
MSISDN: "447725698433",
NetworkCode: "23431",
Price: "0.03330000",
Scts: "1911281330",
Status: "delivered",
To: "447520632433",
}
SignatureToTest := "EFCCB9935B8A55DFC61EDDBAA278C012C743D8DFFE93EAD8F887F870228F7161"
deliveryReceipt.Signature = SignatureToTest
hmacSHA256Hash := GenerateIncomingSignature(testConfig, deliveryReceipt)
if hmacSHA256Hash != deliveryReceipt.Signature {
t.FailNow()
}
})
t.Run("Should generate a SHA256 HASH that matches the expected value. Testing replacing & and = symbols. Tests using the helper function", func(t *testing.T) {
deliveryReceipt := NexmoDeliveryReceipt{
MessageTimestamp: "2019-11-28 12:30:07",
ErrorCode: "0",
APIKey: "ff4df137",
TimeStamp: "1574944796",
Nonce: "c8b&&92f47-8f68-445b-a299-fce7035d&&0b92",
MessageID: "1500=00&00=88E2D6F9",
MSISDN: "447725698433",
NetworkCode: "23431",
Price: "0.03330000",
Scts: "1911281330",
Status: "deli&&vered",
To: "447520632433",
}
SignatureToTest := "D860AC7C025BD3D72B0537CBB5D6E9C1D76E04563F4FBA5F9EE8461F6631C110"
deliveryReceipt.Signature = SignatureToTest
if !ValidateRequest(testConfig, deliveryReceipt) {
t.FailNow()
}
})
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment