package acme import ( "crypto/x509" "encoding/json" "encoding/pem" "fmt" "os" "path/filepath" "github.com/go-acme/lego/certificate" ) // directoryStorage implements storage in a local file directory type directoryStorage string // filename for certificate / key / json file func (d directoryStorage) certFile(name, ext string) string { return filepath.Join(d.certDir(name), name+"."+ext) } func (d directoryStorage) certDir(name string) string { return filepath.Join(string(d), "certificates", name) } func (d directoryStorage) accountDirectory(acmeHost string) string { return filepath.Join(string(d), ".letsencrypt", acmeHost) } func (d directoryStorage) accountFile(acmeHost string) string { return filepath.Join(d.accountDirectory(acmeHost), "account.json") } func (d directoryStorage) accountKeyFile(acmeHost string) string { return filepath.Join(d.accountDirectory(acmeHost), "account.key") } const perms os.FileMode = 0600 const dirPerms os.FileMode = 0700 func (d directoryStorage) GetCertificate(name string) (*certificate.Resource, error) { f, err := os.Open(d.certFile(name, "json")) if err != nil && os.IsNotExist(err) { // if json does not exist, nothing does return nil, nil } if err != nil { return nil, err } defer f.Close() dec := json.NewDecoder(f) cr := &certificate.Resource{} if err = dec.Decode(cr); err != nil { return nil, err } // load cert crtBytes, err := os.ReadFile(d.certFile(name, "crt")) if err != nil { return nil, err } cr.Certificate = crtBytes return cr, nil } func (d directoryStorage) StoreCertificate(name string, cert *certificate.Resource) error { // make sure actual cert data never gets into metadata json if err := os.MkdirAll(d.certDir(name), dirPerms); err != nil { return err } pub := cert.Certificate cert.Certificate = nil priv := cert.PrivateKey cert.PrivateKey = nil combined := []byte(string(pub) + "\n" + string(priv)) jDAt, err := json.MarshalIndent(cert, "", " ") if err != nil { return err } if err = os.WriteFile(d.certFile(name, "json"), jDAt, perms); err != nil { return err } if err = os.WriteFile(d.certFile(name, "crt"), pub, perms); err != nil { return err } if err = os.WriteFile(d.certFile(name, "pem"), combined, perms); err != nil { return err } return os.WriteFile(d.certFile(name, "key"), priv, perms) } func (d directoryStorage) GetAccount(acmeHost string) (*Account, error) { f, err := os.Open(d.accountFile(acmeHost)) if err != nil && os.IsNotExist(err) { return nil, nil } if err != nil { return nil, err } defer f.Close() dec := json.NewDecoder(f) acct := &Account{} if err = dec.Decode(acct); err != nil { return nil, err } keyBytes, err := os.ReadFile(d.accountKeyFile(acmeHost)) if err != nil { return nil, err } keyBlock, _ := pem.Decode(keyBytes) if keyBlock == nil { return nil, fmt.Errorf("error decoding account private key") } acct.key, err = x509.ParseECPrivateKey(keyBlock.Bytes) if err != nil { return nil, err } return acct, nil } func (d directoryStorage) StoreAccount(acmeHost string, account *Account) error { if err := os.MkdirAll(d.accountDirectory(acmeHost), dirPerms); err != nil { return err } acctBytes, err := json.MarshalIndent(account, "", " ") if err != nil { return err } if err = os.WriteFile(d.accountFile(acmeHost), acctBytes, perms); err != nil { return err } keyBytes, err := x509.MarshalECPrivateKey(account.key) if err != nil { return err } pemKey := &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyBytes} pemBytes := pem.EncodeToMemory(pemKey) return os.WriteFile(d.accountKeyFile(acmeHost), pemBytes, perms) }