Skip to content

Commit

Permalink
Merge #3788 #3789
Browse files Browse the repository at this point in the history
3788: [Crypto] KeyGen improvement r=tarakby a=tarakby

So far, key generation implementation assumes the input seed has uniformly distributed entropy. This is only valid if function callers use the output of a secure RNG as a seed. 
This PR relaxes the strong seed assumption and implements further mechanism to extract the input entropy and use it in the rest of the process. A seed with enough entropy is now sufficient to generate keys.


- update BLS key generation seed handling by implementing the[ IETF draft algorithm](https://www.ietf.org/archive/id/draft-irtf-cfrg-bls-signature-05.html#name-keygen) in section 2.3. 
   - this improves compatibility to IETF draft recommendations.
   - improves handling "bad" seeds with non uniform entropy.
   - a test against BLST key gen is added for compatibility check.
- update ECDSA key generation by using HKDF to extract entropy from input seed and expand it into key bytes. 
  - improves handling "bad" seeds with non uniform entropy.
- minor improvement by overwriting sensitive data in memory after computation.
- consolidate minimum seed length as a module constant instead of an algorithm specific length (consequence of the algorithm update) 
  
Side change:
 - add SHA2-256 light computation function (outside of the existing interface). 


3789: [Tools] Add bootstrap command to generate grpc TLS keys r=peterargue a=peterargue

This new command allows generating Access API TLS keys

Co-authored-by: Tarak Ben Youssef <tarak.benyoussef@dapperlabs.com>
Co-authored-by: Tarak Ben Youssef <50252200+tarakby@users.noreply.github.com>
Co-authored-by: Peter Argue <89119817+peterargue@users.noreply.github.com>
  • Loading branch information
4 people committed Feb 10, 2023
3 parents 102407d + 6e1351d + 465383f commit 524ce6c
Show file tree
Hide file tree
Showing 22 changed files with 562 additions and 146 deletions.
136 changes: 136 additions & 0 deletions cmd/bootstrap/cmd/access_keygen.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package cmd

import (
"crypto/ecdsa"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/json"
"encoding/pem"
"fmt"
"math/big"
"os"
"strings"
"time"

"github.com/spf13/cobra"

"github.com/onflow/flow-go/crypto"
"github.com/onflow/flow-go/model/bootstrap"
"github.com/onflow/flow-go/utils/grpcutils"
)

const certValidityPeriod = 100 * 365 * 24 * time.Hour // ~100 years

var (
flagSANs string
flagCommonName string
flagNodeInfoFile string
flagOutputKeyFile string
flagOutputCertFile string
)

var accessKeyCmd = &cobra.Command{
Use: "access-keygen",
Short: "Generate access node grpc TLS key and certificate",
Run: accessKeyCmdRun,
}

func init() {
rootCmd.AddCommand(accessKeyCmd)

accessKeyCmd.Flags().StringVar(&flagNodeInfoFile, "node-info", "", "path to node's node-info.priv.json file")
_ = accessKeyCmd.MarkFlagRequired("node-info")

accessKeyCmd.Flags().StringVar(&flagOutputKeyFile, "key", "./access-tls.key", "path to output private key file")
accessKeyCmd.Flags().StringVar(&flagOutputCertFile, "cert", "./access-tls.crt", "path to output certificate file")
accessKeyCmd.Flags().StringVar(&flagCommonName, "cn", "", "common name to include in the certificate")
accessKeyCmd.Flags().StringVar(&flagSANs, "sans", "", "subject alternative names to include in the certificate, comma separated")
}

// accessKeyCmdRun generate an Access node TLS key and certificate
func accessKeyCmdRun(_ *cobra.Command, _ []string) {
networkKey, err := loadNetworkKey(flagNodeInfoFile)
if err != nil {
log.Fatal().Msgf("could not load node-info file: %v", err)
}

certTmpl, err := defaultCertTemplate()
if err != nil {
log.Fatal().Msgf("could not create certificate template: %v", err)
}

if flagCommonName != "" {
log.Info().Msgf("using cn: %s", flagCommonName)
certTmpl.Subject.CommonName = flagCommonName
}

if flagSANs != "" {
log.Info().Msgf("using SANs: %s", flagSANs)
certTmpl.DNSNames = strings.Split(flagSANs, ",")
}

cert, err := grpcutils.X509Certificate(networkKey, grpcutils.WithCertTemplate(certTmpl))
if err != nil {
log.Fatal().Msgf("could not generate key pair: %v", err)
}

// write cert and private key to disk
keyBytes, err := x509.MarshalECPrivateKey(cert.PrivateKey.(*ecdsa.PrivateKey))
if err != nil {
log.Fatal().Msgf("could not encode private key: %v", err)
}

err = os.WriteFile(flagOutputKeyFile, pem.EncodeToMemory(&pem.Block{
Type: "EC PRIVATE KEY",
Bytes: keyBytes,
}), 0600)
if err != nil {
log.Fatal().Msgf("could not write private key: %v", err)
}

err = os.WriteFile(flagOutputCertFile, pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: cert.Certificate[0],
}), 0600)
if err != nil {
log.Fatal().Msgf("could not write certificate: %v", err)
}
}

