Go: Verify Cryptographic Signatures

Recently, I was implementing a webhook for Travis CI in Go. When a build finishes (depending on the settings), Travis POSTs to a previously specified URL. That requires the URL of the webhook to be publicly accessible (you can still Travis-encrypt the URL, but that’s just security-by-obscurity).

To give you the possibility to verify the POST query actually came from Travis, they not only send the payload but also a signature (as an HTTP header).

If you are interested in the source code for the Travis hook, check it out at https://github.com/jacksgt/travishook.

#  Convert the Public Key

First, we need to convert the public key into a usable format. I’m going to assume you have the public key stored in a string:

1
var rawPubKey = "-----BEGIN PUBLIC KEY-----\nMi..MORE.PUBLIC.KEY..u2YaN\n0QIDAQAB\n-----END PUBLIC KEY-----"

The BEGIN PUBLIC KEY and END PUBLIC KEY sections as well as the line breaks are just for human convenience, they don’t carry and data. However, they show that this public key is encoded in PEM format (like a lot of X.509 certificates).

Hence, we need to decode this format:

1
2
3
4
5
6
import "encoding/pem"

block, _ := pem.Decode([]byte(rawPubKey))
if block == nil {
    return nil
}

We are using the Decode function from the encoding/pem package. This function has a single argument (a byte array of data) and returns a pointer to the key block and remaining input (which we discard here).

To actually use this public key, we have to translate it into Go’s public key interface. We can do this by calling the ParsePKIXPublicKey function of crypto/x509 which returns either *rsa.PublicKey, *dsa.PublicKey or *ecdsa.PublicKey struct, depending on the type of key.

1
2
3
4
5
6
import "crypto/x509"

key, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
    return err
}

If you want to check which type of key you just decoded, simply access its type:

1
key.(type)

In my case (and for the sake of simplicity), I know it is an RSA key. Finally, we have a usable public key in Go:

1
2
3
import "crypto/rsa"

pubKey := rsaKey.(*rsa.PublicKey)

#  Verifying the Signature

After retrieving, decoding and translating the public key, we can now work on the signature. Again I’m going to assume the message and signature are stored as a simple strings:

1
2
var rawSignature = "cnUSDnfnao....sdnsdoaHSO"
var message = "authenticmessage"

In the case of Travis webhooks, the signature string is additionally encoded in Base64. Decoding can be done with DecodeString from encoding/base64:

1
2
3
4
5
6
import "encoding/base64"

signature, err := base64.StdEncoding.DecodeString(rawSignature)
if err != nil {
    return err
}

Note: You might not need this step, but you need to at least convert your signature string into a byte array.

Now we have the public key and signature (stored in the correct format), let’s verify the signature of the message.

We start by hashing the message. This needs to be done with the same Hash algorithm that was used to create the signature. In my case it’s SHA-1, therefore I’m using the Sum function from the crypto/sha1 package:

1
2
3
import "crypto/sha1"

hash := sha1.Sum(message)

Unfortunately, all packages implementing the PublicKey interface (rsa, dsa and ecdsa, as mentioned above) feature a verifying function, but they have different names and (function) signatures:

  • RSA: func VerifyPKCS1v15(pub *PublicKey, hash crypto.Hash, hashed []byte, sig []byte) error
  • DSA: func Verify(pub *PublicKey, hash []byte, r, s *big.Int) bool
  • ECDSA: func Verify(pub *PublicKey, hash []byte, r, s *big.Int) bool

Choose the appropriate function depending on the type of key you have (received).

I’m dealing with an RSA key here. Buckle your seat belts, we’re hitting the home stretch: verifying the message with the key and signature:

1
2
3
4
5
6
7
import "crypto"
import "crypto/rsa"

err := rsa.VerifyPKCS1v15(pubKey, crypto.SHA1, hash[:], signature)
if err != nil {
    return err
}

The VerifyPKCS1v15 function expects four input parameters: the public key (in a public key interface), the type of hashed used (here crypto.SHA1), the complete hash (hash[:]) and the signature stored in a byte array.

If the function returns nil the message was successfully verified, otherwise the signature is invalid or there was some other kind of error.

#  Complete code

Here is the complete sample code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package main

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

var rawPubKey = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvtjdLkS+FP+0fPC09j25\ny/PiuYDDivIT86COVedvlElk99BBYTrqNaJybxjXbIZ1Q6xFNhOY+iTcBr4E1zJu\ntizF3Xi0V9tOuP/M8Wn4Y/1lCWbQKlWrNQuqNBmhovF4K3mDCYswVbpgTmp+JQYu\nBm9QMdieZMNry5s6aiMA9aSjDlNyedvSENYo18F+NYg1J0C0JiPYTxheCb4optr1\n5xNzFKhAkuGs4XTOA5C7Q06GCKtDNf44s/CVE30KODUxBi0MCKaxiXw/yy55zxX2\n/YdGphIyQiA5iO1986ZmZCLLW8udz9uhW5jUr3Jlp9LbmphAC61bVSf4ou2YsJaN\n0QIDAQAB\n-----END PUBLIC KEY-----"
var rawSignature = "c2pkYWpuY2sgZmphbm9panF3b2lqYWRvbmFzbWQgc2EsbWMgc2FuZHBvZHA5cTN1cjA5M3Vyajg4OUoocHEqaDlIUkZKU0ZLQkZPSDk4"
var message = "authenticmessage"

func main() {

    block, _ := pem.Decode([]byte(rawPubKey))
    if block == nil {
        fmt.Println("Invalid PEM Block")
        return
    }

    key, err := x509.ParsePKIXPublicKey(block.Bytes)
    if err != nil {
        fmt.Println(err)
        return
    }

    pubKey := key.(*rsa.PublicKey)

    signature, err := base64.StdEncoding.DecodeString(rawSignature)
    if err != nil {
        fmt.Println(err)
        return
    }

    hash := sha1.Sum([]byte(message))

    err = rsa.VerifyPKCS1v15(pubKey, crypto.SHA1, hash[:], signature)
    if err != nil {
        fmt.Println(err)
        return
    }

    fmt.Println("Successfully verified message with signature and public key")
    return
}

Note: the public key, signature and payload shown above aren’t valid (you should receive a “crypto/rsa: verification error”)

#  Conclusion

This project really made me appreciate Go’s awesome standard library: everything I needed was already built-in and well documented (also I don’t need to worry about random ABI changes or poor maintainership).

I don’t want to miss this ‘feature’ in any modern language any more.