In this example we are going to create a private key and public key using ECDSA. ECDSA is an asymmetric signing method which uses different keys for both signing and verification. This is useful in client and server communications. The message creator would know both private and public keys whereas the user would only know the public key. Message is signed with the private key and verified with the public key.


If you are stuck between RSA and ECDSA, almost always the advice would be to use ECDSA. You can check Internet for more details but here is some of them. More applications are moving towards ECDSA due to its advantages.



Alternative to hex encoder, you can use base64.URLEncoding/base64.RawStdEncoding functions for shorter results.


ECDSA package


package ecdsa

import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"encoding/pem"
"fmt"
)

type keyType string

const (
Public keyType = "PUBLIC KEY"
Private keyType = "PRIVATE KEY" // Or EC PRIVATE KEY
)

type Key struct {
private *ecdsa.PrivateKey
public *ecdsa.PublicKey
}

// NewKey accepts a Curve type to generate keys.
// REF: https://pkg.go.dev/crypto/elliptic#Curve
func NewKey(curve elliptic.Curve) (Key, error) {
prv, err := ecdsa.GenerateKey(curve, rand.Reader)
if err != nil {
return Key{}, err
}

return Key{
private: prv,
public: &prv.PublicKey,
}, nil
}

// NewKeyFromPrivate accepts a private key to reproduce keys.
// REF: https://pkg.go.dev/crypto/elliptic#Curve
func NewKeyFromPrivate(curve elliptic.Curve, private string) (Key, error) {
block, _ := pem.Decode([]byte(private))

prv, err := x509.ParseECPrivateKey(block.Bytes)
if err != nil {
return Key{}, err
}

return Key{
private: prv,
public: &prv.PublicKey,
}, nil
}

func (k Key) String(keyType keyType) (string, error) {
var (
enc []byte
err error
)

switch keyType {
case Public:
enc, err = x509.MarshalPKIXPublicKey(k.public)
case Private:
enc, err = x509.MarshalECPrivateKey(k.private)
default:
return "", fmt.Errorf("invalid key type: %s", keyType)
}

if err != nil {
return "", err
}

return string(pem.EncodeToMemory(&pem.Block{Type: string(keyType), Bytes: enc})), nil
}

package ecdsa

import (
"crypto/ecdsa"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"errors"
)

type Message struct {
key Key
text string
}

// NewMessage returns a new message.
func NewMessage(key Key, text string) Message {
return Message{
key: key,
text: text,
}
}

func (m Message) Sign() (string, error) {
hash := sha256.Sum256([]byte(m.text)) // Prefer blake2b

sig, err := ecdsa.SignASN1(rand.Reader, m.key.private, hash[:])
if err != nil {
return "", err
}

return hex.EncodeToString(sig), nil
}

func (m Message) Verify(signature string) error {
sig, err := hex.DecodeString(signature)
if err != nil {
return err
}

hash := sha256.Sum256([]byte(m.text)) // Prefer blake2b

if !ecdsa.VerifyASN1(m.key.public, hash[:], sig) {
return errors.New("invalid signature")
}

return nil
}

Usage


You can save and reuse the private key in environment variables in order to reproduce same public key. For that, use NewKeyFromPrivate method.


package main

import (
"crypto/elliptic"
"fmt"
"random/ecdsa"
)

func main() {
// FRESH KEY ---------------------------------------------------------------

key, err := ecdsa.NewKey(elliptic.P256())
if err != nil {
panic(err)
}

pub, err := key.String(ecdsa.Public)
if err != nil {
panic(err)
}

prv, err := key.String(ecdsa.Private)
if err != nil {
panic(err)
}

fmt.Printf("PUBLIC KEY (NewKey):\n%s\nPRIVATE KEY (NewKey):\n%s\n", pub, prv)

// KEY FROM EXISTING PRIVATE KEY ------------------------------------------

key, err = ecdsa.NewKeyFromPrivate(elliptic.P256(), prv)
if err != nil {
panic(err)
}

pub, err = key.String(ecdsa.Public)
if err != nil {
panic(err)
}

prv, err = key.String(ecdsa.Private)
if err != nil {
panic(err)
}

fmt.Printf("PUBLIC KEY (NewKeyFromPrivate):\n%s\nPRIVATE KEY (NewKeyFromPrivate):\n%s\n", pub, prv)

// SIGN AND VERIFY MESSAGE -------------------------------------------------

plainMessage := "I am a plain message but I will be signed soon!"

fmt.Printf("PLAIN MESSAGE:\n%s\n", plainMessage)

msg := ecdsa.NewMessage(key, plainMessage)

signature, err := msg.Sign()
if err != nil {
panic(err)
}

fmt.Printf("MESSAGE SIGNATURE:\n%s\n", signature)

if err := msg.Verify(signature); err != nil {
panic(err)
}

fmt.Println("VERIFY SIGNATURE:\nOK")

if err := msg.Verify(signature[:len(signature)-3]+"ABC"); err != nil {
panic(err)
}

fmt.Println("VERIFY SIGNATURE:\nOK")
}

Test


$ go run -race main.go
PUBLIC KEY (NewKey):
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEOAKvZyZ+V8YwhvT3pmbXivGJK/pU
nFJpnQ5yF4w+VcZK/eyuEfaaBqdWPAWf5qIKEqc42vq/SwzYJAvZlpUCsg==
-----END PUBLIC KEY-----

PRIVATE KEY (NewKey):
-----BEGIN PRIVATE KEY-----
MHcCAQEEINPB45II2s55I08oM3Vix56rLApzLcFk6yj5ISEsyoYloAoGCCqGSM49
AwEHoUQDQgAEOAKvZyZ+V8YwhvT3pmbXivGJK/pUnFJpnQ5yF4w+VcZK/eyuEfaa
BqdWPAWf5qIKEqc42vq/SwzYJAvZlpUCsg==
-----END PRIVATE KEY-----

PUBLIC KEY (NewKeyFromPrivate):
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEOAKvZyZ+V8YwhvT3pmbXivGJK/pU
nFJpnQ5yF4w+VcZK/eyuEfaaBqdWPAWf5qIKEqc42vq/SwzYJAvZlpUCsg==
-----END PUBLIC KEY-----

PRIVATE KEY (NewKeyFromPrivate):
-----BEGIN PRIVATE KEY-----
MHcCAQEEINPB45II2s55I08oM3Vix56rLApzLcFk6yj5ISEsyoYloAoGCCqGSM49
AwEHoUQDQgAEOAKvZyZ+V8YwhvT3pmbXivGJK/pUnFJpnQ5yF4w+VcZK/eyuEfaa
BqdWPAWf5qIKEqc42vq/SwzYJAvZlpUCsg==
-----END PRIVATE KEY-----

PLAIN MESSAGE:
I am a plain message but I will be signed soon!
MESSAGE SIGNATURE:
3045022059ca575ac9e672db5a4d8f38408434ab5fb59b6d0e9c5f0e7fd7da54b234a721022100b9b762be232af27ed958cf1aa7e10ed9519296467434547b998e9614653e54a0
VERIFY SIGNATURE:
OK
panic: invalid signature

goroutine 1 [running]:
main.main()
/main.go:70 +0x585
exit status 2