github.com/slackhq/nebula@v1.9.0/cmd/nebula-cert/ca.go (about)

     1  package main
     2  
     3  import (
     4  	"crypto/ecdsa"
     5  	"crypto/elliptic"
     6  	"crypto/rand"
     7  	"flag"
     8  	"fmt"
     9  	"io"
    10  	"math"
    11  	"net"
    12  	"os"
    13  	"strings"
    14  	"time"
    15  
    16  	"github.com/skip2/go-qrcode"
    17  	"github.com/slackhq/nebula/cert"
    18  	"golang.org/x/crypto/ed25519"
    19  )
    20  
    21  type caFlags struct {
    22  	set              *flag.FlagSet
    23  	name             *string
    24  	duration         *time.Duration
    25  	outKeyPath       *string
    26  	outCertPath      *string
    27  	outQRPath        *string
    28  	groups           *string
    29  	ips              *string
    30  	subnets          *string
    31  	argonMemory      *uint
    32  	argonIterations  *uint
    33  	argonParallelism *uint
    34  	encryption       *bool
    35  
    36  	curve *string
    37  }
    38  
    39  func newCaFlags() *caFlags {
    40  	cf := caFlags{set: flag.NewFlagSet("ca", flag.ContinueOnError)}
    41  	cf.set.Usage = func() {}
    42  	cf.name = cf.set.String("name", "", "Required: name of the certificate authority")
    43  	cf.duration = cf.set.Duration("duration", time.Duration(time.Hour*8760), "Optional: amount of time the certificate should be valid for. Valid time units are seconds: \"s\", minutes: \"m\", hours: \"h\"")
    44  	cf.outKeyPath = cf.set.String("out-key", "ca.key", "Optional: path to write the private key to")
    45  	cf.outCertPath = cf.set.String("out-crt", "ca.crt", "Optional: path to write the certificate to")
    46  	cf.outQRPath = cf.set.String("out-qr", "", "Optional: output a qr code image (png) of the certificate")
    47  	cf.groups = cf.set.String("groups", "", "Optional: comma separated list of groups. This will limit which groups subordinate certs can use")
    48  	cf.ips = cf.set.String("ips", "", "Optional: comma separated list of ipv4 address and network in CIDR notation. This will limit which ipv4 addresses and networks subordinate certs can use for ip addresses")
    49  	cf.subnets = cf.set.String("subnets", "", "Optional: comma separated list of ipv4 address and network in CIDR notation. This will limit which ipv4 addresses and networks subordinate certs can use in subnets")
    50  	cf.argonMemory = cf.set.Uint("argon-memory", 2*1024*1024, "Optional: Argon2 memory parameter (in KiB) used for encrypted private key passphrase")
    51  	cf.argonParallelism = cf.set.Uint("argon-parallelism", 4, "Optional: Argon2 parallelism parameter used for encrypted private key passphrase")
    52  	cf.argonIterations = cf.set.Uint("argon-iterations", 1, "Optional: Argon2 iterations parameter used for encrypted private key passphrase")
    53  	cf.encryption = cf.set.Bool("encrypt", false, "Optional: prompt for passphrase and write out-key in an encrypted format")
    54  	cf.curve = cf.set.String("curve", "25519", "EdDSA/ECDSA Curve (25519, P256)")
    55  	return &cf
    56  }
    57  
    58  func parseArgonParameters(memory uint, parallelism uint, iterations uint) (*cert.Argon2Parameters, error) {
    59  	if memory <= 0 || memory > math.MaxUint32 {
    60  		return nil, newHelpErrorf("-argon-memory must be be greater than 0 and no more than %d KiB", uint32(math.MaxUint32))
    61  	}
    62  	if parallelism <= 0 || parallelism > math.MaxUint8 {
    63  		return nil, newHelpErrorf("-argon-parallelism must be be greater than 0 and no more than %d", math.MaxUint8)
    64  	}
    65  	if iterations <= 0 || iterations > math.MaxUint32 {
    66  		return nil, newHelpErrorf("-argon-iterations must be be greater than 0 and no more than %d", uint32(math.MaxUint32))
    67  	}
    68  
    69  	return cert.NewArgon2Parameters(uint32(memory), uint8(parallelism), uint32(iterations)), nil
    70  }
    71  
    72  func ca(args []string, out io.Writer, errOut io.Writer, pr PasswordReader) error {
    73  	cf := newCaFlags()
    74  	err := cf.set.Parse(args)
    75  	if err != nil {
    76  		return err
    77  	}
    78  
    79  	if err := mustFlagString("name", cf.name); err != nil {
    80  		return err
    81  	}
    82  	if err := mustFlagString("out-key", cf.outKeyPath); err != nil {
    83  		return err
    84  	}
    85  	if err := mustFlagString("out-crt", cf.outCertPath); err != nil {
    86  		return err
    87  	}
    88  	var kdfParams *cert.Argon2Parameters
    89  	if *cf.encryption {
    90  		if kdfParams, err = parseArgonParameters(*cf.argonMemory, *cf.argonParallelism, *cf.argonIterations); err != nil {
    91  			return err
    92  		}
    93  	}
    94  
    95  	if *cf.duration <= 0 {
    96  		return &helpError{"-duration must be greater than 0"}
    97  	}
    98  
    99  	var groups []string
   100  	if *cf.groups != "" {
   101  		for _, rg := range strings.Split(*cf.groups, ",") {
   102  			g := strings.TrimSpace(rg)
   103  			if g != "" {
   104  				groups = append(groups, g)
   105  			}
   106  		}
   107  	}
   108  
   109  	var ips []*net.IPNet
   110  	if *cf.ips != "" {
   111  		for _, rs := range strings.Split(*cf.ips, ",") {
   112  			rs := strings.Trim(rs, " ")
   113  			if rs != "" {
   114  				ip, ipNet, err := net.ParseCIDR(rs)
   115  				if err != nil {
   116  					return newHelpErrorf("invalid ip definition: %s", err)
   117  				}
   118  				if ip.To4() == nil {
   119  					return newHelpErrorf("invalid ip definition: can only be ipv4, have %s", rs)
   120  				}
   121  
   122  				ipNet.IP = ip
   123  				ips = append(ips, ipNet)
   124  			}
   125  		}
   126  	}
   127  
   128  	var subnets []*net.IPNet
   129  	if *cf.subnets != "" {
   130  		for _, rs := range strings.Split(*cf.subnets, ",") {
   131  			rs := strings.Trim(rs, " ")
   132  			if rs != "" {
   133  				_, s, err := net.ParseCIDR(rs)
   134  				if err != nil {
   135  					return newHelpErrorf("invalid subnet definition: %s", err)
   136  				}
   137  				if s.IP.To4() == nil {
   138  					return newHelpErrorf("invalid subnet definition: can only be ipv4, have %s", rs)
   139  				}
   140  				subnets = append(subnets, s)
   141  			}
   142  		}
   143  	}
   144  
   145  	var passphrase []byte
   146  	if *cf.encryption {
   147  		for i := 0; i < 5; i++ {
   148  			out.Write([]byte("Enter passphrase: "))
   149  			passphrase, err = pr.ReadPassword()
   150  
   151  			if err == ErrNoTerminal {
   152  				return fmt.Errorf("out-key must be encrypted interactively")
   153  			} else if err != nil {
   154  				return fmt.Errorf("error reading passphrase: %s", err)
   155  			}
   156  
   157  			if len(passphrase) > 0 {
   158  				break
   159  			}
   160  		}
   161  
   162  		if len(passphrase) == 0 {
   163  			return fmt.Errorf("no passphrase specified, remove -encrypt flag to write out-key in plaintext")
   164  		}
   165  	}
   166  
   167  	var curve cert.Curve
   168  	var pub, rawPriv []byte
   169  	switch *cf.curve {
   170  	case "25519", "X25519", "Curve25519", "CURVE25519":
   171  		curve = cert.Curve_CURVE25519
   172  		pub, rawPriv, err = ed25519.GenerateKey(rand.Reader)
   173  		if err != nil {
   174  			return fmt.Errorf("error while generating ed25519 keys: %s", err)
   175  		}
   176  	case "P256":
   177  		var key *ecdsa.PrivateKey
   178  		curve = cert.Curve_P256
   179  		key, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
   180  		if err != nil {
   181  			return fmt.Errorf("error while generating ecdsa keys: %s", err)
   182  		}
   183  
   184  		// ecdh.PrivateKey lets us get at the encoded bytes, even though
   185  		// we aren't using ECDH here.
   186  		eKey, err := key.ECDH()
   187  		if err != nil {
   188  			return fmt.Errorf("error while converting ecdsa key: %s", err)
   189  		}
   190  		rawPriv = eKey.Bytes()
   191  		pub = eKey.PublicKey().Bytes()
   192  	}
   193  
   194  	nc := cert.NebulaCertificate{
   195  		Details: cert.NebulaCertificateDetails{
   196  			Name:      *cf.name,
   197  			Groups:    groups,
   198  			Ips:       ips,
   199  			Subnets:   subnets,
   200  			NotBefore: time.Now(),
   201  			NotAfter:  time.Now().Add(*cf.duration),
   202  			PublicKey: pub,
   203  			IsCA:      true,
   204  			Curve:     curve,
   205  		},
   206  	}
   207  
   208  	if _, err := os.Stat(*cf.outKeyPath); err == nil {
   209  		return fmt.Errorf("refusing to overwrite existing CA key: %s", *cf.outKeyPath)
   210  	}
   211  
   212  	if _, err := os.Stat(*cf.outCertPath); err == nil {
   213  		return fmt.Errorf("refusing to overwrite existing CA cert: %s", *cf.outCertPath)
   214  	}
   215  
   216  	err = nc.Sign(curve, rawPriv)
   217  	if err != nil {
   218  		return fmt.Errorf("error while signing: %s", err)
   219  	}
   220  
   221  	var b []byte
   222  	if *cf.encryption {
   223  		b, err = cert.EncryptAndMarshalSigningPrivateKey(curve, rawPriv, passphrase, kdfParams)
   224  		if err != nil {
   225  			return fmt.Errorf("error while encrypting out-key: %s", err)
   226  		}
   227  	} else {
   228  		b = cert.MarshalSigningPrivateKey(curve, rawPriv)
   229  	}
   230  
   231  	err = os.WriteFile(*cf.outKeyPath, b, 0600)
   232  	if err != nil {
   233  		return fmt.Errorf("error while writing out-key: %s", err)
   234  	}
   235  
   236  	b, err = nc.MarshalToPEM()
   237  	if err != nil {
   238  		return fmt.Errorf("error while marshalling certificate: %s", err)
   239  	}
   240  
   241  	err = os.WriteFile(*cf.outCertPath, b, 0600)
   242  	if err != nil {
   243  		return fmt.Errorf("error while writing out-crt: %s", err)
   244  	}
   245  
   246  	if *cf.outQRPath != "" {
   247  		b, err = qrcode.Encode(string(b), qrcode.Medium, -5)
   248  		if err != nil {
   249  			return fmt.Errorf("error while generating qr code: %s", err)
   250  		}
   251  
   252  		err = os.WriteFile(*cf.outQRPath, b, 0600)
   253  		if err != nil {
   254  			return fmt.Errorf("error while writing out-qr: %s", err)
   255  		}
   256  	}
   257  
   258  	return nil
   259  }
   260  
   261  func caSummary() string {
   262  	return "ca <flags>: create a self signed certificate authority"
   263  }
   264  
   265  func caHelp(out io.Writer) {
   266  	cf := newCaFlags()
   267  	out.Write([]byte("Usage of " + os.Args[0] + " " + caSummary() + "\n"))
   268  	cf.set.SetOutput(out)
   269  	cf.set.PrintDefaults()
   270  }