Browse Source

Initial import

Wijnand Modderman-Lenstra 2 years ago
commit
e3817f68c4
11 changed files with 1285 additions and 0 deletions
  1. 6
    0
      README.md
  2. 418
    0
      checker.go
  3. 1
    0
      config.go
  4. 2
    0
      hook/dump.sh
  5. 21
    0
      hook/mail.sh
  6. 12
    0
      hook/reload-haproxy.sh
  7. 173
    0
      lead.go
  8. 46
    0
      logger.go
  9. 260
    0
      manager.go
  10. 103
    0
      server.go
  11. 243
    0
      util.go

+ 6
- 0
README.md View File

@@ -0,0 +1,6 @@
1
+lead: LetsEncrypt Automation Daemon
2
+===================================
3
+
4
+Lead automagically requests configured certificates and keeps them up to date.
5
+
6
+Requests received on HTTP will be redirected to their HTTPS ports.

+ 418
- 0
checker.go View File

@@ -0,0 +1,418 @@
1
+package main
2
+
3
+import (
4
+	"bytes"
5
+	"context"
6
+	"crypto/rsa"
7
+	"crypto/tls"
8
+	"crypto/x509"
9
+	"encoding/pem"
10
+	"fmt"
11
+	"io"
12
+	"io/ioutil"
13
+	"log"
14
+	"os"
15
+	"os/exec"
16
+	"path"
17
+	"path/filepath"
18
+	"runtime"
19
+	"strings"
20
+	"sync"
21
+	"time"
22
+
23
+	"github.com/google/shlex"
24
+)
25
+
26
+const timeFormat = "2006-02-01 15:04"
27
+
28
+type Checker struct {
29
+	config  *Config
30
+	manager *Manager
31
+
32
+	bundles []*Bundle
33
+	pointer int
34
+}
35
+
36
+func NewChecker(config *Config, manager *Manager) (*Checker, error) {
37
+	c := &Checker{
38
+		config:  config,
39
+		manager: manager,
40
+		bundles: make([]*Bundle, len(config.Site)),
41
+	}
42
+
43
+	for name, site := range config.Site {
44
+		names := []string{name}
45
+		for _, san := range site.SAN {
46
+			if san == name {
47
+				continue
48
+			}
49
+			names = append(names, san)
50
+		}
51
+
52
+		var (
53
+			keyFile    = filepath.Join(config.Keys, name+".key")
54
+			bundleFile = filepath.Join(config.Bundles, name+".pem")
55
+			bundle     *Bundle
56
+			err        error
57
+		)
58
+		if bundle, err = NewBundle(keyFile, bundleFile, site.KeySize, names); err != nil {
59
+			return nil, err
60
+		}
61
+
62
+		if bundle.BeforeIssue, err = newHook(pick(site.BeforeIssue, config.BeforeIssue)); err != nil {
63
+			return nil, err
64
+		}
65
+		if bundle.BeforeRenew, err = newHook(pick(site.BeforeRenew, config.BeforeRenew)); err != nil {
66
+			return nil, err
67
+		}
68
+		if bundle.AfterIssue, err = newHook(pick(site.AfterIssue, config.AfterIssue)); err != nil {
69
+			return nil, err
70
+		}
71
+		if bundle.AfterRenew, err = newHook(pick(site.AfterRenew, config.AfterRenew)); err != nil {
72
+			return nil, err
73
+		}
74
+		bundle.Env = merge(config.Env, site.Env)
75
+		bundle.EnvSet = merge(config.EnvSet, site.EnvSet)
76
+
77
+		log.Printf("checker: adding %s (alternate=%v)", bundle, bundle.Domains[1:])
78
+		c.bundles[c.pointer] = bundle
79
+		c.pointer++
80
+	}
81
+
82
+	return c, nil
83
+}
84
+
85
+func (c *Checker) Run() {
86
+	time.Sleep(checkDelay)
87
+	c.run()
88
+
89
+	// Start periodic checker.
90
+	tick := time.NewTicker(checkInterval)
91
+	for {
92
+		<-tick.C
93
+		c.run()
94
+	}
95
+}
96
+
97
+// run checkers
98
+func (c *Checker) run() {
99
+	var l = len(c.bundles)
100
+	log.Printf("checker: checking %d bundles", l)
101
+	for i := 0; i < l; i++ {
102
+		c.pointer++
103
+		if c.pointer >= l {
104
+			c.pointer = 0
105
+		}
106
+		c.check(c.pointer)
107
+	}
108
+	log.Printf("checker: next check at %s", time.Now().Add(checkInterval).Format(timeFormat))
109
+}
110
+
111
+// check and recover from panics
112
+func (c *Checker) check(pointer int) {
113
+	defer func() {
114
+		if err := recover(); err != nil {
115
+			log.Printf("checker: recover from panic: %s", err)
116
+			trace := make([]byte, 1024)
117
+			count := runtime.Stack(trace, true)
118
+			log.Printf("checker: stack of %d bytes:\n%s", count, string(trace))
119
+		}
120
+	}()
121
+
122
+	bundle := c.bundles[pointer]
123
+	log.Printf("checker: check bundle %s", bundle)
124
+
125
+	if bundle.NeedsIssue() {
126
+		bundle.Issue(c.manager)
127
+	} else if bundle.NeedsRenew() {
128
+		bundle.Renew(c.manager)
129
+	} else {
130
+		log.Printf("checker: %s: ok", bundle)
131
+	}
132
+}
133
+
134
+type Hook interface {
135
+	Run(stdout, stderr io.Writer, env []string, args ...string) bool
136
+}
137
+
138
+// Bundle is a keypair
139
+type Bundle struct {
140
+	Domains     []string
141
+	Bundle      string
142
+	BeforeIssue *hook
143
+	BeforeRenew *hook
144
+	AfterIssue  *hook
145
+	AfterRenew  *hook
146
+	Env         []string
147
+	EnvSet      []string
148
+
149
+	cert       *tls.Certificate
150
+	x509cert   *x509.Certificate
151
+	der        [][]byte        // DER encoded chain
152
+	privateKey *rsa.PrivateKey // RSA private key
153
+
154
+	sync.RWMutex
155
+}
156
+
157
+func NewBundle(keyFile, bundleFile string, keyBits int, names []string) (b *Bundle, err error) {
158
+	b = &Bundle{
159
+		Domains: names,
160
+		Bundle:  bundleFile,
161
+	}
162
+
163
+	log.Printf("%s: loading private key from %s", b, keyFile)
164
+	if keyBits == 0 {
165
+		keyBits = defaultKeyBits
166
+	}
167
+	if b.privateKey, err = loadOrCreateKey(keyFile, keyBits); err != nil {
168
+		return
169
+	}
170
+
171
+	log.Printf("%s: loading bundle from %s", b, bundleFile)
172
+	var raw []byte
173
+	if raw, err = read(b.Bundle, secureMask); err != nil {
174
+		if !os.IsNotExist(err) {
175
+			return
176
+		}
177
+		err = nil
178
+	}
179
+	var p *pem.Block
180
+	for {
181
+		if p, raw = pem.Decode(raw); p == nil {
182
+			break
183
+		}
184
+		if p.Type == "CERTIFICATE" {
185
+			b.der = append(b.der, p.Bytes)
186
+		}
187
+	}
188
+
189
+	// Check bundle for each of the configured names.
190
+	for _, domain := range b.Domains {
191
+		var cerr error
192
+		if b.x509cert, cerr = validCert(domain, b.der, b.privateKey); cerr != nil {
193
+			log.Printf("%s: not valid for domain %s", b, domain)
194
+			b.der = b.der[:0]
195
+			b.x509cert = nil
196
+			break
197
+		}
198
+	}
199
+
200
+	return
201
+}
202
+
203
+func (b *Bundle) Save(ders [][]byte, leaf *x509.Certificate) (err error) {
204
+	for _, domain := range b.Domains {
205
+		if _, err = validCert(domain, ders, b.privateKey); err != nil {
206
+			return
207
+		}
208
+	}
209
+
210
+	log.Printf("%s: saving new bundle to %s", b, b.Bundle)
211
+
212
+	// Do an atomic save, write to TempFile then Rename
213
+	// TempFile opens with 0600
214
+	var f *os.File
215
+	if f, err = ioutil.TempFile(path.Dir(b.Bundle), path.Base(b.Bundle)); err != nil {
216
+		return
217
+	}
218
+	defer f.Close()
219
+
220
+	// Write private key
221
+	if _, err = f.Write(pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(b.privateKey)})); err != nil {
222
+		return
223
+	}
224
+
225
+	// Write certificate chain
226
+	for _, der := range ders {
227
+		if _, err = f.Write(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})); err != nil {
228
+			return
229
+		}
230
+	}
231
+
232
+	f.Close()
233
+	if err = os.Rename(f.Name(), b.Bundle); err != nil {
234
+		return
235
+	}
236
+	if secureMode != 0700 {
237
+		if err = os.Chmod(b.Bundle, secureMode&^0111); err != nil {
238
+			return
239
+		}
240
+	}
241
+
242
+	// Save state
243
+	b.der = ders
244
+	b.x509cert = leaf
245
+
246
+	return
247
+}
248
+
249
+func (b *Bundle) String() string {
250
+	return b.Domains[0]
251
+}
252
+
253
+// Issue a bundle using Manager
254
+func (b *Bundle) Issue(manager *Manager) {
255
+	log.Printf("%s: issue", b)
256
+	if !b.hook(b.BeforeIssue, "issue") {
257
+		log.Printf("%s: issue canceled, before-issue hook failed", b)
258
+		return
259
+	}
260
+
261
+	ctx, cancel := context.WithTimeout(context.Background(), clientTimeout)
262
+	defer cancel()
263
+
264
+	var (
265
+		ders [][]byte
266
+		cert *x509.Certificate
267
+		err  error
268
+	)
269
+	if ders, cert, err = manager.Request(ctx, b.privateKey, b.Domains); err != nil {
270
+		log.Printf("%s: issue failed: %v", b, err)
271
+		return
272
+	}
273
+
274
+	if err = b.Save(ders, cert); err != nil {
275
+		log.Printf("%s: issue save failed: %v", b, err)
276
+		return
277
+	}
278
+
279
+	if !b.hook(b.AfterIssue, "issue") {
280
+		log.Printf("%s: after-issue hook failed", b)
281
+	}
282
+}
283
+
284
+func (b *Bundle) NeedsIssue() bool {
285
+	b.RLock()
286
+	valid := b.x509cert != nil && len(b.der) != 0
287
+	b.RUnlock()
288
+	return !valid
289
+}
290
+
291
+// Renew a bundle using Manager
292
+func (b *Bundle) Renew(*Manager) {
293
+	log.Printf("%s: renew", b)
294
+	if !b.hook(b.BeforeRenew, "renew") {
295
+		log.Printf("%s: renew canceled, before-renew hook failed", b)
296
+		return
297
+	}
298
+	if !b.hook(b.AfterRenew, "renew") {
299
+		log.Printf("%s: after-renew hook failed", b)
300
+	}
301
+}
302
+
303
+func (b *Bundle) NeedsRenew() bool {
304
+	if b.NeedsIssue() {
305
+		return false
306
+	}
307
+	b.RLock()
308
+	var renew bool
309
+	if b.x509cert != nil {
310
+		test := time.Now().Add(defaultRenew)
311
+		log.Printf("%s: valid from %s to %s, renew at ±%s", b,
312
+			b.x509cert.NotBefore.Format(timeFormat),
313
+			b.x509cert.NotAfter.Format(timeFormat),
314
+			test.Format(timeFormat))
315
+
316
+		renew = test.After(b.x509cert.NotAfter) || test.Before(b.x509cert.NotBefore)
317
+	}
318
+	b.RUnlock()
319
+	return renew
320
+}
321
+
322
+// logWriter writes everything to the log
323
+type logWriter struct {
324
+	prefix, suffix string
325
+}
326
+
327
+func (w *logWriter) Write(p []byte) (int, error) {
328
+	for _, chunk := range bytes.Split(p, []byte("\n")) {
329
+		if line := strings.TrimRight(string(chunk), "\r\n"); line != "" {
330
+			log.Printf(w.prefix + line + w.suffix)
331
+		}
332
+	}
333
+	return len(p), nil
334
+}
335
+
336
+// hook runs a command in the context of this bundle
337
+func (b *Bundle) hook(hook *hook, stage string) bool {
338
+	if hook == nil {
339
+		return true
340
+	}
341
+
342
+	stdout := &logWriter{fmt.Sprintf("%s: out: \x1b[1;32m", b), "\x1b[0m"}
343
+	stderr := &logWriter{fmt.Sprintf("%s: err: \x1b[1;31m", b), "\x1b[0m"}
344
+
345
+	// Setup environment
346
+	var env []string
347
+	if len(b.Env) > 0 {
348
+		// Use named environment keys from configuration
349
+		env = make([]string, len(b.Env))
350
+		for i, key := range b.Env {
351
+			env[i] = key + "=" + os.Getenv(key)
352
+		}
353
+
354
+	} else {
355
+		// Use default environment
356
+		env = os.Environ()
357
+	}
358
+
359
+	// Append our setters
360
+	env = append(env, b.EnvSet...)
361
+
362
+	// Append our own environment variables
363
+	env = append(env, []string{
364
+		"LEAD_STAGE=" + stage,
365
+		"LEAD_BUNDLE=" + b.Bundle,
366
+		"LEAD_DOMAIN=" + b.String(),
367
+		"LEAD_DOMAINS=" + strings.Join(b.Domains, " "),
368
+	}...)
369
+
370
+	return hook.Run(stdout, stderr, env, stage, b.String())
371
+}
372
+
373
+type hook struct {
374
+	command string
375
+	args    []string
376
+}
377
+
378
+func newHook(command string) (*hook, error) {
379
+	command = strings.TrimSpace(command)
380
+	if command == "" {
381
+		return nil, nil
382
+	}
383
+
384
+	part, err := shlex.Split(command)
385
+	if err != nil {
386
+		return nil, err
387
+	}
388
+
389
+	hook := new(hook)
390
+	hook.command = part[0]
391
+	if len(part) > 1 {
392
+		hook.args = part[1:]
393
+	} else {
394
+		hook.args = []string{}
395
+	}
396
+
397
+	return hook, nil
398
+}
399
+
400
+func (hook *hook) Run(stdout, stderr io.Writer, env []string, args ...string) bool {
401
+	log.Printf("hook: %s %s", hook.command, strings.Join(append(hook.args, args...), " "))
402
+
403
+	ctx, cancel := context.WithTimeout(context.Background(), hookTimeout)
404
+	defer cancel()
405
+
406
+	cmd := exec.CommandContext(ctx, hook.command, append(hook.args, args...)...)
407
+	cmd.Env = env
408
+	cmd.Stdout = stdout
409
+	cmd.Stderr = stderr
410
+
411
+	// Run the executable
412
+	if err := cmd.Run(); err != nil {
413
+		cmd.Stderr.Write([]byte(err.Error()))
414
+		return false
415
+	}
416
+
417
+	return cmd.ProcessState.Success()
418
+}

