├── example
├── www
│ └── index.html
├── README.md
└── example.go
├── .travis.yml
├── .gitignore
├── setup_test.go
├── CONTRIBUTING.md
├── rw_callback_test.go
├── LICENSE
├── rw.go
├── challengeprovider.go
├── rwkey.go
├── acme_test.go
├── cert.go
├── renew.go
├── configuration.go
├── cert_test.go
├── acme.go
├── README.md
└── acmewrapper.go
/example/www/index.html:
--------------------------------------------------------------------------------
1 |
2 | It works!
3 |
4 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: go
2 | go:
3 | - 1.7
4 | - 1.6
5 | - 1.5
6 | - 1.4
7 | script:
8 | - go build -o example/example example/example.go
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Compiled Object files, Static and Dynamic libs (Shared Objects)
2 | *.o
3 | *.a
4 | *.so
5 |
6 | # Folders
7 | _obj
8 | _test
9 |
10 | # Architecture specific extensions/prefixes
11 | *.[568vq]
12 | [568vq].out
13 |
14 | *.cgo1.go
15 | *.cgo2.c
16 | _cgo_defun.c
17 | _cgo_gotypes.go
18 | _cgo_export.*
19 |
20 | _testmain.go
21 |
22 | *.exe
23 | *.test
24 | *.prof
25 |
26 | *.key
27 | *.crt
28 | *.pem
29 | *.reg
30 | *.bak
31 |
--------------------------------------------------------------------------------
/example/README.md:
--------------------------------------------------------------------------------
1 | # Example
2 |
3 | This is a very basic server which shows how to use acmewrapper
4 |
5 | ```
6 | go build
7 | ```
8 |
9 | will give you the executable
10 |
11 | ## Running
12 |
13 | The server will serve the directory you specify as its last argument:
14 |
15 | ```
16 | ./example -accept -host=:8443 mywebsite.com www.mywebsite.com ./www
17 | ```
18 |
19 | The above will run a server at port 8443 (which 443 is assumed to forward to), with certs for example.com and www.example.com.
20 |
21 | It will serve the www directory.
22 |
23 | Note that when testing, you should use the `-test` flag to make sure you are not generating valid certificates (you might run into the limits accidentally)
24 |
--------------------------------------------------------------------------------
/setup_test.go:
--------------------------------------------------------------------------------
1 | package acmewrapper
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "testing"
7 | )
8 |
9 | const TESTAPI = "https://acme-staging.api.letsencrypt.org/directory"
10 |
11 | var TESTDOMAINS []string
12 | var TLSADDRESS string
13 |
14 | func TestMain(m *testing.M) {
15 |
16 | tlsaddr := os.Getenv("TLSADDRESS")
17 | if tlsaddr == "" {
18 | tlsaddr = ":443"
19 | }
20 | // Set up the domain to use for tests
21 | dom := os.Getenv("DOMAIN_NAME")
22 | if dom == "" {
23 | fmt.Printf("NO DOMAIN SET\n\tSet a valid testing domain name\n\tin the DOMAIN_NAME environmental variable:\n\n\t\texport DOMAIN_NAME=\"example.com\"\n\n")
24 | os.Exit(-1)
25 | }
26 | fmt.Printf("USING DOMAIN_NAME='%s'\nUSING TLSADDRESS='%s'\n", dom, tlsaddr)
27 | TESTDOMAINS = []string{dom}
28 | TLSADDRESS = tlsaddr
29 |
30 | retCode := m.Run()
31 |
32 | // call with result of m.Run()
33 | os.Exit(retCode)
34 | }
35 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to ACMEWrapper
2 |
3 | ## Bug Reports
4 |
5 | Please check that your issue was not yet reported. Make sure to check in closed issues (use issue search) to see if it was already solved.
6 |
7 | If you do not find your issue, create a new one and include all information necessary to reproduce your problem. You should include enough information for someone to be able to reproduce your problem without the need for followup.
8 |
9 | ## Pull Requests
10 |
11 | Patches, new features, and improvements are a great way to help the project. Make sure that your pull request focuses on one feature and does not include extraneous commits.
12 |
13 | If you make significant changes to the code, make sure to include tests!
14 |
15 | ## License
16 |
17 | By contributing to ACMEWrapper, you agree to have the contribution licensed under the MIT license, and claim that by doing so you are not violating any laws/contracts/copyrights, etc.
18 |
--------------------------------------------------------------------------------
/rw_callback_test.go:
--------------------------------------------------------------------------------
1 | package acmewrapper
2 |
3 | import (
4 | "os"
5 | "testing"
6 |
7 | "github.com/stretchr/testify/require"
8 | )
9 |
10 | func TestSaveLoadCallback(t *testing.T) {
11 | memFS := map[string][]byte{}
12 | _, err := New(Config{
13 | Server: TESTAPI,
14 | TOSCallback: TOSAgree,
15 | Domains: TESTDOMAINS,
16 | PrivateKeyFile: "testinguser.key",
17 | RegistrationFile: "testinguser.reg",
18 | Address: TLSADDRESS,
19 | TLSCertFile: "cert.crt",
20 | TLSKeyFile: "key.pem",
21 | SaveFileCallback: func(path string, contents []byte) error {
22 | memFS[path] = contents
23 | return nil
24 | },
25 | LoadFileCallback: func(path string) ([]byte, error) {
26 | contents, ok := memFS[path]
27 | if !ok {
28 | return nil, os.ErrNotExist
29 | }
30 | return contents, nil
31 | },
32 | })
33 | require.NoError(t, err)
34 | require.Len(t, memFS, 4)
35 | require.NotNil(t, memFS["testinguser.key"])
36 | require.NotNil(t, memFS["testinguser.reg"])
37 | require.NotNil(t, memFS["cert.crt"])
38 | require.NotNil(t, memFS["key.pem"])
39 | }
40 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Daniel Kumor
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/rw.go:
--------------------------------------------------------------------------------
1 | package acmewrapper
2 |
3 | import (
4 | "errors"
5 | "io/ioutil"
6 | )
7 |
8 | // ErrNotHandled is returned by read and write file callbacks if the file should be
9 | // read from filesystem.
10 | var ErrNotHandled = errors.New("not handled")
11 |
12 | func (w *AcmeWrapper) loadFile(path string) ([]byte, error) {
13 | //use custom load file callback?
14 | if w.Config.LoadFileCallback != nil {
15 | if b, err := w.Config.LoadFileCallback(path); err == nil {
16 | return b, nil
17 | } else if err != ErrNotHandled {
18 | return nil, err
19 | }
20 | }
21 | //default load from disk
22 | b, err := ioutil.ReadFile(path)
23 | if err != nil {
24 | return nil, err
25 | }
26 | return b, nil
27 | }
28 |
29 | func (w *AcmeWrapper) saveFile(path string, contents []byte) error {
30 | //use custom save file callback?
31 | if w.Config.SaveFileCallback != nil {
32 | if err := w.Config.SaveFileCallback(path, contents); err == nil {
33 | return nil
34 | } else if err != ErrNotHandled {
35 | return err
36 | }
37 | }
38 | //default save to disk (current user read+write only!)
39 | if err := ioutil.WriteFile(path, contents, 0600); err != nil {
40 | return err
41 | }
42 | return nil
43 | }
44 |
--------------------------------------------------------------------------------
/challengeprovider.go:
--------------------------------------------------------------------------------
1 | package acmewrapper
2 |
3 | import "github.com/xenolf/lego/acme"
4 |
5 | // wrapperChallengeProvider is used to fit into the acme.ChallengeProvider interface,
6 | // which allows us to modify our server during runtime to solve the SNI challenge
7 | type wrapperChallengeProvider struct {
8 | w *AcmeWrapper
9 | challengeDomain string
10 | }
11 |
12 | // Present sets up the challenge domain thru SNI. Part of acme.ChallengeProvider interface
13 | func (c *wrapperChallengeProvider) Present(domain, token, keyAuth string) error {
14 | logf("[acmewrapper] Started SNI server modification for %s", domain)
15 |
16 | // Use ACME's SNI challenge cert maker. How nice that it is exported :)
17 | cert, challengedomain, err := acme.TLSSNI01ChallengeCert(keyAuth)
18 | if err != nil {
19 | return err
20 | }
21 |
22 | // Add the cert to our AcmeWrapper. here, the names is the special SNI challenge domain
23 | // in the form "..acme.invalid"
24 | c.w.AddSNI(challengedomain, &cert)
25 |
26 | c.challengeDomain = challengedomain
27 |
28 | return nil
29 |
30 | }
31 |
32 | // CleanUp removes the challenge domain from SNI. Part of acme.ChallengeProvider interface
33 | func (c *wrapperChallengeProvider) CleanUp(domain, token, keyAuth string) error {
34 | logf("[acmewrapper] End of SNI server modification for %s\n", domain)
35 | c.w.RemSNI(c.challengeDomain)
36 | return nil
37 | }
38 |
--------------------------------------------------------------------------------
/rwkey.go:
--------------------------------------------------------------------------------
1 | package acmewrapper
2 |
3 | import (
4 | "bytes"
5 | "crypto"
6 | "crypto/ecdsa"
7 | "crypto/rsa"
8 | "crypto/tls"
9 | "crypto/x509"
10 | "encoding/pem"
11 | "errors"
12 | )
13 |
14 | // savePrivateKey is used to write the given key to file
15 | // This code copied from caddy:
16 | // https://github.com/mholt/caddy/blob/master/caddy/https/crypto.go
17 | func (w *AcmeWrapper) savePrivateKey(filename string, key crypto.PrivateKey) error {
18 | var pemType string
19 | var keyBytes []byte
20 | switch key := key.(type) {
21 | case *ecdsa.PrivateKey:
22 | var err error
23 | pemType = "EC"
24 | keyBytes, err = x509.MarshalECPrivateKey(key)
25 | if err != nil {
26 | return err
27 | }
28 | case *rsa.PrivateKey:
29 | pemType = "RSA"
30 | keyBytes = x509.MarshalPKCS1PrivateKey(key)
31 | }
32 | pemKey := pem.Block{Type: pemType + " PRIVATE KEY", Bytes: keyBytes}
33 | pemEncoded := bytes.Buffer{}
34 | if err := pem.Encode(&pemEncoded, &pemKey); err != nil {
35 | return err
36 | }
37 | return w.saveFile(filename, pemEncoded.Bytes())
38 | }
39 |
40 | // loadPrivateKey reads a key from file
41 | // This code copied from caddy:
42 | // https://github.com/mholt/caddy/blob/master/caddy/https/crypto.go
43 | func (w *AcmeWrapper) loadPrivateKey(filename string) (crypto.PrivateKey, error) {
44 | keyBytes, err := w.loadFile(filename)
45 | if err != nil {
46 | return nil, err
47 | }
48 | keyBlock, _ := pem.Decode(keyBytes)
49 | switch keyBlock.Type {
50 | case "RSA PRIVATE KEY":
51 | return x509.ParsePKCS1PrivateKey(keyBlock.Bytes)
52 | case "EC PRIVATE KEY":
53 | return x509.ParseECPrivateKey(keyBlock.Bytes)
54 | }
55 | return nil, errors.New("unknown private key type")
56 | }
57 |
58 | // AcmeWrapper version of LoadX509KeyPair reads and parses a public/private key pair from a pair of
59 | // files. The files must contain PEM encoded data. Pulled from std lib tls.go.
60 | func (w *AcmeWrapper) loadX509KeyPair(certFile, keyFile string) (tls.Certificate, error) {
61 | certPEMBlock, err := w.loadFile(certFile)
62 | if err != nil {
63 | return tls.Certificate{}, err
64 | }
65 | keyPEMBlock, err := w.loadFile(keyFile)
66 | if err != nil {
67 | return tls.Certificate{}, err
68 | }
69 | return tls.X509KeyPair(certPEMBlock, keyPEMBlock)
70 | }
71 |
--------------------------------------------------------------------------------
/acme_test.go:
--------------------------------------------------------------------------------
1 | package acmewrapper
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "testing"
7 |
8 | "github.com/stretchr/testify/require"
9 | )
10 |
11 | func TestUserErrors(t *testing.T) {
12 | _, err := New(Config{
13 | Server: TESTAPI,
14 | TOSCallback: TOSAgree,
15 | Address: TLSADDRESS,
16 | })
17 | require.Error(t, err)
18 | _, err = New(Config{
19 | Server: TESTAPI,
20 | Domains: TESTDOMAINS,
21 | Address: TLSADDRESS,
22 | })
23 | require.Error(t, err)
24 |
25 | _, err = New(Config{
26 | Server: TESTAPI,
27 | TOSCallback: TOSAgree,
28 | Domains: TESTDOMAINS,
29 | Address: TLSADDRESS,
30 | PrivateKeyFile: "testinguser.key",
31 | })
32 | require.Error(t, err)
33 |
34 | _, err = New(Config{
35 | Server: TESTAPI,
36 | TOSCallback: TOSAgree,
37 | Domains: TESTDOMAINS,
38 | Address: TLSADDRESS,
39 | RegistrationFile: "testinguser.reg",
40 | })
41 | require.Error(t, err)
42 |
43 | _, err = New(Config{
44 | Server: TESTAPI,
45 | TOSCallback: func(tosurl string) bool {
46 | fmt.Printf("TOS URL: %s\n", tosurl)
47 | return false
48 | },
49 | Domains: TESTDOMAINS,
50 | })
51 |
52 | require.Error(t, err)
53 |
54 | }
55 |
56 | func TestUser(t *testing.T) {
57 | // Test that an anonymous user can be successfully created
58 |
59 | w, err := New(Config{
60 | Server: TESTAPI,
61 | TOSCallback: TOSAgree,
62 | Domains: TESTDOMAINS,
63 | Address: TLSADDRESS,
64 | })
65 |
66 | require.NoError(t, err)
67 | require.Equal(t, w.GetEmail(), "")
68 | require.NotNil(t, w.GetRegistration())
69 | require.NotNil(t, w.GetPrivateKey())
70 | require.NotNil(t, w.GetCertificate())
71 | require.False(t, w.CertNeedsUpdate())
72 |
73 | os.Remove("testinguser.key")
74 | os.Remove("testinguser.reg")
75 |
76 | w, err = New(Config{
77 | Server: TESTAPI,
78 | TOSCallback: TOSAgree,
79 | Domains: TESTDOMAINS,
80 | PrivateKeyFile: "testinguser.key",
81 | RegistrationFile: "testinguser.reg",
82 | Address: TLSADDRESS,
83 | })
84 |
85 | require.NoError(t, err)
86 |
87 | // Now that the files are created, it should load fine without TOS
88 | w, err = New(Config{
89 | Server: TESTAPI,
90 | TOSCallback: TOSDecline,
91 | Domains: TESTDOMAINS,
92 | PrivateKeyFile: "testinguser.key",
93 | RegistrationFile: "testinguser.reg",
94 | Address: TLSADDRESS,
95 | })
96 |
97 | require.NoError(t, err)
98 | require.Equal(t, w.GetEmail(), "")
99 | require.NotNil(t, w.GetRegistration())
100 | require.NotNil(t, w.GetPrivateKey())
101 | }
102 |
--------------------------------------------------------------------------------
/example/example.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "crypto/tls"
5 | "flag"
6 | "fmt"
7 | "net/http"
8 | "os"
9 |
10 | "github.com/dkumor/acmewrapper"
11 | )
12 |
13 | /*
14 | This example is the equivalent of SimpleHTTPServer in python, with the twist that it does
15 | Let's Encrypt certificates.
16 | */
17 |
18 | var (
19 | address = flag.String("host", ":443", "The address at which to run server")
20 | cert = flag.String("cert", "cert.crt", "Location of TLS certificate")
21 | key = flag.String("key", "key.pem", "Location of TLS key")
22 | reg = flag.String("reg", "user.reg", "Location to write user registration")
23 | priv = flag.String("priv", "userkey.pem", "Location to store user's private key")
24 | test = flag.Bool("test", false, "Use the Let's Encrypt staging server")
25 | acme = flag.String("server", acmewrapper.DefaultServer, "The ACME server to use")
26 | accept = flag.Bool("accept", false, "Accept the ACME server's TOS?")
27 | email = flag.String("email", "", "The email to use when registering")
28 | help = flag.Bool("help", false, "Show help message")
29 | )
30 |
31 | func main() {
32 | flag.Parse()
33 | if *help || flag.NArg() < 2 {
34 | fmt.Printf("Usage: example -agree mywebsite.com www.mywebsite.com ./www\n will serve the ./www directory with TLS certs for mywebsite.com and www.mywebsite.com\n\n")
35 | flag.Usage()
36 | }
37 | if !*accept {
38 | fmt.Printf("To run the server, you must accept the Let's Encrypt TOS with -accept")
39 | os.Exit(1)
40 | }
41 | if *test {
42 | *acme = "https://acme-staging.api.letsencrypt.org/directory"
43 | }
44 |
45 | mux := http.NewServeMux()
46 | mux.Handle("/", http.FileServer(http.Dir(flag.Arg(flag.NArg()-1))))
47 |
48 | w, err := acmewrapper.New(acmewrapper.Config{
49 | Address: *address,
50 |
51 | Domains: flag.Args()[:flag.NArg()-1],
52 |
53 | Email: *email,
54 |
55 | TLSCertFile: *cert,
56 | TLSKeyFile: *key,
57 |
58 | RegistrationFile: *reg,
59 | PrivateKeyFile: *priv,
60 |
61 | Server: *acme,
62 |
63 | TOSCallback: acmewrapper.TOSAgree,
64 | })
65 | if err != nil {
66 | fmt.Printf("ERROR: %s", err.Error())
67 | os.Exit(1)
68 | }
69 |
70 | tlsconfig := w.TLSConfig()
71 |
72 | listener, err := tls.Listen("tcp", *address, tlsconfig)
73 | if err != nil {
74 | fmt.Printf("ERROR: %s", err.Error())
75 | os.Exit(1)
76 | }
77 |
78 | fmt.Printf("\n\nRunning server at %s\n\n", *address)
79 |
80 | // In order to enable http2, we can't just use http.Serve in go1.6, so we need
81 | // to create a manual http.Server, since it needs the tlsconfig
82 | // https://github.com/golang/go/issues/14374
83 | server := &http.Server{
84 | Addr: *address,
85 | Handler: mux,
86 | TLSConfig: tlsconfig,
87 | }
88 | server.Serve(listener)
89 | }
90 |
--------------------------------------------------------------------------------
/cert.go:
--------------------------------------------------------------------------------
1 | package acmewrapper
2 |
3 | import (
4 | "crypto/tls"
5 | "crypto/x509"
6 | "io/ioutil"
7 | "os"
8 | "time"
9 |
10 | "github.com/xenolf/lego/acme"
11 | )
12 |
13 | // writeCert takes an acme CertificateResource (as returned from the acme.RenewCertificate
14 | // and the acme.ObtainCertificate functions), and writes the cert and key files from it.
15 | // If the files already exist, it renames the old versions by adding .bak to them. This makes
16 | // sure that a little accident doesn't cause too much damage.
17 | func (w *AcmeWrapper) writeCert(certfile, keyfile string, crt acme.CertificateResource) (err error) {
18 | //If user has provided custom file handling, skip backups
19 | if w.Config.SaveFileCallback != nil {
20 | if err := w.saveFile(certfile, crt.Certificate); err != nil {
21 | return err
22 | }
23 | if err := w.saveFile(keyfile, crt.PrivateKey); err != nil {
24 | return err
25 | }
26 | return nil
27 | }
28 | //If the files already exist, move them to backup
29 | err = os.Rename(certfile, certfile+".bak")
30 | if err != nil && !os.IsNotExist(err) {
31 | return err
32 | }
33 | err = os.Rename(keyfile, keyfile+".bak")
34 | if err != nil && !os.IsNotExist(err) {
35 | return err
36 | }
37 | err = ioutil.WriteFile(certfile, crt.Certificate, 0600)
38 | if err != nil {
39 | os.Rename(certfile+".bak", certfile)
40 | os.Rename(keyfile+".bak", keyfile)
41 | return err
42 | }
43 | err = ioutil.WriteFile(keyfile, crt.PrivateKey, 0600)
44 | if err != nil {
45 | os.Remove(certfile)
46 | os.Rename(certfile+".bak", certfile)
47 | os.Rename(keyfile+".bak", keyfile)
48 | return err
49 | }
50 | return nil
51 | }
52 |
53 | func tlsCert(crt acme.CertificateResource) (*tls.Certificate, error) {
54 | cert, err := tls.X509KeyPair(crt.Certificate, crt.PrivateKey)
55 | return &cert, err
56 | }
57 |
58 | // checks if a is a subset of b
59 | func arraySubset(a []string, b []string) bool {
60 | if len(a) > len(b) {
61 | return false
62 | }
63 | for _, i := range a {
64 | if !stringInSlice(i, b) {
65 | return false
66 | }
67 | }
68 | return true
69 | }
70 |
71 | // CertNeedsUpdate returns whether the current certificate either
72 | // does not exist, or is = 2)
125 |
126 | // Stop it from being annoying in the background anymore
127 | w.Config.RenewCheck = 9999 * 24 * time.Hour
128 | w.Config.RetryDelay = 9999 * 24 * time.Hour
129 | w.Config.RenewTime = 30 * time.Hour
130 | listener.Close()
131 | }
132 |
133 | // We now test to make sure that if the domain is changed,
134 | // the cert is changed
135 | func TestDomainChange(t *testing.T) {
136 | os.Remove("cert.crt")
137 | os.Remove("key.pem")
138 | newdomains := []string{"www." + TESTDOMAINS[0], TESTDOMAINS[0]}
139 |
140 | domainchanged := false
141 |
142 | // First set up the domain cert
143 | _, err := New(Config{
144 | Server: TESTAPI,
145 | TOSCallback: TOSAgree,
146 | Domains: TESTDOMAINS,
147 | PrivateKeyFile: "testinguser.key",
148 | RegistrationFile: "testinguser.reg",
149 | Address: TLSADDRESS,
150 |
151 | TLSCertFile: "cert.crt",
152 | TLSKeyFile: "key.pem",
153 |
154 | RenewCallback: func() {
155 | domainchanged = true
156 | },
157 | })
158 |
159 | require.NoError(t, err)
160 | require.True(t, domainchanged)
161 |
162 | // Now running again will not renew
163 | domainchanged = false
164 | // First set up the domain cert
165 | _, err = New(Config{
166 | Server: TESTAPI,
167 | TOSCallback: TOSAgree,
168 | Domains: TESTDOMAINS,
169 | PrivateKeyFile: "testinguser.key",
170 | RegistrationFile: "testinguser.reg",
171 | Address: TLSADDRESS,
172 |
173 | TLSCertFile: "cert.crt",
174 | TLSKeyFile: "key.pem",
175 |
176 | RenewCallback: func() {
177 | domainchanged = true
178 | },
179 | })
180 | require.NoError(t, err)
181 | require.False(t, domainchanged)
182 |
183 | // Finally, we run one more time, but this time we change the domains - and check to see if renew is called
184 | // Now running again will not call domainchanged
185 | domainchanged = false
186 | _, err = New(Config{
187 | Server: TESTAPI,
188 | TOSCallback: TOSAgree,
189 | Domains: newdomains,
190 | PrivateKeyFile: "testinguser.key",
191 | RegistrationFile: "testinguser.reg",
192 | Address: TLSADDRESS,
193 |
194 | TLSCertFile: "cert.crt",
195 | TLSKeyFile: "key.pem",
196 |
197 | RenewCallback: func() {
198 | domainchanged = true
199 | },
200 | })
201 | require.NoError(t, err)
202 | require.True(t, domainchanged)
203 | }
204 |
--------------------------------------------------------------------------------
/acme.go:
--------------------------------------------------------------------------------
1 | package acmewrapper
2 |
3 | import (
4 | "crypto"
5 | "crypto/ecdsa"
6 | "crypto/elliptic"
7 | "crypto/rand"
8 | "crypto/rsa"
9 | "encoding/json"
10 | "errors"
11 | "os"
12 |
13 | "github.com/xenolf/lego/acme"
14 | )
15 |
16 | // generateKey generates a key to use for registration in acme
17 | func generateKey(keytype acme.KeyType) (crypto.PrivateKey, error) {
18 | switch keytype {
19 | case acme.RSA2048:
20 | return rsa.GenerateKey(rand.Reader, 2048)
21 | case acme.RSA4096:
22 | return rsa.GenerateKey(rand.Reader, 4096)
23 | case acme.RSA8192:
24 | return rsa.GenerateKey(rand.Reader, 8192)
25 | case acme.EC256:
26 | return ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
27 | case acme.EC384:
28 | return ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
29 | default:
30 | return nil, errors.New("Unrecognized key type")
31 | }
32 | }
33 |
34 | // initACME initailizes the acme client - it does everything from reading/writing the
35 | // user private key and registration files, to ensuring that the user is registered
36 | // on the ACME server and has accepted the TOS.
37 | // It expects w.Config to be set up.
38 | // It sets up:
39 | // - w.privatekey
40 | // - w.registration
41 | // - w.client (init the user + agree to TOS)
42 | // - gets certificates if they don't exist or are close to expiration.
43 | //
44 | // Its input is whether there is a server running already. If the server is running,
45 | // then the SNI query will succeed. If it isn't (ie, we are just setting up), then
46 | // initACME must set up its own temporary server to get any initial certificates.
47 | func (w *AcmeWrapper) initACME(serverRunning bool) (err error) {
48 | // We are modifying and using some of the config properties, so lock them
49 | w.Lock()
50 | defer w.Unlock()
51 |
52 | // Just in case initACME is being run on an existing AcmeWrapper
53 | w.registration = nil
54 | w.privatekey = nil
55 |
56 | if len(w.Config.Domains) == 0 {
57 | return errors.New("No domains set - can't initialize ACME client")
58 | }
59 | if w.Config.TOSCallback == nil {
60 | return errors.New("TOSCallback is required: you need to agree to the terms of service")
61 | }
62 |
63 | if w.Config.PrivateKeyFile != "" {
64 | if w.Config.RegistrationFile == "" {
65 | return errors.New("A filename was set for the private key but not the registration file")
66 | }
67 |
68 | // We are to use file-backed registration. See if the files exist already. We first load
69 | // the key file, then we load the registration file
70 | w.privatekey, err = w.loadPrivateKey(w.Config.PrivateKeyFile)
71 | if err != nil {
72 | if !os.IsNotExist(err) {
73 | return err
74 | }
75 | // The private key file doesn't exist - w.privatekey is left at nil
76 | }
77 |
78 | regBytes, err := w.loadFile(w.Config.RegistrationFile)
79 | if err == nil {
80 | if err = json.Unmarshal(regBytes, &w.registration); err != nil {
81 | return err
82 | }
83 | } else if !os.IsNotExist(err) {
84 | return err
85 | }
86 |
87 | // If only one exists, but not the other, return an error. Reemember that these are nil if the file didn't exist
88 | if (w.privatekey != nil || w.registration != nil) && (w.privatekey == nil || w.registration == nil) {
89 | return errors.New("One of the files (registration or key) exists, but the other is missing")
90 | }
91 |
92 | } else if w.Config.RegistrationFile != "" {
93 | return errors.New("A filename was set for the registration file but not the private key")
94 | }
95 |
96 | if w.privatekey == nil {
97 | // If privatekey is nil, it means that either there are no files, or we are running in memory only
98 | // Whatever the case, we generate our acme user
99 |
100 | // Generate the key
101 | w.privatekey, err = generateKey(w.Config.PrivateKeyType)
102 | if err != nil {
103 | return err
104 | }
105 |
106 | if w.Config.PrivateKeyFile != "" {
107 | // If we are to use a file, write it now
108 | if err = w.savePrivateKey(w.Config.PrivateKeyFile, w.privatekey); err != nil {
109 | return err
110 | }
111 | }
112 |
113 | }
114 |
115 | // Now that we have the key and necessary setup info, we prepare the ACME client.
116 | w.client, err = acme.NewClient(w.Config.Server, w, w.Config.PrivateKeyType)
117 | if err != nil {
118 | return err
119 | }
120 |
121 | if w.registration == nil {
122 | // There is no registration - register with the ACME server
123 | w.registration, err = w.client.Register()
124 | if err != nil {
125 | return err
126 | }
127 |
128 | if !w.Config.TOSCallback(w.registration.TosURL) {
129 | return errors.New("Terms of service were not accepted")
130 | }
131 |
132 | if err = w.client.AgreeToTOS(); err != nil {
133 | return err
134 | }
135 |
136 | // If we are to use a registration file, write the file now
137 | if w.Config.RegistrationFile != "" {
138 | jsonBytes, err := json.MarshalIndent(w.registration, "", "\t")
139 | if err != nil {
140 | return err
141 | }
142 | if err = w.saveFile(w.Config.RegistrationFile, jsonBytes); err != nil {
143 | return err
144 | }
145 | }
146 | }
147 |
148 | // All of the challenges are disabled EXCEPT SNI
149 | w.client.ExcludeChallenges([]acme.Challenge{acme.HTTP01, acme.DNS01})
150 | w.client.SetTLSAddress(w.Config.Address)
151 |
152 | // Now if we are to renew our certificate, do it now! We do this now if server is not running
153 | // yet, since in this case we use the default SNI provider, which runs a custom server.
154 | // We start a quick custom server
155 | // to get the initial certificates. In the future, we will use our custom SNI provider
156 | // to not need a custom server (and not have any downtime) while updating
157 | if !serverRunning && w.CertNeedsUpdate() {
158 | // Renew sets the config mutex, so unset it now
159 | w.Unlock()
160 | err = w.Renew()
161 | w.Lock()
162 | if err != nil {
163 | return err
164 | }
165 | }
166 |
167 | // Now that the user and client basics are initialized, we set up the client
168 | // so that it uses our custom SNI provider. We don't want
169 | // to start custom servers, but rather plug into our certificate updater once
170 | // we are running. This allows cert updates to be transparent.
171 | w.client.SetChallengeProvider(acme.TLSSNI01, &wrapperChallengeProvider{
172 | w: w,
173 | })
174 |
175 | // If our server IS running already, then we get our certificates NOW. The difference
176 | // between the above version and this one is that we use the custom provider if
177 | // the server is already active rather than starting a new server.
178 |
179 | if serverRunning && w.CertNeedsUpdate() {
180 | w.Unlock()
181 | err = w.Renew()
182 | w.Lock()
183 | if err != nil {
184 | return err
185 | }
186 | }
187 |
188 | return nil
189 |
190 | }
191 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | > **NOTE**: I recommend that new projects use the "official" acme library, [autocert](https://godoc.org/golang.org/x/crypto/acme/autocert). An example of usage can be seen [here](https://play.golang.org/p/Nas2lT_XeY).
2 |
3 | # ACMEWrapper
4 |
5 | Add Let's Encrypt support to your golang server in 10 lines of code.
6 |
7 | [](https://godoc.org/github.com/dkumor/acmewrapper)
8 | [](https://goreportcard.com/report/github.com/dkumor/acmewrapper)
9 | [](https://travis-ci.org/dkumor/acmewrapper)
10 |
11 | ```go
12 | w, err := acmewrapper.New(acmewrapper.Config{
13 | Domains: []string{"example.com","www.example.com"},
14 | Address: ":443",
15 |
16 | TLSCertFile: "cert.pem",
17 | TLSKeyFile: "key.pem",
18 |
19 | // Let's Encrypt stuff
20 | RegistrationFile: "user.reg",
21 | PrivateKeyFile: "user.pem",
22 |
23 | TOSCallback: acmewrapper.TOSAgree,
24 | })
25 |
26 |
27 | if err!=nil {
28 | log.Fatal("acmewrapper: ", err)
29 | }
30 |
31 | listener, err := tls.Listen("tcp", ":443", w.TLSConfig())
32 | ```
33 |
34 | Acmewrapper is built upon https://github.com/xenolf/lego, and handles all certificate generation, renewal
35 | and replacement automatically. After the above code snippet, your certificate will automatically be renewed 30 days before expiring without downtime. Any files that don't exist will be created, and your "cert.pem" and "key.pem" will be kept up to date.
36 |
37 | Since Let's Encrypt is usually an option that can be turned off, the wrapper allows disabling ACME support and just using normal certificates, with the bonus of allowing live reload (ie: change your certificates during runtime).
38 |
39 | And finally, *technically*, none of the file names shown above are actually necessary. The only needed fields are Domains and TOSCallback. Without the given file names, acmewrapper runs in-memory. Beware, though: if you do that, you might run into rate limiting from Let's Encrypt if you restart too often!
40 |
41 | ## How It Works
42 |
43 | Let's Encrypt has SNI support for domain validation. That means we can update our certificate if we control the TLS configuration of a server. That is exactly what acmewrapper does. Not only does it transparently update your server's certificate, but it uses its control of SNI to pass validation tests.
44 |
45 | This means that *no other changes* are needed to your code. You don't need any special handlers or hidden directories. So long as acmewrapper is able to set your TLS configuration, and your TLS server is running on port 443, you can instantly have a working Let's Encrypt certificate.
46 |
47 | ## Notes
48 |
49 | Currently, Go 1.4 and above are supported
50 |
51 | - There was a breaking change on 3/20/16: all time periods in the configuration were switched from int64 to time.Duration.
52 |
53 | ## Example
54 |
55 | You can go into `./example` to find a sample basic http server that will serve a given folder over https with Let's Encrypt.
56 |
57 | Another simple example is given below:
58 |
59 | ### Old Code
60 |
61 | This is sample code before adding Let's Encrypt support:
62 |
63 | ```go
64 | package main
65 |
66 | import (
67 | "io"
68 | "net/http"
69 | "log"
70 | )
71 |
72 | func HelloServer(w http.ResponseWriter, req *http.Request) {
73 | io.WriteString(w, "hello, world!\n")
74 | }
75 |
76 | func main() {
77 | http.HandleFunc("/hello", HelloServer)
78 | err := http.ListenAndServeTLS(":443", "cert.pem", "key.pem", nil)
79 | if err != nil {
80 | log.Fatal("ListenAndServe: ", err)
81 | }
82 | }
83 | ```
84 |
85 | ### New Code
86 |
87 | Adding let's encrypt support is a matter of setting the tls config:
88 |
89 | ```go
90 | package main
91 |
92 | import (
93 | "io"
94 | "net/http"
95 | "log"
96 | "crypto/tls"
97 |
98 | "github.com/dkumor/acmewrapper"
99 | )
100 |
101 | func HelloServer(w http.ResponseWriter, req *http.Request) {
102 | io.WriteString(w, "hello, world!\n")
103 | }
104 |
105 | func main() {
106 | mux := http.NewServeMux()
107 | mux.HandleFunc("/hello", HelloServer)
108 |
109 | w, err := acmewrapper.New(acmewrapper.Config{
110 | Domains: []string{"example.com","www.example.com"},
111 | Address: ":443",
112 |
113 | TLSCertFile: "cert.pem",
114 | TLSKeyFile: "key.pem",
115 |
116 | RegistrationFile: "user.reg",
117 | PrivateKeyFile: "user.pem",
118 |
119 | TOSCallback: acmewrapper.TOSAgree,
120 | })
121 |
122 |
123 | if err!=nil {
124 | log.Fatal("acmewrapper: ", err)
125 | }
126 |
127 | tlsconfig := w.TLSConfig()
128 |
129 | listener, err := tls.Listen("tcp", ":443", tlsconfig)
130 | if err != nil {
131 | log.Fatal("Listener: ", err)
132 | }
133 |
134 | // To enable http2, we need http.Server to have reference to tlsconfig
135 | // https://github.com/golang/go/issues/14374
136 | server := &http.Server{
137 | Addr: ":443",
138 | Handler: mux,
139 | TLSConfig: tlsconfig,
140 | }
141 | server.Serve(listener)
142 | }
143 | ```
144 |
145 | ## Custom File Handlers
146 |
147 | While ACMEWrapper saves certificates to the filesystem by default, you can save all relevant files in your database by overloading the read and write functions
148 |
149 | ```go
150 | w, err := acmewrapper.New(acmewrapper.Config{
151 | Domains: []string{"example.com","www.example.com"},
152 | Address: ":443",
153 |
154 | TLSCertFile: "CERTIFICATE",
155 | TLSKeyFile: "TLSKEY",
156 |
157 | RegistrationFile: "REGISTRATION",
158 | PrivateKeyFile: "PRIVATEKEY",
159 |
160 | TOSCallback: acmewrapper.TOSAgree,
161 |
162 | SaveFileCallback: func(path string, contents []byte) error {
163 | // the path is the file name as set up in the configuration - the certificate will be "CERTIFICATE", etc.
164 | },
165 | // If this callback does not find the file at the provided path, it must return os.ErrNotExist.
166 | // If this callback returns acmewrapper.ErrNotHandled, it will fallback to load file from disk.
167 | LoadFileCallback func(path string) (contents []byte, err error) {
168 | return os.ErrNotExist
169 | },
170 | })
171 |
172 | ```
173 |
174 | ## Testing
175 |
176 | Running the tests is a bit of a chore, since it requires a valid domain name, and access to port 443.
177 | This is because ACMEWrapper uses the Let's Encrypt staging server to make sure the code is working.
178 |
179 | To test on your own server, you need to change the domain name to your domain, and set a custom testing port
180 | that will be routed to 443:
181 |
182 | ```bash
183 | go test -c
184 | sudo setcap cap_net_bind_service=+ep acmewrapper.test
185 | export TLSADDRESS=":443"
186 | export DOMAIN_NAME="example.com"
187 | ./acmewrapper.test
188 | ```
189 |
--------------------------------------------------------------------------------
/acmewrapper.go:
--------------------------------------------------------------------------------
1 | package acmewrapper
2 |
3 | import (
4 | "crypto"
5 | "crypto/tls"
6 | "log"
7 | "os"
8 | "sync"
9 |
10 | "github.com/xenolf/lego/acme"
11 | )
12 |
13 | // LoggerInterface represents anything that can Printf.
14 | type LoggerInterface interface {
15 | Printf(format string, v ...interface{})
16 | }
17 |
18 | // Logger allows to use a custom logger for logging purposes
19 | var Logger LoggerInterface
20 |
21 | func logf(s string, v ...interface{}) {
22 | if Logger == nil {
23 | log.Printf(s, v...)
24 | } else {
25 | Logger.Printf(s, v...)
26 | }
27 | }
28 |
29 | // AcmeWrapper is the main object which controls tls certificates and their renewals
30 | type AcmeWrapper struct {
31 | sync.Mutex // configmutex ensures that settings for the ACME stuff don't happen in parallel
32 | Config Config
33 |
34 | certmutex sync.RWMutex // certmutex is used to make sure that replacing certificates doesn't asplode
35 |
36 | // Our user's private key & registration. Both are needed in order to be able
37 | // to renew/generate new certs.
38 | privatekey crypto.PrivateKey
39 | registration *acme.RegistrationResource
40 |
41 | // A map of custom certificates associated with special SNIs. The SNI request
42 | // passes through here
43 | certs map[string]*tls.Certificate
44 |
45 | // The current TLS cert used for SSL requests when the SNI doesn't match the map
46 | cert *tls.Certificate
47 |
48 | // The ACME client
49 | client *acme.Client
50 | }
51 |
52 | // GetEmail returns the user email (if any)
53 | // NOTE: NOT threadsafe
54 | func (w *AcmeWrapper) GetEmail() string {
55 | return w.Config.Email
56 | }
57 |
58 | // GetRegistration returns the registration currently being used
59 | // NOTE: NOT threadsafe
60 | func (w *AcmeWrapper) GetRegistration() *acme.RegistrationResource {
61 | return w.registration
62 | }
63 |
64 | // GetPrivateKey returns the private key for the given user.
65 | // NOTE: NOT threadsafe
66 | func (w *AcmeWrapper) GetPrivateKey() crypto.PrivateKey {
67 | return w.privatekey
68 | }
69 |
70 | // GetCertificate returns the current TLS certificate
71 | func (w *AcmeWrapper) GetCertificate() *tls.Certificate {
72 | w.certmutex.RLock()
73 | defer w.certmutex.RUnlock()
74 | return w.cert
75 | }
76 |
77 | // AddSNI adds a domain name and certificate pair to the AcmeWrapper.
78 | // Whenever a request is for the passed domain, its associated certifcate is returned.
79 | func (w *AcmeWrapper) AddSNI(domain string, cert *tls.Certificate) {
80 | w.certmutex.Lock()
81 | defer w.certmutex.Unlock()
82 |
83 | w.certs[domain] = cert
84 | }
85 |
86 | // RemSNI removes a domain name and certificate pair from the AcmeWrapper. It is assumed that
87 | // they were added using AddSNI.
88 | func (w *AcmeWrapper) RemSNI(domain string) {
89 | w.certmutex.Lock()
90 | defer w.certmutex.Unlock()
91 |
92 | delete(w.certs, domain)
93 | }
94 |
95 | // TLSConfigGetCertificate is the main function used in the ACME wrapper. This is set in tls.Config to
96 | // the GetCertificate property. Note that Certificates must be empty for it to be called
97 | // correctly, so unless you know what you're doing, just use AcmeWrapper.TLSConfig()
98 | func (w *AcmeWrapper) TLSConfigGetCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
99 | w.certmutex.RLock()
100 | defer w.certmutex.RUnlock()
101 |
102 | // If the SNI is in the certs map, return that cert
103 | if _, ok := w.certs[clientHello.ServerName]; ok {
104 | return w.certs[clientHello.ServerName], nil
105 | }
106 |
107 | // Otherwise, return the default cert
108 | return w.cert, nil
109 | }
110 |
111 | // TLSConfig returns a TLS configuration that will automatically work with the golang ssl listener.
112 | // This sets it up so that the server automatically uses a working cert, and updates the cert when
113 | // necessary.
114 | func (w *AcmeWrapper) TLSConfig() *tls.Config {
115 | return &tls.Config{
116 | //Go 1.6 "allows Listen to succeed when the Config has a nil Certificates, as long as the
117 | //GetCertificate callback is set" See https://golang.org/doc/go1.6#minor_library_changes.
118 | //So to add Go 1.5 support, we provide a certificate that will never be used.
119 | // This must be a valid cert, since if the client does not have SNI enabled in go1.4,
120 | // then there could be issues. AcmeWrapper does not support clients without SNI enabled at this time.
121 | Certificates: []tls.Certificate{*w.cert},
122 | GetCertificate: w.TLSConfigGetCertificate,
123 | }
124 | }
125 |
126 | // AcmeDisabled allows to enable/disable acme-based certificate. Note that it is assumed that
127 | // this function is only called during server runtime (ie, your server is already listening).
128 | // its main purpose is to enable live reload of acme configuration. Do NOT set AcmeDisabled
129 | // in AcmeWrapper.Config, since it will panic.
130 | func (w *AcmeWrapper) AcmeDisabled(set bool) error {
131 | w.Lock()
132 | w.Config.AcmeDisabled = set
133 | w.Unlock()
134 | if !set && w.client == nil {
135 | return w.initACME(true)
136 | }
137 | return nil
138 | }
139 |
140 | // SetNewCert loads a new TLS key/cert from the given files. Running it with the same
141 | // filenames as existing cert will reload them
142 | func (w *AcmeWrapper) SetNewCert(certfile, keyfile string) error {
143 | cert, err := w.loadX509KeyPair(certfile, keyfile)
144 | if err != nil {
145 | return err
146 | }
147 | w.certmutex.Lock()
148 | w.cert = &cert
149 | w.certmutex.Unlock()
150 |
151 | return nil
152 | }
153 |
154 | // New generates an AcmeWrapper given a configuration
155 | func New(c Config) (*AcmeWrapper, error) {
156 | var err error
157 | // First, set up the default values for any settings that require
158 | // values
159 | if c.Server == "" {
160 | c.Server = DefaultServer
161 | }
162 | if c.PrivateKeyType == "" {
163 | c.PrivateKeyType = DefaultKeyType
164 | }
165 | if c.RenewTime == 0 {
166 | c.RenewTime = DefaultRenewTime
167 | }
168 | if c.RetryDelay == 0 {
169 | c.RetryDelay = DefaultRetryDelay
170 | }
171 | if c.RenewCheck == 0 {
172 | c.RenewCheck = DefaultRenewCheck
173 | }
174 | if c.Address == "" {
175 | c.Address = DefaultAddress
176 | }
177 |
178 | // Now set up the actual wrapper
179 |
180 | var w AcmeWrapper
181 | w.Config = c
182 | w.certs = make(map[string]*tls.Certificate)
183 |
184 | // Now load up the key and cert files for TLS if they are set
185 | if c.TLSKeyFile != "" || c.TLSCertFile != "" {
186 | err = w.SetNewCert(c.TLSCertFile, c.TLSKeyFile)
187 | if err != nil {
188 | if !os.IsNotExist(err) || c.AcmeDisabled {
189 | // The TLS key and cert file are only
190 | // allowed to not be there if ACME will generate them
191 | // TODO: We don't check here if both are missing vs 1 missing
192 | return nil, err
193 | }
194 |
195 | }
196 | }
197 |
198 | // If acme is enabled, initialize it!
199 | if !c.AcmeDisabled {
200 | // Initialize the ACME user
201 | // initUser succeeding initializes:
202 | // - w.privatekey
203 | // - w.registration
204 | // - w.client
205 | if err = w.initACME(false); err != nil {
206 | return nil, err
207 | }
208 | }
209 |
210 | // Finally, start the background routine
211 | go backgroundExpirationChecker(&w)
212 |
213 | return &w, nil
214 | }
215 |
--------------------------------------------------------------------------------