package service import ( "archive/zip" "bytes" "context" "crypto/ecdsa" "crypto/rand" "crypto/rsa" "crypto/tls" "crypto/x509" "crypto/x509/pkix" "encoding/pem" "errors" "fmt" "io" "log" "math/big" "net" "os" "path" "strings" "time" "github.com/1Panel-dev/1Panel/agent/app/dto/request" "github.com/1Panel-dev/1Panel/agent/app/dto/response" "github.com/1Panel-dev/1Panel/agent/app/model" "github.com/1Panel-dev/1Panel/agent/app/repo" "github.com/1Panel-dev/1Panel/agent/buserr" "github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/i18n" "github.com/1Panel-dev/1Panel/agent/utils/common" "github.com/1Panel-dev/1Panel/agent/utils/files" "github.com/1Panel-dev/1Panel/agent/utils/ssl" "github.com/go-acme/lego/v5/certcrypto" "gorm.io/gorm" ) type WebsiteCAService struct { } type IWebsiteCAService interface { Import(req request.WebsiteCAImport) error Upload(name string, archive []byte) error Page(search request.WebsiteCASearch) (int64, []response.WebsiteCADTO, error) Create(create request.WebsiteCACreate) (*request.WebsiteCACreate, error) GetCA(id uint) (*response.WebsiteCADTO, error) Delete(id uint) error ObtainSSL(req request.WebsiteCAObtain) (*model.WebsiteSSL, error) DownloadFile(id uint) (*os.File, error) } func NewIWebsiteCAService() IWebsiteCAService { return &WebsiteCAService{} } func (w WebsiteCAService) Page(search request.WebsiteCASearch) (int64, []response.WebsiteCADTO, error) { total, cas, err := websiteCARepo.Page(search.Page, search.PageSize, repo.WithOrderDesc("created_at")) if err != nil { return 0, nil, err } var caDTOs []response.WebsiteCADTO for _, ca := range cas { caDTOs = append(caDTOs, response.WebsiteCADTO{ WebsiteCA: ca, }) } return total, caDTOs, err } func (w WebsiteCAService) Create(create request.WebsiteCACreate) (*request.WebsiteCACreate, error) { if exist, _ := websiteCARepo.GetFirst(repo.WithByName(create.Name)); exist.ID > 0 { return nil, buserr.New("ErrNameIsExist") } ca := &model.WebsiteCA{ Name: create.Name, KeyType: create.KeyType, } pkixName := pkix.Name{ CommonName: create.CommonName, Country: []string{create.Country}, Organization: []string{create.Organization}, OrganizationalUnit: []string{create.OrganizationUint}, } if create.Province != "" { pkixName.Province = []string{create.Province} } if create.City != "" { pkixName.Locality = []string{create.City} } rootCA := &x509.Certificate{ SerialNumber: big.NewInt(time.Now().Unix() + 1), Subject: pkixName, NotBefore: time.Now(), NotAfter: time.Now().AddDate(10, 0, 0), BasicConstraintsValid: true, IsCA: true, MaxPathLen: 1, MaxPathLenZero: false, KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, } interPrivateKey, interPublicKey, privateBytes, err := createPrivateKey(create.KeyType) if err != nil { return nil, err } ca.PrivateKey = string(privateBytes) rootDer, err := x509.CreateCertificate(rand.Reader, rootCA, rootCA, interPublicKey, interPrivateKey) if err != nil { return nil, err } rootCert, err := x509.ParseCertificate(rootDer) if err != nil { return nil, err } certBlock := &pem.Block{ Type: "CERTIFICATE", Bytes: rootCert.Raw, } pemData := pem.EncodeToMemory(certBlock) ca.CSR = string(pemData) if err := websiteCARepo.Create(context.Background(), ca); err != nil { return nil, err } return &create, nil } func (w WebsiteCAService) GetCA(id uint) (*response.WebsiteCADTO, error) { res := &response.WebsiteCADTO{} ca, err := websiteCARepo.GetFirst(repo.WithByID(id)) if err != nil { return nil, err } res.WebsiteCA = ca certBlock, _ := pem.Decode([]byte(ca.CSR)) if certBlock == nil { return nil, buserr.New("ErrSSLCertificateFormat") } cert, err := x509.ParseCertificate(certBlock.Bytes) if err != nil { return nil, err } res.CommonName = cert.Issuer.CommonName res.Organization = strings.Join(cert.Issuer.Organization, ",") res.Country = strings.Join(cert.Issuer.Country, ",") res.Province = strings.Join(cert.Issuer.Province, ",") res.City = strings.Join(cert.Issuer.Locality, ",") res.OrganizationUint = strings.Join(cert.Issuer.OrganizationalUnit, ",") return res, nil } func (w WebsiteCAService) Delete(id uint) error { ssls, _ := websiteSSLRepo.List(websiteSSLRepo.WithByCAID(id)) if len(ssls) > 0 { return buserr.New("ErrDeleteCAWithSSL") } exist, err := websiteCARepo.GetFirst(repo.WithByID(id)) if err != nil { return err } if exist.Name == "1Panel" { return buserr.New("ErrDefaultCA") } return websiteCARepo.DeleteBy(repo.WithByID(id)) } func (w WebsiteCAService) ObtainSSL(req request.WebsiteCAObtain) (*model.WebsiteSSL, error) { var ( domains []string ips []net.IP websiteSSL = &model.WebsiteSSL{} err error ca model.WebsiteCA ) if req.Renew { websiteSSL, err = websiteSSLRepo.GetFirst(repo.WithByID(req.SSLID)) if err != nil { return nil, err } ca, err = websiteCARepo.GetFirst(repo.WithByID(websiteSSL.CaID)) if err != nil { return nil, err } existDomains := []string{websiteSSL.PrimaryDomain} if websiteSSL.Domains != "" { existDomains = append(existDomains, strings.Split(websiteSSL.Domains, ",")...) } for _, domain := range existDomains { if ipAddress := common.ParseIPLoose(domain); ipAddress == nil { domains = append(domains, domain) } else { ips = append(ips, ipAddress) } } } else { ca, err = websiteCARepo.GetFirst(repo.WithByID(req.ID)) if err != nil { return nil, err } websiteSSL = &model.WebsiteSSL{ Provider: constant.SelfSigned, KeyType: req.KeyType, PushDir: req.PushDir, CaID: ca.ID, AutoRenew: req.AutoRenew, Description: req.Description, ExecShell: req.ExecShell, } setSSLPushConfig(websiteSSL, req.PushNode, req.Nodes) if req.ExecShell { websiteSSL.Shell = req.Shell } if req.PushDir { dirs := certificateDirectories(req.Dir) if len(dirs) == 0 { return nil, buserr.New("ErrLinkPathNotFound") } for _, dir := range dirs { info, err := os.Stat(dir) if err != nil || !info.IsDir() { return nil, buserr.New("ErrLinkPathNotFound") } } websiteSSL.Dir = strings.Join(dirs, "\n") } if req.Domains != "" { domainArray := strings.Split(req.Domains, "\n") for _, domain := range domainArray { if ipAddress := common.ParseIPLoose(domain); ipAddress == nil { if domain != "localhost" && !common.IsValidDomain(domain) { err = buserr.WithName("ErrDomainFormat", domain) return nil, err } domains = append(domains, domain) } else { ips = append(ips, ipAddress) } } if len(domains) > 0 { websiteSSL.PrimaryDomain = domains[0] websiteSSL.Domains = strings.Join(domains[1:], ",") } ipStrings := make([]string, len(ips)) for i, ip := range ips { ipStrings[i] = ip.String() } if websiteSSL.PrimaryDomain == "" && len(ips) > 0 { websiteSSL.PrimaryDomain = ipStrings[0] ipStrings = ipStrings[1:] } if len(ipStrings) > 0 { if websiteSSL.Domains != "" { websiteSSL.Domains += "," } websiteSSL.Domains += strings.Join(ipStrings, ",") } } } rootCertBlock, _ := pem.Decode([]byte(ca.CSR)) if rootCertBlock == nil { return nil, buserr.New("ErrSSLCertificateFormat") } rootCsr, err := x509.ParseCertificate(rootCertBlock.Bytes) if err != nil { return nil, err } rootPrivateKeyBlock, _ := pem.Decode([]byte(ca.PrivateKey)) if rootPrivateKeyBlock == nil { return nil, buserr.New("ErrSSLCertificateFormat") } var rootPrivateKey any if ssl.KeyType(ca.KeyType) == certcrypto.EC256 || ssl.KeyType(ca.KeyType) == certcrypto.EC384 { rootPrivateKey, err = x509.ParseECPrivateKey(rootPrivateKeyBlock.Bytes) if err != nil { return nil, err } } else { rootPrivateKey, err = x509.ParsePKCS1PrivateKey(rootPrivateKeyBlock.Bytes) if err != nil { return nil, err } } notAfter := time.Now() if req.Unit == "year" { notAfter = notAfter.AddDate(req.Time, 0, 0) } else { notAfter = notAfter.AddDate(0, 0, req.Time) } _, publicKey, privateKeyBytes, err := createPrivateKey(websiteSSL.KeyType) if err != nil { return nil, err } commonName := "" if len(domains) > 0 { commonName = domains[0] } if len(ips) > 0 { commonName = ips[0].String() } subject := rootCsr.Subject subject.CommonName = commonName csr := &x509.Certificate{ SerialNumber: big.NewInt(time.Now().Unix() + 3), Subject: subject, NotBefore: time.Now(), NotAfter: notAfter, BasicConstraintsValid: true, IsCA: false, KeyUsage: leafKeyUsage(websiteSSL.KeyType), ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, DNSNames: domains, IPAddresses: ips, } der, err := x509.CreateCertificate(rand.Reader, csr, rootCsr, publicKey, rootPrivateKey) if err != nil { return nil, err } cert, err := x509.ParseCertificate(der) if err != nil { return nil, err } certBlock := &pem.Block{ Type: "CERTIFICATE", Bytes: cert.Raw, } websiteSSL.Pem = string(pem.EncodeToMemory(certBlock)) websiteSSL.PrivateKey = string(privateKeyBytes) websiteSSL.ExpireDate = cert.NotAfter websiteSSL.StartDate = cert.NotBefore websiteSSL.Type = cert.Issuer.CommonName websiteSSL.Organization = strings.Join(rootCsr.Subject.Organization, ",") websiteSSL.Status = constant.SSLReady if req.Renew { if err := websiteSSLRepo.Save(websiteSSL); err != nil { return nil, err } } else { if err := websiteSSLRepo.Create(context.Background(), websiteSSL); err != nil { return nil, err } } logFile, _ := os.OpenFile(path.Join(global.Dir.SSLLogDir, fmt.Sprintf("%s-ssl-%d.log", websiteSSL.PrimaryDomain, websiteSSL.ID)), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, constant.FilePerm) defer logFile.Close() logger := log.New(logFile, "", log.LstdFlags) logger.Println(i18n.GetMsgWithMap("ApplySSLSuccess", map[string]interface{}{"domain": strings.Join(domains, ",")})) saveCertificateFile(websiteSSL, logger) if websiteSSL.ExecShell { workDir := global.Dir.DataDir if websiteSSL.PushDir { if dirs := certificateDirectories(websiteSSL.Dir); len(dirs) > 0 { workDir = dirs[0] } } logger.Println(i18n.GetMsgByKey("ExecShellStart")) if err = runShellScriptFile(workDir, websiteSSL.Shell, logger); err != nil { logger.Println(i18n.GetMsgWithMap("ErrExecShell", map[string]interface{}{"err": err.Error()})) } else { logger.Println(i18n.GetMsgByKey("ExecShellSuccess")) } } reloadSystemSSL(websiteSSL, logger) if websiteSSL.PushNode { if err = pushSSLToNode(websiteSSL, logger); err != nil { return nil, err } } return websiteSSL, nil } func leafKeyUsage(keyType string) x509.KeyUsage { usage := x509.KeyUsageDigitalSignature if ssl.KeyType(keyType) != certcrypto.EC256 && ssl.KeyType(keyType) != certcrypto.EC384 { usage |= x509.KeyUsageKeyEncipherment } return usage } func createPrivateKey(keyType string) (privateKey any, publicKey any, privateKeyBytes []byte, err error) { privateKey, err = certcrypto.GeneratePrivateKey(ssl.KeyType(keyType)) if err != nil { return } var ( caPrivateKeyPEM = new(bytes.Buffer) ) if ssl.KeyType(keyType) == certcrypto.EC256 || ssl.KeyType(keyType) == certcrypto.EC384 { publicKey = &privateKey.(*ecdsa.PrivateKey).PublicKey publicKey = publicKey.(*ecdsa.PublicKey) block := &pem.Block{ Type: "EC PRIVATE KEY", } privateBytes, sErr := x509.MarshalECPrivateKey(privateKey.(*ecdsa.PrivateKey)) if sErr != nil { err = sErr return } block.Bytes = privateBytes _ = pem.Encode(caPrivateKeyPEM, block) } else { publicKey = &privateKey.(*rsa.PrivateKey).PublicKey _ = pem.Encode(caPrivateKeyPEM, &pem.Block{ Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey.(*rsa.PrivateKey)), }) } privateKeyBytes = caPrivateKeyPEM.Bytes() return } func (w WebsiteCAService) DownloadFile(id uint) (*os.File, error) { ca, err := websiteCARepo.GetFirst(repo.WithByID(id)) if err != nil { return nil, err } fileOp := files.NewFileOp() dir := path.Join(global.Dir.DataDir, "tmp/ssl", ca.Name) if fileOp.Stat(dir) { if err = fileOp.DeleteDir(dir); err != nil { return nil, err } } if err = fileOp.CreateDir(dir, constant.DirPerm); err != nil { return nil, err } if err = fileOp.WriteFile(path.Join(dir, "ca.crt"), strings.NewReader(ca.CSR), constant.DirPerm); err != nil { return nil, err } if err = fileOp.WriteFile(path.Join(dir, "ca.key"), strings.NewReader(ca.PrivateKey), constant.DirPerm); err != nil { return nil, err } fileName := ca.Name + ".zip" if err = fileOp.Compress(context.Background(), []string{path.Join(dir, "ca.crt"), path.Join(dir, "ca.key")}, dir, fileName, files.SdkZip, "", nil); err != nil { return nil, err } return os.Open(path.Join(dir, fileName)) } func (w WebsiteCAService) Upload(name string, archive []byte) error { ca, err := parseCAArchive(archive) if err != nil { return err } return saveImportedCA(name, ca) } func saveImportedCA(name string, ca *model.WebsiteCA) error { name = strings.TrimSpace(name) if name == "" || len(name) > 128 || name == "." || name == ".." || strings.ContainsAny(name, "/\\\x00") { return buserr.WithDetail("ErrInvalidParams", "name", nil) } exist, err := websiteCARepo.GetFirst(repo.WithByName(name)) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return err } if exist.ID > 0 { return buserr.New("ErrNameIsExist") } ca.Name = name return websiteCARepo.Create(context.Background(), ca) } // Read only the certificate and key in memory; never extract user-supplied paths. func parseCAArchive(archive []byte) (*model.WebsiteCA, error) { invalid := func() (*model.WebsiteCA, error) { return nil, buserr.New("ErrCAUpload") } if len(archive) > 10<<20 { return invalid() } reader, err := zip.NewReader(bytes.NewReader(archive), int64(len(archive))) if err != nil || len(reader.File) > 16 { return invalid() } contents := make(map[string][]byte) for _, file := range reader.File { if file.FileInfo().IsDir() { continue } name := path.Base(file.Name) if (name != "ca.crt" && name != "ca.key") || contents[name] != nil || file.UncompressedSize64 > 1<<20 { return invalid() } stream, err := file.Open() if err != nil { return invalid() } data, err := io.ReadAll(io.LimitReader(stream, (1<<20)+1)) stream.Close() if err != nil || len(data) > 1<<20 { return invalid() } contents[name] = data } return parseAndValidateRootCA(contents["ca.crt"], contents["ca.key"]) } func parseAndValidateRootCA(certificate, privateKey []byte) (*model.WebsiteCA, error) { invalid := func() (*model.WebsiteCA, error) { return nil, buserr.New("ErrCAImport") } if len(certificate) > 1<<20 || len(privateKey) > 1<<20 { return invalid() } pair, err := tls.X509KeyPair(certificate, privateKey) if err != nil || len(pair.Certificate) != 1 { return invalid() } cert := pair.Leaf now := time.Now() if cert == nil || !cert.IsCA || !cert.BasicConstraintsValid || cert.KeyUsage&x509.KeyUsageCertSign == 0 || !bytes.Equal(cert.RawIssuer, cert.RawSubject) || cert.CheckSignatureFrom(cert) != nil || now.Before(cert.NotBefore) || !now.Before(cert.NotAfter) { return invalid() } ca := &model.WebsiteCA{CSR: string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}))} // Normalize keys to the formats used by ObtainSSL, including PKCS#8 uploads. switch key := pair.PrivateKey.(type) { case *ecdsa.PrivateKey: bits := key.Curve.Params().BitSize if bits != 256 && bits != 384 { return invalid() } der, err := x509.MarshalECPrivateKey(key) if err != nil { return invalid() } ca.KeyType = fmt.Sprintf("EC%d", bits) ca.PrivateKey = string(pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: der})) case *rsa.PrivateKey: bits := key.N.BitLen() if bits != 2048 && bits != 3072 && bits != 4096 && bits != 8192 { return invalid() } ca.KeyType = fmt.Sprintf("RSA%d", bits) ca.PrivateKey = string(pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})) default: return invalid() } return ca, nil } func (w WebsiteCAService) Import(req request.WebsiteCAImport) error { certificate, privateKey := []byte(req.Certificate), []byte(req.PrivateKey) switch req.Type { case "local": var err error certificate, err = readCAImportFile(req.CertificatePath) if err != nil { return err } privateKey, err = readCAImportFile(req.PrivateKeyPath) if err != nil { return err } case "paste": default: return buserr.New("ErrCAImport") } ca, err := parseAndValidateRootCA(certificate, privateKey) if err != nil { return err } return saveImportedCA(req.Name, ca) } func readCAImportFile(name string) ([]byte, error) { info, err := os.Stat(name) if err != nil { return nil, err } if !info.Mode().IsRegular() || info.Size() > 1<<20 { return nil, buserr.New("ErrCAImport") } file, err := os.Open(name) if err != nil { return nil, err } defer file.Close() content, err := io.ReadAll(io.LimitReader(file, (1<<20)+1)) if err != nil { return nil, err } if len(content) > 1<<20 { return nil, buserr.New("ErrCAImport") } return content, nil }