236 changes: 236 additions & 0 deletions dgraph/cmd/cm/request.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
/*
* Copyright 2017-2018 Dgraph Labs, Inc.
*
* This file is available under the Apache License, Version 2.0,
* with the Commons Clause restriction.
*/

package cm

import (
"crypto"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/hex"
"encoding/pem"
"errors"
"math"
"math/big"
"net"
"os"
"time"
)

const (
dnOrganization = "Dgraph"
keySizeTooSmall = 512 // XXX: Microsoft is using 1024
keySizeTooLarge = 4096
validNotBefore = time.Hour * -24
)

type config struct {
parent *x509.Certificate
sign crypto.Signer
until int
ca bool
keySize int
force bool
hosts []string
user string
}

type configFunc func(*config) error

// Request describes a cert configuration.
type Request struct {
c config
}

// cfParent sets a parent cert (e.g., CA)
// Returns an error if it cant read the parent cert file.
func cfParent(s string) configFunc {
return func(c *config) error {
parent, err := readCert(s)
if err != nil {
return err
}
c.parent = parent
return nil
}
}

// cfSignKey sets the signing private key
// Returns an error if it cant read the private key file.
func cfSignKey(s string) configFunc {
return func(c *config) error {
key, err := readKey(s)
if err != nil {
return err
}
c.sign = key
return nil
}
}

// cfDuration sets the duration of cert validity
func cfDuration(i int) configFunc {
return func(c *config) error {
c.until = i
return nil
}
}

// cfCA sets that this is a CA cert
func cfCA(t bool) configFunc {
return func(c *config) error {
c.ca = t
return nil
}
}

// cfKeySize sets the key bit length.
// Returns an error if the value is too small or not divisable by 2.
func cfKeySize(i int) configFunc {
return func(c *config) error {
switch {
case i < keySizeTooSmall:
return errors.New("key size value is too small (< 512)")
case i > keySizeTooLarge:
return errors.New("key size value is too large (> 4096)")
case i%2 != 0:
return errors.New("key size value must be a factor of 2")
}
c.keySize = i
return nil
}
}

// cfOverwrite sets the overwrite flag used to create private keys.
func cfOverwrite(t bool) configFunc {
return func(c *config) error {
c.force = t
return nil
}
}

// cfHosts sets the hosts (ip addresses or hostnames) that are allowed in a
// node certificate.
// Returns an error if the list is empty.
func cfHosts(h ...string) configFunc {
return func(c *config) error {
if len(h) == 0 {
return errors.New("the hosts list is empty")
}
c.hosts = h
c.ca = false
return nil
}
}

// cfUser sets the user for a client certificate, using the Subject CommonName field.
func cfUser(s string) configFunc {
return func(c *config) error {
c.user = s
c.ca = false
return nil
}
}

// NewRequest returns a new cert request based on configuration. Each configuration
// value is tested. This function only creates a non-destructive request.
// Returns the new request, or an error otherwise.
func NewRequest(options ...configFunc) (*Request, error) {
var r Request

for _, f := range options {
if err := f(&r.c); err != nil {
return nil, err
}
}

return &r, nil
}

// 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 request values.
// It will generate two files, a key and cert, upon success.
// Returns nil on success, or an error otherwise.
func (r *Request) GeneratePair(keyFile, crtFile string) error {
key, err := makeKey(keyFile, r.c.keySize, r.c.force)
if err != nil {
return err
}

sn, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64))
if err != nil {
return err
}

template := &x509.Certificate{
Subject: pkix.Name{
Organization: []string{dnOrganization},
SerialNumber: hex.EncodeToString(sn.Bytes()[:3]),
},
SerialNumber: sn,
NotBefore: time.Now().AddDate(0, 0, -1),
NotAfter: time.Now().AddDate(0, 0, r.c.until),
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
BasicConstraintsValid: true,
IsCA: r.c.ca,
MaxPathLenZero: r.c.ca,
}

switch {
case r.c.ca:
template.Subject.CommonName = dnOrganization + " Root CA"
template.KeyUsage |= x509.KeyUsageContentCommitment | x509.KeyUsageCertSign

case r.c.hosts != nil:
template.Subject.CommonName = dnOrganization + " Node"
template.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}

for _, h := range r.c.hosts {
if ip := net.ParseIP(h); ip != nil {
template.IPAddresses = append(template.IPAddresses, ip)
} else {
template.DNSNames = append(template.DNSNames, h)
}
}

case r.c.user != "":
template.Subject.CommonName = r.c.user
template.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}
}

if r.c.sign == nil {
r.c.sign = key
}

if r.c.parent == nil {
r.c.parent = template
}

der, err := x509.CreateCertificate(rand.Reader,
template, r.c.parent, key.Public(), r.c.sign)
if err != nil {
return err
}

f, err := os.Create(crtFile)
if err != nil {
return err
}
defer f.Close()

err = pem.Encode(f, &pem.Block{
Type: "CERTIFICATE",
Bytes: der,
})
if err != nil {
return err
}

_, err = x509.ParseCertificate(der)
return err
}
141 changes: 141 additions & 0 deletions dgraph/cmd/cm/run.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/*
* Copyright 2017-2018 Dgraph Labs, Inc.
*
* This file is available under the Apache License, Version 2.0,
* with the Commons Clause restriction.
*/

