1package create
2
3import (
4	"crypto/x509"
5	"flag"
6	"fmt"
7	"io/ioutil"
8	"net"
9	"strings"
10
11	"github.com/hashicorp/consul/command/flags"
12	"github.com/hashicorp/consul/command/tls"
13	"github.com/hashicorp/consul/lib/file"
14	"github.com/hashicorp/consul/tlsutil"
15	"github.com/mitchellh/cli"
16)
17
18func New(ui cli.Ui) *cmd {
19	c := &cmd{UI: ui}
20	c.init()
21	return c
22}
23
24type cmd struct {
25	UI          cli.Ui
26	flags       *flag.FlagSet
27	ca          string
28	key         string
29	server      bool
30	client      bool
31	cli         bool
32	dc          string
33	days        int
34	domain      string
35	help        string
36	node        string
37	dnsnames    flags.AppendSliceValue
38	ipaddresses flags.AppendSliceValue
39	prefix      string
40}
41
42func (c *cmd) init() {
43	c.flags = flag.NewFlagSet("", flag.ContinueOnError)
44	c.flags.StringVar(&c.ca, "ca", "#DOMAIN#-agent-ca.pem", "Provide path to the ca. Defaults to #DOMAIN#-agent-ca.pem.")
45	c.flags.StringVar(&c.key, "key", "#DOMAIN#-agent-ca-key.pem", "Provide path to the key. Defaults to #DOMAIN#-agent-ca-key.pem.")
46	c.flags.BoolVar(&c.server, "server", false, "Generate server certificate.")
47	c.flags.BoolVar(&c.client, "client", false, "Generate client certificate.")
48	c.flags.StringVar(&c.node, "node", "", "When generating a server cert and this is set an additional dns name is included of the form <node>.server.<datacenter>.<domain>.")
49	c.flags.BoolVar(&c.cli, "cli", false, "Generate cli certificate.")
50	c.flags.IntVar(&c.days, "days", 365, "Provide number of days the certificate is valid for from now on. Defaults to 1 year.")
51	c.flags.StringVar(&c.dc, "dc", "dc1", "Provide the datacenter. Matters only for -server certificates. Defaults to dc1.")
52	c.flags.StringVar(&c.domain, "domain", "consul", "Provide the domain. Matters only for -server certificates.")
53	c.flags.Var(&c.dnsnames, "additional-dnsname", "Provide an additional dnsname for Subject Alternative Names. "+
54		"localhost is always included. This flag may be provided multiple times.")
55	c.flags.Var(&c.ipaddresses, "additional-ipaddress", "Provide an additional ipaddress for Subject Alternative Names. "+
56		"127.0.0.1 is always included. This flag may be provided multiple times.")
57	c.help = flags.Usage(help, c.flags)
58}
59
60func (c *cmd) Run(args []string) int {
61	if err := c.flags.Parse(args); err != nil {
62		if err == flag.ErrHelp {
63			return 0
64		}
65		c.UI.Error(fmt.Sprintf("Failed to parse args: %v", err))
66		return 1
67	}
68	if c.ca == "" {
69		c.UI.Error("Please provide the ca")
70		return 1
71	}
72	if c.key == "" {
73		c.UI.Error("Please provide the key")
74		return 1
75	}
76
77	if !((c.server && !c.client && !c.cli) ||
78		(!c.server && c.client && !c.cli) ||
79		(!c.server && !c.client && c.cli)) {
80		c.UI.Error("Please provide either -server, -client, or -cli")
81		return 1
82	}
83
84	if c.node != "" && !c.server {
85		c.UI.Error("-node requires -server")
86		return 1
87	}
88
89	var DNSNames []string
90	var IPAddresses []net.IP
91	var extKeyUsage []x509.ExtKeyUsage
92	var name, prefix string
93
94	for _, d := range c.dnsnames {
95		if len(d) > 0 {
96			DNSNames = append(DNSNames, strings.TrimSpace(d))
97		}
98	}
99
100	for _, i := range c.ipaddresses {
101		if len(i) > 0 {
102			IPAddresses = append(IPAddresses, net.ParseIP(strings.TrimSpace(i)))
103		}
104	}
105
106	if c.server {
107		name = fmt.Sprintf("server.%s.%s", c.dc, c.domain)
108
109		if c.node != "" {
110			nodeName := fmt.Sprintf("%s.server.%s.%s", c.node, c.dc, c.domain)
111			DNSNames = append(DNSNames, nodeName)
112		}
113		DNSNames = append(DNSNames, name)
114		DNSNames = append(DNSNames, "localhost")
115
116		IPAddresses = append(IPAddresses, net.ParseIP("127.0.0.1"))
117		extKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}
118		prefix = fmt.Sprintf("%s-server-%s", c.dc, c.domain)
119
120	} else if c.client {
121		name = fmt.Sprintf("client.%s.%s", c.dc, c.domain)
122		DNSNames = append(DNSNames, []string{name, "localhost"}...)
123		IPAddresses = append(IPAddresses, net.ParseIP("127.0.0.1"))
124		extKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}
125		prefix = fmt.Sprintf("%s-client-%s", c.dc, c.domain)
126	} else if c.cli {
127		name = fmt.Sprintf("cli.%s.%s", c.dc, c.domain)
128		DNSNames = []string{name, "localhost"}
129		prefix = fmt.Sprintf("%s-cli-%s", c.dc, c.domain)
130	} else {
131		c.UI.Error("Neither client, cli nor server - should not happen")
132		return 1
133	}
134
135	var pkFileName, certFileName string
136	max := 10000
137	for i := 0; i <= max; i++ {
138		tmpCert := fmt.Sprintf("%s-%d.pem", prefix, i)
139		tmpPk := fmt.Sprintf("%s-%d-key.pem", prefix, i)
140		if tls.FileDoesNotExist(tmpCert) && tls.FileDoesNotExist(tmpPk) {
141			certFileName = tmpCert
142			pkFileName = tmpPk
143			break
144		}
145		if i == max {
146			c.UI.Error("Could not find a filename that doesn't already exist")
147			return 1
148		}
149	}
150
151	caFile := strings.Replace(c.ca, "#DOMAIN#", c.domain, 1)
152	keyFile := strings.Replace(c.key, "#DOMAIN#", c.domain, 1)
153	cert, err := ioutil.ReadFile(caFile)
154	if err != nil {
155		c.UI.Error(fmt.Sprintf("Error reading CA: %s", err))
156		return 1
157	}
158	key, err := ioutil.ReadFile(keyFile)
159	if err != nil {
160		c.UI.Error(fmt.Sprintf("Error reading CA key: %s", err))
161		return 1
162	}
163
164	if c.server {
165		c.UI.Info(
166			`==> WARNING: Server Certificates grants authority to become a
167    server and access all state in the cluster including root keys
168    and all ACL tokens. Do not distribute them to production hosts
169    that are not server nodes. Store them as securely as CA keys.`)
170	}
171	c.UI.Info("==> Using " + caFile + " and " + keyFile)
172
173	signer, err := tlsutil.ParseSigner(string(key))
174	if err != nil {
175		c.UI.Error(err.Error())
176		return 1
177	}
178
179	pub, priv, err := tlsutil.GenerateCert(tlsutil.CertOpts{
180		Signer: signer, CA: string(cert), Name: name, Days: c.days,
181		DNSNames: DNSNames, IPAddresses: IPAddresses, ExtKeyUsage: extKeyUsage,
182	})
183	if err != nil {
184		c.UI.Error(err.Error())
185		return 1
186	}
187
188	if err = tlsutil.Verify(string(cert), pub, name); err != nil {
189		c.UI.Error(err.Error())
190		return 1
191	}
192
193	if err := file.WriteAtomicWithPerms(certFileName, []byte(pub), 0755, 0666); err != nil {
194		c.UI.Error(err.Error())
195		return 1
196	}
197	c.UI.Output("==> Saved " + certFileName)
198
199	if err := file.WriteAtomicWithPerms(pkFileName, []byte(priv), 0755, 0666); err != nil {
200		c.UI.Error(err.Error())
201		return 1
202	}
203	c.UI.Output("==> Saved " + pkFileName)
204
205	return 0
206}
207
208func (c *cmd) Synopsis() string {
209	return synopsis
210}
211
212func (c *cmd) Help() string {
213	return c.help
214}
215
216const synopsis = "Create a new certificate"
217const help = `
218Usage: consul tls cert create [options]
219
220  Create a new certificate
221
222  $ consul tls cert create -server
223  ==> WARNING: Server Certificates grants authority to become a
224      server and access all state in the cluster including root keys
225      and all ACL tokens. Do not distribute them to production hosts
226      that are not server nodes. Store them as securely as CA keys.
227  ==> Using consul-agent-ca.pem and consul-agent-ca-key.pem
228  ==> Saved dc1-server-consul-0.pem
229  ==> Saved dc1-server-consul-0-key.pem
230  $ consul tls cert create -client
231  ==> Using consul-agent-ca.pem and consul-agent-ca-key.pem
232  ==> Saved dc1-client-consul-0.pem
233  ==> Saved dc1-client-consul-0-key.pem
234`
235