Skip to content

Commit a6504b5

Browse files
committed
feat(package): make it so
0 parents  commit a6504b5

10 files changed

+431
-0
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
vendor/

.travis.yml

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
language: go
2+
3+
go:
4+
- 1.7.x
5+
- 1.8.x
6+
7+
install:
8+
- go get github.com/Masterminds/glide
9+
- glide install
10+
11+
script:
12+
- go test -v $(glide nv)

LICENSE.md

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
> Copyright (c) 2017, Cloudflare. All rights reserved.
2+
>
3+
> Redistribution and use in source and binary forms, with or without
4+
> modification, are permitted provided that the following conditions are mept:
5+
>
6+
> 1. Redistributions of source code must retain the above copyright notice,
7+
> this list of conditions and the following disclaimer.
8+
> 2. Redistributions in binary form must reproduce the above copyright notice,
9+
> this list of conditions and the following disclaimer in the documentation
10+
> and/or other materials provided with the distribution.
11+
> 3. Neither the name of the copyright holder nor the names of its contributors
12+
> may be used to endorse or promote products derived from this software
13+
> without specific prior written permission.
14+
>
15+
> THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16+
> AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17+
> IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18+
> ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
19+
> LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
20+
> CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
21+
> SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
22+
> INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
23+
> CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
24+
> ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
25+
> POSSIBILITY OF SUCH DAMAGE.

README.md

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# certinel
2+
3+
Certinel is a Go library that makes it even easier to implement zero-hit
4+
TLS certificate changes by watching for certificate changes for you. The
5+
methods required by `tls.TLSConfig` are already implemented for you.
6+
7+
Right now there's support for listening to file system events on Linux,
8+
BSDs, and Windows using the [fsnotify][fsnotify] library.
9+
10+
[fsnotify]: https://github.com/fsnotify/fsnotify
11+
12+
## Usage
13+
14+
Create the certinel instance, start it with `Watch`, then pass the
15+
`GetCertificate` method to your `tls.TLSConfig` instance.
16+
17+
```go
18+
package main
19+
20+
import (
21+
"crypto/tls"
22+
"log"
23+
"net/http"
24+
25+
"github.com/cloudflare/certinel"
26+
"github.com/cloudflare/certinel/fswatcher"
27+
)
28+
29+
func main() {
30+
watcher, err := fswatcher.New("/etc/ssl/app.pem", "/etc/ssl/app.key")
31+
if err != nil {
32+
log.Fatalf("fatal: unable to read server certificate. err='%s'", err)
33+
}
34+
sentinel := certinel.New(watcher, func(err error) {
35+
log.Printf("error: certinel was unable to reload the certificate. err='%s'", err)
36+
})
37+
38+
sentinel.Watch()
39+
40+
server := http.Server{
41+
Addr: ":8000",
42+
TLSConfig: &tls.Config{
43+
GetCertificate: sentinel.GetCertificate,
44+
},
45+
}
46+
47+
server.ListenAndServeTLS("", "")
48+
}
49+
```
50+
51+
On Go 1.8+, `GetClientCertificate` is also implemented to support
52+
rotating the client certificate.

