diff --git a/backend/app/dto/request/website_ssl.go b/backend/app/dto/request/website_ssl.go index 42a6d7e41..fef59547f 100644 --- a/backend/app/dto/request/website_ssl.go +++ b/backend/app/dto/request/website_ssl.go @@ -14,6 +14,7 @@ type WebsiteSSLCreate struct { AcmeAccountID uint `json:"acmeAccountId" validate:"required"` DnsAccountID uint `json:"dnsAccountId"` AutoRenew bool `json:"autoRenew"` + KeyType string `json:"keyType"` } type WebsiteDNSReq struct { @@ -28,6 +29,7 @@ type WebsiteSSLRenew struct { type WebsiteAcmeAccountCreate struct { Email string `json:"email" validate:"required"` Type string `json:"type" validate:"required,oneof=letsencrypt zerossl buypass google"` + KeyType string `json:"keyType" validate:"required,oneof=P256 P384 2048 3072 4096 8192"` EabKid string `json:"eabKid"` EabHmacKey string `json:"eabHmacKey"` } diff --git a/backend/app/model/website_acme_account.go b/backend/app/model/website_acme_account.go index d0e468f5d..eeafc0187 100644 --- a/backend/app/model/website_acme_account.go +++ b/backend/app/model/website_acme_account.go @@ -2,12 +2,13 @@ package model type WebsiteAcmeAccount struct { BaseModel - Email string `gorm:"type:varchar(256);not null" json:"email"` - URL string `gorm:"type:varchar(256);not null" json:"url"` - PrivateKey string `gorm:"type:longtext;not null" json:"-"` - Type string `gorm:"type:varchar(64);not null;default:letsencrypt" json:"type"` - EabKid string `gorm:"type:varchar(256);" json:"eabKid"` - EabHmacKey string `gorm:"type:varchar(256);" json:"eabHmacKey"` + Email string `gorm:"not null" json:"email"` + URL string `gorm:"not null" json:"url"` + PrivateKey string `gorm:"not null" json:"-"` + Type string `gorm:"not null;default:letsencrypt" json:"type"` + EabKid string `gorm:"default:null;" json:"eabKid"` + EabHmacKey string `gorm:"default:null" json:"eabHmacKey"` + KeyType string `gorm:"not null;default:2048" json:"keyType"` } func (w WebsiteAcmeAccount) TableName() string { diff --git a/backend/app/service/website_acme_account.go b/backend/app/service/website_acme_account.go index 8b1718b56..711fa0b95 100644 --- a/backend/app/service/website_acme_account.go +++ b/backend/app/service/website_acme_account.go @@ -15,7 +15,7 @@ type WebsiteAcmeAccountService struct { type IWebsiteAcmeAccountService interface { Page(search dto.PageInfo) (int64, []response.WebsiteAcmeAccountDTO, error) - Create(create request.WebsiteAcmeAccountCreate) (response.WebsiteAcmeAccountDTO, error) + Create(create request.WebsiteAcmeAccountCreate) (*response.WebsiteAcmeAccountDTO, error) Delete(id uint) error } @@ -34,34 +34,39 @@ func (w WebsiteAcmeAccountService) Page(search dto.PageInfo) (int64, []response. return total, accountDTOs, err } -func (w WebsiteAcmeAccountService) Create(create request.WebsiteAcmeAccountCreate) (response.WebsiteAcmeAccountDTO, error) { +func (w WebsiteAcmeAccountService) Create(create request.WebsiteAcmeAccountCreate) (*response.WebsiteAcmeAccountDTO, error) { exist, _ := websiteAcmeRepo.GetFirst(websiteAcmeRepo.WithEmail(create.Email)) if exist != nil { - return response.WebsiteAcmeAccountDTO{}, buserr.New(constant.ErrEmailIsExist) + return nil, buserr.New(constant.ErrEmailIsExist) } if create.Type == "google" && (create.EabKid == "" || create.EabHmacKey == "") { - return response.WebsiteAcmeAccountDTO{}, buserr.New(constant.ErrEabKidOrEabHmacKeyCannotBlank) + return nil, buserr.New(constant.ErrEabKidOrEabHmacKeyCannotBlank) } else { create.EabKid = "" create.EabHmacKey = "" } acmeAccount := &model.WebsiteAcmeAccount{ - Email: create.Email, - Type: create.Type, + Email: create.Email, + Type: create.Type, + KeyType: create.KeyType, } client, err := ssl.NewAcmeClient(acmeAccount) if err != nil { - return response.WebsiteAcmeAccountDTO{}, err + return nil, err } - acmeAccount.PrivateKey = string(ssl.GetPrivateKey(client.User.GetPrivateKey())) + privateKey, err := ssl.GetPrivateKey(client.User.GetPrivateKey(), ssl.KeyType(create.KeyType)) + if err != nil { + return nil, err + } + acmeAccount.PrivateKey = string(privateKey) acmeAccount.URL = client.User.Registration.URI if err := websiteAcmeRepo.Create(*acmeAccount); err != nil { - return response.WebsiteAcmeAccountDTO{}, err + return nil, err } - return response.WebsiteAcmeAccountDTO{WebsiteAcmeAccount: *acmeAccount}, nil + return &response.WebsiteAcmeAccountDTO{WebsiteAcmeAccount: *acmeAccount}, nil } func (w WebsiteAcmeAccountService) Delete(id uint) error { diff --git a/backend/app/service/website_ssl.go b/backend/app/service/website_ssl.go index 3a62cd0c4..6ed1e3057 100644 --- a/backend/app/service/website_ssl.go +++ b/backend/app/service/website_ssl.go @@ -2,6 +2,7 @@ package service import ( "context" + "crypto" "crypto/x509" "encoding/pem" "github.com/1Panel-dev/1Panel/backend/app/dto/request" @@ -13,6 +14,7 @@ import ( "github.com/1Panel-dev/1Panel/backend/global" "github.com/1Panel-dev/1Panel/backend/utils/files" "github.com/1Panel-dev/1Panel/backend/utils/ssl" + "github.com/go-acme/lego/v4/certcrypto" "path" "strconv" "strings" @@ -132,7 +134,21 @@ func (w WebsiteSSLService) Create(create request.WebsiteSSLCreate) (request.Webs if create.OtherDomains != "" { domains = append(otherDomainArray, domains...) } - resource, err := client.ObtainSSL(domains) + var privateKey crypto.PrivateKey + if create.KeyType != acmeAccount.KeyType { + privateKey, err = certcrypto.GeneratePrivateKey(ssl.KeyType(create.KeyType)) + if err != nil { + return res, err + } + } else { + block, _ := pem.Decode([]byte(acmeAccount.PrivateKey)) + privateKey, err = x509.ParseECPrivateKey(block.Bytes) + if err != nil { + return res, err + } + } + + resource, err := client.ObtainSSL(domains, privateKey) if err != nil { return res, err } diff --git a/backend/init/migration/migrations/v_1_9.go b/backend/init/migration/migrations/v_1_9.go index be29d53b3..3c2236b04 100644 --- a/backend/init/migration/migrations/v_1_9.go +++ b/backend/init/migration/migrations/v_1_9.go @@ -7,7 +7,7 @@ import ( ) var UpdateAcmeAccount = &gormigrate.Migration{ - ID: "20231115-update-acme-account", + ID: "20231117-update-acme-account", Migrate: func(tx *gorm.DB) error { if err := tx.AutoMigrate(&model.WebsiteAcmeAccount{}); err != nil { return err diff --git a/backend/utils/ssl/acme.go b/backend/utils/ssl/acme.go index 36470d669..25cf683e2 100644 --- a/backend/utils/ssl/acme.go +++ b/backend/utils/ssl/acme.go @@ -2,7 +2,7 @@ package ssl import ( "crypto" - "crypto/rand" + "crypto/ecdsa" "crypto/rsa" "crypto/x509" "encoding/json" @@ -28,30 +28,70 @@ type zeroSSLRes struct { EabHmacKey string `json:"eab_hmac_key"` } -func GetPrivateKey(priKey crypto.PrivateKey) []byte { - rsaKey := priKey.(*rsa.PrivateKey) - derStream := x509.MarshalPKCS1PrivateKey(rsaKey) - block := &pem.Block{ - Type: "privateKey", - Bytes: derStream, +type KeyType = certcrypto.KeyType + +const ( + KeyEC256 = certcrypto.EC256 + KeyEC384 = certcrypto.EC384 + KeyRSA2048 = certcrypto.RSA2048 + KeyRSA3072 = certcrypto.RSA3072 + KeyRSA4096 = certcrypto.RSA4096 +) + +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) + + return pem.EncodeToMemory(block), nil } func NewRegisterClient(acmeAccount *model.WebsiteAcmeAccount) (*AcmeClient, error) { var ( - priKey *rsa.PrivateKey + priKey crypto.PrivateKey err error ) if acmeAccount.PrivateKey != "" { - block, _ := pem.Decode([]byte(acmeAccount.PrivateKey)) - priKey, err = x509.ParsePKCS1PrivateKey(block.Bytes) - if err != nil { - return nil, err + switch KeyType(acmeAccount.KeyType) { + case KeyEC256, KeyEC384: + block, _ := pem.Decode([]byte(acmeAccount.PrivateKey)) + priKey, err = x509.ParseECPrivateKey(block.Bytes) + if err != nil { + return nil, err + } + case KeyRSA2048, KeyRSA3072, KeyRSA4096: + block, _ := pem.Decode([]byte(acmeAccount.PrivateKey)) + priKey, err = x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + return nil, err + } } + } else { - priKey, err = rsa.GenerateKey(rand.Reader, 2048) + priKey, err = certcrypto.GeneratePrivateKey(KeyType(acmeAccount.KeyType)) if err != nil { return nil, err } diff --git a/backend/utils/ssl/client.go b/backend/utils/ssl/client.go index 9f849a26a..bacf46a26 100644 --- a/backend/utils/ssl/client.go +++ b/backend/utils/ssl/client.go @@ -131,10 +131,11 @@ func (c *AcmeClient) UseHTTP(path string) error { return nil } -func (c *AcmeClient) ObtainSSL(domains []string) (certificate.Resource, error) { +func (c *AcmeClient) ObtainSSL(domains []string, privateKey crypto.PrivateKey) (certificate.Resource, error) { request := certificate.ObtainRequest{ - Domains: domains, - Bundle: true, + Domains: domains, + Bundle: true, + PrivateKey: privateKey, } certificates, err := c.Client.Certificate.Obtain(request) @@ -222,10 +223,10 @@ func (c *AcmeClient) GetDNSResolve(domains []string) (map[string]Resolve, error) resolves[domain] = Resolve{Err: err.Error()} continue } - fqdn, value := dns01.GetRecord(domain, keyAuth) + challengeInfo := dns01.GetChallengeInfo(domain, keyAuth) resolves[domain] = Resolve{ - Key: fqdn, - Value: value, + Key: challengeInfo.FQDN, + Value: challengeInfo.Value, } } diff --git a/frontend/src/global/mimetype.ts b/frontend/src/global/mimetype.ts index e065cab3b..7eb8d09f0 100644 --- a/frontend/src/global/mimetype.ts +++ b/frontend/src/global/mimetype.ts @@ -129,3 +129,11 @@ export const AcmeAccountTypes = [ { label: 'Buypass', value: 'buypass' }, { label: 'Google Cloud', value: 'google' }, ]; + +export const KeyTypes = [ + { label: 'EC 256', value: 'P256' }, + { label: 'EC 384', value: 'P384' }, + { label: 'RSA 2048', value: '2048' }, + { label: 'RSA 3072', value: '3072' }, + { label: 'RSA 4096', value: '4096' }, +]; diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index 70e7aadcf..2ec25745d 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -1665,6 +1665,7 @@ const message = { 'OpenResty default HTTP port: {0} HTTPS port: {1}, which may affect website domain name access and HTTPS forced redirect', primaryDomainHelper: 'Support domain name: port', acmeAccountType: 'Account Type', + keyType: 'Key algorithm', }, php: { short_open_tag: 'Short tag support', diff --git a/frontend/src/lang/modules/tw.ts b/frontend/src/lang/modules/tw.ts index c0137b517..953ed1398 100644 --- a/frontend/src/lang/modules/tw.ts +++ b/frontend/src/lang/modules/tw.ts @@ -1578,6 +1578,7 @@ const message = { openrestryHelper: 'OpenResty 默認 HTTP 端口:{0} HTTPS 端口:{1},可能影響網站域名訪問和 HTTPS 強制跳轉', primaryDomainHelper: '支援網域:port', acmeAccountType: '賬號類型', + keyType: '密鑰演算法', }, php: { short_open_tag: '短標簽支持', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index 4263273bd..473a20994 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -1578,6 +1578,7 @@ const message = { openrestryHelper: 'OpenResty 默认 HTTP 端口:{0} HTTPS 端口 :{1},可能影响网站域名访问和 HTTPS 强制跳转', primaryDomainHelper: '支持域名:端口', acmeAccountType: '账号类型', + keyType: '密钥算法', }, php: { short_open_tag: '短标签支持', diff --git a/frontend/src/views/website/ssl/acme-account/create/index.vue b/frontend/src/views/website/ssl/acme-account/create/index.vue index 5897ac17f..8a4c2afcf 100644 --- a/frontend/src/views/website/ssl/acme-account/create/index.vue +++ b/frontend/src/views/website/ssl/acme-account/create/index.vue @@ -22,6 +22,16 @@ > + + + + +
@@ -50,7 +60,7 @@ import { Rules } from '@/global/form-rules'; import { CreateAcmeAccount } from '@/api/modules/website'; import i18n from '@/lang'; import { MsgSuccess } from '@/utils/message'; -import { AcmeAccountTypes } from '@/global/mimetype'; +import { AcmeAccountTypes, KeyTypes } from '@/global/mimetype'; const open = ref(); const loading = ref(false); @@ -60,6 +70,7 @@ const rules = ref({ type: [Rules.requiredSelect], eabKid: [Rules.requiredInput], eabHmacKey: [Rules.requiredInput], + keyType: [Rules.requiredSelect], }); const initData = () => ({ @@ -67,6 +78,7 @@ const initData = () => ({ type: 'letsencrypt', eabKid: '', eabHmacKey: '', + keyType: 'P256', }); const account = ref(initData()); diff --git a/frontend/src/views/website/ssl/acme-account/index.vue b/frontend/src/views/website/ssl/acme-account/index.vue index 33a0c89d2..86f5ef777 100644 --- a/frontend/src/views/website/ssl/acme-account/index.vue +++ b/frontend/src/views/website/ssl/acme-account/index.vue @@ -1,5 +1,5 @@ + + + { } }; +const getKeyType = (type: string) => { + for (const i of KeyTypes) { + if (i.value === type) { + return i.label; + } + } +}; defineExpose({ acceptParams, }); diff --git a/frontend/src/views/website/ssl/create/index.vue b/frontend/src/views/website/ssl/create/index.vue index 8965823d4..61d1cf63e 100644 --- a/frontend/src/views/website/ssl/create/index.vue +++ b/frontend/src/views/website/ssl/create/index.vue @@ -26,6 +26,16 @@ > + + + + + {{ $t('website.dnsAccount') }} @@ -82,6 +92,7 @@ import i18n from '@/lang'; import { FormInstance } from 'element-plus'; import { computed, reactive, ref } from 'vue'; import { MsgSuccess } from '@/utils/message'; +import { KeyTypes } from '@/global/mimetype'; const props = defineProps({ id: { @@ -94,27 +105,29 @@ const id = computed(() => { return props.id; }); -let open = ref(false); -let loading = ref(false); -let dnsReq = reactive({ +const open = ref(false); +const loading = ref(false); +const dnsReq = reactive({ page: 1, pageSize: 20, }); -let acmeReq = reactive({ +const acmeReq = reactive({ page: 1, pageSize: 20, }); -let dnsAccounts = ref(); -let acmeAccounts = ref(); -let sslForm = ref(); -let rules = ref({ +const dnsAccounts = ref(); +const acmeAccounts = ref(); +const sslForm = ref(); +const rules = ref({ primaryDomain: [Rules.requiredInput, Rules.domain], acmeAccountId: [Rules.requiredSelectBusiness], dnsAccountId: [Rules.requiredSelectBusiness], provider: [Rules.requiredInput], autoRenew: [Rules.requiredInput], + keyType: [Rules.requiredInput], }); -let ssl = ref({ + +const initData = () => ({ primaryDomain: '', otherDomains: '', provider: 'dnsAccount', @@ -122,9 +135,12 @@ let ssl = ref({ acmeAccountId: undefined, dnsAccountId: undefined, autoRenew: true, + keyType: 'P256', }); -let dnsResolve = ref([]); -let hasResolve = ref(false); + +const ssl = ref(initData()); +const dnsResolve = ref([]); +const hasResolve = ref(false); const em = defineEmits(['close']); const handleClose = () => { @@ -135,15 +151,7 @@ const handleClose = () => { const resetForm = () => { sslForm.value?.resetFields(); dnsResolve.value = []; - ssl.value = { - primaryDomain: '', - otherDomains: '', - provider: 'dnsAccount', - websiteId: 0, - acmeAccountId: undefined, - dnsAccountId: undefined, - autoRenew: true, - }; + ssl.value = initData(); }; const acceptParams = () => {