Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Crypto] DecodePublicKeyPEM supports ECDSA secp256k1 keys #183

Merged
merged 3 commits into from
May 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 65 additions & 26 deletions crypto/crypto.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@
package crypto

import (
"crypto/ecdsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/hex"
"encoding/pem"
"errors"
"fmt"

"github.com/onflow/flow-go/crypto"
Expand Down Expand Up @@ -218,42 +219,80 @@ func DecodePublicKeyHex(sigAlgo SignatureAlgorithm, s string) (PublicKey, error)
return DecodePublicKey(sigAlgo, b)
}

// DecodePublicKeyHex decodes a PEM ECDSA public key with the given curve.
// DecodePublicKeyHex decodes a PEM ECDSA public key with the given curve, encoded in `sigAlgo`.
//
// The function only supports ECDSA with P256 and secp256k1 curves.
func DecodePublicKeyPEM(sigAlgo SignatureAlgorithm, s string) (PublicKey, error) {

if sigAlgo != ECDSA_P256 && sigAlgo != ECDSA_secp256k1 {
return nil, fmt.Errorf("crypto: only ECDSA algorithms are supported")
}

block, rest := pem.Decode([]byte(s))
if len(rest) > 0 {
return nil, fmt.Errorf("crypto: failed to parse PEM string, all not bytes in PEM key were decoded: %s", string(rest))
return nil, fmt.Errorf("crypto: failed to parse PEM string, not all bytes in PEM key were decoded: %x", rest)
}

// TODO: Replace with function that is compatible with secp256k1
publicKey, err := x509.ParsePKIXPublicKey(block.Bytes)
// parse the public key data and extract the raw public key
pkBytes, err := x509ParseECDSAPublicKey(block.Bytes)
if err != nil {
return nil, fmt.Errorf("crypto: failed to parse PEM string: %w", err)
}

goPublicKey, ok := publicKey.(*ecdsa.PublicKey)
if !ok {
return nil, fmt.Errorf("only ECDSA public keys are supported")
}
xBytes := goPublicKey.X.Bytes()
yBytes := goPublicKey.Y.Bytes()
expectedLength := bitsToBytes(goPublicKey.Params().P.BitLen())
// If an expected length for the point byte slice sizes, make sure to
// pad up to the expected length
rawPublicKey := make([]byte, 0, 2*expectedLength)
rawPublicKey = appendWithLeftPad(rawPublicKey, xBytes, expectedLength)
rawPublicKey = appendWithLeftPad(rawPublicKey, yBytes, expectedLength)

return DecodePublicKey(sigAlgo, rawPublicKey)
// decode the point and check the resulting key is a valid point on the curve
return DecodePublicKey(sigAlgo, pkBytes)
}