+ 1
- 0
config.go View File

@@ -0,0 +1 @@
1
+package main

+ 2
- 0
hook/dump.sh View File

@@ -0,0 +1,2 @@
1
+#!/bin/sh
2
+env

+ 21
- 0
hook/mail.sh View File

@@ -0,0 +1,21 @@
1
+#!/bin/sh
2
+
3
+if [ -z "${MAILTO}" ]; then
4
+	echo "$0: no MAILTO set, defauting to <root@localhost>" >&2
5
+	export MAILTO="<root@localhost>"
6
+fi
7
+
8
+LEAD_HOST=$(hostname -f)
9
+LEAD_X509=$(openssl x509 -noout -text -in ${LEAD_BUNDLE} 2>&1)
10
+
11
+sendmail -i "$MAILTO" <<EOF
12
+To: $MAILTO
13
+From: lead@${LEAD_HOST}
14
+Subject: [lead] ${LEAD_STAGE} ${LEAD_DOMAIN}
15
+
16
+${LEAD_STAGE} for ${LEAD_DOMAIN} (${LEAD_DOMAINS}):
17
+
18
+${LEAD_X509}
19
+
20
+~ lead on ${LEAD_HOST}
21
+EOF

+ 12
- 0
hook/reload-haproxy.sh View File