certinel.go

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package certinel
2+
3+
import (
4+
"crypto/tls"
5+
"sync"
6+
)
7+
8+
// Certinel is a container for zero-hit tls.Certificate changes, by
9+
// receiving new certificates from sentries and presenting the functions
10+
// expected by Go's tls.Config{}.
11+
// Reading certificates from a Certinel instance is safe across multiple
12+
// goroutines.
13+
type Certinel struct {
14+
mu sync.RWMutex
15+
certificate *tls.Certificate
16+
watcher Watcher
17+
errBack func(error)
18+
}
19+
20+
// Watchers provide a way to construct (and close!) channels that
21+
// bind a Certinel to a changing certificate.
22+
type Watcher interface {
23+
Watch() (<-chan tls.Certificate, <-chan error)
24+
Close() error
25+
}
26+
27+
// New creates a Certinel that watches for changes with the provided
28+
// Watcher.
29+
func New(w Watcher, errBack func(error)) *Certinel {
30+
if errBack == nil {
31+
errBack = func(error) {}
32+
}
33+
34+
return &Certinel{
35+
watcher: w,
36+
errBack: errBack,
37+
}
38+
}
39+
40+
// Watch setups the Certinel's channel handles and calls Watch on the
41+
// held Watcher instance.
42+
func (c *Certinel) Watch() {
43+
go func() {
44+
tlsChan, errChan := c.watcher.Watch()
45+
for {
46+
select {
47+
case certificate := <-tlsChan:
48+
c.loadCertificate(certificate)
49+
case err := <-errChan:
50+
c.errBack(err)
51+
}
52+
}
53+
}()
54+
55+
return
56+
}
57+
58+
// Close calls Close on the held Watcher instance. After closing it
59+
// is no longer safe to use this Certinel instance.
60+
func (c *Certinel) Close() error {
61+
return c.watcher.Close()
62+
}
63+
64+
// GetCertificate returns the current tls.Certificate instance. The function
65+
// can be passed as the GetCertificate member in a tls.Config object. It is
66+
// safe to call across multiple goroutines.
67+
func (c *Certinel) GetCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
68+
c.mu.RLock()
69+
defer c.mu.RUnlock()
70+
71+
return c.certificate, nil
72+
}
73+
74+
func (c *Certinel) loadCertificate(certificate tls.Certificate) {
75+
c.mu.Lock()
76+
defer c.mu.Unlock()
77+
78+
c.certificate = &certificate
79+
}

certinel_go1.8.go

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// +build go1.8
2+
3+
package certinel
4+
5+
import (
6+
"crypto/tls"
7+
)
8+
9+
// GetClientCertificate returns the current tls.Certificate instance. The function
10+
// can be passed as the GetClientCertificate member in a tls.Config object. It is
11+
// safe to call across multiple goroutines.
12+
func (c *Certinel) GetClientCertificate(certificateRequest *tls.CertificateRequestInfo) (*tls.Certificate, error) {
13+
c.mu.RLock()
14+
defer c.mu.RUnlock()
15+
16+
return c.certificate, nil
17+
}

certinel_test.go

+110
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package certinel
2+
3+
import (
4+
"crypto/tls"
5+
"testing"
6+
"time"
7+
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/mock"
10+
)
11+
12+
type MockWatcher struct {
13+
mock.Mock
14+
}
15+
16+
func (o *MockWatcher) Watch() (<-chan tls.Certificate, <-chan error) {
17+
args := o.Called()
18+
19+
return args.Get(0).(chan tls.Certificate), args.Get(1).(chan error)
20+
}
21+
22+
func (o *MockWatcher) Close() error {
23+
args := o.Called()
24+
25+
return args.Error(0)
26+
}
27+
28+
func TestGetCertificate(t *testing.T) {
29+
tlsChan := make(chan tls.Certificate)
30+
errChan := make(chan error)
31+
cert := tls.Certificate{}
32+
clientHello := &tls.ClientHelloInfo{}
33+
watcher := &MockWatcher{}
34+
35+
subject := &Certinel{
36+
watcher: watcher,
37+
errBack: func(error) {},
38+
}
39+
40+
watcher.On("Watch").Return(tlsChan, errChan)
41+
watcher.On("Close").Return(nil)
42+
43+
gotCert, err := subject.GetCertificate(clientHello)
44+
if assert.NoError(t, err) {
45+
assert.Nil(t, gotCert)
46+
}
47+
48+
subject.Watch()
49+
tlsChan <- cert
50+
<-time.After(time.Duration(1) * time.Millisecond)
51+
52+
gotCert, err = subject.GetCertificate(clientHello)
53+
if assert.NoError(t, err) {
54+
assert.Equal(t, cert, *gotCert)
55+
}
56+
57+
subject.Close() // nolint: errcheck
58+
59+
watcher.AssertExpectations(t)
60+
}
61+
62+
func TestGetCertificateAfterChange(t *testing.T) {
63+
tlsChan := make(chan tls.Certificate)
64+
errChan := make(chan error)
65+
cert1 := tls.Certificate{
66+
Certificate: [][]byte{
67+
[]byte("Hello"),
68+
},
69+
}
70+
cert2 := tls.Certificate{
71+
Certificate: [][]byte{
72+
[]byte("Goodbye"),
73+
},
74+
}
75+
clientHello := &tls.ClientHelloInfo{}
76+
watcher := &MockWatcher{}
77+
78+
subject := &Certinel{
79+
watcher: watcher,
80+
errBack: func(error) {},
81+
}
82+
83+
watcher.On("Watch").Return(tlsChan, errChan)
84+
watcher.On("Close").Return(nil)
85+
86+
gotCert, err := subject.GetCertificate(clientHello)
87+
if assert.NoError(t, err) {
88+
assert.Nil(t, gotCert)
89+
}
90+
91+
subject.Watch()
92+
tlsChan <- cert1
93+
<-time.After(time.Duration(1) * time.Millisecond)
94+
95+
gotCert, err = subject.GetCertificate(clientHello)
96+
if assert.NoError(t, err) {
97+
assert.Equal(t, cert1, *gotCert)
98+
}
99+
100+
tlsChan <- cert2
101+
<-time.After(time.Duration(1) * time.Millisecond)
102+
gotCert, err = subject.GetCertificate(clientHello)
103+
if assert.NoError(t, err) {
104+
assert.Equal(t, cert2, *gotCert)
105+
}
106+
107+
subject.Close() // nolint: errcheck
108+
109+
watcher.AssertExpectations(t)
110+
}

