Verifying Paddle signature in Go

Paddle is an amazing payment solution for SaaS businesses — mostly because one can integrate it in 20 minutes (thanks to webhooks!).

However, when you use Go, you also need to write some code to verify signature from an incoming webhook. It's not a difficult thing to do — the signature itself is just a SHA1 hash of a serialized map of all form parameters. The serialization is done using PHP serialize(), but instead of using any library that performs PHP serialization, I decided to keep things simple and use a simple concatenation (it's more than sufficient in this specific situation).

Here is the code:

package payments

import (
"crypto"
"crypto/rsa"
"crypto/sha1"
"crypto/x509"
"encoding/pem"
"encoding/base64"
"fmt"
"net/url"
"sort"
)

var paddleKey = `-----BEGIN PUBLIC KEY-----
<Your paddle public key>
-----END PUBLIC KEY-----`

type paddle struct { }

func (p *paddle) parsePublicKey() (*rsa.PublicKey, error) {
decoded, _ := pem.Decode([]byte(paddleKey))
re, err := x509.ParsePKIXPublicKey(decoded.Bytes)
if err != nil {
return nil, err
}
pub := re.(*rsa.PublicKey)
if err != nil {
return nil, err
}
return pub, nil
}

func (p *paddle) phpEncodeValues(formValues url.Values) []byte {
var keys []string
for k, _ := range formValues {
keys = append(keys, k)
}
sort.Strings(keys)

var phpEncoded = fmt.Sprintf("a:%d:{", len(keys) - 1)
for _, k := range keys {
if k != "p_signature" {
val := formValues[k][0]
phpEncoded += fmt.Sprintf("s:%d:\"%s\";s:%d:\"%s\";", len(k), k, len(val), val)
}
}
phpEncoded += "}"

return []byte(phpEncoded)
}

func (p *paddle) sha1(xs []byte) []byte {
h := sha1.New()
h.Write(xs)
return h.Sum(nil)
}

func VerifyPaddleSignature(formValues url.Values) error {
p := paddle{}

// parse a public key first
pubKey, err := p.parsePublicKey()
if err != nil {
return err
}

// get sha1 hash of php encoded form values
hash := p.sha1(p.phpEncodeValues(formValues))

// decode signature
decodedSignature, err := base64.StdEncoding.DecodeString(formValues["p_signature"][0])
if err != nil {
return err
}

// verify
err = rsa.VerifyPKCS1v15(pubKey, crypto.SHA1, hash, decodedSignature)
if err != nil {
return err
}

return nil
}

Subscribe to the blog if it helped you.

June 11, 2019