Validating ECDSA Signatures in Golang

原文作者:Thane Thomson

By Thane on Fri 30 November 2018

cryptography go golang software

The Go programming language is widely used in the Kubernetes and blockchain communities. So naturally, since I have been getting into those technologies over the past year or so, I have tended to write more and more software in Go, and am really enjoying it. I really love the simplicity of the language (once you get over its quirks, like how the GOPATH works, and dependency management).

Elliptic curve cryptography (ECC) seems to be used quite extensively in blockchain applications, and so naturally digital signatures based on ECC are quite important for message validation. Validating ECDSA signatures in Golang seems trivial at first, but then one quickly gets lost down a rabbit hole of cryptography and different data representation formats. I thought I would document how I personally went about doing this when transmitting ECDSA signatures in JSON messages, to be validated using Golang.

Note that this article assumes a basic working knowledge of public key cryptography, as well as elliptic curve cryptography. For a great intro to these fields, please see this Cloudflare blog post. Also, I am a big fan of the Python programming language, so I will be using Python extensively here for illustration purposes when constructing signatures, but then Go to validate them.

EDIT: I updated the way that I do the JSON serialisation/deserialisation in this post based on feedback from this Reddit thread to try to make it more secure.

The Problem#

Recently I have had to implement some blockchain-oriented code (built on top ofTendermint) that cryptographically validates that a particular user sent a given message. The structure of the message envelope (envelope) is something like the following, where the structure of the message and transaction fields could vary from message to message:

 2    "message": {
 3        "type": "transactionType",
 4        "userId": 123,
 5        "transaction": {
 6            "amount": 10123.50
 7        }
 8    },
 9    "signature": "...ECDSA signature..."

The reason why we only have a message and signature field in our envelope is that we need to be able to validate the signature against raw bytes. For this, we will use the raw bytes of the message field prior to JSON deserialisation.

What we ultimately want is a way of validating that the user with IDenvelope.message.userId actually did generate this message and the cryptographic signature in envelope.signature. To do this, we define (in pseudocode) the function validateSignature, which returns a boolean value:

1isValid = validateSignature(
2    user.publicKey,
3    envelope.message,
4    envelope.signature

Of course, the generation of the signature in the first place must take place by way of a function roughly like the following:

1envelope.signature = computeSignature(
2    user.privateKey,
3    envelope.message

The major questions/problems I ran into while trying to implement this were:

  1. How do I represent ECDSA signatures in my JSON messages?
  2. How do I generate reliable test data to test my Golang ECDSA message validation? I didn't want to write the generation code in Golang, because it increased the chances of me making mistakes.

To answer the first question, I first needed to understand the ECDSA algorithm a little and how it's practically used, as well as how other tools (like OpenSSL) represent these signatures. It wasn't easy at all to find this sort of information directly - I had to stumble upon it while hunting through somewhat-related StackOverflow posts.

To answer the second one, I used a combination of OpenSSL and Python, where there are some really cool libraries available for Python to handle ECDSA signatures.

Elliptic Curve DSA#

The Elliptic Curve Digital Signature Algorithm is a great way at present to cryptographically sign a message. Here we basically expand our computeSignaturepseudocode above into the following rough algorithms:

 1// For generating the ECDSA signature
 2signature = ecdsa(
 3    user.privateKey,
 4    sha256(envelope.message)
 7// For validating the ECDSA signature
 8isValid = validateSignature(
 9    user.publicKey,
10    sha256(envelope.message),
11    envelope.signature

This is because one generally seems to compute ECDSA signatures from message hashes as opposed to the entire message, and in this case we are using the SHA256 algorithm for computing the hash of the most important fields in the message.

Generating Keys#

Before we do anything else, we need our user to have an elliptic curve public/private key pair. For this, we'll use OpenSSL:

 1# Generate the private key in PEM format
 2> openssl ecparam -name prime256v1 -genkey -noout -out user1.key
 4# Generate the public key from the private key, also in PEM format
 5> openssl ec -in user1.key -pubout -out
 7# Take a look at our private key
 8> cat user1.key
13-----END EC PRIVATE KEY-----
15# Take a look at our public key
16> cat
17-----BEGIN PUBLIC KEY-----
20-----END PUBLIC KEY-----

Building Our Message#

Let's say our message envelope looks as follows:

2    "message": {
3        "type": "issueTx",
4        "userId": 1,
5        "transaction": {
6            "amount": 10123.50
7        }
8    }

Hash it!#

To quickly compute the SHA256 hash of the message, I would run a trusty old Python repl and use the hashlib library:

 1import hashlib
 2import struct
 4# we have 2 levels of indentation because this is precisely how Golang would
 5# extract it into the json.RawMessage field
 6msg = """{
 7        "type": "issueTx",
 8        "userId": 1,
 9        "transaction": {
10            "amount": 10123.50
11        }
12    }"""
14h = hashlib.sha256()
15# the .encode("utf-8") is to convert from a Python string to raw bytes
18# print out the hexadecimal version of the digest
20# should print:
21# 47b17caac45041a19dc8b03921389c55756d9719ad091125ef8f139b99becb96
23# print out the binary version of the digest (this is what we really want)
25# should print:
26# b'G\xb1|\xaa\xc4PA\xa1\x9d\xc8\xb09!8\x9cUum\x97\x19\xad\t\x11%\xef\x8f\x13\x9b\x99\xbe\xcb\x96'

Generate the Signature#

What we actually want now, however, is the ECDSA signature computed from the above hash and the user's private key. For this, there's a cool Python library called python-ecdsa:

 1import ecdsa
 3# load the private/signing key from the key we generated earlier using OpenSSL
 4sk = ecdsa.SigningKey.from_pem(open("user1.key").read())
 5# use the hash from earlier to compute the signature
 6msg_sha256_hash = b'G\xb1|\xaa\xc4PA\xa1\x9d\xc8\xb09!8\x9cUum\x97\x19\xad\t\x11%\xef\x8f\x13\x9b\x99\xbe\xcb\x96'
 7# compute the signature! (and convert to DER format)
 8sig = sk.sign_digest(
 9    msg_sha256_hash,
10    sigencode=ecdsa.util.sigencode_der,
13# on my machine, with my key:
14# b'0E\x02 \x19(\xa3\x11\xb6\xb8V^HG\x9a\x7f\x95\xe1\xe6\x15\x8b\xc5\xc2\x863\x10\x99\xcd\xf9\xcf\xb2\x13\xa1\xdbl\xb6\x02!\x00\xc1R\xc0hh\\qK\xfcR\x18\x02\xdb\xddj5kq\xacf\xb0_jO\xb0\x8e\xd4P\x0f\xfb@\xb3'

Inspect the Signature (Python)#

The sig variable above will be a binary string encoded using DER encoding. Technically, the signature actually has 2 components to it - understanding this is critical to being able to decode the signature later on from your Go code. For now, using the asn1crypto library for Python, we can easily decode the two numbers from the above signature:

 1from asn1crypto.core import Sequence
 3# our ECDSA signature from earlier
 4sig = b'0E\x02 \x19(\xa3\x11\xb6\xb8V^HG\x9a\x7f\x95\xe1\xe6\x15\x8b\xc5\xc2\x863\x10\x99\xcd\xf9\xcf\xb2\x13\xa1\xdbl\xb6\x02!\x00\xc1R\xc0hh\\qK\xfcR\x18\x02\xdb\xddj5kq\xacf\xb0_jO\xb0\x8e\xd4P\x0f\xfb@\xb3'
 5# parse the ASN.1 sequence from this signature
 6seq = Sequence.load(sig)
 7# print the native (Pythonic) representation of this ASN.1 object
 9# on my machine, prints:
10# OrderedDict([('0', 11379620559389084367780510252548132663400275028223528508518721806165041966262), ('1', 87442589186005784642307971049779867575540489022841522355105800395127625826483)])
12# print out the key/value pairs embedded in the sequence in hexadecimal
13for k, v in seq.native.items():
14    print("%s => %X" % (k, v))
15# on my machine, prints:
16# 0 => 1928A311B6B8565E48479A7F95E1E6158BC5C286331099CDF9CFB213A1DB6CB6
17# 1 => C152C068685C714BFC521802DBDD6A356B71AC66B05F6A4FB08ED4500FFB40B3

Inspect the Signature (OpenSSL)#

Alternatively, if we want to inspect the signature using OpenSSL, because it's already encoded in DER format, simply do the following from Python:

1# write our ECDSA signature from earlier to the file "signature.der"
2with open("signature.der", "wb") as f:
3    f.write(b'0E\x02 \x19(\xa3\x11\xb6\xb8V^HG\x9a\x7f\x95\xe1\xe6\x15\x8b\xc5\xc2\x863\x10\x99\xcd\xf9\xcf\xb2\x13\xa1\xdbl\xb6\x02!\x00\xc1R\xc0hh\\qK\xfcR\x18\x02\xdb\xddj5kq\xacf\xb0_jO\xb0\x8e\xd4P\x0f\xfb@\xb3')

And then from BASH:

1> openssl asn1parse -inform DER -in signature.der
2    0:d=0  hl=2 l=  69 cons: SEQUENCE          
3    2:d=1  hl=2 l=  32 prim: INTEGER           :1928A311B6B8565E48479A7F95E1E6158BC5C286331099CDF9CFB213A1DB6CB6
4   36:d=1  hl=2 l=  33 prim: INTEGER           :C152C068685C714BFC521802DBDD6A356B71AC66B05F6A4FB08ED4500FFB40B3

Compare the hexadecimal representation of these two components to the two printed earlier from Python - you will see that they are precisely the same.

Base64-Encode the Signature for Transmission#

To be able to send this binary data across to our Golang application, however, we are going to need to encode it in such a way as to be able to encapsulate it in a JSON message. The easiest way to do this is to Base64-encode it. This is a trivial task using Python:

1import base64
3# our ECDSA signature from earlier
4sig = b'0E\x02 \x19(\xa3\x11\xb6\xb8V^HG\x9a\x7f\x95\xe1\xe6\x15\x8b\xc5\xc2\x863\x10\x99\xcd\xf9\xcf\xb2\x13\xa1\xdbl\xb6\x02!\x00\xc1R\xc0hh\\qK\xfcR\x18\x02\xdb\xddj5kq\xacf\xb0_jO\xb0\x8e\xd4P\x0f\xfb@\xb3'
5# now base64-encode the signature
6b64sig = base64.b64encode(sig)
8# on my machine, prints:
9# b'MEUCIBkooxG2uFZeSEeaf5Xh5hWLxcKGMxCZzfnPshOh22y2AiEAwVLAaGhccUv8UhgC291qNWtxrGawX2pPsI7UUA/7QLM='

Final Envelope Structure#

The final envelope therefore will look something like this:

 2    "message": {
 3        "type": "issueTx",
 4        "userId": 1,
 5        "transaction": {
 6            "amount": 10123.50
 7        }
 8    },
 9    "signature": "MEUCIBkooxG2uFZeSEeaf5Xh5hWLxcKGMxCZzfnPshOh22y2AiEAwVLAaGhccUv8UhgC291qNWtxrGawX2pPsI7UUA/7QLM="

Validating the Signature from Golang#

Assuming that all our Golang application is going to receive from the user is just the above JSON message payload, and no other authentication information, here is how we would validate it:

 1package messaging
 3import (
 4    "crypto/ecdsa"
 5    "crypto/sha256"
 6    "encoding/asn1"
 7    "encoding/base64"
 8    "encoding/json"
 9    "errors"
10    "math/big"
13// Represents the two mathematical components of an ECDSA signature once
14// decomposed.
15type ECDSASignature struct {
16    R, S *big.Int
19// Encapsulates the overall message we're trying to decode and validate.
20type Envelope struct {
21    RawMessage json.RawMessage `json:"message"`
22    Message    interface{}     `json:"-"`
23    Signature  string          `json:"signature"`
26// The body of the message to be contained in the Message field of our Envelope
27// structure.
28type MessageBody struct {
29    Type        string          `json:"type"`
30    UserID      uint32          `json:"userId"`
31    Transaction json.RawMessage `json:"transaction"`
34// Helper function to compute the SHA256 hash of the given string of bytes.
35func hash(b []byte) []byte {
36    h := sha256.New()
37    // hash the body bytes
38    h.Write(b)
39    // compute the SHA256 hash
40    return h.Sum(nil)
43// Attempts to create a new envelope structure from the given JSON string.
44func NewEnvelopeFromJSON(s string) (*Envelope, error) {
45    var e Envelope
46    if err := json.Unmarshal([]byte(s), &e); err != nil {
47        return nil, err
48    }
49    // now attempt to unmarshal the message body itself from the raw message
50    var body MessageBody
51    if err := json.Unmarshal(e.RawMessage, &body); err != nil {
52        return nil, err
53    }
54    e.Message = body
55    return &e, nil
58// The central validation routine that validates this message against the given
59// public key. On success, returns nil, on failure returns a relevant error.
60func (e *Envelope) Validate(publicKey *ecdsa.PublicKey) error {
61    // first decode the signature to extract the DER-encoded byte string
62    der, err := base64.StdEncoding.DecodeString(e.Signature)
63    if err != nil {
64        return err
65    }
66    // unmarshal the R and S components of the ASN.1-encoded signature into our
67    // signature data structure
68    sig := &ECDSASignature{}
69    _, err = asn1.Unmarshal(der, sig)
70    if err != nil {
71        return err
72    }
73    // compute the SHA256 hash of our message
74    h := hash(e.RawMessage)
75    // validate the signature!
76    valid := ecdsa.Verify(
77        publicKey,
78        h,
79        sig.R,
80        sig.S,
81    )
82    if !valid {
83        return errors.New("Signature validation failed")
84    }
85    // signature is valid
86    return nil

Testing Our Signature Validation#

To test the signature validation, we need to write a simple unit test using the public key we generated with OpenSSL, along with the test data we generated from our Python scripts above.

 1package messaging
 3import (
 4    "crypto/ecdsa"
 5    "crypto/x509"
 6    "encoding/pem"
 7    "errors"
 8    "testing"
11const (
12    TestPublicKey string = `-----BEGIN PUBLIC KEY-----
15-----END PUBLIC KEY-----`
17    // NB: make sure to use SPACES here in the test message instead of tabs,
18    // otherwise validation will fail
19    TestMessage string = `{
20    "message": {
21        "type": "issueTx",
22        "userId": 1,
23        "transaction": {
24            "amount": 10123.50
25        }
26    },
27    "signature": "MEUCIBkooxG2uFZeSEeaf5Xh5hWLxcKGMxCZzfnPshOh22y2AiEAwVLAaGhccUv8UhgC291qNWtxrGawX2pPsI7UUA/7QLM="
31func loadPublicKey(publicKey string) (*ecdsa.PublicKey, error) {
32    // decode the key, assuming it's in PEM format
33    block, _ := pem.Decode([]byte(publicKey))
34    if block == nil {
35        return nil, errors.New("Failed to decode PEM public key")
36    }
37    pub, err := x509.ParsePKIXPublicKey(block.Bytes)
38    if err != nil {
39        return nil, errors.New("Failed to parse ECDSA public key")
40    }
41    switch pub := pub.(type) {
42    case *ecdsa.PublicKey:
43        return pub, nil
44    }
45    return nil, errors.New("Unsupported public key type")
48func TestEnvelopeValidation(t *testing.T) {
49    // our test message
50    envelope, err := NewEnvelopeFromJSON(TestMessage)
51    if err != nil {
52        t.Error("Expected to be able to deserialise test message, but failed with err =", err)
53    }
54    // extract the public key from the test key string
55    publicKey, err := loadPublicKey(TestPublicKey)
56    if err != nil {
57        t.Error("Failed to parse test public key:", err)
58    }
59    // now we validate the signature against the public key
60    if err := envelope.Validate(publicKey); err != nil {
61        t.Error("Expected nil error from message envelope validation routine, but got:", err)
62    }

And voilà! You have yourself a tested ECDSA signature validation mechanism in Golang for JSON messages. It should be relatively straightforward from this point on to extend this example to different kinds of JSON messages.

For a great example of how to dynamically unmarshal JSON using Golang, see this post.


原文发布于微信公众号 - Golang语言社区(Golangweb)





0 条评论
登录 后参与评论