fswatcher/fswatcher.go

+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package fswatcher
2+
3+
import (
4+
"crypto/tls"
5+
"crypto/x509"
6+
7+
"github.com/fsnotify/fsnotify"
8+
"github.com/pkg/errors"
9+
)
10+
11+
type Sentry struct {
12+
fsnotify *fsnotify.Watcher
13+
certPath string
14+
keyPath string
15+
tlsChan chan tls.Certificate
16+
errChan chan error
17+
}
18+
19+
const (
20+
errAddWatcher = "fswatcher: error adding path to watcher"
21+
errCreateWatcher = "fswatcher: error creating watcher"
22+
errLoadCertificate = "fswatcher: error loading certificate"
23+
)
24+
25+
// New creates a Sentry to watch for file system changes.
26+
func New(cert, key string) (*Sentry, error) {
27+
watcher, err := fsnotify.NewWatcher()
28+
if err != nil {
29+
return nil, errors.Wrap(err, errCreateWatcher)
30+
}
31+
32+
if err != nil {
33+
return nil, errors.Wrap(err, errAddWatcher)
34+
}
35+
36+
fsw := &Sentry{
37+
fsnotify: watcher,
38+
certPath: cert,
39+
keyPath: key,
40+
tlsChan: make(chan tls.Certificate),
41+
errChan: make(chan error),
42+
}
43+
44+
go func() {
45+
for {
46+
select {
47+
case event := <-watcher.Events:
48+
if event.Op&fsnotify.Write == fsnotify.Write {
49+
fsw.loadCertificate()
50+
}
51+
case err := <-watcher.Errors:
52+
fsw.errChan <- err
53+
}
54+
}
55+
}()
56+
57+
return fsw, nil
58+
}
59+
60+
func (w *Sentry) Watch() (<-chan tls.Certificate, <-chan error) {
61+
go func() {
62+
w.loadCertificate()
63+
err := w.fsnotify.Add(w.certPath)
64+
65+
if err != nil {
66+
w.errChan <- err
67+
}
68+
}()
69+
return w.tlsChan, w.errChan
70+
}
71+
72+
func (w *Sentry) Close() error {
73+
err := w.fsnotify.Close()
74+
75+
close(w.tlsChan)
76+
close(w.errChan)
77+
return err
78+
}
79+
80+
func (w *Sentry) loadCertificate() {
81+
certificate, err := tls.LoadX509KeyPair(w.certPath, w.keyPath)
82+
if err != nil {
83+
w.errChan <- errors.Wrap(err, errLoadCertificate)
84+
return
85+
}
86+
87+
leaf, err := x509.ParseCertificate(certificate.Certificate[0])
88+
if err != nil {
89+
w.errChan <- errors.Wrap(err, errLoadCertificate)
90+
return
91+
}
92+
93+
certificate.Leaf = leaf
94+
95+
w.tlsChan <- certificate
96+
}

0 commit comments

Comments
 (0)