Use certmagic, simplify config, set HTTP timeouts and a modern TLSConfig

This commit is contained in:
Ken-Håvard Lieng 2018-12-16 12:19:16 +01:00
parent c5a9a5b1c1
commit 6c3a5777c4
171 changed files with 30701 additions and 4912 deletions

View file

@ -1,38 +0,0 @@
package letsencrypt
import (
"path/filepath"
)
type Directory string
func (d Directory) Domain(domain string) string {
return filepath.Join(string(d), "certs", domain)
}
func (d Directory) Cert(domain string) string {
return filepath.Join(d.Domain(domain), "cert.pem")
}
func (d Directory) Key(domain string) string {
return filepath.Join(d.Domain(domain), "key.pem")
}
func (d Directory) Meta(domain string) string {
return filepath.Join(d.Domain(domain), "metadata.json")
}
func (d Directory) User(email string) string {
if email == "" {
email = defaultUser
}
return filepath.Join(string(d), "users", email)
}
func (d Directory) UserRegistration(email string) string {
return filepath.Join(d.User(email), "registration.json")
}
func (d Directory) UserKey(email string) string {
return filepath.Join(d.User(email), "key.pem")
}

View file

@ -1,265 +0,0 @@
package letsencrypt
import (
"crypto/tls"
"encoding/json"
"io/ioutil"
"os"
"sync"
"time"
"github.com/xenolf/lego/acme"
)
const URL = "https://acme-v02.api.letsencrypt.org/directory"
const KeySize = 2048
var directory Directory
func Run(dir, domain, email, port string) (*state, error) {
directory = Directory(dir)
user, err := getUser(email)
if err != nil {
return nil, err
}
client, err := acme.NewClient(URL, &user, acme.RSA2048)
if err != nil {
return nil, err
}
client.SetHTTPAddress(port)
if user.Registration == nil {
user.Registration, err = client.Register(true)
if err != nil {
return nil, err
}
err = saveUser(user)
if err != nil {
return nil, err
}
}
s := &state{
client: client,
domain: domain,
}
if certExists(domain) {
if !s.renew() {
err = s.loadCert()
if err != nil {
return nil, err
}
}
s.refreshOCSP()
} else {
err = s.obtain()
if err != nil {
return nil, err
}
}
go s.maintain()
return s, nil
}
type state struct {
client *acme.Client
domain string
cert *tls.Certificate
certPEM []byte
lock sync.Mutex
}
func (s *state) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
s.lock.Lock()
cert := s.cert
s.lock.Unlock()
return cert, nil
}
func (s *state) getCertPEM() []byte {
s.lock.Lock()
certPEM := s.certPEM
s.lock.Unlock()
return certPEM
}
func (s *state) setCert(meta *acme.CertificateResource) {
cert, err := tls.X509KeyPair(meta.Certificate, meta.PrivateKey)
if err == nil {
s.lock.Lock()
if s.cert != nil {
cert.OCSPStaple = s.cert.OCSPStaple
}
s.cert = &cert
s.certPEM = meta.Certificate
s.lock.Unlock()
}
}
func (s *state) setOCSP(ocsp []byte) {
cert := tls.Certificate{
OCSPStaple: ocsp,
}
s.lock.Lock()
if s.cert != nil {
cert.Certificate = s.cert.Certificate
cert.PrivateKey = s.cert.PrivateKey
}
s.cert = &cert
s.lock.Unlock()
}
func (s *state) obtain() error {
cert, err := s.client.ObtainCertificate([]string{s.domain}, true, nil, false)
if err != nil {
return err
}
s.setCert(cert)
s.refreshOCSP()
err = saveCert(cert)
if err != nil {
return err
}
return nil
}
func (s *state) renew() bool {
cert, err := ioutil.ReadFile(directory.Cert(s.domain))
if err != nil {
return false
}
exp, err := acme.GetPEMCertExpiration(cert)
if err != nil {
return false
}
daysLeft := int(exp.Sub(time.Now().UTC()).Hours() / 24)
if daysLeft <= 30 {
metaBytes, err := ioutil.ReadFile(directory.Meta(s.domain))
if err != nil {
return false
}
key, err := ioutil.ReadFile(directory.Key(s.domain))
if err != nil {
return false
}
var meta acme.CertificateResource
err = json.Unmarshal(metaBytes, &meta)
if err != nil {
return false
}
meta.Certificate = cert
meta.PrivateKey = key
newMeta, err := s.client.RenewCertificate(meta, true, false)
if err != nil {
return false
}
s.setCert(newMeta)
err = saveCert(newMeta)
if err != nil {
return false
}
return true
}
return false
}
func (s *state) refreshOCSP() {
ocsp, resp, err := acme.GetOCSPForCert(s.getCertPEM())
if err == nil && resp.Status == acme.OCSPGood {
s.setOCSP(ocsp)
}
}
func (s *state) maintain() {
renew := time.Tick(24 * time.Hour)
ocsp := time.Tick(1 * time.Hour)
for {
select {
case <-renew:
s.renew()
case <-ocsp:
s.refreshOCSP()
}
}
}
func (s *state) loadCert() error {
cert, err := ioutil.ReadFile(directory.Cert(s.domain))
if err != nil {
return err
}
key, err := ioutil.ReadFile(directory.Key(s.domain))
if err != nil {
return err
}
s.setCert(&acme.CertificateResource{
Certificate: cert,
PrivateKey: key,
})
return nil
}
func certExists(domain string) bool {
if _, err := os.Stat(directory.Cert(domain)); err != nil {
return false
}
if _, err := os.Stat(directory.Key(domain)); err != nil {
return false
}
return true
}
func saveCert(cert *acme.CertificateResource) error {
err := os.MkdirAll(directory.Domain(cert.Domain), 0700)
if err != nil {
return err
}
err = ioutil.WriteFile(directory.Cert(cert.Domain), cert.Certificate, 0600)
if err != nil {
return err
}
err = ioutil.WriteFile(directory.Key(cert.Domain), cert.PrivateKey, 0600)
if err != nil {
return err
}
jsonBytes, err := json.MarshalIndent(&cert, "", " ")
if err != nil {
return err
}
err = ioutil.WriteFile(directory.Meta(cert.Domain), jsonBytes, 0600)
if err != nil {
return err
}
return nil
}