func loadNetworkKey(nodeInfoPath string) (crypto.PrivateKey, error) {
data, err := os.ReadFile(nodeInfoPath)
if err != nil {
return nil, fmt.Errorf("could not read private node info (path=%s): %w", nodeInfoPath, err)
}

var info bootstrap.NodeInfoPriv
err = json.Unmarshal(data, &info)
if err != nil {
return nil, fmt.Errorf("could not parse private node info (path=%s): %w", nodeInfoPath, err)
}

return info.NetworkPrivKey.PrivateKey, nil
}

func defaultCertTemplate() (*x509.Certificate, error) {
bigNum := big.NewInt(1 << 62)
sn, err := rand.Int(rand.Reader, bigNum)
if err != nil {
return nil, err
}

subjectSN, err := rand.Int(rand.Reader, bigNum)
if err != nil {
return nil, err
}

return &x509.Certificate{
SerialNumber: sn,
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(certValidityPeriod),
// According to RFC 3280, the issuer field must be set,
// see https://datatracker.ietf.org/doc/html/rfc3280#section-4.1.2.4.
Subject: pkix.Name{SerialNumber: subjectSN.String()},
}, nil
}
95 changes: 95 additions & 0 deletions cmd/bootstrap/cmd/access_keygen_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package cmd

import (
"crypto/ecdsa"
"crypto/x509"
"encoding/asn1"
"encoding/pem"
"os"
"path/filepath"
"strings"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/onflow/flow-go/utils/unittest"
)

func TestAccessKeyFileCreated(t *testing.T) {
unittest.RunWithTempDir(t, func(bootDir string) {
hook := zeroLoggerHook{logs: &strings.Builder{}}
log = log.Hook(hook)

// generate test node keys
flagRole = "access"
flagAddress = "localhost:1234"
flagOutdir = bootDir

keyCmdRun(nil, nil)

// find the node-info.priv.json file
// the path includes a random hex string, so we need to find it
err := filepath.Walk(bootDir, func(path string, info os.FileInfo, err error) error {
if err == nil && info.Name() == "node-info.priv.json" {
flagNodeInfoFile = path
}
return nil
})
require.NoError(t, err)

sans := []string{"unittest1.onflow.org", "unittest2.onflow.org"}

flagSANs = strings.Join(sans, ",")
flagCommonName = "unittest.onflow.org"
flagOutputKeyFile = filepath.Join(bootDir, "test-access-key.key")
flagOutputCertFile = filepath.Join(bootDir, "test-access-key.cert")

// run command with flags
accessKeyCmdRun(nil, nil)

// make sure key/cert files exists (regex checks this too)
require.FileExists(t, flagOutputKeyFile)
require.FileExists(t, flagOutputCertFile)

// decode key and cert and make sure they match
keyData, err := os.ReadFile(flagOutputKeyFile)
require.NoError(t, err)

certData, err := os.ReadFile(flagOutputCertFile)
require.NoError(t, err)

privKey, cert := decodeKeys(t, keyData, certData)

// check that the public key from the cert matches the private key
ecdsaPubKey, ok := cert.PublicKey.(*ecdsa.PublicKey)
require.True(t, ok)
require.Equal(t, privKey.PublicKey, *ecdsaPubKey)

// check that the common name and subject alt names are correct
assert.Equal(t, flagCommonName, cert.Subject.CommonName, "expected %s, got %s", flagCommonName, cert.Subject.CommonName)
assert.Equal(t, flagCommonName, cert.Issuer.CommonName, "expected %s, got %s", flagCommonName, cert.Issuer.CommonName)
assert.ElementsMatch(t, sans, cert.DNSNames)

// check that the libp2p extension is present
found := false
for _, ext := range cert.Extensions {
if ext.Id.Equal(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 53594, 1, 1}) {
found = true
}
}
assert.True(t, found, "expected to find libp2p extension")
})
}

