diff --git a/go.mod b/go.mod
index b37a07ff..7e272b97 100644
--- a/go.mod
+++ b/go.mod
@@ -19,6 +19,7 @@ require (
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.114
github.com/labstack/echo/v5 v5.0.0-20230722203903-ec5b858dab61
github.com/nikoksr/notify v1.0.0
+ github.com/pavlo-v-chernykh/keystore-go/v4 v4.5.0
github.com/pkg/sftp v1.13.6
github.com/pocketbase/dbx v1.10.1
github.com/pocketbase/pocketbase v0.22.18
diff --git a/go.sum b/go.sum
index afa71518..2e3575b4 100644
--- a/go.sum
+++ b/go.sum
@@ -398,6 +398,8 @@ github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw=
github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro=
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A=
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU=
+github.com/pavlo-v-chernykh/keystore-go/v4 v4.5.0 h1:2nosf3P75OZv2/ZO/9Px5ZgZ5gbKrzA3joN1QMfOGMQ=
+github.com/pavlo-v-chernykh/keystore-go/v4 v4.5.0/go.mod h1:lAVhWwbNaveeJmxrxuSTxMgKpF6DjnuVpn6T8WiBwYQ=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
diff --git a/internal/deployer/deployer.go b/internal/deployer/deployer.go
index c00d9ff3..9dc537f3 100644
--- a/internal/deployer/deployer.go
+++ b/internal/deployer/deployer.go
@@ -1,11 +1,15 @@
package deployer
import (
+ "bytes"
"context"
"encoding/json"
+ "encoding/pem"
"errors"
"fmt"
+ "time"
+ "github.com/pavlo-v-chernykh/keystore-go/v4"
"github.com/pocketbase/pocketbase/models"
"software.sslmate.com/src/go-pkcs12"
@@ -187,7 +191,7 @@ func getDeployVariables(conf domain.DeployConfig) map[string]string {
return rs
}
-func convertPemToPfx(certificate string, privateKey string, password string) ([]byte, error) {
+func convertPEMToPFX(certificate string, privateKey string, password string) ([]byte, error) {
cert, err := x509.ParseCertificateFromPEM(certificate)
if err != nil {
return nil, err
@@ -205,3 +209,38 @@ func convertPemToPfx(certificate string, privateKey string, password string) ([]
return pfxData, nil
}
+
+func convertPEMToJKS(certificate string, privateKey string, alias string, keypass string, storepass string) ([]byte, error) {
+ certBlock, _ := pem.Decode([]byte(certificate))
+ if certBlock == nil {
+ return nil, errors.New("failed to decode certificate PEM")
+ }
+
+ privkeyBlock, _ := pem.Decode([]byte(privateKey))
+ if privkeyBlock == nil {
+ return nil, errors.New("failed to decode private key PEM")
+ }
+
+ ks := keystore.New()
+ entry := keystore.PrivateKeyEntry{
+ CreationTime: time.Now(),
+ PrivateKey: privkeyBlock.Bytes,
+ CertificateChain: []keystore.Certificate{
+ {
+ Type: "X509",
+ Content: certBlock.Bytes,
+ },
+ },
+ }
+
+ if err := ks.SetPrivateKeyEntry(alias, entry, []byte(keypass)); err != nil {
+ return nil, err
+ }
+
+ var buf bytes.Buffer
+ if err := ks.Store(&buf, []byte(storepass)); err != nil {
+ return nil, err
+ }
+
+ return buf.Bytes(), nil
+}
diff --git a/internal/deployer/local.go b/internal/deployer/local.go
index 8e87d67c..0c839fa4 100644
--- a/internal/deployer/local.go
+++ b/internal/deployer/local.go
@@ -17,6 +17,18 @@ type LocalDeployer struct {
infos []string
}
+const (
+ certFormatPEM = "pem"
+ certFormatPFX = "pfx"
+ certFormatJKS = "jks"
+)
+
+const (
+ shellEnvSh = "sh"
+ shellEnvCmd = "cmd"
+ shellEnvPowershell = "powershell"
+)
+
func NewLocalDeployer(option *DeployerOption) (Deployer, error) {
return &LocalDeployer{
option: option,
@@ -50,8 +62,8 @@ func (d *LocalDeployer) Deploy(ctx context.Context) error {
}
// 写入证书和私钥文件
- switch d.option.DeployConfig.GetConfigOrDefaultAsString("format", "pem") {
- case "pem":
+ switch d.option.DeployConfig.GetConfigOrDefaultAsString("format", certFormatPEM) {
+ case certFormatPEM:
if err := fs.WriteFileString(d.option.DeployConfig.GetConfigAsString("certPath"), d.option.Certificate.Certificate); err != nil {
return fmt.Errorf("failed to save certificate file: %w", err)
}
@@ -64,8 +76,12 @@ func (d *LocalDeployer) Deploy(ctx context.Context) error {
d.infos = append(d.infos, toStr("保存私钥成功", nil))
- case "pfx":
- pfxData, err := convertPemToPfx(d.option.Certificate.Certificate, d.option.Certificate.PrivateKey, d.option.DeployConfig.GetConfigAsString("pfxPassword"))
+ case certFormatPFX:
+ pfxData, err := convertPEMToPFX(
+ d.option.Certificate.Certificate,
+ d.option.Certificate.PrivateKey,
+ d.option.DeployConfig.GetConfigAsString("pfxPassword"),
+ )
if err != nil {
return fmt.Errorf("failed to convert pem to pfx %w", err)
}
@@ -74,6 +90,24 @@ func (d *LocalDeployer) Deploy(ctx context.Context) error {
return fmt.Errorf("failed to save certificate file: %w", err)
}
+ d.infos = append(d.infos, toStr("保存证书成功", nil))
+
+ case certFormatJKS:
+ jksData, err := convertPEMToJKS(
+ d.option.Certificate.Certificate,
+ d.option.Certificate.PrivateKey,
+ d.option.DeployConfig.GetConfigAsString("jksAlias"),
+ d.option.DeployConfig.GetConfigAsString("jksKeypass"),
+ d.option.DeployConfig.GetConfigAsString("jksStorepass"),
+ )
+ if err != nil {
+ return fmt.Errorf("failed to convert pem to pfx %w", err)
+ }
+
+ if err := fs.WriteFile(d.option.DeployConfig.GetConfigAsString("certPath"), jksData); err != nil {
+ return fmt.Errorf("failed to save certificate file: %w", err)
+ }
+
d.infos = append(d.infos, toStr("保存证书成功", nil))
}
@@ -95,13 +129,13 @@ func (d *LocalDeployer) execCommand(command string) (string, string, error) {
var cmd *exec.Cmd
switch d.option.DeployConfig.GetConfigAsString("shell") {
- case "sh":
+ case shellEnvSh:
cmd = exec.Command("sh", "-c", command)
- case "cmd":
+ case shellEnvCmd:
cmd = exec.Command("cmd", "/C", command)
- case "powershell":
+ case shellEnvPowershell:
cmd = exec.Command("powershell", "-Command", command)
case "":
diff --git a/internal/deployer/ssh.go b/internal/deployer/ssh.go
index 1a959cbb..98c4de48 100644
--- a/internal/deployer/ssh.go
+++ b/internal/deployer/ssh.go
@@ -12,6 +12,7 @@ import (
"golang.org/x/crypto/ssh"
"github.com/usual2970/certimate/internal/domain"
+ "github.com/usual2970/certimate/internal/pkg/utils/fs"
)
type SSHDeployer struct {
@@ -61,8 +62,8 @@ func (d *SSHDeployer) Deploy(ctx context.Context) error {
}
// 上传证书和私钥文件
- switch d.option.DeployConfig.GetConfigOrDefaultAsString("format", "pem") {
- case "pem":
+ switch d.option.DeployConfig.GetConfigOrDefaultAsString("format", certFormatPEM) {
+ case certFormatPEM:
if err := d.writeSftpFileString(client, d.option.DeployConfig.GetConfigAsString("certPath"), d.option.Certificate.Certificate); err != nil {
return fmt.Errorf("failed to upload certificate file: %w", err)
}
@@ -75,8 +76,12 @@ func (d *SSHDeployer) Deploy(ctx context.Context) error {
d.infos = append(d.infos, toStr("SSH 上传私钥成功", nil))
- case "pfx":
- pfxData, err := convertPemToPfx(d.option.Certificate.Certificate, d.option.Certificate.PrivateKey, d.option.DeployConfig.GetConfigAsString("pfxPassword"))
+ case certFormatPFX:
+ pfxData, err := convertPEMToPFX(
+ d.option.Certificate.Certificate,
+ d.option.Certificate.PrivateKey,
+ d.option.DeployConfig.GetConfigAsString("pfxPassword"),
+ )
if err != nil {
return fmt.Errorf("failed to convert pem to pfx %w", err)
}
@@ -86,6 +91,24 @@ func (d *SSHDeployer) Deploy(ctx context.Context) error {
}
d.infos = append(d.infos, toStr("SSH 上传证书成功", nil))
+
+ case certFormatJKS:
+ jksData, err := convertPEMToJKS(
+ d.option.Certificate.Certificate,
+ d.option.Certificate.PrivateKey,
+ d.option.DeployConfig.GetConfigAsString("jksAlias"),
+ d.option.DeployConfig.GetConfigAsString("jksKeypass"),
+ d.option.DeployConfig.GetConfigAsString("jksStorepass"),
+ )
+ if err != nil {
+ return fmt.Errorf("failed to convert pem to pfx %w", err)
+ }
+
+ if err := fs.WriteFile(d.option.DeployConfig.GetConfigAsString("certPath"), jksData); err != nil {
+ return fmt.Errorf("failed to save certificate file: %w", err)
+ }
+
+ d.infos = append(d.infos, toStr("保存证书成功", nil))
}
// 执行命令
diff --git a/ui/src/components/certimate/DeployToLocal.tsx b/ui/src/components/certimate/DeployToLocal.tsx
index 9f7fffd1..672cf52c 100644
--- a/ui/src/components/certimate/DeployToLocal.tsx
+++ b/ui/src/components/certimate/DeployToLocal.tsx
@@ -26,6 +26,9 @@ const DeployToLocal = () => {
certPath: "/etc/nginx/ssl/nginx.crt",
keyPath: "/etc/nginx/ssl/nginx.key",
pfxPassword: "",
+ jksAlias: "",
+ jksKeypass: "",
+ jksStorepass: "",
shell: "sh",
preCommand: "",
command: "sudo service nginx reload",
@@ -40,7 +43,7 @@ const DeployToLocal = () => {
const formSchema = z
.object({
- format: z.union([z.literal("pem"), z.literal("pfx")], {
+ format: z.union([z.literal("pem"), z.literal("pfx"), z.literal("jks")], {
message: t("domain.deployment.form.file_format.placeholder"),
}),
certPath: z
@@ -52,6 +55,9 @@ const DeployToLocal = () => {
.min(0, t("domain.deployment.form.file_key_path.placeholder"))
.max(255, t("common.errmsg.string_max", { max: 255 })),
pfxPassword: z.string().optional(),
+ jksAlias: z.string().optional(),
+ jksKeypass: z.string().optional(),
+ jksStorepass: z.string().optional(),
shell: z.union([z.literal("sh"), z.literal("cmd"), z.literal("powershell")], {
message: t("domain.deployment.form.shell.placeholder"),
}),
@@ -65,6 +71,18 @@ const DeployToLocal = () => {
.refine((data) => (data.format === "pfx" ? !!data.pfxPassword?.trim() : true), {
message: t("domain.deployment.form.file_pfx_password.placeholder"),
path: ["pfxPassword"],
+ })
+ .refine((data) => (data.format === "jks" ? !!data.jksAlias?.trim() : true), {
+ message: t("domain.deployment.form.file_jks_alias.placeholder"),
+ path: ["jksAlias"],
+ })
+ .refine((data) => (data.format === "jks" ? !!data.jksKeypass?.trim() : true), {
+ message: t("domain.deployment.form.file_jks_keypass.placeholder"),
+ path: ["jksKeypass"],
+ })
+ .refine((data) => (data.format === "jks" ? !!data.jksStorepass?.trim() : true), {
+ message: t("domain.deployment.form.file_jks_storepass.placeholder"),
+ path: ["jksStorepass"],
});
useEffect(() => {
@@ -72,8 +90,13 @@ const DeployToLocal = () => {
if (!res.success) {
setError({
...error,
+ format: res.error.errors.find((e) => e.path[0] === "format")?.message,
certPath: res.error.errors.find((e) => e.path[0] === "certPath")?.message,
keyPath: res.error.errors.find((e) => e.path[0] === "keyPath")?.message,
+ pfxPassword: res.error.errors.find((e) => e.path[0] === "pfxPassword")?.message,
+ jksAlias: res.error.errors.find((e) => e.path[0] === "jksAlias")?.message,
+ jksKeypass: res.error.errors.find((e) => e.path[0] === "jksKeypass")?.message,
+ jksStorepass: res.error.errors.find((e) => e.path[0] === "jksStorepass")?.message,
shell: res.error.errors.find((e) => e.path[0] === "shell")?.message,
preCommand: res.error.errors.find((e) => e.path[0] === "preCommand")?.message,
command: res.error.errors.find((e) => e.path[0] === "command")?.message,
@@ -81,8 +104,13 @@ const DeployToLocal = () => {
} else {
setError({
...error,
+ format: undefined,
certPath: undefined,
keyPath: undefined,
+ pfxPassword: undefined,
+ jksAlias: undefined,
+ jksKeypass: undefined,
+ jksStorepass: undefined,
shell: undefined,
preCommand: undefined,
command: undefined,
@@ -92,18 +120,26 @@ const DeployToLocal = () => {
useEffect(() => {
if (data.config?.format === "pem") {
- if (data.config.certPath && data.config.certPath.endsWith(".pfx")) {
+ if (/(.pfx|.jks)$/.test(data.config.certPath)) {
const newData = produce(data, (draft) => {
draft.config ??= {};
- draft.config.certPath = data.config!.certPath.replace(/.pfx$/, ".crt");
+ draft.config.certPath = data.config!.certPath.replace(/(.pfx|.jks)$/, ".crt");
});
setDeploy(newData);
}
} else if (data.config?.format === "pfx") {
- if (data.config.certPath && data.config.certPath.endsWith(".crt")) {
+ if (/(.crt|.jks)$/.test(data.config.certPath)) {
const newData = produce(data, (draft) => {
draft.config ??= {};
- draft.config.certPath = data.config!.certPath.replace(/.crt$/, ".pfx");
+ draft.config.certPath = data.config!.certPath.replace(/(.crt|.jks)$/, ".pfx");
+ });
+ setDeploy(newData);
+ }
+ } else if (data.config?.format === "jks") {
+ if (/(.crt|.pfx)$/.test(data.config.certPath)) {
+ const newData = produce(data, (draft) => {
+ draft.config ??= {};
+ draft.config.certPath = data.config!.certPath.replace(/(.crt|.pfx)$/, ".jks");
});
setDeploy(newData);
}
@@ -194,6 +230,7 @@ Remove-Item -Path "$pfxPath" -Force