func bitsToBytes(bits int) int {
return (bits + 7) >> 3
type publicKeyInfo struct {
Raw asn1.RawContent
Algorithm pkix.AlgorithmIdentifier
PublicKey asn1.BitString
}

func appendWithLeftPad(dst, src []byte, length int) []byte {
for i := 0; i < length-len(src); i++ {
dst = append(dst, byte(0))
var (
// object IDs of ECDSA and the 2 supported curves (https://www.secg.org/sec2-v2.pdf)
oidPublicKeyECDSA = asn1.ObjectIdentifier{1, 2, 840, 10045, 2, 1}
oidNamedCurveP256 = asn1.ObjectIdentifier{1, 2, 840, 10045, 3, 1, 7}
oidNamedCurveSECP256K1 = asn1.ObjectIdentifier{1, 3, 132, 0, 10}
)

// x509ParseECDSAPublicKey parses an ECDSA public key in PKIX, ASN.1 DER form.
//
// The function only supports curves P256 and secp256k1. It doesn't check the
// encoding represents a valid point on the curve.
func x509ParseECDSAPublicKey(derBytes []byte) ([]byte, error) {

var pki publicKeyInfo
if rest, err := asn1.Unmarshal(derBytes, &pki); err != nil {
return nil, err
} else if len(rest) != 0 {
return nil, errors.New("x509: trailing data after ASN.1 of public-key")
}

// Only ECDSA is supported
if !pki.Algorithm.Algorithm.Equal(oidPublicKeyECDSA) {
return nil, errors.New("x509: unknown public key algorithm")
}

asn1Data := pki.PublicKey.RightAlign()
paramsData := pki.Algorithm.Parameters.FullBytes
namedCurveOID := new(asn1.ObjectIdentifier)
rest, err := asn1.Unmarshal(paramsData, namedCurveOID)
if err != nil {
return nil, errors.New("x509: failed to parse ECDSA parameters as named curve")
}
if len(rest) != 0 {
return nil, errors.New("x509: trailing data after ECDSA parameters")
}

// Check the curve is supported
if !(namedCurveOID.Equal(oidNamedCurveP256) || namedCurveOID.Equal(oidNamedCurveSECP256K1)) {
return nil, errors.New("x509: unsupported elliptic curve")
}

// the candidate field length - this function doesn't check the length is valid
if asn1Data[0] != 4 { // uncompressed form
return nil, errors.New("x509: only uncompressed keys are supported")
}
return append(dst, src...)
return asn1Data[1:], nil
}
60 changes: 56 additions & 4 deletions crypto/crypto_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@
package crypto_test

import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"encoding/pem"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -130,12 +135,59 @@ func makeSeed(l int) []byte {
return seed
}

const TestPEM = `-----BEGIN PUBLIC KEY-----
func TestDecodePublicKeyPEM(t *testing.T) {

const pemECDSAKeySECP256K1 = `-----BEGIN -----
MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEaN+NInGJauSEx4ErF8GwtlNTjQvjXINA
wQ86xRvlkcKK2RSaGdKyS4Dy6NAOCucCQOvK09nBhARyqwh3VLooow==
-----END -----`
const pemKeyWithLeadingZero = `-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAECi6YPHhCRPZWg0sUeNAi7QdpH5E8
hbOhaN5CWXjw0HQAZeXqjoswiWlVH0baBuwAPwFcdk5fG/KW60QvOYPExA==
-----END PUBLIC KEY-----`

func TestDecodePublicKeyPEM(t *testing.T) {
_, err := crypto.DecodePublicKeyPEM(crypto.ECDSA_P256, TestPEM)
assert.NoError(t, err)
expected := map[string]string{
pemECDSAKeySECP256K1: "0x68df8d2271896ae484c7812b17c1b0b653538d0be35c8340c10f3ac51be591c28ad9149a19d2b24b80f2e8d00e0ae70240ebcad3d9c1840472ab087754ba28a3",
pemKeyWithLeadingZero: "0x0a2e983c784244f656834b1478d022ed07691f913c85b3a168de425978f0d0740065e5ea8e8b308969551f46da06ec003f015c764e5f1bf296eb442f3983c4c4",
}

t.Run("ECDSA_P256", func(t *testing.T) {
// generate a random public key
sk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)
pk, ok := sk.Public().(*ecdsa.PublicKey)
require.True(t, ok)
// encode the public key
pkEncoding, err := x509.MarshalPKIXPublicKey(pk)
require.NoError(t, err)
block := pem.Block{
Bytes: pkEncoding,
}
pemPkEncoding := pem.EncodeToMemory(&block)
require.NotNil(t, pemPkEncoding)

// decode the public key
decodedPk, err := crypto.DecodePublicKeyPEM(crypto.ECDSA_P256, string(pemPkEncoding))
require.NoError(t, err)
// check the decoded key is the same as the initila key
expectedPk := elliptic.Marshal(elliptic.P256(), pk.X, pk.Y)[1:]
t.Logf("%x\n", expectedPk)
assert.Equal(t, expectedPk, decodedPk.Encode())
})

t.Run("ECDSA_secp256k1", func(t *testing.T) {
key := pemECDSAKeySECP256K1
pk, err := crypto.DecodePublicKeyPEM(crypto.ECDSA_secp256k1, key)
require.NoError(t, err)

assert.Equal(t, expected[key], pk.String())
})

t.Run("Key with leading zeros", func(t *testing.T) {
key := pemKeyWithLeadingZero
pk, err := crypto.DecodePublicKeyPEM(crypto.ECDSA_P256, key)
require.NoError(t, err)

assert.Equal(t, expected[key], pk.String())
})
}