func decodeKeys(t *testing.T, pemEncoded []byte, pemEncodedPub []byte) (*ecdsa.PrivateKey, *x509.Certificate) {
block, _ := pem.Decode(pemEncoded)
privateKey, err := x509.ParseECPrivateKey(block.Bytes)
require.NoError(t, err)

blockPub, _ := pem.Decode(pemEncodedPub)
cert, err := x509.ParseCertificate(blockPub.Bytes)
require.NoError(t, err)

return privateKey, cert
}
82 changes: 59 additions & 23 deletions crypto/bls.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,12 @@ import "C"

import (
"bytes"
"crypto/sha256"
"errors"
"fmt"

"golang.org/x/crypto/hkdf"

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

Expand All @@ -60,14 +63,14 @@ const (
SignatureLenBLSBLS12381 = fieldSize * (2 - serializationG1) // the length is divided by 2 if compression is on
PrKeyLenBLSBLS12381 = 32
// PubKeyLenBLSBLS12381 is the size of G2 elements
PubKeyLenBLSBLS12381 = 2 * fieldSize * (2 - serializationG2) // the length is divided by 2 if compression is on
KeyGenSeedMinLenBLSBLS12381 = PrKeyLenBLSBLS12381 + (securityBits / 8)
KeyGenSeedMaxLenBLSBLS12381 = maxScalarSize
PubKeyLenBLSBLS12381 = 2 * fieldSize * (2 - serializationG2) // the length is divided by 2 if compression is on

// Hash to curve params
// expandMsgOutput is the output length of the expand_message step as required by the hash_to_curve algorithm
expandMsgOutput = 2 * (fieldSize + (securityBits / 8))
// hash to curve suite ID of the form : CurveID_ || HashID_ || MapID_ || encodingVariant_
h2cSuiteID = "BLS12381G1_XOF:KMAC128_SSWU_RO_"
// scheme implemented as a countermasure for Rogue attacks of the form : SchemeTag_
// scheme implemented as a countermasure for rogue attacks of the form : SchemeTag_
schemeTag = "POP_"
// Cipher suite used for BLS signatures of the form : BLS_SIG_ || h2cSuiteID || SchemeTag_
blsSigCipherSuite = "BLS_SIG_" + h2cSuiteID + schemeTag
Expand Down Expand Up @@ -116,10 +119,6 @@ func NewExpandMsgXOFKMAC128(domainTag string) hash.Hasher {
// the hash-to-curve function, hashing data to G1 on BLS12 381.
// The key is used as a customizer rather than a MAC key.
func internalExpandMsgXOFKMAC128(key string) hash.Hasher {
// UTF-8 is used by Go to convert strings into bytes.
// UTF-8 is a non-ambiguous encoding as required by draft-irtf-cfrg-hash-to-curve
// (similarly to the recommended ASCII).

// blsKMACFunction is the customizer used for KMAC in BLS
const blsKMACFunction = "H2C"
// the error is ignored as the parameter lengths are chosen to be in the correct range for kmac
Expand Down Expand Up @@ -250,30 +249,67 @@ func IsBLSSignatureIdentity(s Signature) bool {
return bytes.Equal(s, identityBLSSignature)
}

// generatePrivateKey generates a private key for BLS on BLS12-381 curve.
// The minimum size of the input seed is 48 bytes.
// generatePrivateKey deterministically generates a private key for BLS on BLS12-381 curve.
// The minimum size of the input seed is 32 bytes.
//
// It is recommended to use a secure crypto RNG to generate the seed.
// The seed must have enough entropy and should be sampled uniformly at random.
// Otherwise, the seed must have enough entropy.
//
// The generated private key (resp. its corresponding public key) are guaranteed
// not to be equal to the identity element of Z_r (resp. G2).
func (a *blsBLS12381Algo) generatePrivateKey(seed []byte) (PrivateKey, error) {
if len(seed) < KeyGenSeedMinLenBLSBLS12381 || len(seed) > KeyGenSeedMaxLenBLSBLS12381 {
// The generated private key (resp. its corresponding public key) is guaranteed
// to not be equal to the identity element of Z_r (resp. G2).
func (a *blsBLS12381Algo) generatePrivateKey(ikm []byte) (PrivateKey, error) {
if len(ikm) < KeyGenSeedMinLen || len(ikm) > KeyGenSeedMaxLen {
return nil, invalidInputsErrorf(
"seed length should be between %d and %d bytes",
KeyGenSeedMinLenBLSBLS12381,
KeyGenSeedMaxLenBLSBLS12381)
"seed length should be at least %d bytes and at most %d bytes",
KeyGenSeedMinLen, KeyGenSeedMaxLen)
}

// HKDF parameters

// use SHA2-256 as the building block H in HKDF
hashFunction := sha256.New
// salt = H(UTF-8("BLS-SIG-KEYGEN-SALT-")) as per draft-irtf-cfrg-bls-signature-05 section 2.3.
saltString := "BLS-SIG-KEYGEN-SALT-"
hasher := hashFunction()
hasher.Write([]byte(saltString))
salt := make([]byte, hasher.Size())
hasher.Sum(salt[:0])

// L is the OKM length
// L = ceil((3 * ceil(log2(r))) / 16) which makes L (security_bits/8)-larger than r size
okmLength := (3 * PrKeyLenBLSBLS12381) / 2

// HKDF secret = IKM || I2OSP(0, 1)
secret := make([]byte, len(ikm)+1)
copy(secret, ikm)
defer overwrite(secret) // overwrite secret
// HKDF info = key_info || I2OSP(L, 2)
keyInfo := "" // use empty key diversifier. TODO: update header to accept input identifier
info := append([]byte(keyInfo), byte(okmLength>>8), byte(okmLength))

sk := newPrKeyBLSBLS12381(nil)
for {
// instantiate HKDF and extract L bytes
reader := hkdf.New(hashFunction, secret, salt, info)
okm := make([]byte, okmLength)
n, err := reader.Read(okm)
if err != nil || n != okmLength {
return nil, fmt.Errorf("key generation failed because of the HKDF reader, %d bytes were read: %w",
n, err)
}
defer overwrite(okm) // overwrite okm

// maps the seed to a private key
err := mapToZrStar(&sk.scalar, seed)
if err != nil {
return nil, invalidInputsErrorf("private key generation failed %w", err)
// map the bytes to a private key : SK = OS2IP(OKM) mod r
isZero := mapToZr(&sk.scalar, okm)
if !isZero {
return sk, nil
}

// update salt = H(salt)
hasher.Reset()
hasher.Write(salt)
salt = hasher.Sum(salt[:0])
}
return sk, nil
}

const invalidBLSSignatureHeader = byte(0xE0)
Expand Down
Loading

0 comments on commit 524ce6c

Please sign in to comment.