package cm

import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"

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

var Cert x.SubCommand

func init() {
flagInit()

Cert.Cmd = &cobra.Command{
Use: "cm",
Short: "Dgraph certificate management",
}
Cert.Cmd.AddCommand(subcmds...)
Cert.EnvPrefix = "DGRAPH_CERT"
}

func runCreateCA() error {
return createCAPair(
opt.Dir,
opt.CAKey,
defaultCACert,
opt.KeySize,
opt.Days,
opt.Force,
)
}

func runCreateNode() error {
if opt.Nodes == nil || len(opt.Nodes) == 0 {
return errors.New("required at least one node (ip address or host)")
}

return createNodePair(
opt.Dir,
opt.CAKey,
defaultNodeCert,
opt.KeySize,
opt.Days,
opt.Force,
opt.Nodes,
)
}

func runCreateClient() error {
if opt.User == "" {
return errors.New("a user name is required")
}

return createClientPair(
opt.Dir,
opt.CAKey,
"",
opt.KeySize,
opt.Days,
opt.Force,
opt.User,
)
}

func runList() error {
var fileList [][4]string
var widths [4]int

if err := os.Chdir(opt.Dir); err != nil {
return err
}

max := func(a, b int) int {
if a > b {
return a
}
return b
}

fmt.Printf("Scanning: %s ...\n\n", opt.Dir)

err := filepath.Walk(".",
func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}

if info.IsDir() {
return nil
}

ci := certInfo(path)
dexp, mode := fmt.Sprintf("%x", ci), info.Mode().String()

fileList = append(fileList, [4]string{
ci.Name, path, mode, dexp,
})

widths[0] = max(widths[0], len(ci.Name))
widths[1] = max(widths[1], len(path))
widths[2] = max(widths[2], len(mode))
widths[3] = max(widths[3], len(dexp))

return nil
})
if err != nil {
return err
}

fmt.Printf("%-[2]*[1]s | %-[4]*[3]s | %-[6]*[5]s | %-[8]*[7]s\n",
"Name", widths[0],
"File", widths[1],
"Mode", widths[2],
"Expires", widths[3],
)

fmt.Printf("%s\n", strings.Repeat("=", widths[0]+widths[1]+widths[2]+widths[3]+9))

for i := range fileList {
fmt.Printf("%-[2]*[1]s | %-[4]*[3]s | %-[6]*[5]s | %-[8]*[7]s\n",
fileList[i][0], widths[0],
fileList[i][1], widths[1],
fileList[i][2], widths[2],
fileList[i][3], widths[3],
)
}

return nil
}
89 changes: 89 additions & 0 deletions dgraph/cmd/cm/subcmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* Copyright 2017-2018 Dgraph Labs, Inc.
*
* This file is available under the Apache License, Version 2.0,
* with the Commons Clause restriction.
*/

package cm

import (
"github.com/spf13/cobra"
)

const (
defaultDir = "tls"
defaultDays = 1826
defaultCACert = "ca.crt"
defaultCAKey = "ca.key"
defaultKeySize = 2048
defaultNodeCert = "node.crt"
defaultNodeKey = "node.key"
)

var opt struct {
Dir, CAKey, User string
Force bool
KeySize, Days int
Nodes []string
}

var subcmds = []*cobra.Command{
&cobra.Command{
Use: "make-ca",
Short: "create root CA certificate and key",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return runCreateCA()
},
},
&cobra.Command{
Use: "make-node",
Short: "create node certificates and keys",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return runCreateNode()
},
},
&cobra.Command{
Use: "make-client",
Short: "create client a certificate and key",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return runCreateClient()
},
},
&cobra.Command{
Use: "list",
Short: "list certificates and keys",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return runList()
},
},
}

func flagInit() {
for i := range subcmds {
flag := subcmds[i].Flags()
flag.StringVarP(&opt.Dir, "dir", "d", defaultDir,
"directory where to store TLS certs and keys")

// list only needs the dir
if subcmds[i].Use == "list" {
continue
}

flag.StringVarP(&opt.CAKey, "ca-key", "k", defaultCAKey, "path to the CA private key")
flag.BoolVar(&opt.Force, "force", false, "overwrite any existing key and cert")
flag.IntVar(&opt.KeySize, "key-size", defaultKeySize, "RSA key bit size")
flag.IntVar(&opt.Days, "duration", defaultDays, "duration of cert validity in days")

switch subcmds[i].Use {
case "make-node":
flag.StringSliceVarP(&opt.Nodes, "nodes", "n", nil, "node0 ... nodeN (ipaddr | host)")
case "make-client":
flag.StringVarP(&opt.User, "user", "u", "", "user name")
}
}
}
2 changes: 1 addition & 1 deletion dgraph/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ func init() {
flag.CommandLine.AddGoFlagSet(goflag.CommandLine)

var subcommands = []*x.SubCommand{
&bulk.Bulk, &live.Live, &server.Server, &zero.Zero, &version.Version, &debug.Debug,
&bulk.Bulk, &cert.Cert, &live.Live, &server.Server, &zero.Zero, &version.Version, &debug.Debug,
}
for _, sc := range subcommands {
RootCmd.AddCommand(sc.Cmd)
Expand Down