@@ -0,0 +1,12 @@
1
+#!/bin/bash
2
+
3
+if [ -x /bin/systemctl ]; then
4
+	/bin/systemctl reload haproxy
5
+elif [ -x /usr/sbin/service ]; then
6
+	/usr/sbin/service haproxy reload
7
+else
8
+	echo "no service control found" >&2
9
+	exit 1
10
+fi
11
+
12
+exit $?

+ 173
- 0
lead.go View File

@@ -0,0 +1,173 @@
1
+package main
2
+
3
+import (
4
+	"errors"
5
+	"flag"
6
+	"io"
7
+	"log"
8
+	"os"
9
+	"path"
10
+	"path/filepath"
11
+	"sync"
12
+	"time"
13
+
14
+	"golang.org/x/crypto/acme"
15
+)
16
+
17
+// Flags
18
+var (
19
+	configFile = flag.String("config", "lead.conf", "configuration file")
20
+
21
+	// Listen is the listen address for the LEAD server.
22
+	listen = flag.String("listen", ":80", "listen address")
23
+
24
+	// ListenTLS is the listen address for TLS SNI requests.
25
+	listenTLS = flag.String("listentls", ":443", "TLS listen address")
26
+
27
+	// Server used for the ACME protocol.
28
+	server = flag.String("server", acme.LetsEncryptURL, "ACME server")
29
+
30
+	// Use the staging server (overrides -server)
31
+	staging = flag.Bool("staging", false, "use staging server")
32
+
33
+	// Paranoid mode means files with private data may only be readable by the
34
+	// current user.
35
+	paranoid = flag.Bool("paranoid", false, "be extra strict in secure modes")
36
+
37
+	// acceptTOS indicates the user accepted the CA's TOS
38
+	acceptTOS = flag.Bool("accept-tos", false, "accept the CA's TOS")
39
+)
40
+
41
+// Defaults
42
+var (
43
+	accountKeyBits = 4096
44
+	defaultKeyBits = 2048
45
+	secureMode     = os.FileMode(0750)
46
+	secureMask     = os.FileMode(0027)
47
+	defaultRenew   = 30 * 24 * time.Hour // 30 days renew time
48
+	defaultExpiry  = 90 * 24 * time.Hour // 90 days valid time
49
+	checkDelay     = time.Second * 5     // check delay after start
50
+	checkInterval  = time.Hour           // check every hour
51
+	clientTimeout  = time.Minute * 5     // client dead timer
52
+	hookTimeout    = time.Minute         // hook dead timer
53
+	stagingServer  = "https://acme-staging.api.letsencrypt.org/directory"
54
+)
55
+
56
+// SiteConfig is a single TLS-enabled site.
57
+type SiteConfig struct {
58
+	Name        string
59
+	SAN         []string
60
+	KeySize     int
61
+	BeforeIssue string   `toml:"before-issue"` // Hook to execute before issue
62
+	BeforeRenew string   `toml:"before-renew"` // Hook to execute before renew
63
+	AfterIssue  string   `toml:"after-issue"`  // Hook to execute after issue
64
+	AfterRenew  string   `toml:"after-renew"`  // Hook to execute after renew
65
+	Env         []string `toml:"env"`          // Environment variables to keep, default keep all
66
+	EnvSet      []string `toml:"env-set"`      // Environment variables to add
67
+}
68
+
69
+// Config is the main configuration entry point.
70
+type Config struct {
71
+	Path         string
72
+	AccessLog    string `toml:"access-log"`
73
+	AccountKey   string `toml:"account-key"`
74
+	Email        string
75
+	KeySize      int
76
+	Keys         string
77
+	Certificates string
78
+	Bundles      string
79
+	BeforeIssue  string   `toml:"before-issue"` // Hook to execute before issue
80
+	BeforeRenew  string   `toml:"before-renew"` // Hook to execute before renew
81
+	AfterIssue   string   `toml:"after-issue"`  // Hook to execute after issue
82
+	AfterRenew   string   `toml:"after-renew"`  // Hook to execute after renew
83
+	Env          []string `toml:"env"`          // Environment variables to keep, default keep all
84
+	EnvSet       []string `toml:"env-set"`      // Environment variables to add
85
+	Account      string
86
+	Site         map[string]*SiteConfig
87
+	io.Writer
88
+}
89
+
90
+// Check configuration settings.
91
+func (c *Config) Check() (err error) {
92
+	if c.Email == "" {
93
+		return errors.New("config: email must be set")
94
+	}
95
+
96
+	if c.Path == "" {
97
+		if c.Path, err = os.Getwd(); err != nil {
98
+			return
99
+		}
100
+	} else if !filepath.IsAbs(c.Path) {
101
+		if c.Path, err = filepath.Abs(c.Path); err != nil {
102
+			return
103
+		}
104
+	}
105
+
106
+	if c.KeySize == 0 {
107
+		c.KeySize = defaultKeyBits
108
+	}
109
+
110
+	c.Account = pick(c.Account, filepath.Join(c.Path, "account.conf"))
111
+	c.Certificates = pick(c.Certificates, filepath.Join(c.Path, "certificates"))
112
+	c.Bundles = pick(c.Bundles, filepath.Join(c.Path, "bundles"))
113
+	c.Keys = pick(c.Keys, filepath.Join(c.Path, "keys"))
114
+
115
+	for dir, modes := range map[string][]os.FileMode{
116
+		path.Dir(c.Account): []os.FileMode{secureMode, secureMask},
117
+		c.Certificates:      []os.FileMode{0755, 0000},
118
+		c.Bundles:           []os.FileMode{secureMode, secureMask},
119
+		c.Keys:              []os.FileMode{secureMode, secureMask},
120
+	} {
121
+		if err = mkdir(dir, modes[0], modes[1]); err != nil {
122
+			return
123
+		}
124
+	}
125
+
126
+	return
127
+}
128
+
129
+func main() {
130
+	flag.Parse()
131
+
132
+	log.Println("main: lead starting")
133
+
134
+	if *staging {
135
+		*server = stagingServer
136
+	}
137
+	log.Println("main: using CA", *server)
138
+
139
+	if *paranoid {
140
+		secureMode = 0700
141
+		secureMask = 0077
142
+	}
143
+	log.Printf("main: secure mode=%04o,mask=%04o", secureMode, secureMask)
144
+
145
+	config := new(Config)
146
+	if err := loadConf(*configFile, config); err != nil {
147
+		log.Fatalln(err)
148
+	}
149
+	if err := config.Check(); err != nil {
150
+		log.Fatalln(err)
151
+	}
152
+
153
+	server, err := NewServer(config)
154
+	if err != nil {
155
+		log.Fatalln(err)
156
+	}
157
+
158
+	checker, err := NewChecker(config, server.Manager)
159
+	if err != nil {
160
+		log.Fatalln(err)
161
+	}
162
+
163
+	var wg sync.WaitGroup
164
+	wg.Add(1)
165
+	go func(wg *sync.WaitGroup) {
166
+		// Spawn checker as soon as the servers are listening. Before this time
167
+		// the CA won't be able to talk to use anyway.
168
+		wg.Wait()
169
+		checker.Run()
170
+	}(&wg)
171
+
172
+	log.Fatalln(server.Serve(*listen, *listenTLS, &wg))
173
+}

