-
Notifications
You must be signed in to change notification settings - Fork 176
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
Showing
22 changed files
with
562 additions
and
146 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.