Let's Encrypt
This commit is contained in:
parent
22892a4073
commit
b55cb13e44
82 changed files with 13536 additions and 107 deletions
38
letsencrypt/directory.go
Normal file
38
letsencrypt/directory.go
Normal file
|
@ -0,0 +1,38 @@
|
|||
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")
|
||||
}
|
183
letsencrypt/letsencrypt.go
Normal file
183
letsencrypt/letsencrypt.go
Normal file
|
@ -0,0 +1,183 @@
|
|||
package letsencrypt
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/khlieng/dispatch/Godeps/_workspace/src/github.com/xenolf/lego/acme"
|
||||
)
|
||||
|
||||
const URL = "https://acme-v01.api.letsencrypt.org/directory"
|
||||
const KeySize = 2048
|
||||
|
||||
var directory Directory
|
||||
|
||||
func Run(dir, domain, email, port string, onChange func()) (string, string, error) {
|
||||
directory = Directory(dir)
|
||||
|
||||
user, err := getUser(email)
|
||||
if err != nil {
|
||||
return "", "", nil
|
||||
}
|
||||
|
||||
client, err := acme.NewClient(URL, &user, KeySize)
|
||||
client.ExcludeChallenges([]string{"tls-sni-01"})
|
||||
client.SetHTTPPort(port)
|
||||
|
||||
if user.Registration == nil {
|
||||
user.Registration, err = client.Register()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
err = client.AgreeToTOS()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
err = saveUser(user)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
}
|
||||
|
||||
if certExists(domain) {
|
||||
renew(client, domain)
|
||||
} else {
|
||||
err = obtain(client, domain)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
}
|
||||
|
||||
go keepRenewed(client, domain, onChange)
|
||||
|
||||
return directory.Cert(domain), directory.Key(domain), nil
|
||||
}
|
||||
|
||||
func obtain(client *acme.Client, domain string) error {
|
||||
cert, errors := client.ObtainCertificate([]string{domain}, false)
|
||||
if err := errors[domain]; err != nil {
|
||||
if _, ok := err.(acme.TOSError); ok {
|
||||
err := client.AgreeToTOS()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return obtain(client, domain)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
err := saveCert(cert)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func renew(client *acme.Client, domain string) bool {
|
||||
cert, err := ioutil.ReadFile(directory.Cert(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(domain))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
key, err := ioutil.ReadFile(directory.Key(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
|
||||
|
||||
Renew:
|
||||
newMeta, err := client.RenewCertificate(meta, false)
|
||||
if err != nil {
|
||||
if _, ok := err.(acme.TOSError); ok {
|
||||
err := client.AgreeToTOS()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
goto Renew
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
err = saveCert(newMeta)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func keepRenewed(client *acme.Client, domain string, onChange func()) {
|
||||
for {
|
||||
time.Sleep(24 * time.Hour)
|
||||
if renew(client, domain) {
|
||||
onChange()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
106
letsencrypt/user.go
Normal file
106
letsencrypt/user.go
Normal file
|
@ -0,0 +1,106 @@
|
|||
package letsencrypt
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"github.com/khlieng/dispatch/Godeps/_workspace/src/github.com/xenolf/lego/acme"
|
||||
)
|
||||
|
||||
const defaultUser = "default"
|
||||
|
||||
type User struct {
|
||||
Email string
|
||||
Registration *acme.RegistrationResource
|
||||
key *rsa.PrivateKey
|
||||
}
|
||||
|
||||
func (u User) GetEmail() string {
|
||||
return u.Email
|
||||
}
|
||||
|
||||
func (u User) GetRegistration() *acme.RegistrationResource {
|
||||
return u.Registration
|
||||
}
|
||||
|
||||
func (u User) GetPrivateKey() *rsa.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) (*rsa.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 *rsa.PrivateKey, file string) error {
|
||||
pemKey := pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}
|
||||
keyOut, err := os.Create(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer keyOut.Close()
|
||||
return pem.Encode(keyOut, &pemKey)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue