ECDSA

ECDSA is the famous elliptic curve signature algorithm that provides security comparable to RSA3072 with a 256-bit private key size.

The Golang standard library provides crypto/ecdsa, which provides the ability to sign, verify and generate public and private keys. However, in general we do not use Go to generate and save public-private keys, but rather OpenSSL to do so.

For the OpenSSL command to generate public-private keys, we generally use prime256v1 or secp384r1, the two recommended curve parameters, here we choose prime256v1.

1
2
openssl ecparam -name prime256v1 -genkey -noout -out priv_key.pem
openssl pkey -in priv_key.pem -pubout -out pub_key.pem

The generated private key priv_key.pem, public key pub_key.pem, are saved as pem certificates in x509 format.

Signing

For digital signature, you need to hash the data body first, get the message digest, and then sign it with the private key. After sorting, you can get the following Golang function.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func Sign(r io.Reader, privKey []byte) (string, error) {
	// Load the private key file in x509 format
	block, _ := pem.Decode(privKey)
	if block == nil {
		return "", errors.New("privKey no pem data found")
	}
	pk, err := x509.ParseECPrivateKey(block.Bytes)
	if err != nil {
		return "", err
	}
	// Hash the input to get a summary of the information
	h := sha256.New()
	_, err = io.Copy(h, r)
	if err != nil {
		return "", err
	}
	hash := h.Sum(nil)
	// ECDSA Signing
	sign, err := ecdsa.SignASN1(rand.Reader, pk, hash)
	if err != nil {
		return "", err
	}
	return base64.StdEncoding.EncodeToString(sign), nil
}

Signature verification

Validation also requires digesting before verification. Note that signature verification uses a public key, which can be stored publicly within the service.

 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
func Verify(r io.Reader, pubKey []byte, sign string) (bool, error) {
	// Load the public key in x509 format
	block, _ := pem.Decode(pubKey)
	if block == nil {
		return false, errors.New("pubKey no pem data found")
	}
	genericPublicKey, err := x509.ParsePKIXPublicKey(block.Bytes)
	if err != nil {
		return false, err
	}
	pk := genericPublicKey.(*ecdsa.PublicKey)
	// Hash the input to get a summary of the information
	h := sha256.New()
	_, err = io.Copy(h, r)
	if err != nil {
		return false, err
	}
	hash := h.Sum(nil)
	// ECDSA Validation
	bSign, err := base64.StdEncoding.DecodeString(sign)
	if err != nil {
		return false, err
	}
	return ecdsa.VerifyASN1(pk, hash, bSign), nil
}

Finally

For general use, I chose to wrap the x509 format certificate in a binary via the new embed added in 1.16. Given that my usage scenario is purely web services, I don’t need to worry about the binary being cracked locally, and I don’t need to obfuscate the public key part too much.