1Panel/agent/utils/ssl/manual_client.go

308 lines
7.8 KiB
Go

package ssl
import (
"context"
"crypto"
"crypto/ecdsa"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/go-acme/lego/v4/certificate"
"golang.org/x/crypto/acme"
"log"
"net"
"strings"
"time"
)
type ManualClient struct {
client *acme.Client
account *model.WebsiteAcmeAccount
logger *log.Logger
}
type RequestCertRequest struct {
WebsiteSSL *model.WebsiteSSL
}
func NewCustomAcmeClient(acmeAccount *model.WebsiteAcmeAccount, logger *log.Logger) (*ManualClient, error) {
var (
key crypto.PrivateKey
err error
)
switch KeyType(acmeAccount.KeyType) {
case KeyEC256, KeyEC384:
block, _ := pem.Decode([]byte(acmeAccount.PrivateKey))
key, err = x509.ParseECPrivateKey(block.Bytes)
if err != nil {
return nil, err
}
case KeyRSA2048, KeyRSA3072, KeyRSA4096:
block, _ := pem.Decode([]byte(acmeAccount.PrivateKey))
key, err = x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return nil, err
}
}
if logger == nil {
logger = log.Default()
}
client := &acme.Client{
Key: key.(crypto.Signer),
DirectoryURL: getCaDirURL(acmeAccount.Type, acmeAccount.CaDirURL),
}
return &ManualClient{
client: client,
account: acmeAccount,
logger: logger,
}, nil
}
type Resolve struct {
Key string
Value string
Err string
}
func (c *ManualClient) GetDNSResolve(ctx context.Context, websiteSSL *model.WebsiteSSL) (map[string]Resolve, error) {
order, err := c.client.AuthorizeOrder(ctx, acme.DomainIDs(getWebsiteSSLDomains(websiteSSL)...))
if err != nil {
return nil, err
}
Orders[websiteSSL.ID] = order
records := make(map[string]Resolve)
for _, authzURL := range order.AuthzURLs {
authz, err := c.client.GetAuthorization(ctx, authzURL)
if err != nil {
return nil, err
}
domain := authz.Identifier.Value
var dnsChallenge *acme.Challenge
for _, challenge := range authz.Challenges {
if challenge.Type == "dns-01" {
dnsChallenge = challenge
break
}
}
if dnsChallenge == nil {
return nil, fmt.Errorf("no DNS-01 challenge found for domain %s", domain)
}
txtValue, err := c.client.DNS01ChallengeRecord(dnsChallenge.Token)
if err != nil {
return nil, err
}
records[domain] = Resolve{
Key: fmt.Sprintf("_acme-challenge.%s", domain),
Value: txtValue,
}
}
return records, nil
}
func queryDNSRecords(domain string) (map[string]string, error) {
recordName := fmt.Sprintf("_acme-challenge.%s", domain)
txts, err := net.LookupTXT(recordName)
if err != nil {
return nil, err
}
records := make(map[string]string)
if len(txts) > 0 {
records[recordName] = txts[0]
}
return records, nil
}
func (c *ManualClient) handleAuthorization(ctx context.Context, authzURL string) error {
authz, err := c.client.GetAuthorization(ctx, authzURL)
if err != nil {
return fmt.Errorf("failed to get authorization: %v", err)
}
domain := authz.Identifier.Value
c.logger.Printf("[INFO] [%s] AuthURL: %s", domain, authzURL)
if authz.Status == acme.StatusValid {
return nil
}
var dnsChallenge *acme.Challenge
for _, challenge := range authz.Challenges {
if challenge.Type == "dns-01" {
dnsChallenge = challenge
break
}
}
c.logger.Printf("[INFO] [%s] acme: use dns-01 solver", domain)
if dnsChallenge == nil {
return fmt.Errorf("no DNS-01 challenge found for domain %s", domain)
}
deadline := time.Now().Add(manualDnsTimeout)
expectedRecord, err := c.client.DNS01ChallengeRecord(dnsChallenge.Token)
if err != nil {
return fmt.Errorf("failed to compute DNS challenge record: %v", err)
}
c.logger.Printf("[INFO] [%s] acme: Checking TXT record %s", domain, expectedRecord)
for {
c.logger.Printf("[INFO] [%s] acme: Checking DNS record propagation.", domain)
currentRecords, err := queryDNSRecords(domain)
if err != nil {
return fmt.Errorf("failed to query DNS records: %v", err)
}
recordName := fmt.Sprintf("_acme-challenge.%s", domain)
providedRecord, exists := currentRecords[recordName]
if exists && providedRecord == expectedRecord {
break
}
if time.Now().After(deadline) {
if !exists {
return fmt.Errorf("TXT record not provided for domain %s after retrying", domain)
}
c.logger.Printf("[INFO] [%s] TXT record mismatch for %s: expected %s, got %s\"", domain, domain, expectedRecord, providedRecord)
return fmt.Errorf("TXT record mismatch for %s: expected %s, got %s", domain, expectedRecord, providedRecord)
}
time.Sleep(pollingInterval)
}
_, err = c.client.Accept(ctx, dnsChallenge)
if err != nil {
return fmt.Errorf("failed to accept challenge: %v", err)
}
for {
time.Sleep(pollingInterval)
authz, err = c.client.GetAuthorization(ctx, authzURL)
if err != nil {
return fmt.Errorf("failed to get authorization while polling: %v", err)
}
if authz.Status == acme.StatusValid {
break
} else if authz.Status == acme.StatusInvalid {
return fmt.Errorf("authorization failed for domain %s", domain)
}
}
return nil
}
func (c *ManualClient) createCSR(keyType string, privateKey string, domains []string) ([]byte, crypto.PrivateKey, error) {
var certKey crypto.PrivateKey
var err error
certKey, err = GetPrivateKeyByType(keyType, privateKey)
if err != nil {
return nil, nil, err
}
template := x509.CertificateRequest{
Subject: pkix.Name{CommonName: domains[0]},
DNSNames: domains,
}
csrBytes, err := x509.CreateCertificateRequest(rand.Reader, &template, certKey)
if err != nil {
return nil, nil, err
}
return csrBytes, certKey, nil
}
func (c *ManualClient) encodePrivateKey(key crypto.PrivateKey) (string, error) {
var keyBytes []byte
var keyType string
var err error
switch k := key.(type) {
case *ecdsa.PrivateKey:
keyBytes, err = x509.MarshalECPrivateKey(k)
keyType = "EC PRIVATE KEY"
case *rsa.PrivateKey:
keyBytes = x509.MarshalPKCS1PrivateKey(k)
keyType = "RSA PRIVATE KEY"
default:
return "", fmt.Errorf("unsupported key type")
}
if err != nil {
return "", err
}
block := &pem.Block{
Type: keyType,
Bytes: keyBytes,
}
return string(pem.EncodeToMemory(block)), nil
}
func (c *ManualClient) RequestCertificate(ctx context.Context, websiteSSL *model.WebsiteSSL) (certificate.Resource, error) {
var res certificate.Resource
domains := []string{websiteSSL.PrimaryDomain}
if websiteSSL.Domains != "" {
domains = append(domains, strings.Split(websiteSSL.Domains, ",")...)
}
c.logger.Printf("[INFO] Requesting certificate for domains: %v\n", domains)
csr, certKey, err := c.createCSR(websiteSSL.KeyType, websiteSSL.PrivateKey, domains)
if err != nil {
return res, err
}
order, ok := Orders[websiteSSL.ID]
if !ok {
return res, fmt.Errorf("order not found")
}
defer delete(Orders, websiteSSL.ID)
for _, authzURL := range order.AuthzURLs {
if err := c.handleAuthorization(ctx, authzURL); err != nil {
return res, err
}
}
c.logger.Printf("[INFO] acme: Validations succeeded; requesting certificates")
order, err = c.client.WaitOrder(ctx, order.URI)
if err != nil {
return res, err
}
if order.Status != acme.StatusReady {
return res, fmt.Errorf("order not ready: %s", order.Status)
}
certBytes, certURL, err := c.client.CreateOrderCert(ctx, order.FinalizeURL, csr, true)
if err != nil {
return res, fmt.Errorf("failed to finalize order: %v", err)
}
privateKeyPEM, err := c.encodePrivateKey(certKey)
if err != nil {
return res, err
}
var certPEM []byte
for _, cert := range certBytes {
block := &pem.Block{
Type: "CERTIFICATE",
Bytes: cert,
}
certPEM = append(certPEM, pem.EncodeToMemory(block)...)
}
c.logger.Printf("[INFO] acme: Server responded with a certificate.")
resource := certificate.Resource{
Domain: domains[0],
CertURL: certURL,
CertStableURL: certURL,
PrivateKey: []byte(privateKeyPEM),
Certificate: certPEM,
CSR: csr,
}
return resource, nil
}