42941ccea6
- Packages - Isolate code used by the CLI into the package `cmd` - (experimental) Add e2e tests for HTTP01, TLS-ALPN-01 and DNS-01, use [Pebble](https://github.com/letsencrypt/pebble) and [challtestsrv](https://github.com/letsencrypt/boulder/tree/master/test/challtestsrv) - Support non-ascii domain name (punnycode) - Check all challenges in a predictable order - No more global exported variables - Archive revoked certificates - Fixes revocation for subdomains and non-ascii domains - Disable pending authorizations - use pointer for RemoteError/ProblemDetails - Poll authz URL instead of challenge URL - The ability for a DNS provider to solve the challenge sequentially - Check all nameservers in a predictable order - Option to disable the complete propagation Requirement - CLI, support for renew with CSR - CLI, add SAN on renew - Add command to list certificates. - Logs every iteration of waiting for the propagation - update DNSimple client - update github.com/miekg/dns
251 lines
6.6 KiB
Go
251 lines
6.6 KiB
Go
package cmd
|
|
|
|
import (
|
|
"crypto"
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"crypto/rand"
|
|
"crypto/x509"
|
|
"encoding/json"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/urfave/cli"
|
|
"github.com/xenolf/lego/lego"
|
|
"github.com/xenolf/lego/log"
|
|
"github.com/xenolf/lego/registration"
|
|
)
|
|
|
|
const (
|
|
baseAccountsRootFolderName = "accounts"
|
|
baseKeysFolderName = "keys"
|
|
accountFileName = "account.json"
|
|
)
|
|
|
|
// AccountsStorage A storage for account data.
|
|
//
|
|
// rootPath:
|
|
//
|
|
// ./.lego/accounts/
|
|
// │ └── root accounts directory
|
|
// └── "path" option
|
|
//
|
|
// rootUserPath:
|
|
//
|
|
// ./.lego/accounts/localhost_14000/hubert@hubert.com/
|
|
// │ │ │ └── userID ("email" option)
|
|
// │ │ └── CA server ("server" option)
|
|
// │ └── root accounts directory
|
|
// └── "path" option
|
|
//
|
|
// keysPath:
|
|
//
|
|
// ./.lego/accounts/localhost_14000/hubert@hubert.com/keys/
|
|
// │ │ │ │ └── root keys directory
|
|
// │ │ │ └── userID ("email" option)
|
|
// │ │ └── CA server ("server" option)
|
|
// │ └── root accounts directory
|
|
// └── "path" option
|
|
//
|
|
// accountFilePath:
|
|
//
|
|
// ./.lego/accounts/localhost_14000/hubert@hubert.com/account.json
|
|
// │ │ │ │ └── account file
|
|
// │ │ │ └── userID ("email" option)
|
|
// │ │ └── CA server ("server" option)
|
|
// │ └── root accounts directory
|
|
// └── "path" option
|
|
//
|
|
type AccountsStorage struct {
|
|
userID string
|
|
rootPath string
|
|
rootUserPath string
|
|
keysPath string
|
|
accountFilePath string
|
|
ctx *cli.Context
|
|
}
|
|
|
|
// NewAccountsStorage Creates a new AccountsStorage.
|
|
func NewAccountsStorage(ctx *cli.Context) *AccountsStorage {
|
|
// TODO: move to account struct? Currently MUST pass email.
|
|
email := getEmail(ctx)
|
|
|
|
serverURL, err := url.Parse(ctx.GlobalString("server"))
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
rootPath := filepath.Join(ctx.GlobalString("path"), baseAccountsRootFolderName)
|
|
serverPath := strings.NewReplacer(":", "_", "/", string(os.PathSeparator)).Replace(serverURL.Host)
|
|
accountsPath := filepath.Join(rootPath, serverPath)
|
|
rootUserPath := filepath.Join(accountsPath, email)
|
|
|
|
return &AccountsStorage{
|
|
userID: email,
|
|
rootPath: rootPath,
|
|
rootUserPath: rootUserPath,
|
|
keysPath: filepath.Join(rootUserPath, baseKeysFolderName),
|
|
accountFilePath: filepath.Join(rootUserPath, accountFileName),
|
|
ctx: ctx,
|
|
}
|
|
}
|
|
|
|
func (s *AccountsStorage) ExistsAccountFilePath() bool {
|
|
accountFile := filepath.Join(s.rootUserPath, accountFileName)
|
|
if _, err := os.Stat(accountFile); os.IsNotExist(err) {
|
|
return false
|
|
} else if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (s *AccountsStorage) GetRootPath() string {
|
|
return s.rootPath
|
|
}
|
|
|
|
func (s *AccountsStorage) GetRootUserPath() string {
|
|
return s.rootUserPath
|
|
}
|
|
|
|
func (s *AccountsStorage) GetUserID() string {
|
|
return s.userID
|
|
}
|
|
|
|
func (s *AccountsStorage) Save(account *Account) error {
|
|
jsonBytes, err := json.MarshalIndent(account, "", "\t")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return ioutil.WriteFile(s.accountFilePath, jsonBytes, filePerm)
|
|
}
|
|
|
|
func (s *AccountsStorage) LoadAccount(privateKey crypto.PrivateKey) *Account {
|
|
fileBytes, err := ioutil.ReadFile(s.accountFilePath)
|
|
if err != nil {
|
|
log.Fatalf("Could not load file for account %s -> %v", s.userID, err)
|
|
}
|
|
|
|
var account Account
|
|
err = json.Unmarshal(fileBytes, &account)
|
|
if err != nil {
|
|
log.Fatalf("Could not parse file for account %s -> %v", s.userID, err)
|
|
}
|
|
|
|
account.key = privateKey
|
|
|
|
if account.Registration == nil || account.Registration.Body.Status == "" {
|
|
reg, err := tryRecoverRegistration(s.ctx, privateKey)
|
|
if err != nil {
|
|
log.Fatalf("Could not load account for %s. Registration is nil -> %#v", s.userID, err)
|
|
}
|
|
|
|
account.Registration = reg
|
|
err = s.Save(&account)
|
|
if err != nil {
|
|
log.Fatalf("Could not save account for %s. Registration is nil -> %#v", s.userID, err)
|
|
}
|
|
}
|
|
|
|
return &account
|
|
}
|
|
|
|
func (s *AccountsStorage) GetPrivateKey() crypto.PrivateKey {
|
|
accKeyPath := filepath.Join(s.keysPath, s.userID+".key")
|
|
|
|
if _, err := os.Stat(accKeyPath); os.IsNotExist(err) {
|
|
log.Printf("No key found for account %s. Generating a curve P384 EC key.", s.userID)
|
|
s.createKeysFolder()
|
|
|
|
privateKey, err := generatePrivateKey(accKeyPath)
|
|
if err != nil {
|
|
log.Fatalf("Could not generate RSA private account key for account %s: %v", s.userID, err)
|
|
}
|
|
|
|
log.Printf("Saved key to %s", accKeyPath)
|
|
return privateKey
|
|
}
|
|
|
|
privateKey, err := loadPrivateKey(accKeyPath)
|
|
if err != nil {
|
|
log.Fatalf("Could not load RSA private key from file %s: %v", accKeyPath, err)
|
|
}
|
|
|
|
return privateKey
|
|
}
|
|
|
|
func (s *AccountsStorage) createKeysFolder() {
|
|
if err := createNonExistingFolder(s.keysPath); err != nil {
|
|
log.Fatalf("Could not check/create directory for account %s: %v", s.userID, err)
|
|
}
|
|
}
|
|
|
|
func generatePrivateKey(file string) (crypto.PrivateKey, error) {
|
|
privateKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
keyBytes, err := x509.MarshalECPrivateKey(privateKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
pemKey := pem.Block{Type: "EC PRIVATE KEY", Bytes: keyBytes}
|
|
|
|
certOut, err := os.Create(file)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer certOut.Close()
|
|
|
|
err = pem.Encode(certOut, &pemKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return privateKey, nil
|
|
}
|
|
|
|
func loadPrivateKey(file string) (crypto.PrivateKey, error) {
|
|
keyBytes, err := ioutil.ReadFile(file)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
keyBlock, _ := pem.Decode(keyBytes)
|
|
|
|
switch keyBlock.Type {
|
|
case "RSA PRIVATE KEY":
|
|
return x509.ParsePKCS1PrivateKey(keyBlock.Bytes)
|
|
case "EC PRIVATE KEY":
|
|
return x509.ParseECPrivateKey(keyBlock.Bytes)
|
|
}
|
|
|
|
return nil, errors.New("unknown private key type")
|
|
}
|
|
|
|
func tryRecoverRegistration(ctx *cli.Context, privateKey crypto.PrivateKey) (*registration.Resource, error) {
|
|
// couldn't load account but got a key. Try to look the account up.
|
|
config := lego.NewConfig(&Account{key: privateKey})
|
|
config.CADirURL = ctx.GlobalString("server")
|
|
config.UserAgent = fmt.Sprintf("lego-cli/%s", ctx.App.Version)
|
|
|
|
client, err := lego.NewClient(config)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
reg, err := client.Registration.ResolveAccountByKey()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return reg, nil
|
|
}
|