View file

@ -1,109 +0,0 @@
package letsencrypt
import (
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/json"
"encoding/pem"
"io/ioutil"
"os"
"github.com/xenolf/lego/acme"
)
const defaultUser = "default"
type User struct {
Email string
Registration *acme.RegistrationResource
key crypto.PrivateKey
}
func (u User) GetEmail() string {
return u.Email
}
func (u User) GetRegistration() *acme.RegistrationResource {
return u.Registration
}
func (u User) GetPrivateKey() crypto.PrivateKey {
return u.key
}
func newUser(email string) (User, error) {
var err error
user := User{Email: email}
user.key, err = rsa.GenerateKey(rand.Reader, KeySize)
if err != nil {
return user, err
}
return user, nil
}
func getUser(email string) (User, error) {
var user User
reg, err := os.Open(directory.UserRegistration(email))
if err != nil {
if os.IsNotExist(err) {
return newUser(email)
}
return user, err
}
defer reg.Close()
err = json.NewDecoder(reg).Decode(&user)
if err != nil {
return user, err
}
user.key, err = loadRSAPrivateKey(directory.UserKey(email))
if err != nil {
return user, err
}
return user, nil
}
func saveUser(user User) error {
err := os.MkdirAll(directory.User(user.Email), 0700)
if err != nil {
return err
}
err = saveRSAPrivateKey(user.key, directory.UserKey(user.Email))
if err != nil {
return err
}
jsonBytes, err := json.MarshalIndent(&user, "", " ")
if err != nil {
return err
}
return ioutil.WriteFile(directory.UserRegistration(user.Email), jsonBytes, 0600)
}
func loadRSAPrivateKey(file string) (crypto.PrivateKey, error) {
keyBytes, err := ioutil.ReadFile(file)
if err != nil {
return nil, err
}
keyBlock, _ := pem.Decode(keyBytes)
return x509.ParsePKCS1PrivateKey(keyBlock.Bytes)
}
func saveRSAPrivateKey(key crypto.PrivateKey, file string) error {
pemKey := pem.Block{
Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key.(*rsa.PrivateKey)),
}
keyOut, err := os.Create(file)
if err != nil {
return err
}
defer keyOut.Close()
return pem.Encode(keyOut, &pemKey)
}

View file

@ -1,35 +0,0 @@
package letsencrypt
import (
"io/ioutil"
"testing"
"github.com/stretchr/testify/assert"
)
func tempdir() string {
f, _ := ioutil.TempDir("", "")
return f
}
func testUser(t *testing.T, email string) {
user, err := newUser(email)
assert.Nil(t, err)
key := user.GetPrivateKey()
assert.NotNil(t, key)
err = saveUser(user)
assert.Nil(t, err)
user, err = getUser(email)
assert.Nil(t, err)
assert.Equal(t, email, user.GetEmail())
assert.Equal(t, key, user.GetPrivateKey())
}
func TestUser(t *testing.T) {
directory = Directory(tempdir())
testUser(t, "test@test.com")
testUser(t, "")
}