package ssl import ( "context" "crypto" "crypto/ecdsa" "crypto/rsa" "crypto/tls" "crypto/x509" "encoding/json" "encoding/pem" "errors" "fmt" "io" "net" "net/http" "net/url" "os" "strconv" "strings" "time" "github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/app/model" "github.com/1Panel-dev/1Panel/agent/buserr" legoacme "github.com/go-acme/lego/v5/acme" "github.com/go-acme/lego/v5/certcrypto" "github.com/go-acme/lego/v5/certcrypto/compat" "github.com/go-acme/lego/v5/lego" "github.com/go-acme/lego/v5/registration" "golang.org/x/crypto/acme" ) // parsePrivateKeyPEM decodes a PEM-encoded private key produced by either // lego v4 (SEC1 for EC, PKCS#1 for RSA) or lego v5 (PKCS#8 for both). It tries // PKCS#8 first because that is what v5 and any modern tooling emits, then // falls back to the legacy formats so account keys persisted under v4 keep // working without manual conversion. func parsePrivateKeyPEM(pemBytes []byte) (crypto.Signer, error) { block, _ := pem.Decode(pemBytes) if block == nil { return nil, buserr.New("invalid PEM block") } if key, err := x509.ParsePKCS8PrivateKey(block.Bytes); err == nil { signer, ok := key.(crypto.Signer) if !ok { return nil, fmt.Errorf("PKCS#8 key does not implement crypto.Signer") } return signer, nil } if key, err := x509.ParseECPrivateKey(block.Bytes); err == nil { return key, nil } if key, err := x509.ParsePKCS1PrivateKey(block.Bytes); err == nil { return key, nil } return nil, fmt.Errorf("unsupported private key format") } type zeroSSLRes struct { Success bool `json:"success"` EabKid string `json:"eab_kid"` EabHmacKey string `json:"eab_hmac_key"` } type KeyType = certcrypto.KeyType const ( KeyEC256 = certcrypto.EC256 KeyEC384 = certcrypto.EC384 KeyRSA2048 = certcrypto.RSA2048 KeyRSA3072 = certcrypto.RSA3072 KeyRSA4096 = certcrypto.RSA4096 ) // normalizeKeyType maps legacy v4 KeyType strings (P256/P384/2048/...) to the // v5 form (EC256/EC384/RSA2048/...) using lego v5's official compat helper, so // any account/SSL row written under v4 keeps loading without manual conversion. // Unknown values are returned as-is and let the downstream caller error out. func normalizeKeyType(stored string) KeyType { var k compat.KeyTypeCompat if err := k.UnmarshalText([]byte(stored)); err != nil { return KeyType(stored) } return KeyType(k) } // AcmeUser implements registration.User. Key is crypto.Signer // (lego v5 changed the field type from crypto.PrivateKey to crypto.Signer). type AcmeUser struct { Email string Registration *legoacme.ExtendedAccount Key crypto.Signer } func (u *AcmeUser) GetEmail() string { return u.Email } func (u *AcmeUser) GetRegistration() *legoacme.ExtendedAccount { return u.Registration } func (u *AcmeUser) GetPrivateKey() crypto.Signer { return u.Key } func GetPrivateKey(priKey crypto.PrivateKey, keyType KeyType) ([]byte, error) { var ( marshal []byte block *pem.Block err error ) switch keyType { case KeyEC256, KeyEC384: key := priKey.(*ecdsa.PrivateKey) marshal, err = x509.MarshalECPrivateKey(key) if err != nil { return nil, err } block = &pem.Block{ Type: "EC PRIVATE KEY", Bytes: marshal, } case KeyRSA2048, KeyRSA3072, KeyRSA4096: key := priKey.(*rsa.PrivateKey) marshal = x509.MarshalPKCS1PrivateKey(key) block = &pem.Block{ Type: "RSA PRIVATE KEY", Bytes: marshal, } } return pem.EncodeToMemory(block), nil } func NewRegisterClient(acmeAccount *model.WebsiteAcmeAccount, proxy *dto.SystemProxy) (*AcmeClient, error) { return NewRegisterClientWithContext(context.Background(), acmeAccount, proxy) } func NewRegisterClientWithContext(ctx context.Context, acmeAccount *model.WebsiteAcmeAccount, proxy *dto.SystemProxy) (*AcmeClient, error) { client, err := newAcmeClient(acmeAccount, proxy, nil) if err != nil { return nil, err } var reg *legoacme.ExtendedAccount if acmeAccount.Type == "zerossl" || acmeAccount.Type == "google" || acmeAccount.Type == "freessl" || (acmeAccount.Type == "custom" && acmeAccount.UseEAB) { if acmeAccount.Type == "zerossl" { var res *zeroSSLRes res, err = getZeroSSLEabCredentials(ctx, acmeAccount.Email) if err != nil { return nil, err } if res.Success { acmeAccount.EabKid = res.EabKid acmeAccount.EabHmacKey = res.EabHmacKey } else { return nil, fmt.Errorf("get zero ssl eab credentials failed") } } eabOptions := registration.RegisterEABOptions{ TermsOfServiceAgreed: true, Kid: acmeAccount.EabKid, HmacEncoded: acmeAccount.EabHmacKey, } reg, err = client.Client.Registration.RegisterWithExternalAccountBinding(ctx, eabOptions) if err != nil { return nil, err } } else { reg, err = client.Client.Registration.Register(ctx, registration.RegisterOptions{TermsOfServiceAgreed: true}) if err != nil { return nil, err } } client.User.Registration = reg if reg != nil { acmeAccount.URL = reg.Location } if acmeAccount.PrivateKey == "" { privateKey, err := GetPrivateKey(client.User.GetPrivateKey(), normalizeKeyType(acmeAccount.KeyType)) if err != nil { return nil, err } acmeAccount.PrivateKey = string(privateKey) } return client, nil } func newAcmeClient(acmeAccount *model.WebsiteAcmeAccount, proxy *dto.SystemProxy, registration *legoacme.ExtendedAccount) (*AcmeClient, error) { var ( priKey crypto.Signer err error ) keyType := normalizeKeyType(acmeAccount.KeyType) if acmeAccount.PrivateKey != "" { signer, parseErr := parsePrivateKeyPEM([]byte(acmeAccount.PrivateKey)) if parseErr != nil { return nil, parseErr } priKey = signer } else { generated, genErr := certcrypto.GeneratePrivateKey(keyType) if genErr != nil { return nil, genErr } signer, ok := generated.(crypto.Signer) if !ok { return nil, fmt.Errorf("generated key does not implement crypto.Signer") } priKey = signer } myUser := &AcmeUser{ Email: acmeAccount.Email, Registration: registration, Key: priKey, } config := NewConfigWithProxy(myUser, acmeAccount.Type, acmeAccount.CaDirURL, proxy) client, err := lego.NewClient(config) if err != nil { return nil, err } acmeClient := &AcmeClient{ User: myUser, Client: client, Config: config, } return acmeClient, nil } func getCaDirURL(accountType, customCaURL string) string { var caDirURL string switch accountType { case "letsencrypt": caDirURL = "https://acme-v02.api.letsencrypt.org/directory" case "zerossl": caDirURL = "https://acme.zerossl.com/v2/DV90" case "buypass": caDirURL = "https://api.buypass.com/acme/directory" case "google": caDirURL = "https://dv.acme-v02.api.pki.goog/directory" case "freessl": caDirURL = "https://acmepro.freessl.cn/v2/DV" case "custom": caDirURL = customCaURL } return caDirURL } func NewConfigWithProxy(user registration.User, accountType, customCaURL string, systemProxy *dto.SystemProxy) *lego.Config { var ( caDirURL string proxyURL string proxyUser string proxyPassword string ) caDirURL = getCaDirURL(accountType, customCaURL) if systemProxy != nil { proxyURL = fmt.Sprintf("%s://%s:%s", systemProxy.Type, systemProxy.URL, systemProxy.Port) proxyUser = systemProxy.User proxyPassword = systemProxy.Password } // lego v5 removed lego.CertificateConfig.KeyType; the key type is now // taken from the user's account for each ObtainRequest call. config := lego.NewConfig(user) config.CADirURL = caDirURL config.UserAgent = "1Panel" config.HTTPClient = createHTTPClientWithProxy(proxyURL, proxyUser, proxyPassword) config.Certificate.Timeout = 60 * time.Second return config } func initCertPool() *x509.CertPool { customCACertsPath := os.Getenv("LEGO_CA_CERTIFICATES") if customCACertsPath == "" { return nil } useSystemCertPool, _ := strconv.ParseBool(os.Getenv("LEGO_CA_SYSTEM_CERT_POOL")) caCerts := strings.Split(customCACertsPath, string(os.PathListSeparator)) certPool, err := lego.CreateCertPool(caCerts, useSystemCertPool) if err != nil { panic(fmt.Sprintf("create certificates pool: %v", err)) } return certPool } func createHTTPClientWithProxy(proxyURL, username, password string) *http.Client { var proxyFunc func(*http.Request) (*url.URL, error) if proxyURL != "" { parsedURL, err := url.Parse(proxyURL) if err != nil { proxyFunc = http.ProxyFromEnvironment } else { if username != "" && password != "" { parsedURL.User = url.UserPassword(username, password) } else if username != "" { parsedURL.User = url.User(username) } proxyFunc = http.ProxyURL(parsedURL) } } else { proxyFunc = func(_ *http.Request) (*url.URL, error) { return nil, nil } } return &http.Client{ Timeout: 2 * time.Minute, Transport: &http.Transport{ Proxy: proxyFunc, DialContext: (&net.Dialer{ Timeout: 60 * time.Second, KeepAlive: 60 * time.Second, }).DialContext, TLSHandshakeTimeout: 60 * time.Second, ResponseHeaderTimeout: 60 * time.Second, TLSClientConfig: &tls.Config{ ServerName: os.Getenv("LEGO_CA_SERVER_NAME"), RootCAs: initCertPool(), }, }, } } func getZeroSSLEabCredentials(ctx context.Context, email string) (*zeroSSLRes, error) { baseURL := "https://api.zerossl.com/acme/eab-credentials-email" params := url.Values{} params.Add("email", email) requestURL := fmt.Sprintf("%s?%s", baseURL, params.Encode()) req, err := http.NewRequestWithContext(ctx, "POST", requestURL, nil) if err != nil { return nil, err } req.Header.Set("Content-Type", "application/json") client := &http.Client{Timeout: 2 * time.Minute} defer client.CloseIdleConnections() resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("server returned non-200 status: %d %s", resp.StatusCode, http.StatusText(resp.StatusCode)) } var result zeroSSLRes err = json.Unmarshal(body, &result) if err != nil { return nil, err } return &result, nil } // GetPrivateKeyByType returns a crypto.Signer (lego v5 changed from crypto.PrivateKey). func GetPrivateKeyByType(keyType, sslPrivateKey string) (crypto.Signer, error) { kType := normalizeKeyType(keyType) if sslPrivateKey == "" { generated, genErr := certcrypto.GeneratePrivateKey(kType) if genErr != nil { return nil, genErr } signer, ok := generated.(crypto.Signer) if !ok { return nil, fmt.Errorf("generated key does not implement crypto.Signer") } return signer, nil } return parsePrivateKeyPEM([]byte(sslPrivateKey)) } func getWebsiteSSLDomains(websiteSSL *model.WebsiteSSL) []string { domains := []string{websiteSSL.PrimaryDomain} if websiteSSL.Domains != "" { domains = append(domains, strings.Split(websiteSSL.Domains, ",")...) } return domains } const ( maxRetryAttempts = 3 retryDelayOn503 = 30 * time.Second ) // isHTTP503Error checks if an error is an HTTP 503 Service Unavailable error. func isHTTP503Error(err error) bool { if err == nil { return false } // Check for golang.org/x/crypto/acme.Error (used in manual_client.go). var acmeErr *acme.Error if errors.As(err, &acmeErr) { return acmeErr.StatusCode == http.StatusServiceUnavailable } // Check error message for 503 (fallback for lego library errors). errMsg := err.Error() return strings.Contains(errMsg, "503") || strings.Contains(errMsg, "Service busy") }