+ 46
- 0
logger.go View File

@@ -0,0 +1,46 @@
1
+package main
2
+
3
+import (
4
+	"io"
5
+	"log"
6
+	"net/http"
7
+	"time"
8
+)
9
+
10
+type statusWriter struct {
11
+	http.ResponseWriter
12
+	status int
13
+	length int
14
+}
15
+
16
+func (w *statusWriter) WriteHeader(status int) {
17
+	w.status = status
18
+	w.ResponseWriter.WriteHeader(status)
19
+}
20
+
21
+func (w *statusWriter) Write(b []byte) (int, error) {
22
+	if w.status == 0 {
23
+		w.status = 200
24
+	}
25
+	w.length = len(b)
26
+	return w.ResponseWriter.Write(b)
27
+}
28
+
29
+// AccessLog Logs the Http Status for a request into fileHandler and returns a httphandler function which is a wrapper to log the requests.
30
+func AccessLog(handle http.Handler, output io.Writer) http.HandlerFunc {
31
+	logger := log.New(output, "", 0)
32
+	return func(w http.ResponseWriter, request *http.Request) {
33
+		start := time.Now()
34
+		writer := statusWriter{w, 0, 0}
35
+		handle.ServeHTTP(&writer, request)
36
+		end := time.Now()
37
+		latency := end.Sub(start)
38
+		statusCode := writer.status
39
+		length := writer.length
40
+		if request.URL.RawQuery != "" {
41
+			logger.Printf("%v %s %s \"%s %s%s%s %s\" %d %d \"%s\" %v", end.Format("2006/01/02 15:04:05"), request.Host, request.RemoteAddr, request.Method, request.URL.Path, "?", request.URL.RawQuery, request.Proto, statusCode, length, request.Header.Get("User-Agent"), latency)
42
+		} else {
43
+			logger.Printf("%v %s %s \"%s %s %s\" %d %d \"%s\" %v", end.Format("2006/01/02 15:04:05"), request.Host, request.RemoteAddr, request.Method, request.URL.Path, request.Proto, statusCode, length, request.Header.Get("User-Agent"), latency)
44
+		}
45
+	}
46
+}

