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

[network, crypto] Compressed serialization, key conversions and message signing #1129

Merged
merged 10 commits into from Aug 17, 2021
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 {
huitseeker marked this conversation as resolved.
Show resolved Hide resolved
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)
huitseeker marked this conversation as resolved.
Show resolved Hide resolved
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)
huitseeker marked this conversation as resolved.
Show resolved Hide resolved
} 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)
huitseeker marked this conversation as resolved.
Show resolved Hide resolved
}

// 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
huitseeker marked this conversation as resolved.
Show resolved Hide resolved

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
huitseeker marked this conversation as resolved.
Show resolved Hide resolved
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
huitseeker marked this conversation as resolved.
Show resolved Hide resolved
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.
huitseeker marked this conversation as resolved.
Show resolved Hide resolved
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe making more distinct error types helps to spot the issue upon happening, i.e., this error type is the same as the next case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This error and the next will not actually happen: I'm marshalling the key material and then reinterpreting it as bytes using the underlying x509 library manually, because libp2p deprecated its Bytes function. This is just bypassing lack of access to private members.

}
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