mirror of
https://github.com/hanzozt/zt.git
synced 2026-11-03 05:01:58 +00:00
261 lines
7.1 KiB
Go
261 lines
7.1 KiB
Go
/*
|
|
Copyright NetFoundry Inc.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
https://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package lets_encrypt
|
|
|
|
import (
|
|
"crypto"
|
|
"crypto/x509"
|
|
"encoding/json"
|
|
"encoding/pem"
|
|
"errors"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/michaelquigley/pfxlog"
|
|
"github.com/openziti/ziti/ziti/internal/log"
|
|
|
|
"github.com/go-acme/lego/v4/certcrypto"
|
|
"github.com/go-acme/lego/v4/lego"
|
|
"github.com/go-acme/lego/v4/registration"
|
|
)
|
|
|
|
const (
|
|
baseAccountsRootFolderName = "accounts"
|
|
baseKeysFolderName = "keys"
|
|
accountFileName = "account.json"
|
|
)
|
|
|
|
// AccountsStorage A storage for account data.
|
|
//
|
|
// rootPath:
|
|
//
|
|
// ./.path/accounts/
|
|
// │ └── root accounts directory
|
|
// └── "path" option
|
|
//
|
|
// rootUserPath:
|
|
//
|
|
// ./.path/accounts/localhost_14000/[email protected]/
|
|
// │ │ │ └── userID ("email" option)
|
|
// │ │ └── CA server ("server" option)
|
|
// │ └── root accounts directory
|
|
// └── "path" option
|
|
//
|
|
// keysPath:
|
|
//
|
|
// ./.path/accounts/localhost_14000/[email protected]/keys/
|
|
// │ │ │ │ └── root keys directory
|
|
// │ │ │ └── userID ("email" option)
|
|
// │ │ └── CA server ("server" option)
|
|
// │ └── root accounts directory
|
|
// └── "path" option
|
|
//
|
|
// accountFilePath:
|
|
//
|
|
// ./.path/accounts/localhost_14000/[email protected]/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
|
|
}
|
|
|
|
// NewAccountsStorage Creates a new AccountsStorage.
|
|
func NewAccountsStorage(options *leOptions) *AccountsStorage {
|
|
|
|
serverURL, err := url.Parse(options.acmeserver)
|
|
if err != nil {
|
|
log.Fatalf("%v", err)
|
|
}
|
|
|
|
rootPath := filepath.Join(options.path, baseAccountsRootFolderName)
|
|
serverPath := strings.NewReplacer(":", "_", "/", string(os.PathSeparator)).Replace(serverURL.Host)
|
|
accountsPath := filepath.Join(rootPath, serverPath)
|
|
rootUserPath := filepath.Join(accountsPath, options.email)
|
|
|
|
return &AccountsStorage{
|
|
userID: options.email,
|
|
rootPath: rootPath,
|
|
rootUserPath: rootUserPath,
|
|
keysPath: filepath.Join(rootUserPath, baseKeysFolderName),
|
|
accountFilePath: filepath.Join(rootUserPath, accountFileName),
|
|
}
|
|
}
|
|
|
|
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.Fatalf("%v", 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 os.WriteFile(s.accountFilePath, jsonBytes, filePerm)
|
|
}
|
|
|
|
func (s *AccountsStorage) LoadAccount(options *leOptions, privateKey crypto.PrivateKey) *Account {
|
|
fileBytes, err := os.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(options, 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(keyType certcrypto.KeyType) crypto.PrivateKey {
|
|
accKeyPath := filepath.Join(s.keysPath, s.userID+".key")
|
|
|
|
if _, err := os.Stat(accKeyPath); os.IsNotExist(err) {
|
|
log.Infof("No key found for account %s. Generating a %s key.", s.userID, keyType)
|
|
s.createKeysFolder()
|
|
|
|
privateKey, err := generatePrivateKey(accKeyPath, keyType)
|
|
if err != nil {
|
|
log.Fatalf("Could not generate RSA private account key for account %s: %v", s.userID, err)
|
|
}
|
|
|
|
log.Infof("Saved key to %s", accKeyPath)
|
|
return privateKey
|
|
}
|
|
|
|
privateKey, err := loadPrivateKey(accKeyPath)
|
|
if err != nil {
|
|
log.Fatalf("Could not load 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, keyType certcrypto.KeyType) (crypto.PrivateKey, error) {
|
|
privateKey, err := certcrypto.GeneratePrivateKey(keyType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
certOut, err := os.Create(file)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
defer func() {
|
|
if closeErr := certOut.Close(); closeErr != nil {
|
|
pfxlog.Logger().WithError(closeErr).Error("Could not close file for private key")
|
|
}
|
|
}()
|
|
|
|
pemKey := certcrypto.PEMBlock(privateKey)
|
|
err = pem.Encode(certOut, pemKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return privateKey, nil
|
|
}
|
|
|
|
func loadPrivateKey(file string) (crypto.PrivateKey, error) {
|
|
keyBytes, err := os.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)
|
|
case "PRIVATE KEY":
|
|
return x509.ParsePKCS8PrivateKey(keyBlock.Bytes)
|
|
}
|
|
|
|
return nil, errors.New("unknown private key type")
|
|
}
|
|
|
|
func tryRecoverRegistration(options *leOptions, 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 = options.acmeserver
|
|
config.UserAgent = "ziti-cli"
|
|
|
|
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
|
|
}
|