+ 260
- 0
manager.go View File

@@ -0,0 +1,260 @@
1
+package main
2
+
3
+import (
4
+	"context"
5
+	"crypto"
6
+	"crypto/tls"
7
+	"crypto/x509"
8
+	"errors"
9
+	"fmt"
10
+	"log"
11
+	"net/http"
12
+	"os"
13
+	"strings"
14
+	"sync"
15
+	"time"
16
+
17
+	"golang.org/x/crypto/acme"
18
+)
19
+
20
+const (
21
+	challengeHTTP01   = "http-01"
22
+	challengeTLSSNI01 = "tls-sni-01"
23
+	challengeTLSSNI02 = "tls-sni-02"
24
+)
25
+
26
+// Manager takes care of talking to the CA.
27
+type Manager struct {
28
+	account    *acme.Account
29
+	accountKey crypto.Signer
30
+	client     *acme.Client
31
+
32
+	// Tokens for ACME challenges
33
+	certToken map[string]*tls.Certificate // For tls-sni-01,tls-sni-02
34
+	certMutex sync.RWMutex                //
35
+	httpToken map[string]string           // For http-01
36
+	httpMutex sync.RWMutex                //
37
+}
38
+
39
+// NewManager sets up the ACME client and registers the account.
40
+func NewManager(account, accountKey string, contact []string) (*Manager, error) {
41
+	m := &Manager{
42
+		account: &acme.Account{},
43
+		client: &acme.Client{
44
+			DirectoryURL: *server,
45
+		},
46
+	}
47
+
48
+	var err error
49
+	if m.client.Key, err = loadOrCreateKey(accountKey, accountKeyBits); err != nil {
50
+		return nil, err
51
+	}
52
+
53
+	log.Println("manager: loading account data from", account)
54
+	if err = loadConf(account, m.account); err != nil {
55
+		if !os.IsNotExist(err) {
56
+			return nil, err
57
+		}
58
+
59
+		log.Println("manager: no account available, registering")
60
+		if err = m.registerAccount(account, contact); err != nil {
61
+			return nil, err
62
+		}
63
+	}
64
+
65
+	return m, nil
66
+}
67
+
68
+func (m *Manager) registerAccount(account string, contact []string) (err error) {
69
+	m.account.Contact = contact
70
+
71
+	ctx := context.Background()
72
+	wait, cancel := context.WithTimeout(ctx, time.Minute)
73
+	defer cancel()
74
+
75
+	log.Println("manager: registering account")
76
+	if m.account, err = m.client.Register(wait, m.account, m.acceptTOS); err != nil {
77
+		return
78
+	}
79
+
80
+	log.Printf("manager: saving account registration to %s", account)
81
+	return saveConf(account, m.account)
82
+}
83
+
84
+func (m *Manager) acceptTOS(tosURL string) bool {
85
+	if *acceptTOS {
86
+		log.Printf("manager: accepting TOS at %s", tosURL)
87
+		return true
88
+	}
89
+	log.Fatalf("manager: to accept the TOS at %s, run with -accept-tos", tosURL)
90
+	return false
91
+}
92
+
93
+// GetCertificate implements the tls.Config.GetCertificate interface.
94
+func (m *Manager) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
95
+	name := hello.ServerName
96
+	if name == "" {
97
+		return nil, errors.New("manager: missing name in TLS Client Hello")
98
+	}
99
+
100
+	if strings.HasSuffix(name, ".acme.invalid") {
101
+		m.certMutex.RLock()
102
+		defer m.certMutex.RUnlock()
103
+		if cert, ok := m.certToken[name]; ok {
104
+			log.Printf("manager: returning TLS-SNI token for %s", name)
105
+			return cert, nil
106
+		}
107
+	}
108
+
109
+	return nil, fmt.Errorf("manager: no cert available for %s", name)
110
+}
111
+
112
+// ServeHTTP implements the http.Handler interface.
113
+func (m *Manager) ServeHTTP(w http.ResponseWriter, r *http.Request) {
114
+	m.httpMutex.RLock()
115
+	defer m.httpMutex.RUnlock()
116
+
117
+	if response, ok := m.httpToken[r.URL.Path]; ok {
118
+		log.Printf("manager: returning HTTP token for %s", r.URL.Path)
119
+		w.Write([]byte(response))
120
+		return
121
+	}
122
+
123
+	http.Error(w, "Not Found", http.StatusNotFound)
124
+}
125
+
126
+// Request a signed certificate.
127
+func (m *Manager) Request(ctx context.Context, key crypto.Signer, domains []string) (der [][]byte, leaf *x509.Certificate, err error) {
128
+	// Verify all names.
129
+	for _, domain := range domains {
130
+		if err = m.Verify(ctx, domain); err != nil {
131
+			return
132
+		}
133
+	}
134
+
135
+	var csr []byte
136
+	if csr, err = certRequest(key, domains...); err != nil {
137
+		return
138
+	}
139
+
140
+	if der, _, err = m.client.CreateCert(ctx, csr, 0, true); err != nil {
141
+		return
142
+	}
143
+
144
+	if leaf, err = validCert(domains[0], der, key); err != nil {
145
+		return
146
+	}
147
+
148
+	return
149
+}
150
+
151
+// Verify starts verification for a domain.
152
+func (m *Manager) Verify(ctx context.Context, domain string) error {
153
+	log.Printf("manager: verify %s", domain)
154
+
155
+	authz, err := m.client.Authorize(ctx, domain)
156
+	if err != nil {
157
+		return err
158
+	}
159
+
160
+	// Maybe we're done here already.
161
+	if authz.Status == acme.StatusValid {
162
+		return nil
163
+	}
164
+
165
+	// Find an acceptable challenge
166
+	// FIXME(maze): respect authz.Combinations
167
+	var chal *acme.Challenge
168
+	for _, c := range authz.Challenges {
169
+		switch c.Type {
170
+		case challengeHTTP01:
171
+			// Accept as fallback
172
+			if chal == nil {
173
+				chal = c
174
+			}
175
+		case challengeTLSSNI01:
176
+			// Accept as fallback, prefer over http-01
177
+			if chal == nil || chal.Type == challengeHTTP01 {
178
+				chal = c
179
+			}
180
+		case challengeTLSSNI02:
181
+			// Accept
182
+			chal = c
183
+		}
184
+	}
185
+	if chal == nil {
186
+		return errors.New("manager: no supported challenge type found")
187
+	}
188
+
189
+	var (
190
+		cert tls.Certificate
191
+		name string
192
+	)
193
+	switch chal.Type {
194
+	case challengeHTTP01:
195
+		if err = m.newHTTPToken(chal.Token); err != nil {
196
+			return err
197
+		}
198
+		defer m.delHTTPToken(chal.Token)
199
+	case challengeTLSSNI01:
200
+		if cert, name, err = m.client.TLSSNI01ChallengeCert(chal.Token); err != nil {
201
+			return err
202
+		}
203
+	case challengeTLSSNI02:
204
+		if cert, name, err = m.client.TLSSNI02ChallengeCert(chal.Token); err != nil {
205
+			return err
206
+		}
207
+	default:
208
+		return fmt.Errorf("manager: unknown challenge type %q", chal.Type)
209
+	}
210
+
211
+	if name != "" {
212
+		m.newCertToken(&cert, name)
213
+		defer m.delCertToken(name)
214
+	}
215
+
216
+	if _, err = m.client.Accept(ctx, chal); err != nil {
217
+		return err
218
+	}
219
+
220
+	_, err = m.client.WaitAuthorization(ctx, authz.URI)
221
+	return err
222
+}
223
+
224
+func (m *Manager) delCertToken(name string) {
225
+	m.certMutex.Lock()
226
+	delete(m.certToken, name)
227
+	m.certMutex.Unlock()
228
+}
229
+
230
+func (m *Manager) newCertToken(cert *tls.Certificate, name string) {
231
+	m.certMutex.Lock()
232
+	if m.certToken == nil {
233
+		m.certToken = make(map[string]*tls.Certificate)
234
+	}
235
+	m.certToken[name] = cert
236
+	m.certMutex.Unlock()
237
+}
238
+
239
+func (m *Manager) delHTTPToken(token string) {
240
+	m.httpMutex.Lock()
241
+	delete(m.httpToken, token)
242
+	m.httpMutex.Unlock()
243
+}
244
+
245
+func (m *Manager) newHTTPToken(token string) (err error) {
246
+	m.httpMutex.Lock()
247
+	defer m.httpMutex.Unlock()
248
+
249
+	var response string
250
+	if response, err = m.client.HTTP01ChallengeResponse(token); err != nil {
251
+		return err
252
+	}
253
+
254
+	if m.httpToken == nil {
255
+		m.httpToken = make(map[string]string)
256
+	}
257
+
258
+	m.httpToken[m.client.HTTP01ChallengePath(token)] = response
259
+	return nil
260
+}

