Skip to content

Commit

Permalink
Merge pull request #1129 from huitseeker/keyTranslator_inverse_
Browse files Browse the repository at this point in the history
[network, crypto] Compressed serialization, key conversions and message signing
  • Loading branch information
huitseeker committed Aug 17, 2021
2 parents eccee3b + d1e58f6 commit 1b33269
Show file tree
Hide file tree
Showing 13 changed files with 233 additions and 29 deletions.
18 changes: 18 additions & 0 deletions crypto/bls.go
Expand Up @@ -232,6 +232,15 @@ func (a *blsBLS12381Algo) decodePublicKey(publicKeyBytes []byte) (PublicKey, err
return &pk, nil
}

// decodePublicKeyCompressed decodes a slice of bytes into a public key.
// since we use the compressed representation by default, this checks the default and delegates to decodePublicKeyCompressed
func (a *blsBLS12381Algo) decodePublicKeyCompressed(publicKeyBytes []byte) (PublicKey, error) {
if serializationG2 != compressed {
panic("library is not configured to use compressed public key serialization")
}
return a.decodePublicKey(publicKeyBytes)
}

// PrKeyBLSBLS12381 is the private key of BLS using BLS12_381, it implements PrivateKey
type PrKeyBLSBLS12381 struct {
// public key
Expand Down Expand Up @@ -339,6 +348,15 @@ func (pk *PubKeyBLSBLS12381) Size() int {
// Encode returns a byte encoding of the public key.
// The encoding is a compressed encoding of the point
// [zcash] https://github.com/zkcrypto/pairing/blob/master/src/bls12_381/README.md#serialization
func (a *PubKeyBLSBLS12381) EncodeCompressed() []byte {
if serializationG2 != compressed {
panic("library is not configured to use compressed public key serialization")
}
return a.Encode()
}

// Encode returns a byte encoding of the public key.
// Since we use a compressed encoding by default, this delegates to EncodeCompressed
func (a *PubKeyBLSBLS12381) Encode() []byte {
dest := make([]byte, pubKeyLengthBLSBLS12381)
writePointG2(dest, &a.point)
Expand Down
48 changes: 46 additions & 2 deletions crypto/ecdsa.go
Expand Up @@ -14,6 +14,7 @@ import (
"fmt"
"math/big"

"github.com/btcsuite/btcd/btcec"
"github.com/onflow/flow-go/crypto/hash"
)

Expand Down Expand Up @@ -62,7 +63,7 @@ func (sk *PrKeyECDSA) signHash(h hash.Hash) (Signature, error) {
// where r and s are padded to the curve order size.
func (sk *PrKeyECDSA) Sign(data []byte, alg hash.Hasher) (Signature, error) {
// no need to check the hasher output size as all supported hash algos
// have at lease 32 bytes output
// have at least 32 bytes output
if alg == nil {
return nil, newInvalidInputsError("Sign requires a Hasher")
}
Expand Down Expand Up @@ -213,21 +214,54 @@ func (a *ecdsaAlgo) rawDecodePublicKey(der []byte) (PublicKey, error) {
// all the curves supported for now have a cofactor equal to 1,
// so that IsOnCurve guarantees the point is on the right subgroup.
if x.Cmp(p) >= 0 || y.Cmp(p) >= 0 || !a.curve.IsOnCurve(&x, &y) {
return nil, newInvalidInputsError("input is not a valid %s key", a.algo)
return nil, newInvalidInputsError("input %x is not a valid %s key", der, a.algo)
}

pk := goecdsa.PublicKey{
Curve: a.curve,
X: &x,
Y: &y,
}

return &PubKeyECDSA{a, &pk}, nil
}

func (a *ecdsaAlgo) decodePublicKey(der []byte) (PublicKey, error) {
return a.rawDecodePublicKey(der)
}

// decodePublicKeyCompressed returns a public key given the bytes of a compressed public key according to X9.62 section 4.3.6.
// this compressed representation uses an extra byte to disambiguate sign
func (a *ecdsaAlgo) decodePublicKeyCompressed(pkBytes []byte) (PublicKey, error) {
expectedLen := bitsToBytes(a.curve.Params().BitSize) + 1
if len(pkBytes) != expectedLen {
return nil, newInvalidInputsError(fmt.Sprintf("input length incompatible, expected %d, got %d", expectedLen, len(pkBytes)))
}
var goPubKey *goecdsa.PublicKey

if a.curve == elliptic.P256() {
x, y := elliptic.UnmarshalCompressed(a.curve, pkBytes)
if x == nil {
return nil, newInvalidInputsError("Key %x can't be interpreted as %v", pkBytes, a.algo.String())
}
goPubKey = new(goecdsa.PublicKey)
goPubKey.Curve = a.curve
goPubKey.X = x
goPubKey.Y = y

} else if a.curve == btcec.S256() {
pk, err := btcec.ParsePubKey(pkBytes, btcec.S256())
if err != nil {
return nil, newInvalidInputsError("Key %x can't be interpreted as %v", pkBytes, a.algo.String())
}
// This assertion never fails
goPubKey = (*goecdsa.PublicKey)(pk)
} else {
return nil, newInvalidInputsError("the input curve is not supported")
}
return &PubKeyECDSA{a, goPubKey}, nil
}

// PrKeyECDSA is the private key of ECDSA, it implements the generic PrivateKey
type PrKeyECDSA struct {
// the signature algo
Expand Down Expand Up @@ -316,6 +350,16 @@ func (pk *PubKeyECDSA) Size() int {
return 2 * bitsToBytes((pk.goPubKey.Params().P).BitLen())
}

// EncodeCompressed returns a compressed encoding according to X9.62 section 4.3.6.
// This compressed representation uses an extra byte to disambiguate sign.
// The expected input is a public key (x,y).
func (pk *PubKeyECDSA) EncodeCompressed() []byte {
if pk.alg.curve == btcec.S256() {
return (*btcec.PublicKey)(pk.goPubKey).SerializeCompressed()
}
return elliptic.MarshalCompressed(pk.goPubKey, pk.goPubKey.X, pk.goPubKey.Y)
}

// given a public key (x,y), returns a raw uncompressed encoding bytes(x)||bytes(y)
// x and y are padded to the field size
func (pk *PubKeyECDSA) rawEncode() []byte {
Expand Down
35 changes: 35 additions & 0 deletions crypto/ecdsa_test.go
@@ -1,12 +1,14 @@
package crypto

import (
"encoding/hex"
"testing"

"crypto/elliptic"
"crypto/rand"
"math/big"

"github.com/btcsuite/btcd/btcec"
"github.com/onflow/flow-go/crypto/hash"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -284,3 +286,36 @@ func TestSignatureFormatCheck(t *testing.T) {
})
}
}

func TestEllipticUnmarshalSecp256k1(t *testing.T) {

testVectors := []string{
"028b10bf56476bf7da39a3286e29df389177a2fa0fca2d73348ff78887515d8da1", // IsOnCurve for elliptic returns false
"03d39427f07f680d202fe8504306eb29041aceaf4b628c2c69b0ec248155443166", // negative, IsOnCurve for elliptic returns false
"0267d1942a6cbe4daec242ea7e01c6cdb82dadb6e7077092deb55c845bf851433e", // arith of sqrt in elliptic doesn't match secp256k1
"0345d45eda6d087918b041453a96303b78c478dce89a4ae9b3c933a018888c5e06", // negative, arith of sqrt in elliptic doesn't match secp256k1
}

s := ECDSASecp256k1

for _, testVector := range testVectors {

// get the compressed bytes
publicBytes, err := hex.DecodeString(testVector)
require.NoError(t, err)

// decompress, check that those are perfectly valid Secp256k1 public keys
retrieved, err := DecodePublicKeyCompressed(s, publicBytes)
require.NoError(t, err)

// check the compression is canonical by re-compressing to the same bytes
require.Equal(t, retrieved.EncodeCompressed(), publicBytes)

// check that elliptic fails at decompressing them
x, y := elliptic.UnmarshalCompressed(btcec.S256(), publicBytes)
require.Nil(t, x)
require.Nil(t, y)

}

}
21 changes: 18 additions & 3 deletions crypto/sign.go
Expand Up @@ -16,12 +16,14 @@ import (

// Signer interface
type signer interface {
// generatePrKey generates a private key
// generatePrivateKey generates a private key
generatePrivateKey([]byte) (PrivateKey, error)
// decodePrKey loads a private key from a byte array
// decodePrivateKey loads a private key from a byte array
decodePrivateKey([]byte) (PrivateKey, error)
// decodePubKey loads a public key from a byte array
// decodePublicKey loads a public key from a byte array
decodePublicKey([]byte) (PublicKey, error)
// decodePublicKeyCompressed loads a public key from a byte array representing a point in compressed form
decodePublicKeyCompressed([]byte) (PublicKey, error)
}

// newNonRelicSigner returns a signer that does not depend on Relic library.
Expand Down Expand Up @@ -107,6 +109,15 @@ func DecodePublicKey(algo SigningAlgorithm, data []byte) (PublicKey, error) {
return signer.decodePublicKey(data)
}

// DecodePublicKeyCompressed decodes an array of bytes given in a compressed representation into a public key of the given algorithm
func DecodePublicKeyCompressed(algo SigningAlgorithm, data []byte) (PublicKey, error) {
signer, err := newSigner(algo)
if err != nil {
return nil, newInvalidInputsError("decode public key failed: %s", err)
}
return signer.decodePublicKeyCompressed(data)
}

// Signature type tools

// Bytes returns a byte array of the signature data
Expand Down Expand Up @@ -153,6 +164,10 @@ type PublicKey interface {
Verify(Signature, []byte, hash.Hasher) (bool, error)
// Encode returns a bytes representation of the public key.
Encode() []byte
// Encode returns a compressed byte representation of the public key.
// The compressed serialization concept is generic to elliptic curves,
// but we refer to individual curve parameters for details of the compressed format
EncodeCompressed() []byte
// Equals returns true if the given PublicKeys are equal. Keys are considered unequal if their algorithms are
// unequal or if their encoded representations are unequal. If the encoding of either key fails, they are considered
// unequal as well.
Expand Down
10 changes: 10 additions & 0 deletions crypto/sign_test_utils.go
Expand Up @@ -134,6 +134,16 @@ func testEncodeDecode(t *testing.T, salg SigningAlgorithm) {
assert.Equal(t, pkBytes, pkCheckBytes, "keys should be equal")
distinctPkBytes := distinctSk.PublicKey().Encode()
assert.NotEqual(t, pkBytes, distinctPkBytes, "keys should be different")

// same for the compressed encoding
pkComprBytes := pk.EncodeCompressed()
pkComprCheck, err := DecodePublicKeyCompressed(salg, pkComprBytes)
require.Nil(t, err, "the key decoding has failed")
assert.True(t, pk.Equals(pkComprCheck), "key equality check failed")
pkCheckComprBytes := pkComprCheck.EncodeCompressed()
assert.Equal(t, pkComprBytes, pkCheckComprBytes, "keys should be equal")
distinctPkComprBytes := distinctSk.PublicKey().EncodeCompressed()
assert.NotEqual(t, pkComprBytes, distinctPkComprBytes, "keys should be different")
}

// test invalid private keys (equal to the curve group order)
Expand Down
65 changes: 62 additions & 3 deletions network/p2p/keyTranslator.go
Expand Up @@ -13,6 +13,15 @@ import (
fcrypto "github.com/onflow/flow-go/crypto"
)

// This module is meant to help libp2p <-> flow public key conversions
// Flow's Network Public Keys and LibP2P's public keys are a marshalling standard away from each other and inter-convertible.
// Libp2p supports keys as ECDSA public Keys on either the NIST P-256 curve or the "Bitcoin" secp256k1 curve, see https://github.com/libp2p/specs/blob/master/peer-ids/peer-ids.md#peer-ids
// libp2p represents the P-256 keys as ASN1-DER and the secp256k1 keys as X9.62 encodings in compressed format
//
// While Flow's key types supports both secp256k1 and P-256 keys (under crypto/ecdsa), note that Flow's networking keys are always P-256 keys.
// Flow represents both the P-256 keys and the secp256k1 keys in uncompressed representation, but their byte serializations (Encode) do not include the X9.62 compression bit
// Flow also makes a X9.62 compressed format (with compression bit) accessible (EncodeCompressed)

// assigns a big.Int input to a Go ecdsa private key
func setPrKey(c elliptic.Curve, k *big.Int) *goecdsa.PrivateKey {
priv := new(goecdsa.PrivateKey)
Expand All @@ -35,7 +44,7 @@ func setPubKey(c elliptic.Curve, x *big.Int, y *big.Int) *goecdsa.PublicKey {
// These utility functions convert a Flow crypto key to a LibP2P key (Flow --> LibP2P)

// PrivKey converts a Flow private key to a LibP2P Private key
func PrivKey(fpk fcrypto.PrivateKey) (lcrypto.PrivKey, error) {
func LibP2PPrivKeyFromFlow(fpk fcrypto.PrivateKey) (lcrypto.PrivKey, error) {
// get the signature algorithm
keyType, err := keyType(fpk.Algorithm())
if err != nil {
Expand Down Expand Up @@ -66,7 +75,7 @@ func PrivKey(fpk fcrypto.PrivateKey) (lcrypto.PrivKey, error) {
}

// PublicKey converts a Flow public key to a LibP2P public key
func PublicKey(fpk fcrypto.PublicKey) (lcrypto.PubKey, error) {
func LibP2PPublicKeyFromFlow(fpk fcrypto.PublicKey) (lcrypto.PubKey, error) {
keyType, err := keyType(fpk.Algorithm())
if err != nil {
return nil, err
Expand All @@ -92,13 +101,63 @@ func PublicKey(fpk fcrypto.PublicKey) (lcrypto.PubKey, error) {
}
} else if keyType == lcrypto_pb.KeyType_Secp256k1 {
bytes = make([]byte, crypto.PubKeyLenECDSASecp256k1+1) // libp2p requires an extra byte
bytes[0] = 4 // magic number in libp2p to refer to an uncompressed key
bytes[0] = 4 // signals uncompressed form as specified in section 4.3.6/7 of ANSI X9.62.
copy(bytes[1:], tempBytes)
}

return um(bytes)
}

// This converts some libp2p PubKeys to a flow PublicKey
// - the supported key types are ECDSA P-256 and ECDSA Secp256k1 public keys,
// - libp2p also supports RSA and Ed25519 keys, which Flow doesn't, their conversion will return an error.
func FlowPublicKeyFromLibP2P(lpk lcrypto.PubKey) (fcrypto.PublicKey, error) {

switch ktype := lpk.Type(); ktype {
case lcrypto_pb.KeyType_ECDSA:
pubB, err := lpk.Raw()
if err != nil {
return nil, lcrypto.ErrBadKeyType
}
key, err := x509.ParsePKIXPublicKey(pubB)
if err != nil {
// impossible to decode from ASN1.DER
return nil, lcrypto.ErrBadKeyType
}
cryptoKey, ok := key.(*goecdsa.PublicKey)
if !ok {
// not recognized as crypto.P-256
return nil, lcrypto.ErrNotECDSAPubKey
}
// ferrying through DecodePublicKey to get the curve checks
pk_uncompressed := elliptic.Marshal(cryptoKey, cryptoKey.X, cryptoKey.Y)
// the first bit is the compression bit of X9.62
pubKey, err := crypto.DecodePublicKey(crypto.ECDSAP256, pk_uncompressed[1:])
if err != nil {
return nil, lcrypto.ErrNotECDSAPubKey
}
return pubKey, nil
case lcrypto_pb.KeyType_Secp256k1:
// libp2p uses the compressed representation, flow the uncompressed one
lpk_secp256k1, ok := lpk.(*lcrypto.Secp256k1PublicKey)
if !ok {
return nil, lcrypto.ErrBadKeyType
}
secpBytes, err := lpk_secp256k1.Raw()
if err != nil { // this never errors
return nil, lcrypto.ErrBadKeyType
}
pk, err := crypto.DecodePublicKeyCompressed(crypto.ECDSASecp256k1, secpBytes)
if err != nil {
return nil, lcrypto.ErrNotECDSAPubKey
}
return pk, nil
default:
return nil, lcrypto.ErrBadKeyType
}

}

// keyType translates Flow signing algorithm constants to the corresponding LibP2P constants
func keyType(sa fcrypto.SigningAlgorithm) (lcrypto_pb.KeyType, error) {
switch sa {
Expand Down
33 changes: 30 additions & 3 deletions network/p2p/keyTranslator_test.go
Expand Up @@ -45,7 +45,7 @@ func (k *KeyTranslatorTestSuite) TestPrivateKeyConversion() {
require.NoError(k.T(), err)

// convert it to a LibP2P private key
lpk, err := PrivKey(fpk)
lpk, err := LibP2PPrivKeyFromFlow(fpk)
require.NoError(k.T(), err)

// get the raw bytes of both the keys
Expand Down Expand Up @@ -91,7 +91,7 @@ func (k *KeyTranslatorTestSuite) TestPublicKeyConversion() {
fpublic := fpk.PublicKey()

// convert the Flow public key to a Libp2p public key
lpublic, err := PublicKey(fpublic)
lpublic, err := LibP2PPublicKeyFromFlow(fpublic)
require.NoError(k.T(), err)

// compare raw bytes of the public keys
Expand All @@ -110,6 +110,33 @@ func (k *KeyTranslatorTestSuite) TestPublicKeyConversion() {
}
}

func (k *KeyTranslatorTestSuite) TestPublicKeyRoundTrip() {
sa := []fcrypto.SigningAlgorithm{fcrypto.ECDSAP256, fcrypto.ECDSASecp256k1}
loops := 50
for _, s := range sa {
for i := 0; i < loops; i++ {

// generate seed
seed := k.createSeed()
fpk, err := fcrypto.GeneratePrivateKey(s, seed)
require.NoError(k.T(), err)

// get the Flow public key
fpublic := fpk.PublicKey()

// convert the Flow public key to a Libp2p public key
lpublic, err := LibP2PPublicKeyFromFlow(fpublic)
require.NoError(k.T(), err)

fpublic2, err := FlowPublicKeyFromLibP2P(lpublic)
require.NoError(k.T(), err)
require.Equal(k.T(), fpublic, fpublic2)

}
}

}

// TestLibP2PIDGenerationIsConsistent tests that a LibP2P peer ID generated using Flow ECDSA key is deterministic
func (k *KeyTranslatorTestSuite) TestPeerIDGenerationIsConsistent() {
// generate a seed which will be used for both - Flow keys and Libp2p keys
Expand All @@ -123,7 +150,7 @@ func (k *KeyTranslatorTestSuite) TestPeerIDGenerationIsConsistent() {
fpublic := fpk.PublicKey()

// convert it to the Libp2p Public key
lconverted, err := PublicKey(fpublic)
lconverted, err := LibP2PPublicKeyFromFlow(fpublic)
require.NoError(k.T(), err)

// check that the LibP2P Id generation is deterministic
Expand Down

0 comments on commit 1b33269

Please sign in to comment.