mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-01-19 00:09:16 +08:00
feat: 增加申请 ECC 类型证书 (#2965)
Refs https://github.com/1Panel-dev/1Panel/issues/2081 Refs https://github.com/1Panel-dev/1Panel/issues/615
This commit is contained in:
parent
d044cf2c2c
commit
d254837c12
@ -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"`
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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' },
|
||||
];
|
||||
|
@ -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',
|
||||
|
@ -1578,6 +1578,7 @@ const message = {
|
||||
openrestryHelper: 'OpenResty 默認 HTTP 端口:{0} HTTPS 端口:{1},可能影響網站域名訪問和 HTTPS 強制跳轉',
|
||||
primaryDomainHelper: '支援網域:port',
|
||||
acmeAccountType: '賬號類型',
|
||||
keyType: '密鑰演算法',
|
||||
},
|
||||
php: {
|
||||
short_open_tag: '短標簽支持',
|
||||
|
@ -1578,6 +1578,7 @@ const message = {
|
||||
openrestryHelper: 'OpenResty 默认 HTTP 端口:{0} HTTPS 端口 :{1},可能影响网站域名访问和 HTTPS 强制跳转',
|
||||
primaryDomainHelper: '支持域名:端口',
|
||||
acmeAccountType: '账号类型',
|
||||
keyType: '密钥算法',
|
||||
},
|
||||
php: {
|
||||
short_open_tag: '短标签支持',
|
||||
|
@ -22,6 +22,16 @@
|
||||
></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('website.keyType')" prop="keyType">
|
||||
<el-select v-model="account.keyType">
|
||||
<el-option
|
||||
v-for="(keyType, index) in KeyTypes"
|
||||
:key="index"
|
||||
:label="keyType.label"
|
||||
:value="keyType.value"
|
||||
></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<div v-if="account.type == 'google'">
|
||||
<el-form-item label="EAB kid" prop="eabKid">
|
||||
<el-input v-model.trim="account.eabKid"></el-input>
|
||||
@ -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());
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<el-drawer :close-on-click-modal="false" v-model="open" :size="'50%'">
|
||||
<el-drawer :close-on-click-modal="false" v-model="open" size="60%">
|
||||
<template #header>
|
||||
<DrawerHeader :header="$t('website.acmeAccountManage')" :back="handleClose" />
|
||||
</template>
|
||||
@ -22,6 +22,11 @@
|
||||
{{ getAccountType(row.type) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('website.keyType')" fix show-overflow-tooltip prop="keyType">
|
||||
<template #default="{ row }">
|
||||
{{ getKeyType(row.keyType) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="URL" show-overflow-tooltip prop="url" min-width="300px"></el-table-column>
|
||||
<fu-table-operations
|
||||
:ellipsis="1"
|
||||
@ -45,7 +50,7 @@ import { DeleteAcmeAccount, SearchAcmeAccount } from '@/api/modules/website';
|
||||
import i18n from '@/lang';
|
||||
import { reactive, ref } from 'vue';
|
||||
import Create from './create/index.vue';
|
||||
import { AcmeAccountTypes } from '@/global/mimetype';
|
||||
import { AcmeAccountTypes, KeyTypes } from '@/global/mimetype';
|
||||
|
||||
const open = ref(false);
|
||||
const loading = ref(false);
|
||||
@ -113,6 +118,13 @@ const getAccountType = (type: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
const getKeyType = (type: string) => {
|
||||
for (const i of KeyTypes) {
|
||||
if (i.value === type) {
|
||||
return i.label;
|
||||
}
|
||||
}
|
||||
};
|
||||
defineExpose({
|
||||
acceptParams,
|
||||
});
|
||||
|
@ -26,6 +26,16 @@
|
||||
></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('website.keyType')" prop="keyType">
|
||||
<el-select v-model="ssl.keyType">
|
||||
<el-option
|
||||
v-for="(keyType, index) in KeyTypes"
|
||||
:key="index"
|
||||
:label="keyType.label"
|
||||
:value="keyType.value"
|
||||
></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('website.provider')" prop="provider">
|
||||
<el-radio-group v-model="ssl.provider" @change="changeProvider()">
|
||||
<el-radio label="dnsAccount">{{ $t('website.dnsAccount') }}</el-radio>
|
||||
@ -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<Website.DnsAccount[]>();
|
||||
let acmeAccounts = ref<Website.AcmeAccount[]>();
|
||||
let sslForm = ref<FormInstance>();
|
||||
let rules = ref({
|
||||
const dnsAccounts = ref<Website.DnsAccount[]>();
|
||||
const acmeAccounts = ref<Website.AcmeAccount[]>();
|
||||
const sslForm = ref<FormInstance>();
|
||||
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<Website.DNSResolve[]>([]);
|
||||
let hasResolve = ref(false);
|
||||
|
||||
const ssl = ref(initData());
|
||||
const dnsResolve = ref<Website.DNSResolve[]>([]);
|
||||
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 = () => {
|
||||
|
Loading…
x
Reference in New Issue
Block a user