1
0
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:
zhengkunwang 2023-11-16 14:38:07 +08:00 committed by GitHub
parent d044cf2c2c
commit d254837c12
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 169 additions and 61 deletions

View File

@ -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"`
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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
}

View File

@ -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

View File

@ -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
}

View File

@ -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,
}
}

View File

@ -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' },
];

View File

@ -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',

View File

@ -1578,6 +1578,7 @@ const message = {
openrestryHelper: 'OpenResty 默認 HTTP 端口{0} HTTPS 端口{1}可能影響網站域名訪問和 HTTPS 強制跳轉',
primaryDomainHelper: '支援網域:port',
acmeAccountType: '賬號類型',
keyType: '密鑰演算法',
},
php: {
short_open_tag: '短標簽支持',

View File

@ -1578,6 +1578,7 @@ const message = {
openrestryHelper: 'OpenResty 默认 HTTP 端口{0} HTTPS 端口 {1}可能影响网站域名访问和 HTTPS 强制跳转',
primaryDomainHelper: '支持域名:端口',
acmeAccountType: '账号类型',
keyType: '密钥算法',
},
php: {
short_open_tag: '短标签支持',

View File

@ -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());

View File

@ -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,
});

View File

@ -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 = () => {