+ 103
- 0
server.go View File

@@ -0,0 +1,103 @@
1
+package main
2
+
3
+import (
4
+	"crypto/tls"
5
+	"errors"
6
+	"io"
7
+	"log"
8
+	"net/http"
9
+	"os"
10
+	"strings"
11
+	"sync"
12
+
13
+	lumberjack "gopkg.in/natefinch/lumberjack.v2"
14
+)
15
+
16
+// Server handles talking to the cruel outside world
17
+type Server struct {
18
+	Manager *Manager
19
+	config  *Config
20
+	log     io.Writer
21
+}
22
+
23
+// NewServer starts a manager and sets up the server structure
24
+func NewServer(config *Config) (*Server, error) {
25
+	s := &Server{
26
+		config: config,
27
+	}
28
+
29
+	var err error
30
+	if s.Manager, err = NewManager(config.Account, config.AccountKey, []string{"mailto:" + config.Email}); err != nil {
31
+		return nil, err
32
+	}
33
+
34
+	if config.AccessLog == "" {
35
+		log.Println("server: logging to stdout")
36
+		s.log = os.Stdout
37
+	} else {
38
+		log.Println("server: logging to", config.AccessLog)
39
+		s.log = &lumberjack.Logger{Filename: config.AccessLog}
40
+	}
41
+
42
+	return s, nil
43
+}
44
+
45
+// Serve content starting the HTTP/HTTPS servers.
46
+func (s *Server) Serve(addr, addrTLS string, wait *sync.WaitGroup) (err error) {
47
+	var wg sync.WaitGroup
48
+	wg.Add(1)
49
+
50
+	hs := &http.Server{
51
+		Addr:    addr,
52
+		Handler: AccessLog(s, s.log),
53
+	}
54
+
55
+	go func(wg *sync.WaitGroup) {
56
+		defer wg.Done()
57
+		log.Println("server: starting HTTP server on http://" + hs.Addr)
58
+		log.Println("server:", hs.ListenAndServe())
59
+	}(&wg)
60
+
61
+	ts := &http.Server{
62
+		Addr:    addrTLS,
63
+		Handler: AccessLog(s, s.log),
64
+		TLSConfig: &tls.Config{
65
+			GetCertificate: s.Manager.GetCertificate,
66
+		},
67
+	}
68
+
69
+	go func(wg *sync.WaitGroup) {
70
+		defer wg.Done()
71
+		log.Println("server: starting TLS server on https://" + ts.Addr)
72
+		log.Println("server:", ts.ListenAndServeTLS("", ""))
73
+	}(&wg)
74
+
75
+	// Signal the other jobs that we've spawned the listeners.
76
+	wait.Done()
77
+
78
+	// If either one of the servers dies, we'll quit.
79
+	wg.Wait()
80
+
81
+	return errors.New("server: died")
82
+}
83
+
84
+func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
85
+	w.Header().Set("Server", "lead (+https://git.maze.io/maze/lead)")
86
+
87
+	// Dispatch acme-challenge requests to the manager
88
+	if strings.HasPrefix(r.URL.Path, "/.well-known/acme-challenge/") {
89
+		s.Manager.ServeHTTP(w, r)
90
+		return
91
+	}
92
+
93
+	// Redirect to HTTPS
94
+	u := r.URL
95
+	u.Host = r.Host
96
+	u.Scheme = "https"
97
+
98
+	if pos := strings.IndexByte(u.Host, ':'); pos > -1 {
99
+		u.Host = u.Host[:pos]
100
+	}
101
+
102
+	http.Redirect(w, r, u.String(), http.StatusMovedPermanently)
103
+}

