├── .gitignore ├── examples ├── http │ ├── client │ │ └── main.go │ └── server │ │ └── main.go └── grpc │ ├── helloworld │ ├── helloworld.proto │ └── helloworld.pb.go │ ├── server │ └── main.go │ └── client │ └── main.go ├── cover.sh ├── backend ├── backends │ ├── null │ │ └── null.go │ ├── backends.go │ ├── fs │ │ └── fs.go │ └── s3 │ │ └── s3.go └── backend.go ├── LICENSE ├── provider.go ├── Makefile ├── types ├── account.go └── certificate.go ├── crypto.go ├── README.md └── acme.go /.gitignore: -------------------------------------------------------------------------------- 1 | /coverage.out 2 | /vendor/ 3 | !/vendor/vendor.json -------------------------------------------------------------------------------- /examples/http/client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "io" 6 | "net/http" 7 | "os" 8 | 9 | log "github.com/Sirupsen/logrus" 10 | ) 11 | 12 | var address string 13 | 14 | func main() { 15 | flag.Parse() 16 | resp, err := http.Get("https://" + address) 17 | if err != nil { 18 | log.Fatalf("Did not connect: %v", err) 19 | } 20 | defer resp.Body.Close() 21 | if _, err = io.Copy(os.Stdout, resp.Body); err != nil { 22 | log.Fatal(err) 23 | } 24 | } 25 | 26 | func init() { 27 | flag.StringVar(&address, "address", "", "Address of the server e.g. foo.bar.com:443") 28 | } 29 | -------------------------------------------------------------------------------- /examples/grpc/helloworld/helloworld.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option java_multiple_files = true; 4 | option java_package = "io.grpc.examples.helloworld"; 5 | option java_outer_classname = "HelloWorldProto"; 6 | 7 | package helloworld; 8 | 9 | // The greeting service definition. 10 | service Greeter { 11 | // Sends a greeting 12 | rpc SayHello (HelloRequest) returns (HelloReply) {} 13 | } 14 | 15 | // The request message containing the user's name. 16 | message HelloRequest { 17 | string name = 1; 18 | } 19 | 20 | // The response message containing the greetings 21 | message HelloReply { 22 | string message = 1; 23 | } -------------------------------------------------------------------------------- /cover.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | function die() { 4 | echo $* 5 | exit 1 6 | } 7 | 8 | # Initialize coverage.out 9 | echo "mode: count" > coverage.out 10 | 11 | # Initialize error tracking 12 | ERROR="" 13 | 14 | declare -a packages=('backend' 'backend/backends/fs' 'backend/backends/s3' 'backend/backends/null' 'types'); 15 | 16 | # Test each package and append coverage profile info to coverage.out 17 | for pkg in "${packages[@]}" 18 | do 19 | go test -v -covermode=count -coverprofile=coverage_tmp.out "github.com/jtblin/go-acme/$pkg" || ERROR="Error testing $pkg" 20 | tail -n +2 coverage_tmp.out >> coverage.out 2> /dev/null ||: 21 | done 22 | 23 | rm -f coverage_tmp.out 24 | 25 | if [ ! -z "$ERROR" ] 26 | then 27 | die "Encountered error, last error was: $ERROR" 28 | fi 29 | -------------------------------------------------------------------------------- /backend/backends/null/null.go: -------------------------------------------------------------------------------- 1 | package null 2 | 3 | import ( 4 | "github.com/jtblin/go-acme/backend" 5 | "github.com/jtblin/go-acme/types" 6 | ) 7 | 8 | const ( 9 | backendName = "null" 10 | ) 11 | 12 | type null struct{} 13 | 14 | // Name returns the display name of the backend. 15 | func (null *null) Name() string { 16 | return backendName 17 | } 18 | 19 | // SaveAccount saves the account to null. 20 | func (null *null) SaveAccount(account *types.Account) error { 21 | return nil 22 | } 23 | 24 | // LoadAccount loads the account from null. 25 | func (null *null) LoadAccount(domain string) (*types.Account, error) { 26 | return &types.Account{}, nil 27 | } 28 | 29 | func newBackend() (backend.Interface, error) { 30 | return &null{}, nil 31 | } 32 | 33 | func init() { 34 | backend.RegisterBackend(backendName, func() (backend.Interface, error) { 35 | return newBackend() 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /backend/backends/backends.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 The Kubernetes Authors All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package backends 18 | 19 | import ( 20 | // initialise all backends. 21 | _ "github.com/jtblin/go-acme/backend/backends/fs" 22 | _ "github.com/jtblin/go-acme/backend/backends/null" 23 | _ "github.com/jtblin/go-acme/backend/backends/s3" 24 | ) 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Jerome Touffe-Blin ("Author") 2 | All rights reserved. 3 | 4 | The BSD License 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions 8 | are met: 9 | 10 | 1. Redistributions of source code must retain the above copyright 11 | notice, this list of conditions and the following disclaimer. 12 | 13 | 2. Redistributions in binary form must reproduce the above copyright 14 | notice, this list of conditions and the following disclaimer in the 15 | documentation and/or other materials provided with the distribution. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 18 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 20 | PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS 21 | BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 22 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 23 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 24 | BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 25 | WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 26 | OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN 27 | IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /provider.go: -------------------------------------------------------------------------------- 1 | package acme 2 | 3 | import ( 4 | "github.com/xenolf/lego/acme" 5 | "github.com/xenolf/lego/providers/dns/cloudflare" 6 | "github.com/xenolf/lego/providers/dns/digitalocean" 7 | "github.com/xenolf/lego/providers/dns/dnsimple" 8 | "github.com/xenolf/lego/providers/dns/dyn" 9 | "github.com/xenolf/lego/providers/dns/gandi" 10 | "github.com/xenolf/lego/providers/dns/googlecloud" 11 | "github.com/xenolf/lego/providers/dns/namecheap" 12 | "github.com/xenolf/lego/providers/dns/rfc2136" 13 | "github.com/xenolf/lego/providers/dns/route53" 14 | "github.com/xenolf/lego/providers/dns/vultr" 15 | ) 16 | 17 | func newDNSProvider(dns string) (acme.ChallengeProvider, error) { 18 | switch dns { 19 | case "cloudflare": 20 | return cloudflare.NewDNSProvider() 21 | case "digitalocean": 22 | return digitalocean.NewDNSProvider() 23 | case "dnsimple": 24 | return dnsimple.NewDNSProvider() 25 | case "dyn": 26 | return dyn.NewDNSProvider() 27 | case "gandi": 28 | return gandi.NewDNSProvider() 29 | case "gcloud": 30 | return googlecloud.NewDNSProvider() 31 | case "manual": 32 | return acme.NewDNSProviderManual() 33 | case "namecheap": 34 | return namecheap.NewDNSProvider() 35 | case "route53": 36 | return route53.NewDNSProvider() 37 | case "rfc2136": 38 | return rfc2136.NewDNSProvider() 39 | case "vultr": 40 | return vultr.NewDNSProvider() 41 | default: 42 | panic("Unknown dns provider " + dns) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | METALINTER_CONCURRENCY ?= 10 2 | 3 | setup: 4 | go get -v -u github.com/Masterminds/glide 5 | go get -v -u github.com/githubnemo/CompileDaemon 6 | go get -v -u github.com/alecthomas/gometalinter 7 | go get -v -u github.com/jstemmer/go-junit-report 8 | gometalinter --install --update 9 | go get -u github.com/tools/godep 10 | godep restore 11 | 12 | build: *.go fmt 13 | go build . 14 | 15 | fmt: 16 | gofmt -w=true -s $$(find . -type f -name '*.go' -not -path "./vendor/*") 17 | goimports -w=true -d $$(find . -type f -name '*.go' -not -path "./vendor/*") 18 | 19 | test: 20 | go test $$(glide nv) 21 | 22 | test-race: 23 | go test -race $$(glide nv) 24 | 25 | cover: 26 | ./cover.sh 27 | go tool cover -func=coverage.out 28 | go tool cover -html=coverage.out 29 | 30 | coveralls: 31 | ./cover.sh 32 | goveralls -coverprofile=coverage.out -service=travis-ci 33 | 34 | junit-test: build 35 | go test -v $$(glide nv) | go-junit-report > test-report.xml 36 | 37 | check: 38 | go install 39 | go install ./examples/... 40 | gometalinter --concurrency=$(METALINTER_CONCURRENCY) --deadline=180s ./... --vendor --linter='errcheck:errcheck:-ignore=net:Close' --cyclo-over=25 \ 41 | --linter='vet:go tool vet -composites=false {paths}:PATH:LINE:MESSAGE' --disable=interfacer --dupl-threshold=65 --exclude=helloworld.pb.go 42 | 43 | watch: 44 | CompileDaemon -color=true -build "make test" 45 | 46 | protobuf: 47 | protoc -I ./examples/grpc/helloworld examples/grpc/helloworld/*.proto --go_out=plugins=grpc:examples/grpc/helloworld 48 | -------------------------------------------------------------------------------- /examples/http/server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/tls" 5 | "flag" 6 | "net/http" 7 | 8 | log "github.com/Sirupsen/logrus" 9 | "github.com/jtblin/go-acme" 10 | "github.com/jtblin/go-acme/types" 11 | ) 12 | 13 | var email, domain string 14 | var staging, verbose bool 15 | 16 | func main() { 17 | flag.Parse() 18 | if verbose { 19 | log.SetLevel(log.DebugLevel) 20 | } 21 | ACME := &acme.ACME{ 22 | DNSProvider: "route53", 23 | Email: email, 24 | Domain: &types.Domain{Main: domain}, 25 | Logger: log.New(), 26 | } 27 | if staging { 28 | ACME.CAServer = "https://acme-staging.api.letsencrypt.org/directory" 29 | } 30 | tlsConfig := &tls.Config{} 31 | if err := ACME.CreateConfig(tlsConfig); err != nil { 32 | panic(err) 33 | } 34 | listener, err := tls.Listen("tcp", ":8443", tlsConfig) 35 | if err != nil { 36 | panic("Listener: " + err.Error()) 37 | } 38 | 39 | mux := http.NewServeMux() 40 | mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("ok")) }) 41 | 42 | // To enable http2, we need http.Server to have reference to tlsConfig 43 | // https://github.com/golang/go/issues/14374 44 | server := &http.Server{ 45 | Addr: ":8443", 46 | Handler: mux, 47 | TLSConfig: tlsConfig, 48 | } 49 | server.Serve(listener) 50 | } 51 | 52 | func init() { 53 | flag.StringVar(&email, "email", "", "Email address to register account") 54 | flag.StringVar(&domain, "domain", "", "Domain for which to generatec ertificates") 55 | flag.BoolVar(&staging, "staging", false, "Use staging ACME server") 56 | flag.BoolVar(&verbose, "verbose", false, "Verbose logging") 57 | } 58 | -------------------------------------------------------------------------------- /examples/grpc/server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/tls" 5 | "flag" 6 | "net" 7 | 8 | "google.golang.org/grpc" 9 | "google.golang.org/grpc/credentials" 10 | 11 | log "github.com/Sirupsen/logrus" 12 | "github.com/jtblin/go-acme" 13 | pb "github.com/jtblin/go-acme/examples/grpc/helloworld" 14 | "github.com/jtblin/go-acme/types" 15 | "golang.org/x/net/context" 16 | ) 17 | 18 | var address, email, domain string 19 | var verbose bool 20 | 21 | type server struct{} 22 | 23 | func (s *server) SayHello(cxt context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) { 24 | return &pb.HelloReply{Message: "Hello " + in.Name}, nil 25 | } 26 | 27 | func main() { 28 | flag.Parse() 29 | if verbose { 30 | log.SetLevel(log.DebugLevel) 31 | } 32 | 33 | ACME := &acme.ACME{ 34 | Email: email, 35 | DNSProvider: "route53", 36 | Domain: &types.Domain{Main: domain}, 37 | Logger: log.New(), 38 | } 39 | tlsConfig := &tls.Config{} 40 | if err := ACME.CreateConfig(tlsConfig); err != nil { 41 | panic(err) 42 | } 43 | ta := credentials.NewTLS(tlsConfig) 44 | listener, err := net.Listen("tcp", address) 45 | if err != nil { 46 | panic("failed to listen: " + err.Error()) 47 | } 48 | grpcServer := grpc.NewServer(grpc.Creds(ta)) 49 | pb.RegisterGreeterServer(grpcServer, &server{}) 50 | if err = grpcServer.Serve(listener); err != nil { 51 | panic(err) 52 | } 53 | } 54 | 55 | func init() { 56 | flag.StringVar(&address, "address", ":8443", "Listener address e.g. :443") 57 | flag.StringVar(&email, "email", "", "Email address to register account") 58 | flag.StringVar(&domain, "domain", "", "Domain for which to generate certificates") 59 | flag.BoolVar(&verbose, "verbose", false, "Verbose logging") 60 | } 61 | -------------------------------------------------------------------------------- /types/account.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "crypto" 5 | "crypto/rand" 6 | "crypto/rsa" 7 | "crypto/x509" 8 | 9 | "github.com/jtblin/go-logger" 10 | "github.com/xenolf/lego/acme" 11 | ) 12 | 13 | // Account is used to store lets encrypt registration info 14 | // and implements the acme.User interface. 15 | type Account struct { 16 | Email string 17 | DomainsCertificate *DomainCertificate 18 | Logger logger.Interface 19 | PrivateKey []byte 20 | Registration *acme.RegistrationResource 21 | } 22 | 23 | // GetEmail returns email. 24 | func (a Account) GetEmail() string { 25 | return a.Email 26 | } 27 | 28 | // GetRegistration returns lets encrypt registration resource. 29 | func (a Account) GetRegistration() *acme.RegistrationResource { 30 | return a.Registration 31 | } 32 | 33 | // GetPrivateKey returns private key. 34 | func (a Account) GetPrivateKey() crypto.PrivateKey { 35 | if privateKey, err := x509.ParsePKCS1PrivateKey(a.PrivateKey); err == nil { 36 | return privateKey 37 | } 38 | a.Logger.Printf("Cannot unmarshall private key %+v\n", a.PrivateKey) 39 | return nil 40 | } 41 | 42 | // NewAccount creates a new account for the specified email and domain. 43 | func NewAccount(email string, domain *Domain, logger logger.Interface) (*Account, error) { 44 | // Create a user. New accounts need an email and private key to start 45 | privateKey, err := rsa.GenerateKey(rand.Reader, 4096) 46 | if err != nil { 47 | return nil, err 48 | } 49 | account := &Account{ 50 | Email: email, 51 | Logger: logger, 52 | PrivateKey: x509.MarshalPKCS1PrivateKey(privateKey), 53 | } 54 | account.DomainsCertificate = &DomainCertificate{ 55 | Certificate: &Certificate{}, 56 | Domain: domain, 57 | } 58 | return account, nil 59 | } 60 | -------------------------------------------------------------------------------- /types/certificate.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "crypto/tls" 5 | "errors" 6 | "reflect" 7 | ) 8 | 9 | // Certificate is used to store certificate info. 10 | type Certificate struct { 11 | Domain string 12 | CertURL string 13 | CertStableURL string 14 | PrivateKey []byte 15 | Cert []byte 16 | } 17 | 18 | // DomainCertificate contains a certificate for a domain and SANs. 19 | type DomainCertificate struct { 20 | Certificate *Certificate 21 | Domain *Domain 22 | TLSCert *tls.Certificate `json:"-"` 23 | } 24 | 25 | // Domain holds a domain name with SANs. 26 | type Domain struct { 27 | Main string 28 | SANs []string 29 | } 30 | 31 | func (dc *DomainCertificate) tlsCert() (*tls.Certificate, error) { 32 | cert, err := tls.X509KeyPair(dc.Certificate.Cert, dc.Certificate.PrivateKey) 33 | if err != nil { 34 | return nil, err 35 | } 36 | return &cert, nil 37 | } 38 | 39 | // Init initialises the tls certificate. 40 | func (dc *DomainCertificate) Init() error { 41 | tlsCert, err := dc.tlsCert() 42 | if err != nil { 43 | return err 44 | } 45 | dc.TLSCert = tlsCert 46 | return nil 47 | } 48 | 49 | // RenewCertificate renew the certificate for the domain. 50 | func (dc *DomainCertificate) RenewCertificate(acmeCert *Certificate, domain *Domain) error { 51 | if reflect.DeepEqual(domain, dc.Domain) { 52 | dc.Certificate = acmeCert 53 | if err := dc.Init(); err != nil { 54 | return err 55 | } 56 | return nil 57 | } 58 | return errors.New("Certificate to renew not found for domain " + domain.Main) 59 | } 60 | 61 | // AddCertificate add the certificate for the domain. 62 | func (dc *DomainCertificate) AddCertificate(acmeCert *Certificate, domain *Domain) error { 63 | dc.Domain = domain 64 | dc.Certificate = acmeCert 65 | return dc.Init() 66 | } 67 | -------------------------------------------------------------------------------- /crypto.go: -------------------------------------------------------------------------------- 1 | package acme 2 | 3 | import ( 4 | "crypto/rand" 5 | "crypto/rsa" 6 | "crypto/tls" 7 | "crypto/x509" 8 | "crypto/x509/pkix" 9 | "encoding/pem" 10 | "math/big" 11 | "time" 12 | ) 13 | 14 | func generateSelfSignedCertificate(domain string) (*tls.Certificate, error) { 15 | rsaPrivKey, err := rsa.GenerateKey(rand.Reader, 2048) 16 | if err != nil { 17 | return nil, err 18 | } 19 | rsaPrivatePEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(rsaPrivKey)}) 20 | 21 | tempCertPEM, err := generatePemCert(rsaPrivKey, domain) 22 | if err != nil { 23 | return nil, err 24 | } 25 | 26 | certificate, err := tls.X509KeyPair(tempCertPEM, rsaPrivatePEM) 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | return &certificate, nil 32 | } 33 | func generatePemCert(privateKey *rsa.PrivateKey, domain string) ([]byte, error) { 34 | derBytes, err := generateDerCert(privateKey, time.Time{}, domain) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}), nil 40 | } 41 | 42 | func generateDerCert(privateKey *rsa.PrivateKey, expiration time.Time, domain string) ([]byte, error) { 43 | serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) 44 | serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | if expiration.IsZero() { 50 | expiration = time.Now().Add(365) 51 | } 52 | 53 | template := x509.Certificate{ 54 | SerialNumber: serialNumber, 55 | Subject: pkix.Name{ 56 | CommonName: "DEFAULT CERT", 57 | }, 58 | NotBefore: time.Now(), 59 | NotAfter: expiration, 60 | 61 | KeyUsage: x509.KeyUsageKeyEncipherment, 62 | BasicConstraintsValid: true, 63 | DNSNames: []string{domain}, 64 | } 65 | 66 | return x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey) 67 | } 68 | -------------------------------------------------------------------------------- /backend/backends/fs/fs.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "path" 9 | "sync" 10 | 11 | "github.com/jtblin/go-acme/backend" 12 | "github.com/jtblin/go-acme/types" 13 | ) 14 | 15 | const ( 16 | backendName = "fs" 17 | storageDirEnv = "STORAGE_DIR" 18 | ) 19 | 20 | type storage struct { 21 | StorageDir string 22 | storageLock sync.RWMutex 23 | } 24 | 25 | // Name returns the display name of the backend. 26 | func (s *storage) Name() string { 27 | return backendName 28 | } 29 | 30 | func (s *storage) key(domain string) string { 31 | return path.Join(s.StorageDir, domain) + ".json" 32 | } 33 | 34 | // SaveAccount saves the account to the filesystem. 35 | func (s *storage) SaveAccount(account *types.Account) error { 36 | s.storageLock.Lock() 37 | defer s.storageLock.Unlock() 38 | // write account to file 39 | data, err := json.MarshalIndent(account, "", " ") 40 | if err != nil { 41 | return err 42 | } 43 | return ioutil.WriteFile(s.key(account.DomainsCertificate.Domain.Main), data, 0644) 44 | } 45 | 46 | // LoadAccount loads the account from the filesystem. 47 | func (s *storage) LoadAccount(domain string) (*types.Account, error) { 48 | storageFile := s.key(domain) 49 | // if certificates in storage, load them 50 | if fileInfo, err := os.Stat(storageFile); err != nil || fileInfo.Size() == 0 { 51 | if os.IsNotExist(err) { 52 | return nil, nil 53 | } 54 | return nil, err 55 | } 56 | 57 | s.storageLock.RLock() 58 | defer s.storageLock.RUnlock() 59 | 60 | account := types.Account{ 61 | DomainsCertificate: &types.DomainCertificate{}, 62 | } 63 | file, err := ioutil.ReadFile(storageFile) 64 | if err != nil { 65 | return nil, err 66 | } 67 | if err := json.Unmarshal(file, &account); err != nil { 68 | return nil, fmt.Errorf("Error loading account: %v", err) 69 | } 70 | return &account, nil 71 | } 72 | 73 | func newBackend() (backend.Interface, error) { 74 | storageDir := os.Getenv(storageDirEnv) 75 | if storageDir != "" { 76 | return &storage{StorageDir: storageDir}, nil 77 | 78 | } 79 | // default to current directory 80 | cwd, err := os.Getwd() 81 | if err != nil { 82 | return nil, err 83 | } 84 | return &storage{StorageDir: cwd}, nil 85 | } 86 | 87 | func init() { 88 | backend.RegisterBackend(backendName, func() (backend.Interface, error) { 89 | return newBackend() 90 | }) 91 | } 92 | -------------------------------------------------------------------------------- /backend/backend.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 The Kubernetes Authors All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package backend 18 | 19 | import ( 20 | "fmt" 21 | "sync" 22 | 23 | "github.com/jtblin/go-acme/types" 24 | ) 25 | 26 | // All registered backends. 27 | var backendsMutex sync.Mutex 28 | var backends = make(map[string]Factory) 29 | 30 | // Factory is a function that returns a backend.Interface. 31 | type Factory func() (Interface, error) 32 | 33 | // Interface represents a backend. 34 | type Interface interface { 35 | // LoadAccount loads the account from the backend store. 36 | LoadAccount(domain string) (*types.Account, error) 37 | // Name returns the display name of the backend. 38 | Name() string 39 | // SaveAccount saves the account to the backend store. 40 | SaveAccount(*types.Account) error 41 | } 42 | 43 | // RegisterBackend registers a backend. 44 | func RegisterBackend(name string, backend Factory) { 45 | backendsMutex.Lock() 46 | defer backendsMutex.Unlock() 47 | if _, found := backends[name]; found { 48 | panic(fmt.Sprintf("Authenticator backend %q was registered twice\n", name)) 49 | } 50 | backends[name] = backend 51 | } 52 | 53 | // GetBackend creates an instance of the named backend, or nil if 54 | // the name is not known. The error return is only used if the named provider 55 | // was known but failed to initialize. 56 | func GetBackend(name string) (Interface, error) { 57 | backendsMutex.Lock() 58 | defer backendsMutex.Unlock() 59 | f, found := backends[name] 60 | if !found { 61 | return nil, nil 62 | } 63 | return f() 64 | } 65 | 66 | // InitBackend creates an instance of the named backend. 67 | func InitBackend(name string) (Interface, error) { 68 | var backend Interface 69 | var err error 70 | 71 | if name == "" { 72 | return nil, nil 73 | } 74 | 75 | backend, err = GetBackend(name) 76 | if err != nil { 77 | return nil, fmt.Errorf("Could not init backend %q: %v", name, err) 78 | } 79 | if backend == nil { 80 | return nil, fmt.Errorf("Unknown backend %q", name) 81 | } 82 | 83 | return backend, nil 84 | } 85 | -------------------------------------------------------------------------------- /examples/grpc/client/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Copyright 2015, Google Inc. 4 | * All rights reserved. 5 | * 6 | * Redistribution and use in source and binary forms, with or without 7 | * modification, are permitted provided that the following conditions are 8 | * met: 9 | * 10 | * * Redistributions of source code must retain the above copyright 11 | * notice, this list of conditions and the following disclaimer. 12 | * * Redistributions in binary form must reproduce the above 13 | * copyright notice, this list of conditions and the following disclaimer 14 | * in the documentation and/or other materials provided with the 15 | * distribution. 16 | * * Neither the name of Google Inc. nor the names of its 17 | * contributors may be used to endorse or promote products derived from 18 | * this software without specific prior written permission. 19 | * 20 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | * 32 | */ 33 | 34 | package main 35 | 36 | import ( 37 | "flag" 38 | 39 | log "github.com/Sirupsen/logrus" 40 | pb "github.com/jtblin/go-acme/examples/grpc/helloworld" 41 | "golang.org/x/net/context" 42 | "google.golang.org/grpc" 43 | "google.golang.org/grpc/credentials" 44 | ) 45 | 46 | const ( 47 | defaultName = "world" 48 | ) 49 | 50 | var address, name string 51 | 52 | func main() { 53 | flag.Parse() 54 | // Set up a connection to the server. 55 | conn, err := grpc.Dial(address, grpc.WithTransportCredentials(credentials.NewClientTLSFromCert(nil, ""))) 56 | if err != nil { 57 | log.Fatalf("Did not connect: %v", err) 58 | } 59 | defer conn.Close() 60 | c := pb.NewGreeterClient(conn) 61 | 62 | // Contact the server and print out its response. 63 | n := defaultName 64 | if len(name) > 0 { 65 | n = name 66 | } 67 | r, err := c.SayHello(context.Background(), &pb.HelloRequest{Name: n}) 68 | if err != nil { 69 | log.Fatalf("could not greet: %v", err) 70 | } 71 | log.Printf("Greeting: %s", r.Message) 72 | } 73 | 74 | func init() { 75 | flag.StringVar(&address, "address", "", "Address of the server e.g. foo.bar.com:443") 76 | flag.StringVar(&name, "name", "", "Name argument") 77 | } 78 | -------------------------------------------------------------------------------- /examples/grpc/helloworld/helloworld.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. 2 | // source: helloworld.proto 3 | // DO NOT EDIT! 4 | 5 | /* 6 | Package helloworld is a generated protocol buffer package. 7 | 8 | It is generated from these files: 9 | helloworld.proto 10 | 11 | It has these top-level messages: 12 | HelloRequest 13 | HelloReply 14 | */ 15 | package helloworld 16 | 17 | import proto "github.com/golang/protobuf/proto" 18 | import fmt "fmt" 19 | import math "math" 20 | 21 | import ( 22 | context "golang.org/x/net/context" 23 | grpc "google.golang.org/grpc" 24 | ) 25 | 26 | // Reference imports to suppress errors if they are not otherwise used. 27 | var _ = proto.Marshal 28 | var _ = fmt.Errorf 29 | var _ = math.Inf 30 | 31 | // This is a compile-time assertion to ensure that this generated file 32 | // is compatible with the proto package it is being compiled against. 33 | // A compilation error at this line likely means your copy of the 34 | // proto package needs to be updated. 35 | const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package 36 | 37 | // The request message containing the user's name. 38 | type HelloRequest struct { 39 | Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` 40 | } 41 | 42 | func (m *HelloRequest) Reset() { *m = HelloRequest{} } 43 | func (m *HelloRequest) String() string { return proto.CompactTextString(m) } 44 | func (*HelloRequest) ProtoMessage() {} 45 | func (*HelloRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} } 46 | 47 | // The response message containing the greetings 48 | type HelloReply struct { 49 | Message string `protobuf:"bytes,1,opt,name=message" json:"message,omitempty"` 50 | } 51 | 52 | func (m *HelloReply) Reset() { *m = HelloReply{} } 53 | func (m *HelloReply) String() string { return proto.CompactTextString(m) } 54 | func (*HelloReply) ProtoMessage() {} 55 | func (*HelloReply) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{1} } 56 | 57 | func init() { 58 | proto.RegisterType((*HelloRequest)(nil), "helloworld.HelloRequest") 59 | proto.RegisterType((*HelloReply)(nil), "helloworld.HelloReply") 60 | } 61 | 62 | // Reference imports to suppress errors if they are not otherwise used. 63 | var _ context.Context 64 | var _ grpc.ClientConn 65 | 66 | // This is a compile-time assertion to ensure that this generated file 67 | // is compatible with the grpc package it is being compiled against. 68 | const _ = grpc.SupportPackageIsVersion2 69 | 70 | // Client API for Greeter service 71 | 72 | type GreeterClient interface { 73 | // Sends a greeting 74 | SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) 75 | } 76 | 77 | type greeterClient struct { 78 | cc *grpc.ClientConn 79 | } 80 | 81 | func NewGreeterClient(cc *grpc.ClientConn) GreeterClient { 82 | return &greeterClient{cc} 83 | } 84 | 85 | func (c *greeterClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) { 86 | out := new(HelloReply) 87 | err := grpc.Invoke(ctx, "/helloworld.Greeter/SayHello", in, out, c.cc, opts...) 88 | if err != nil { 89 | return nil, err 90 | } 91 | return out, nil 92 | } 93 | 94 | // Server API for Greeter service 95 | 96 | type GreeterServer interface { 97 | // Sends a greeting 98 | SayHello(context.Context, *HelloRequest) (*HelloReply, error) 99 | } 100 | 101 | func RegisterGreeterServer(s *grpc.Server, srv GreeterServer) { 102 | s.RegisterService(&_Greeter_serviceDesc, srv) 103 | } 104 | 105 | func _Greeter_SayHello_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 106 | in := new(HelloRequest) 107 | if err := dec(in); err != nil { 108 | return nil, err 109 | } 110 | if interceptor == nil { 111 | return srv.(GreeterServer).SayHello(ctx, in) 112 | } 113 | info := &grpc.UnaryServerInfo{ 114 | Server: srv, 115 | FullMethod: "/helloworld.Greeter/SayHello", 116 | } 117 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 118 | return srv.(GreeterServer).SayHello(ctx, req.(*HelloRequest)) 119 | } 120 | return interceptor(ctx, in, info, handler) 121 | } 122 | 123 | var _Greeter_serviceDesc = grpc.ServiceDesc{ 124 | ServiceName: "helloworld.Greeter", 125 | HandlerType: (*GreeterServer)(nil), 126 | Methods: []grpc.MethodDesc{ 127 | { 128 | MethodName: "SayHello", 129 | Handler: _Greeter_SayHello_Handler, 130 | }, 131 | }, 132 | Streams: []grpc.StreamDesc{}, 133 | } 134 | 135 | var fileDescriptor0 = []byte{ 136 | // 174 bytes of a gzipped FileDescriptorProto 137 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0x12, 0xc8, 0x48, 0xcd, 0xc9, 138 | 0xc9, 0x2f, 0xcf, 0x2f, 0xca, 0x49, 0xd1, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x42, 0x88, 139 | 0x28, 0x29, 0x71, 0xf1, 0x78, 0x80, 0x78, 0x41, 0xa9, 0x85, 0xa5, 0xa9, 0xc5, 0x25, 0x42, 0x42, 140 | 0x5c, 0x2c, 0x79, 0x89, 0xb9, 0xa9, 0x12, 0x8c, 0x0a, 0x8c, 0x1a, 0x9c, 0x41, 0x60, 0xb6, 0x92, 141 | 0x1a, 0x17, 0x17, 0x54, 0x4d, 0x41, 0x4e, 0xa5, 0x90, 0x04, 0x17, 0x7b, 0x6e, 0x6a, 0x71, 0x71, 142 | 0x62, 0x3a, 0x4c, 0x11, 0x8c, 0x6b, 0xe4, 0xc9, 0xc5, 0xee, 0x5e, 0x94, 0x9a, 0x5a, 0x92, 0x5a, 143 | 0x24, 0x64, 0xc7, 0xc5, 0x11, 0x9c, 0x58, 0x09, 0xd6, 0x25, 0x24, 0xa1, 0x87, 0xe4, 0x02, 0x64, 144 | 0xcb, 0xa4, 0xc4, 0xb0, 0xc8, 0x00, 0xad, 0x50, 0x62, 0x70, 0x32, 0xe0, 0x92, 0xce, 0xcc, 0xd7, 145 | 0x4b, 0x2f, 0x2a, 0x48, 0xd6, 0x4b, 0xad, 0x48, 0xcc, 0x2d, 0xc8, 0x49, 0x2d, 0x46, 0x52, 0xeb, 146 | 0xc4, 0x0f, 0x56, 0x1c, 0x0e, 0x62, 0x07, 0x80, 0xbc, 0x14, 0xc0, 0x98, 0xc4, 0x06, 0xf6, 0x9b, 147 | 0x31, 0x20, 0x00, 0x00, 0xff, 0xff, 0x0f, 0xb7, 0xcd, 0xf2, 0xef, 0x00, 0x00, 0x00, 148 | } 149 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-acme 2 | 3 | Add [Let's Encrypt](https://letsencrypt.org/) (ACME) support to generate and renew SSL certificates to go servers 4 | using the DNS provider challenge so that it can be used for internal servers. 5 | 6 | The library is built upon [lego](https://github.com/xenolf/lego). It will generate the certificates and 7 | store them in a pluggable storage backend. It will renew the certificates automatically 7 days 8 | before they expire. 9 | 10 | If the certificates are found in the storage backend, they will be reused, which prevents from hitting 11 | [Let’s Encrypt rate limits](https://community.letsencrypt.org/t/rate-limits-for-lets-encrypt/6769) of 12 | 20 certificates per domain per week. It is recommended to use a distributed storage backend to avoid 13 | this issue (currently only `s3` is implemented). 14 | 15 | For local development, it can generate self signed certificates instead of calling Let's Encrypt. 16 | 17 | ## Usage 18 | 19 | Example with a standard http server: 20 | 21 | ``` 22 | ACME := &acme.ACME{ 23 | BackendName: "s3", 24 | Email: "user@gmail.com", 25 | DNSProvider: "route53", 26 | Domain: &types.Domain{Main: "foo.my-domain.io"}, 27 | } 28 | tlsConfig := &tls.Config{} 29 | if err := ACME.CreateConfig(tlsConfig); err != nil { 30 | panic(err) 31 | } 32 | listener, err := tls.Listen("tcp", ":443", tlsConfig) 33 | if err != nil { 34 | panic("Listener: " + err.Error()) 35 | } 36 | 37 | mux := http.NewServeMux() 38 | mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("ok")) }) 39 | 40 | // To enable http2, we need http.Server to have reference to tlsConfig 41 | // https://github.com/golang/go/issues/14374 42 | server := &http.Server{ 43 | Addr: ":443", 44 | Handler: mux, 45 | TLSConfig: tlsConfig, 46 | } 47 | server.Serve(listener) 48 | ``` 49 | 50 | Example with a [gRPC.io](github.com/grpc/grpc-go) server: 51 | 52 | ``` 53 | func main() { 54 | flag.Parse() 55 | 56 | ACME := &acme.ACME{ 57 | Email: email, 58 | DNSProvider: "route53", 59 | Domain: &types.Domain{Main: domain}, 60 | } 61 | tlsConfig := &tls.Config{} 62 | if err := ACME.CreateConfig(tlsConfig); err != nil { 63 | panic(err) 64 | } 65 | ta := credentials.NewTLS(tlsConfig) 66 | listener, err := net.Listen("tcp", address) 67 | if err != nil { 68 | panic("failed to listen: " + err.Error()) 69 | } 70 | grpcServer := grpc.NewServer(grpc.Creds(ta)) 71 | pb.RegisterGreeterServer(grpcServer, &server{}) 72 | if err = grpcServer.Serve(listener); err != nil { 73 | panic(err) 74 | } 75 | } 76 | ``` 77 | 78 | See [examples](examples/) for complete http and gRPC implementations. 79 | 80 | ### ACME config 81 | 82 | * `BackendName`: the name of the storage backend e.g. fs, s3 (default `fs`), see below for environment variables 83 | * `CAServer`: optional CA server url (default to `https://acme-v01.api.letsencrypt.org/directory`) 84 | * `DNSProvider`: mandatory DNS provider name e.g. `route53`. 85 | * `Domain`: struct containing the main domain name and optional SANs (Subject Alternate Names) 86 | * `Email`: email address to register the account 87 | * `SelfSigned`: set to true if you want to generate self signed certificates instead of Let's Encrypt ones 88 | 89 | ## DNS providers 90 | 91 | All DNS providers offered by [lego](https://github.com/xenolf/lego) at the time of publishing 92 | are supported. Environment variables need to be set depending on provider as per [lego](https://github.com/xenolf/lego). 93 | 94 | ## Storage backends 95 | 96 | Pluggable storage backends are supported, and only need to implement the [backend.Interface](backend/backend.go). 97 | Currently the following backend are supported: 98 | 99 | ### fs 100 | 101 | This backend stores the account details and certificate on the filesystem. 102 | The following environment variables can be set: 103 | 104 | * `STORAGE_DIR`: set the directory to store the account and certificate information (default to current directory). 105 | The information will be saved to a `domain.name.json` file. 106 | 107 | ### s3 108 | 109 | This backend stores the account details and certificate on the filesystem. 110 | The following environment variables can to be set: 111 | 112 | * `AWS_BUCKET`: set the bucket to store the account and certificate information. 113 | The information will be saved to a `name/domain/cert.json` file e.g. `bucket/io/domain/label/cert.json`. 114 | * `AWS_REGION`: set the region for the bucket. 115 | * `AWS_ENCRYPTION_KEY`: set the encryption key for s3 server side encryption (optional). 116 | * `AWS_ENCRYPTION_ALG`: set the encryption algorithm for s3 server side encryption e.g. `AES256` (optional). 117 | 118 | # Disclaimer 119 | 120 | This project is in an alpha state, and therefore should be considered as unreliable and the API is likely to 121 | have breaking changes in the future. 122 | 123 | # Credits, reference and similar projects 124 | 125 | * [traefik](https://github.com/containous/traefik) is a reverse proxy and load balancer that supports several backends 126 | e.g. etcd, kubernetes, etc. and allow generating certificates automatically. The `go-acme` library is based on 127 | `traefik`'s original code. 128 | * [acmewrapper](https://github.com/dkumor/acmewrapper) allows generating certificate using the HTTP/TLS challenge. 129 | So not appropriate for internal services with no public internet access. Only offers a filesystem storage backend. 130 | * [caddy](https://github.com/mholt/caddy) server is another go reverse proxy with support 131 | for Let's Encrypt certificates. 132 | * [Generate and Use Free TLS Certificates with Lego](https://blog.gopheracademy.com/advent-2015/generate-free-tls-certs-with-lego/) 133 | 134 | # Author 135 | 136 | Jerome Touffe-Blin, [@jtblin](https://twitter.com/jtblin), [About me](http://about.me/jtblin) 137 | 138 | # License 139 | 140 | go-acme is copyright 2015 Jerome Touffe-Blin and contributors. 141 | It is licensed under the BSD license. See the include LICENSE file for details. 142 | -------------------------------------------------------------------------------- /acme.go: -------------------------------------------------------------------------------- 1 | package acme 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | "errors" 7 | "fmt" 8 | "io/ioutil" 9 | "log" 10 | "os" 11 | "time" 12 | 13 | "github.com/jtblin/go-logger" 14 | "github.com/xenolf/lego/acme" 15 | 16 | "github.com/jtblin/go-acme/backend" 17 | _ "github.com/jtblin/go-acme/backend/backends" // import all backends. 18 | "github.com/jtblin/go-acme/types" 19 | ) 20 | 21 | const ( 22 | // #2 - important set to true to bundle CA with certificate and 23 | // avoid "transport: x509: certificate signed by unknown authority" error 24 | bundleCA = true 25 | defaultCAServer = "https://acme-v01.api.letsencrypt.org/directory" 26 | ) 27 | 28 | // ACME allows to connect to lets encrypt and retrieve certs. 29 | type ACME struct { 30 | backend backend.Interface 31 | Domain *types.Domain 32 | Logger logger.Interface 33 | BackendName string 34 | CAServer string 35 | DNSProvider string 36 | Email string 37 | SelfSigned bool 38 | } 39 | 40 | func (a *ACME) retrieveCertificate(client *acme.Client, account *types.Account) (*tls.Certificate, error) { 41 | a.Logger.Println("Retrieving ACME certificate...") 42 | domain := []string{} 43 | domain = append(domain, a.Domain.Main) 44 | domain = append(domain, a.Domain.SANs...) 45 | certificate, err := a.getDomainCertificate(client, domain) 46 | if err != nil { 47 | return nil, fmt.Errorf("Error getting ACME certificate for domain %s: %s", domain, err.Error()) 48 | } 49 | if err = account.DomainsCertificate.AddCertificate(certificate, a.Domain); err != nil { 50 | return nil, fmt.Errorf("Error adding ACME certificate for domain %s: %s", domain, err.Error()) 51 | } 52 | if err = a.backend.SaveAccount(account); err != nil { 53 | return nil, fmt.Errorf("Error Saving ACME account %+v: %s", account, err.Error()) 54 | } 55 | a.Logger.Println("Retrieved ACME certificate") 56 | return account.DomainsCertificate.TLSCert, nil 57 | } 58 | 59 | func needsUpdate(cert *tls.Certificate) bool { 60 | // Leaf will be nil because the parsed form of the certificate is not retained 61 | // so we need to parse the certificate manually. 62 | for _, c := range cert.Certificate { 63 | crt, err := x509.ParseCertificate(c) 64 | // If there's an error, we assume the cert is broken, and needs update. 65 | // <= 7 days left, renew certificate. 66 | if err != nil || crt.NotAfter.Before(time.Now().Add(24*7*time.Hour)) { 67 | return true 68 | } 69 | } 70 | return false 71 | } 72 | 73 | func (a *ACME) renewCertificate(client *acme.Client, account *types.Account) error { 74 | dc := account.DomainsCertificate 75 | if needsUpdate(dc.TLSCert) { 76 | renewedCert, err := client.RenewCertificate(acme.CertificateResource{ 77 | Domain: dc.Certificate.Domain, 78 | CertURL: dc.Certificate.CertURL, 79 | CertStableURL: dc.Certificate.CertStableURL, 80 | PrivateKey: dc.Certificate.PrivateKey, 81 | Certificate: dc.Certificate.Cert, 82 | }, false) 83 | if err != nil { 84 | return err 85 | } 86 | renewedACMECert := &types.Certificate{ 87 | Domain: renewedCert.Domain, 88 | CertURL: renewedCert.CertURL, 89 | CertStableURL: renewedCert.CertStableURL, 90 | PrivateKey: renewedCert.PrivateKey, 91 | Cert: renewedCert.Certificate, 92 | } 93 | err = dc.RenewCertificate(renewedACMECert, dc.Domain) 94 | if err != nil { 95 | return err 96 | } 97 | if err = a.backend.SaveAccount(account); err != nil { 98 | return err 99 | } 100 | } 101 | return nil 102 | } 103 | 104 | func (a *ACME) buildACMEClient(Account *types.Account) (*acme.Client, error) { 105 | caServer := defaultCAServer 106 | if len(a.CAServer) > 0 { 107 | caServer = a.CAServer 108 | } 109 | client, err := acme.NewClient(caServer, Account, acme.RSA4096) 110 | if err != nil { 111 | return nil, err 112 | } 113 | 114 | return client, nil 115 | } 116 | 117 | func (a *ACME) getDomainCertificate(client *acme.Client, domains []string) (*types.Certificate, error) { 118 | certificate, failures := client.ObtainCertificate(domains, bundleCA, nil) 119 | if len(failures) > 0 { 120 | return nil, fmt.Errorf("Cannot obtain certificates %s+v", failures) 121 | } 122 | a.Logger.Printf("Loaded ACME certificates %s\n", domains) 123 | return &types.Certificate{ 124 | Domain: certificate.Domain, 125 | CertURL: certificate.CertURL, 126 | CertStableURL: certificate.CertStableURL, 127 | PrivateKey: certificate.PrivateKey, 128 | Cert: certificate.Certificate, 129 | }, nil 130 | } 131 | 132 | // CreateConfig creates a tls.config from using ACME configuration 133 | func (a *ACME) CreateConfig(tlsConfig *tls.Config) error { 134 | if a.Logger == nil { 135 | a.Logger = log.New(os.Stdout, "[go-acme] ", log.Ldate|log.Ltime|log.Lshortfile) 136 | } 137 | if a.Domain == nil || a.Domain.Main == "" { 138 | a.Logger.Panic("The main domain name must be provided") 139 | } 140 | if a.SelfSigned { 141 | a.Logger.Println("Generating self signed certificate...") 142 | cert, err := generateSelfSignedCertificate(a.Domain.Main) 143 | if err != nil { 144 | return err 145 | } 146 | tlsConfig.Certificates = []tls.Certificate{*cert} 147 | return nil 148 | } 149 | 150 | acme.Logger = log.New(ioutil.Discard, "", 0) 151 | 152 | if a.BackendName == "" { 153 | a.BackendName = "fs" 154 | } 155 | b, err := backend.InitBackend(a.BackendName) 156 | if err != nil { 157 | return err 158 | } 159 | a.backend = b 160 | 161 | var account *types.Account 162 | var needRegister bool 163 | 164 | a.Logger.Println("Loading ACME certificate...") 165 | account, err = a.backend.LoadAccount(a.Domain.Main) 166 | if err != nil { 167 | return err 168 | } 169 | if account != nil { 170 | a.Logger.Printf("Loaded ACME config from storage %q\n", a.backend.Name()) 171 | if err = account.DomainsCertificate.Init(); err != nil { 172 | return err 173 | } 174 | } else { 175 | a.Logger.Println("Generating ACME Account...") 176 | account, err = types.NewAccount(a.Email, a.Domain, a.Logger) 177 | if err != nil { 178 | return err 179 | } 180 | needRegister = true 181 | } 182 | 183 | client, err := a.buildACMEClient(account) 184 | if err != nil { 185 | return err 186 | } 187 | client.ExcludeChallenges([]acme.Challenge{acme.HTTP01, acme.TLSSNI01}) 188 | provider, err := newDNSProvider(a.DNSProvider) 189 | if err != nil { 190 | return err 191 | } 192 | client.SetChallengeProvider(acme.DNS01, provider) 193 | 194 | if needRegister { 195 | // New users need to register. 196 | reg, err := client.Register() 197 | if err != nil { 198 | return err 199 | } 200 | account.Registration = reg 201 | 202 | // The client has a URL to the current Let's Encrypt Subscriber 203 | // Agreement. The user needs to agree to it. 204 | err = client.AgreeToTOS() 205 | if err != nil { 206 | return err 207 | } 208 | } 209 | 210 | dc := account.DomainsCertificate 211 | if len(dc.Certificate.Cert) > 0 && len(dc.Certificate.PrivateKey) > 0 { 212 | go func() { 213 | if err := a.renewCertificate(client, account); err != nil { 214 | a.Logger.Printf("Error renewing ACME certificate for %q: %s\n", 215 | account.DomainsCertificate.Domain.Main, err.Error()) 216 | } 217 | }() 218 | } else { 219 | if _, err := a.retrieveCertificate(client, account); err != nil { 220 | return err 221 | } 222 | } 223 | tlsConfig.GetCertificate = func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { 224 | if clientHello.ServerName != a.Domain.Main { 225 | return nil, errors.New("Unknown server name") 226 | } 227 | return dc.TLSCert, nil 228 | } 229 | a.Logger.Println("Loaded certificate...") 230 | 231 | ticker := time.NewTicker(24 * time.Hour) 232 | go func() { 233 | for range ticker.C { 234 | if err := a.renewCertificate(client, account); err != nil { 235 | a.Logger.Printf("Error renewing ACME certificate %q: %s\n", 236 | account.DomainsCertificate.Domain.Main, err.Error()) 237 | } 238 | } 239 | }() 240 | return nil 241 | } 242 | -------------------------------------------------------------------------------- /backend/backends/s3/s3.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 The Kubernetes Authors All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package s3 18 | 19 | import ( 20 | "bytes" 21 | "crypto/md5" 22 | "encoding/json" 23 | "errors" 24 | "fmt" 25 | "io/ioutil" 26 | "os" 27 | "strings" 28 | "sync" 29 | 30 | "github.com/aws/aws-sdk-go/aws" 31 | "github.com/aws/aws-sdk-go/aws/awserr" 32 | "github.com/aws/aws-sdk-go/aws/credentials" 33 | "github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds" 34 | "github.com/aws/aws-sdk-go/aws/ec2metadata" 35 | "github.com/aws/aws-sdk-go/aws/session" 36 | "github.com/aws/aws-sdk-go/service/s3" 37 | 38 | "github.com/jtblin/go-acme/backend" 39 | "github.com/jtblin/go-acme/types" 40 | ) 41 | 42 | const ( 43 | backendName = "s3" 44 | awsBucketEnv = "AWS_BUCKET" 45 | awsEncryptKeyEnv = "AWS_ENCRYPTION_KEY" 46 | awsEncryptAlgEnv = "AWS_ENCRYPTION_ALG" 47 | awsErrorNotFound = "NoSuchKey" 48 | awsRegionEnv = "AWS_REGION" 49 | storageFilename = "cert.json" 50 | ) 51 | 52 | type storage struct { 53 | bucket string 54 | encryptionAlgorithm string 55 | encryptionKey string 56 | s3 S3 57 | storageLock sync.RWMutex 58 | } 59 | 60 | type awsSDKProvider struct { 61 | creds *credentials.Credentials 62 | } 63 | 64 | // Services is an abstraction over AWS, to allow mocking/other implementations. 65 | type Services interface { 66 | Metadata() (EC2Metadata, error) 67 | Storage(region string) (S3, error) 68 | } 69 | 70 | // EC2Metadata is an abstraction over the AWS metadata service. 71 | type EC2Metadata interface { 72 | // Query the EC2 metadata service (used to discover instance-id etc). 73 | GetMetadata(path string) (string, error) 74 | } 75 | 76 | // S3 is an abstraction over S3, to allow mocking/other implementations. 77 | // Note that the ListX functions return a list, so callers don't need to deal with paging. 78 | type S3 interface { 79 | // Get an object from S3. 80 | GetObject(request *s3.GetObjectInput) (*s3.GetObjectOutput, error) 81 | // Put an object in S3. 82 | PutObject(request *s3.PutObjectInput) (*s3.PutObjectOutput, error) 83 | } 84 | 85 | // awsSdkS3 is an implementation of the S3 interface, backed by aws-sdk-go. 86 | type awsSdkS3 struct { 87 | s3 *s3.S3 88 | } 89 | 90 | // GetObject gets an object from an s3 bucket. 91 | func (s *awsSdkS3) GetObject(request *s3.GetObjectInput) (*s3.GetObjectOutput, error) { 92 | response, err := s.s3.GetObject(request) 93 | if err != nil { 94 | if awsErr, ok := err.(awserr.Error); ok { 95 | if awsErr.Code() == awsErrorNotFound { 96 | return nil, nil 97 | } 98 | } 99 | 100 | return nil, err 101 | } 102 | return response, nil 103 | } 104 | 105 | // PutObject puts an object in an s3 bucket. 106 | func (s *awsSdkS3) PutObject(request *s3.PutObjectInput) (*s3.PutObjectOutput, error) { 107 | return s.s3.PutObject(request) 108 | } 109 | 110 | // Metadata is an implementation of EC2 Metadata. 111 | func (p *awsSDKProvider) Metadata() (EC2Metadata, error) { 112 | client := ec2metadata.New(session.New(&aws.Config{})) 113 | return client, nil 114 | } 115 | 116 | // Storage is an implementation of S3 Storage. 117 | func (p *awsSDKProvider) Storage(regionName string) (S3, error) { 118 | service := s3.New(session.New(&aws.Config{ 119 | Region: ®ionName, 120 | Credentials: p.creds, 121 | })) 122 | 123 | s3 := &awsSdkS3{ 124 | s3: service, 125 | } 126 | return s3, nil 127 | } 128 | 129 | // Name returns the display name of the backend. 130 | func (s *storage) Name() string { 131 | return backendName 132 | } 133 | 134 | func key(domain string) string { 135 | parts := strings.Split(domain, ".") 136 | keySlice := make([]string, len(parts)) 137 | l := len(parts) - 1 138 | for idx, part := range parts { 139 | keySlice[l-idx] = part 140 | } 141 | keySlice = append(keySlice, storageFilename) 142 | return strings.Join(keySlice, "/") 143 | } 144 | 145 | // SaveAccount saves the account to s3. 146 | func (s *storage) SaveAccount(account *types.Account) error { 147 | s.storageLock.Lock() 148 | defer s.storageLock.Unlock() 149 | 150 | data, err := json.MarshalIndent(account, "", " ") 151 | if err != nil { 152 | return err 153 | } 154 | 155 | req := &s3.PutObjectInput{ 156 | Body: bytes.NewReader(data), 157 | Bucket: aws.String(s.bucket), 158 | Key: aws.String(key(account.DomainsCertificate.Domain.Main)), 159 | } 160 | if s.encryptionAlgorithm != "" && s.encryptionKey != "" { 161 | req.SSECustomerAlgorithm = aws.String(s.encryptionAlgorithm) 162 | req.SSECustomerKey = aws.String(s.encryptionKey) 163 | req.SSECustomerKeyMD5 = aws.String(fmt.Sprintf("%x", md5.Sum([]byte(s.encryptionKey)))) 164 | } 165 | _, err = s.s3.PutObject(req) 166 | return err 167 | } 168 | 169 | // LoadAccount loads the account from s3. 170 | func (s *storage) LoadAccount(domain string) (*types.Account, error) { 171 | s.storageLock.RLock() 172 | defer s.storageLock.RUnlock() 173 | 174 | req := &s3.GetObjectInput{ 175 | Bucket: aws.String(s.bucket), 176 | Key: aws.String(key(domain)), 177 | } 178 | if s.encryptionAlgorithm != "" && s.encryptionKey != "" { 179 | req.SSECustomerAlgorithm = aws.String(s.encryptionAlgorithm) 180 | req.SSECustomerKey = aws.String(s.encryptionKey) 181 | req.SSECustomerKeyMD5 = aws.String(fmt.Sprintf("%x", md5.Sum([]byte(s.encryptionKey)))) 182 | } 183 | 184 | resp, err := s.s3.GetObject(req) 185 | if err != nil || resp == nil { 186 | return nil, err 187 | } 188 | 189 | defer resp.Body.Close() 190 | file, err := ioutil.ReadAll(resp.Body) 191 | if err != nil { 192 | return nil, err 193 | } 194 | account := types.Account{ 195 | DomainsCertificate: &types.DomainCertificate{}, 196 | } 197 | if err := json.Unmarshal(file, &account); err != nil { 198 | return nil, fmt.Errorf("Error loading account: %v", err) 199 | } 200 | 201 | return &account, nil 202 | } 203 | 204 | func newBackend(awsServices Services) (backend.Interface, error) { 205 | bucket := os.Getenv(awsBucketEnv) 206 | if bucket == "" { 207 | return nil, errors.New("missing bucket name") 208 | } 209 | region := os.Getenv(awsRegionEnv) 210 | if region == "" { 211 | metadata, err := awsServices.Metadata() 212 | if err != nil { 213 | return nil, fmt.Errorf("error creating AWS metadata client: %v", err) 214 | } 215 | 216 | var document struct{ region string } 217 | doc, err := metadata.GetMetadata("dynamic/instance-identity/document") 218 | if err != nil { 219 | return nil, fmt.Errorf("error getting region: %v", err) 220 | } 221 | if err = json.Unmarshal([]byte(doc), &document); err != nil { 222 | return nil, fmt.Errorf("error parsing region: %v", err) 223 | } 224 | region = document.region 225 | } 226 | s3, err := awsServices.Storage(region) 227 | if err != nil { 228 | return nil, err 229 | } 230 | return &storage{ 231 | bucket: bucket, 232 | encryptionAlgorithm: os.Getenv(awsEncryptAlgEnv), 233 | encryptionKey: os.Getenv(awsEncryptKeyEnv), 234 | s3: s3, 235 | }, nil 236 | } 237 | 238 | func newAWSSDKProvider(creds *credentials.Credentials) *awsSDKProvider { 239 | return &awsSDKProvider{creds: creds} 240 | } 241 | 242 | func init() { 243 | backend.RegisterBackend(backendName, func() (backend.Interface, error) { 244 | creds := credentials.NewChainCredentials( 245 | []credentials.Provider{ 246 | &credentials.EnvProvider{}, 247 | &ec2rolecreds.EC2RoleProvider{ 248 | Client: ec2metadata.New(session.New(&aws.Config{})), 249 | }, 250 | &credentials.SharedCredentialsProvider{}, 251 | }) 252 | aws := newAWSSDKProvider(creds) 253 | return newBackend(aws) 254 | }) 255 | } 256 | --------------------------------------------------------------------------------