gox509elliptic-curve

go parse in ecdsa public key in uncompressed hex format to ecdsa.PublicKey


From a third party I'm receiving an ecdsa public key in uncompressed format

I wrote a basic parser (without any checks) that works for the specific key I am getting:

func HexToPublicKey(data []byte) *ecdsa.PublicKey {
    x := new(big.Int).SetBytes(data[1:33])
    y := new(big.Int).SetBytes(data[33:65])

    pub := ecdsa.PublicKey{Curve: elliptic.P256(), X: x, Y: y}

    return &pub
}

Can anyone tell me if there is a more reliable method of doing this?

EDIT: (removed reference and added code)

The key I am receiving is in this format:

privateKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
ep := privateKey.PublicKey
ep2, _ := ep.ECDH()
fmt.Printf("received public key %x\n", ep2.Bytes())

output

received public key 0496cf21e333678b9c5cd0f6642e93f81e4544ef199ac0144404866309a139a421c5cf948b5ec8baefebaaf2af248835bf8b3f5c8570ff300c6f693f4015a39095

if I now split it, I get

start byte 04
X 96cf21e333678b9c5cd0f6642e93f81e4544ef199ac0144404866309a139a421
Y c5cf948b5ec8baefebaaf2af248835bf8b3f5c8570ff300c6f693f4015a39095

Solution

  • Before answering, I need to draw your attention to the fact that (contrary to its name) your implementation takes raw binary, not hexadecimal. The hexadecimal formatting was due to printing the bytes with the %x format specifier. If you need to decode from hexadecimal, use encoding/hex.DecodeString first.

    Normally you would use crypto/elliptic.Unmarshal, but it was deprecated. There is a proposal to replace it, but it hasn't yet been accepted. Until then, you can use this function based on a comment in that proposal:

    package main
    
    import (
        "crypto/ecdh"
        "crypto/ecdsa"
        "crypto/elliptic"
        "crypto/rand"
        "errors"
        "fmt"
        "math/big"
    )
    
    // TODO: awaiting proposal acceptance: https://github.com/golang/go/issues/63963
    func UnmarshalPublicKey(ecdsaCurve elliptic.Curve, bytes []byte) (*ecdsa.PublicKey, error) {
        var curve ecdh.Curve
        switch ecdsaCurve {
        case elliptic.P256():
            curve = ecdh.P256()
        case elliptic.P384():
            curve = ecdh.P384()
        case elliptic.P521():
            curve = ecdh.P521()
        default:
            return nil, errors.New("non-NIST curve")
        }
    
        // For error checking.
        key, err := curve.NewPublicKey(bytes)
        if err != nil {
            return nil, err
        }
    
        // https://github.com/golang/go/issues/63963#issuecomment-1794706080
        rawKey := key.Bytes()
        switch key.Curve() {
        case ecdh.P256():
            return &ecdsa.PublicKey{
                Curve: elliptic.P256(),
                X:     big.NewInt(0).SetBytes(rawKey[1:33]),
                Y:     big.NewInt(0).SetBytes(rawKey[33:]),
            }, nil
        case ecdh.P384():
            return &ecdsa.PublicKey{
                Curve: elliptic.P384(),
                X:     big.NewInt(0).SetBytes(rawKey[1:49]),
                Y:     big.NewInt(0).SetBytes(rawKey[49:]),
            }, nil
        case ecdh.P521():
            return &ecdsa.PublicKey{
                Curve: elliptic.P521(),
                X:     big.NewInt(0).SetBytes(rawKey[1:67]),
                Y:     big.NewInt(0).SetBytes(rawKey[67:]),
            }, nil
        default:
            return nil, errors.New("cannot convert non-NIST *ecdh.PublicKey to *ecdsa.PublicKey")
        }
    }
    
    func main() {
        privateKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
        publicKey := privateKey.PublicKey
        ecdhKey, _ := publicKey.ECDH()
        bytes := ecdhKey.Bytes()
    
        unmarshaledPublicKey, err := UnmarshalPublicKey(elliptic.P256(), bytes)
        if err != nil {
            panic(err)
        }
        fmt.Printf("%t", publicKey.Equal(unmarshaledPublicKey)) // true
    }