+ 243
- 0
util.go View File

@@ -0,0 +1,243 @@
1
+package main
2
+
3
+import (
4
+	"crypto"
5
+	"crypto/ecdsa"
6
+	"crypto/rand"
7
+	"crypto/rsa"
8
+	"crypto/x509"
9
+	"crypto/x509/pkix"
10
+	"encoding/pem"
11
+	"errors"
12
+	"fmt"
13
+	"io/ioutil"
14
+	"log"
15
+	"os"
16
+	"time"
17
+
18
+	"github.com/BurntSushi/toml"
19
+)
20
+
21
+func panicf(format string, v ...interface{}) {
22
+	panic(fmt.Sprintf(format, v...))
23
+}
24
+
25
+func loadConf(name string, v interface{}) (err error) {
26
+	var b []byte
27
+	if b, err = ioutil.ReadFile(name); err != nil {
28
+		return
29
+	}
30
+	return toml.Unmarshal(b, v)
31
+}
32
+
33
+func saveConf(name string, v interface{}, perm ...os.FileMode) (err error) {
34
+	var mode = os.FileMode(0600)
35
+	if len(perm) == 1 {
36
+		mode = perm[0]
37
+	}
38
+
39
+	var f *os.File
40
+	if f, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode); err != nil {
41
+		return
42
+	}
43
+	defer f.Close()
44
+
45
+	var e = toml.NewEncoder(f)
46
+	return e.Encode(v)
47
+}
48
+
49
+func mkdir(path string, perm, mask os.FileMode) (err error) {
50
+	if err = os.MkdirAll(path, perm); err != nil {
51
+		return
52
+	}
53
+
54
+	return secure(path, mask, false)
55
+}
56
+
57
+func read(path string, mask os.FileMode) (b []byte, err error) {
58
+	if err = secure(path, mask, false); err != nil {
59
+		return
60
+	}
61
+	return ioutil.ReadFile(path)
62
+}
63
+
64
+func writeTemp(dir, prefix string, data []byte) (string, error) {
65
+	// TempFile uses 0600 permissions
66
+	f, err := ioutil.TempFile(dir, prefix)
67
+	if err != nil {
68
+		return "", err
69
+	}
70
+	if _, err := f.Write(data); err != nil {
71
+		f.Close()
72
+		return "", err
73
+	}
74
+	return f.Name(), f.Close()
75
+}
76
+
77
+func secure(path string, mask os.FileMode, missingOK bool) (err error) {
78
+	var stat os.FileInfo
79
+	if stat, err = os.Stat(path); err != nil {
80
+		if !missingOK {
81
+			return
82
+		}
83
+		if !os.IsNotExist(err) {
84
+			return
85
+		}
86
+	}
87
+
88
+	var mode = stat.Mode()
89
+	if mode&mask != 0 {
90
+		return fmt.Errorf("insecure mask on %s: %04o (%04o&%04o)", path, mode&07777, mode, mask)
91
+	}
92
+
93
+	return nil
94
+}
95
+
96
+func certRequest(key crypto.Signer, domains ...string) ([]byte, error) {
97
+	req := &x509.CertificateRequest{
98
+		Subject:  pkix.Name{CommonName: domains[0]},
99
+		DNSNames: domains,
100
+	}
101
+	return x509.CreateCertificateRequest(rand.Reader, req, key)
102
+}
103
+
104
+// validCert parses a cert chain provided as der argument and verifies the leaf, der[0],
105
+// corresponds to the private key, as well as the domain match and expiration dates.
106
+// It doesn't do any revocation checking.
107
+//
108
+// The returned value is the verified leaf cert.
109
+func validCert(domain string, der [][]byte, key crypto.Signer) (leaf *x509.Certificate, err error) {
110
+	// parse public part(s)
111
+	var n int
112
+	for _, b := range der {
113
+		n += len(b)
114
+	}
115
+	pub := make([]byte, n)
116
+	n = 0
117
+	for _, b := range der {
118
+		n += copy(pub[n:], b)
119
+	}
120
+	x509Cert, err := x509.ParseCertificates(pub)
121
+	if len(x509Cert) == 0 {
122
+		return nil, errors.New("no public key found")
123
+	}
124
+	// verify the leaf is not expired and matches the domain name
125
+	leaf = x509Cert[0]
126
+	now := time.Now()
127
+	if now.Before(leaf.NotBefore) {
128
+		return nil, errors.New("certificate is not valid yet")
129
+	}
130
+	if now.After(leaf.NotAfter) {
131
+		return nil, errors.New("expired certificate")
132
+	}
133
+	if err := leaf.VerifyHostname(domain); err != nil {
134
+		return nil, err
135
+	}
136
+	// ensure the leaf corresponds to the private key
137
+	switch pub := leaf.PublicKey.(type) {
138
+	case *rsa.PublicKey:
139
+		prv, ok := key.(*rsa.PrivateKey)
140
+		if !ok {
141
+			return nil, errors.New("private key type does not match public key type")
142
+		}
143
+		if pub.N.Cmp(prv.N) != 0 {
144
+			return nil, errors.New("private key does not match public key")
145
+		}
146
+	case *ecdsa.PublicKey:
147
+		prv, ok := key.(*ecdsa.PrivateKey)
148
+		if !ok {
149
+			return nil, errors.New("private key type does not match public key type")
150
+		}
151
+		if pub.X.Cmp(prv.X) != 0 || pub.Y.Cmp(prv.Y) != 0 {
152
+			return nil, errors.New("private key does not match public key")
153
+		}
154
+	default:
155
+		return nil, errors.New("unknown public key algorithm")
156
+	}
157
+	return leaf, nil
158
+}
159
+
160
+func loadKey(path string) (*rsa.PrivateKey, error) {
161
+	b, err := read(path, secureMask)
162
+	if err != nil {
163
+		return nil, err
164
+	}
165
+
166
+	var block = parsePEM(b, "RSA PRIVATE KEY")
167
+	if block == nil {
168
+		return nil, fmt.Errorf("no RSA PRIVATE KEY in %s", path)
169
+	}
170
+
171
+	return x509.ParsePKCS1PrivateKey(block.Bytes)
172
+}
173
+
174
+func loadOrCreateKey(path string, bits int) (*rsa.PrivateKey, error) {
175
+	key, err := loadKey(path)
176
+	if err != nil {
177
+		if !os.IsNotExist(err) {
178
+			return nil, err
179
+		}
180
+	} else if key != nil {
181
+		return key, nil
182
+	}
183
+
184
+	if key, err = generateKey(bits); err != nil {
185
+		return nil, err
186
+	}
187
+
188
+	d := pem.EncodeToMemory(&pem.Block{
189
+		Type:  "RSA PRIVATE KEY",
190
+		Bytes: x509.MarshalPKCS1PrivateKey(key),
191
+	})
192
+
193
+	log.Println("writing private key to", path)
194
+	if err = ioutil.WriteFile(path, d, 0600); err != nil {
195
+		return nil, err
196
+	}
197
+
198
+	return key, nil
199
+}
200
+
201
+func generateKey(bits int) (*rsa.PrivateKey, error) {
202
+	log.Printf("generating %d bits RSA private key", bits)
203
+	return rsa.GenerateKey(rand.Reader, bits)
204
+}
205
+
206
+func parsePEM(b []byte, typ string) *pem.Block {
207
+	var block *pem.Block
208
+	for {
209
+		block, b = pem.Decode(b)
210
+		if block == nil {
211
+			return nil
212
+		} else if block.Type == typ {
213
+			return block
214
+		} else if len(b) == 0 {
215
+			return nil
216
+		}
217
+	}
218
+}
219
+
220
+func pick(options ...string) string {
221
+	for _, option := range options {
222
+		if option != "" {
223
+			return option
224
+		}
225
+	}
226
+	return ""
227
+}
228
+
229
+func merge(slices ...[]string) []string {
230
+	var (
231
+		seen = make(map[string]bool)
232
+		list = make([]string, 0)
233
+	)
234
+	for _, slice := range slices {
235
+		for _, item := range slice {
236
+			if !seen[item] {
237
+				list = append(list, item)
238
+				seen[item] = true
239
+			}
240
+		}
241
+	}
242
+	return list
243
+}

Loading…
Cancel
Save