Skip to content

Commit

Permalink
Add support for ECDSA in dgraph cert (dgraph-io#3269)
Browse files Browse the repository at this point in the history
Add support for ECDSA keys in dgraph cert and update cert API for adding more encryption types.
  • Loading branch information
srfrog authored and dna2github committed Jul 19, 2019
1 parent b62645d commit 2b19978
Show file tree
Hide file tree
Showing 4 changed files with 127 additions and 53 deletions.
10 changes: 6 additions & 4 deletions dgraph/cmd/cert/cert.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,19 @@ type certConfig struct {
force bool
hosts []string
client string
curve string
}

// generatePair makes a new key/cert pair from a request. This function
// will do a best guess of the cert to create based on the certConfig values.
// It will generate two files, a key and cert, upon success.
// Returns nil on success, or an error otherwise.
func (c *certConfig) generatePair(keyFile, certFile string) error {
key, err := makeKey(keyFile, c.keySize, c.force)
priv, err := makeKey(keyFile, c)
if err != nil {
return err
}
key := priv.(crypto.Signer)

sn, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64))
if err != nil {
Expand Down Expand Up @@ -117,17 +119,17 @@ func (c *certConfig) generatePair(keyFile, certFile string) error {
return err
}

f, err := safeCreate(certFile, c.force, 0666)
fp, err := safeCreate(certFile, c.force, 0666)
if err != nil {
// check the existing cert.
if os.IsExist(err) {
_, err = readCert(certFile)
}
return err
}
defer f.Close()
defer fp.Close()

err = pem.Encode(f, &pem.Block{
err = pem.Encode(fp, &pem.Block{
Type: "CERTIFICATE",
Bytes: der,
})
Expand Down
115 changes: 80 additions & 35 deletions dgraph/cmd/cert/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
package cert

import (
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
Expand All @@ -27,6 +30,8 @@ import (
"os"
"path"
"path/filepath"

"github.com/dgraph-io/dgraph/x"
)

const (
Expand All @@ -42,40 +47,63 @@ const (
keySizeTooLarge = 4096
)

// makeKey generates an RSA private key of bitSize length, storing it in the
// file fn. If force is true, the file is replaced.
// Returns the RSA private key, or error otherwise.
func makeKey(fn string, bitSize int, force bool) (*rsa.PrivateKey, error) {
f, err := safeCreate(fn, force, 0600)
// makeKey generates an RSA or ECDSA private key using the configuration in 'c'.
// The new private key is stored in the path at 'keyFile'.
// If force is true, any existing file at the path is replaced.
// For RSA, the configuration keySize is used for length.
// For ECDSA, the configuration elliptical curve is used.
// Returns the RSA or ECDSA private key, or error otherwise.
func makeKey(keyFile string, c *certConfig) (crypto.PrivateKey, error) {
fp, err := safeCreate(keyFile, c.force, 0600)
if err != nil {
// reuse the existing key, if possible.
if os.IsExist(err) {
return readKey(fn)
return readKey(keyFile)
}
return nil, err
}
defer f.Close()
defer fp.Close()

key, err := rsa.GenerateKey(rand.Reader, bitSize)
if err != nil {
return nil, err
var key crypto.PrivateKey
switch c.curve {
case "":
key, err = rsa.GenerateKey(rand.Reader, c.keySize)
case "P224":
key, err = ecdsa.GenerateKey(elliptic.P224(), rand.Reader)
case "P256":
key, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
case "P384":
key, err = ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
case "P521":
key, err = ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
}

err = pem.Encode(f, &pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(key),
})
if err != nil {
return nil, err
}

return key, nil
switch k := key.(type) {
case *ecdsa.PrivateKey:
b, err := x509.MarshalECPrivateKey(k)
if err != nil {
return nil, err
}
return key, pem.Encode(fp, &pem.Block{
Type: "EC PRIVATE KEY",
Bytes: b,
})
case *rsa.PrivateKey:
return key, pem.Encode(fp, &pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(k),
})
}
return nil, x.Errorf("Unsupported key type: %T", key)
}

// readKey tries to read and decode the contents of a private key at fn.
// Returns the RSA private key, or error otherwise.
func readKey(fn string) (*rsa.PrivateKey, error) {
b, err := ioutil.ReadFile(fn)
// readKey tries to read and decode the contents of a private key file.
// Returns the private key, or error otherwise.
func readKey(keyFile string) (crypto.PrivateKey, error) {
b, err := ioutil.ReadFile(keyFile)
if err != nil {
return nil, err
}
Expand All @@ -84,17 +112,18 @@ func readKey(fn string) (*rsa.PrivateKey, error) {
switch {
case block == nil:
return nil, fmt.Errorf("Failed to read key block")
case block.Type != "RSA PRIVATE KEY":
return nil, fmt.Errorf("Unknown PEM type: %s", block.Type)
case block.Type == "EC PRIVATE KEY":
return x509.ParseECPrivateKey(block.Bytes)
case block.Type == "RSA PRIVATE KEY":
return x509.ParsePKCS1PrivateKey(block.Bytes)
}

return x509.ParsePKCS1PrivateKey(block.Bytes)
return nil, fmt.Errorf("Unknown PEM type: %s", block.Type)
}

// readCert tries to read and decode the contents of an RSA-signed cert at fn.
// readCert tries to read and decode the contents of a signed cert file.
// Returns the x509v3 cert, or error otherwise.
func readCert(fn string) (*x509.Certificate, error) {
b, err := ioutil.ReadFile(fn)
func readCert(certFile string) (*x509.Certificate, error) {
b, err := ioutil.ReadFile(certFile)
if err != nil {
return nil, err
}
Expand All @@ -111,12 +140,12 @@ func readCert(fn string) (*x509.Certificate, error) {
}

// safeCreate only creates a file if it doesn't exist or we force overwrite.
func safeCreate(fn string, overwrite bool, perm os.FileMode) (*os.File, error) {
func safeCreate(name string, overwrite bool, perm os.FileMode) (*os.File, error) {
flag := os.O_WRONLY | os.O_CREATE | os.O_TRUNC
if !overwrite {
flag |= os.O_EXCL
}
return os.OpenFile(fn, flag, perm)
return os.OpenFile(name, flag, perm)
}

// createCAPair creates a CA certificate and key pair. The key file is created only
Expand All @@ -129,6 +158,7 @@ func createCAPair(opt options) error {
until: defaultCADays,
keySize: opt.keySize,
force: opt.force,
curve: opt.curve,
}
if err := cc.generatePair(opt.caKey, opt.caCert); err != nil {
return err
Expand All @@ -152,16 +182,20 @@ func createNodePair(opt options) error {
keySize: opt.keySize,
force: opt.force,
hosts: opt.nodes,
curve: opt.curve,
}

var err error
cc.parent, err = readCert(opt.caCert)
if err != nil {
return err
}
cc.signer, err = readKey(opt.caKey)
if err != nil {
return err
{
priv, err := readKey(opt.caKey)
if err != nil {
return err
}
cc.signer = priv.(crypto.Signer)
}

certFile := filepath.Join(opt.dir, defaultNodeCert)
Expand All @@ -188,16 +222,20 @@ func createClientPair(opt options) error {
keySize: opt.keySize,
force: opt.force,
client: opt.client,
curve: opt.curve,
}

var err error
cc.parent, err = readCert(opt.caCert)
if err != nil {
return err
}
cc.signer, err = readKey(opt.caKey)
if err != nil {
return err
{
priv, err := readKey(opt.caKey)
if err != nil {
return err
}
cc.signer = priv.(crypto.Signer)
}

certFile := filepath.Join(opt.dir, fmt.Sprint("client.", opt.client, ".crt"))
Expand Down Expand Up @@ -229,6 +267,13 @@ func createCerts(opt options) error {
return errors.New("Key size value must be a factor of 2")
}

switch opt.curve {
case "":
case "P224", "P256", "P384", "P521":
default:
return errors.New(`Elliptic curve value must be one of: P224, P256, P384 or P521`)
}

// no path then save it in certsDir.
if path.Base(opt.caKey) == opt.caKey {
opt.caKey = filepath.Join(opt.dir, opt.caKey)
Expand Down
31 changes: 26 additions & 5 deletions dgraph/cmd/cert/info.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
package cert

import (
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rsa"
"crypto/sha256"
"encoding/hex"
Expand All @@ -25,6 +28,8 @@ import (
"path/filepath"
"strings"
"time"

"github.com/dgraph-io/dgraph/x"
)

type certInfo struct {
Expand All @@ -34,6 +39,7 @@ type certInfo struct {
serialNumber string
verifiedCA string
digest string
algo string
expireDate time.Time
hosts []string
fileMode string
Expand Down Expand Up @@ -75,10 +81,13 @@ func getFileInfo(file string) *certInfo {
return &info
}

if key, ok := cert.PublicKey.(*rsa.PublicKey); ok {
switch key := cert.PublicKey.(type) {
case *rsa.PublicKey:
info.digest = getHexDigest(key.N.Bytes())
} else {
info.digest = "Invalid RSA public key"
case *ecdsa.PublicKey:
info.digest = getHexDigest(elliptic.Marshal(key.Curve, key.X, key.Y))
default:
info.digest = "Invalid public key"
}

if file != defaultCACert {
Expand Down Expand Up @@ -109,12 +118,24 @@ func getFileInfo(file string) *certInfo {
return &info
}

key, err := readKey(file)
priv, err := readKey(file)
if err != nil {
info.err = err
return &info
}
info.digest = getHexDigest(key.PublicKey.N.Bytes())
key, ok := priv.(crypto.Signer)
if !ok {
info.err = x.Errorf("Unknown private key type: %T", key)
}
switch k := key.(type) {
case *ecdsa.PrivateKey:
info.algo = fmt.Sprintf("ECDSA %s (FIPS-3)", k.PublicKey.Curve.Params().Name)
info.digest = getHexDigest(elliptic.Marshal(k.PublicKey.Curve,
k.PublicKey.X, k.PublicKey.Y))
case *rsa.PrivateKey:
info.algo = fmt.Sprintf("RSA %d bits (PKCS#1)", k.PublicKey.N.BitLen())
info.digest = getHexDigest(k.PublicKey.N.Bytes())
}

default:
info.err = fmt.Errorf("Unsupported file")
Expand Down
24 changes: 15 additions & 9 deletions dgraph/cmd/cert/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ import (
var Cert x.SubCommand

type options struct {
dir, caKey, caCert, client string
force, verify bool
keySize, days int
nodes []string
dir, caKey, caCert, client, curve string
force, verify bool
keySize, days int
nodes []string
}

var opt options
Expand All @@ -40,16 +40,18 @@ func init() {
Use: "cert",
Short: "Dgraph TLS certificate management",
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
RunE: func(cmd *cobra.Command, args []string) error {
defer x.StartProfile(Cert.Conf).Stop()
run()
return run()
},
}

flag := Cert.Cmd.Flags()
flag.StringP("dir", "d", defaultDir, "directory containing TLS certs and keys")
flag.StringP("ca-key", "k", defaultCAKey, "path to the CA private key")
flag.Int("keysize", defaultKeySize, "RSA key bit size for creating new keys")
flag.IntP("keysize", "r", defaultKeySize, "RSA key bit size for creating new keys")
flag.StringP("elliptic-curve", "e", "",
`ECDSA curve for private key. Values are: "P224", "P256", "P384", "P521".`)
flag.Int("duration", defaultDays, "duration of cert validity in days")
flag.StringSliceP("nodes", "n", nil, "creates cert/key pair for nodes")
flag.StringP("client", "c", "", "create cert/key pair for a client name")
Expand All @@ -68,7 +70,7 @@ func init() {
Cert.Cmd.AddCommand(cmdList)
}

func run() {
func run() error {
opt = options{
dir: Cert.Conf.GetString("dir"),
caKey: Cert.Conf.GetString("ca-key"),
Expand All @@ -78,9 +80,10 @@ func run() {
nodes: Cert.Conf.GetStringSlice("nodes"),
force: Cert.Conf.GetBool("force"),
verify: Cert.Conf.GetBool("verify"),
curve: Cert.Conf.GetString("elliptic-curve"),
}

x.Check(createCerts(opt))
return createCerts(opt)
}

// listCerts handles the subcommand of "dgraph cert ls".
Expand Down Expand Up @@ -135,6 +138,9 @@ func listCerts() error {
if f.hosts != nil {
fmt.Printf("%14s: %s\n", "Hosts", strings.Join(f.hosts, ", "))
}
if f.algo != "" {
fmt.Printf("%14s: %s\n", "Algorithm", f.algo)
}
fmt.Printf("%14s: %s\n\n", "SHA-256 Digest", f.digest)
}

Expand Down

0 comments on commit 2b19978

Please sign in to comment.