refactor: extract x509 utils

This commit is contained in:
Fu Diwei 2025-12-24 21:40:30 +08:00 committed by RHQYZ
parent 5dcb7e4ae0
commit de6384205a
9 changed files with 270 additions and 36 deletions

View File

@ -2,8 +2,11 @@
import (
"crypto/x509"
"encoding/asn1"
)
var oidSubjectAlternativeNameExtension = asn1.ObjectIdentifier{2, 5, 29, 17}
// 返回指定 x509.Certificate 对象的主题名称。
// 如果主题名称为空,则返回第一个主题替代名称。
//
@ -38,18 +41,21 @@ func GetSubjectAltNames(cert *x509.Certificate) []string {
sans := make([]string, 0)
if cert != nil {
for _, dnsName := range cert.DNSNames {
sans = append(sans, dnsName)
}
for _, ipAddr := range cert.IPAddresses {
sans = append(sans, ipAddr.String())
}
for _, email := range cert.EmailAddresses {
sans = append(sans, email)
}
for _, uri := range cert.URIs {
if uri != nil {
sans = append(sans, uri.String())
// 注意,这里不直接使用 `DNSNames`、`IPAddresses` 等字段,以保证原始顺序不变
for _, ext := range cert.Extensions {
if ext.Id.Equal(oidSubjectAlternativeNameExtension) {
var raw asn1.RawValue
_, err := asn1.Unmarshal(ext.Value, &raw)
if err != nil {
continue
}
var seq asn1.RawValue
if _, err := asn1.Unmarshal(raw.Bytes, &seq); err != nil {
continue
}
sans = append(sans, string(seq.Bytes))
}
}
}

203
ui/package-lock.json generated
View File

@ -14,6 +14,7 @@
"@flowgram.ai/document": "0.5.6",
"@flowgram.ai/fixed-layout-editor": "0.5.6",
"@flowgram.ai/minimap-plugin": "0.5.6",
"@peculiar/x509": "^1.14.2",
"@tabler/icons-react": "^3.36.0",
"@uiw/codemirror-extensions-basic-setup": "^4.25.4",
"@uiw/codemirror-theme-vscode": "^4.25.4",
@ -37,6 +38,7 @@
"react-i18next": "^16.5.0",
"react-router": "^7.11.0",
"react-router-dom": "^7.11.0",
"reflect-metadata": "^0.2.2",
"tailwind-merge": "^3.4.0",
"yaml": "^2.8.2",
"zod": "^4.2.1",
@ -3177,6 +3179,154 @@
"@tybys/wasm-util": "^0.9.0"
}
},
"node_modules/@peculiar/asn1-cms": {
"version": "2.6.0",
"resolved": "https://registry.npmmirror.com/@peculiar/asn1-cms/-/asn1-cms-2.6.0.tgz",
"integrity": "sha512-2uZqP+ggSncESeUF/9Su8rWqGclEfEiz1SyU02WX5fUONFfkjzS2Z/F1Li0ofSmf4JqYXIOdCAZqIXAIBAT1OA==",
"license": "MIT",
"dependencies": {
"@peculiar/asn1-schema": "^2.6.0",
"@peculiar/asn1-x509": "^2.6.0",
"@peculiar/asn1-x509-attr": "^2.6.0",
"asn1js": "^3.0.6",
"tslib": "^2.8.1"
}
},
"node_modules/@peculiar/asn1-csr": {
"version": "2.6.0",
"resolved": "https://registry.npmmirror.com/@peculiar/asn1-csr/-/asn1-csr-2.6.0.tgz",
"integrity": "sha512-BeWIu5VpTIhfRysfEp73SGbwjjoLL/JWXhJ/9mo4vXnz3tRGm+NGm3KNcRzQ9VMVqwYS2RHlolz21svzRXIHPQ==",
"license": "MIT",
"dependencies": {
"@peculiar/asn1-schema": "^2.6.0",
"@peculiar/asn1-x509": "^2.6.0",
"asn1js": "^3.0.6",
"tslib": "^2.8.1"
}
},
"node_modules/@peculiar/asn1-ecc": {
"version": "2.6.0",
"resolved": "https://registry.npmmirror.com/@peculiar/asn1-ecc/-/asn1-ecc-2.6.0.tgz",
"integrity": "sha512-FF3LMGq6SfAOwUG2sKpPXblibn6XnEIKa+SryvUl5Pik+WR9rmRA3OCiwz8R3lVXnYnyRkSZsSLdml8H3UiOcw==",
"license": "MIT",
"dependencies": {
"@peculiar/asn1-schema": "^2.6.0",
"@peculiar/asn1-x509": "^2.6.0",
"asn1js": "^3.0.6",
"tslib": "^2.8.1"
}
},
"node_modules/@peculiar/asn1-pfx": {
"version": "2.6.0",
"resolved": "https://registry.npmmirror.com/@peculiar/asn1-pfx/-/asn1-pfx-2.6.0.tgz",
"integrity": "sha512-rtUvtf+tyKGgokHHmZzeUojRZJYPxoD/jaN1+VAB4kKR7tXrnDCA/RAWXAIhMJJC+7W27IIRGe9djvxKgsldCQ==",
"license": "MIT",
"dependencies": {
"@peculiar/asn1-cms": "^2.6.0",
"@peculiar/asn1-pkcs8": "^2.6.0",
"@peculiar/asn1-rsa": "^2.6.0",
"@peculiar/asn1-schema": "^2.6.0",
"asn1js": "^3.0.6",
"tslib": "^2.8.1"
}
},
"node_modules/@peculiar/asn1-pkcs8": {
"version": "2.6.0",
"resolved": "https://registry.npmmirror.com/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.6.0.tgz",
"integrity": "sha512-KyQ4D8G/NrS7Fw3XCJrngxmjwO/3htnA0lL9gDICvEQ+GJ+EPFqldcJQTwPIdvx98Tua+WjkdKHSC0/Km7T+lA==",
"license": "MIT",
"dependencies": {
"@peculiar/asn1-schema": "^2.6.0",
"@peculiar/asn1-x509": "^2.6.0",
"asn1js": "^3.0.6",
"tslib": "^2.8.1"
}
},
"node_modules/@peculiar/asn1-pkcs9": {
"version": "2.6.0",
"resolved": "https://registry.npmmirror.com/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.6.0.tgz",
"integrity": "sha512-b78OQ6OciW0aqZxdzliXGYHASeCvvw5caqidbpQRYW2mBtXIX2WhofNXTEe7NyxTb0P6J62kAAWLwn0HuMF1Fw==",
"license": "MIT",
"dependencies": {
"@peculiar/asn1-cms": "^2.6.0",
"@peculiar/asn1-pfx": "^2.6.0",
"@peculiar/asn1-pkcs8": "^2.6.0",
"@peculiar/asn1-schema": "^2.6.0",
"@peculiar/asn1-x509": "^2.6.0",
"@peculiar/asn1-x509-attr": "^2.6.0",
"asn1js": "^3.0.6",
"tslib": "^2.8.1"
}
},
"node_modules/@peculiar/asn1-rsa": {
"version": "2.6.0",
"resolved": "https://registry.npmmirror.com/@peculiar/asn1-rsa/-/asn1-rsa-2.6.0.tgz",
"integrity": "sha512-Nu4C19tsrTsCp9fDrH+sdcOKoVfdfoQQ7S3VqjJU6vedR7tY3RLkQ5oguOIB3zFW33USDUuYZnPEQYySlgha4w==",
"license": "MIT",
"dependencies": {
"@peculiar/asn1-schema": "^2.6.0",
"@peculiar/asn1-x509": "^2.6.0",
"asn1js": "^3.0.6",
"tslib": "^2.8.1"
}
},
"node_modules/@peculiar/asn1-schema": {
"version": "2.6.0",
"resolved": "https://registry.npmmirror.com/@peculiar/asn1-schema/-/asn1-schema-2.6.0.tgz",
"integrity": "sha512-xNLYLBFTBKkCzEZIw842BxytQQATQv+lDTCEMZ8C196iJcJJMBUZxrhSTxLaohMyKK8QlzRNTRkUmanucnDSqg==",
"license": "MIT",
"dependencies": {
"asn1js": "^3.0.6",
"pvtsutils": "^1.3.6",
"tslib": "^2.8.1"
}
},
"node_modules/@peculiar/asn1-x509": {
"version": "2.6.0",
"resolved": "https://registry.npmmirror.com/@peculiar/asn1-x509/-/asn1-x509-2.6.0.tgz",
"integrity": "sha512-uzYbPEpoQiBoTq0/+jZtpM6Gq6zADBx+JNFP3yqRgziWBxQ/Dt/HcuvRfm9zJTPdRcBqPNdaRHTVwpyiq6iNMA==",
"license": "MIT",
"dependencies": {
"@peculiar/asn1-schema": "^2.6.0",
"asn1js": "^3.0.6",
"pvtsutils": "^1.3.6",
"tslib": "^2.8.1"
}
},
"node_modules/@peculiar/asn1-x509-attr": {
"version": "2.6.0",
"resolved": "https://registry.npmmirror.com/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.6.0.tgz",
"integrity": "sha512-MuIAXFX3/dc8gmoZBkwJWxUWOSvG4MMDntXhrOZpJVMkYX+MYc/rUAU2uJOved9iJEoiUx7//3D8oG83a78UJA==",
"license": "MIT",
"dependencies": {
"@peculiar/asn1-schema": "^2.6.0",
"@peculiar/asn1-x509": "^2.6.0",
"asn1js": "^3.0.6",
"tslib": "^2.8.1"
}
},
"node_modules/@peculiar/x509": {
"version": "1.14.2",
"resolved": "https://registry.npmmirror.com/@peculiar/x509/-/x509-1.14.2.tgz",
"integrity": "sha512-r2w1Hg6pODDs0zfAKHkSS5HLkOLSeburtcgwvlLLWWCixw+MmW3U6kD5ddyvc2Y2YdbGuVwCF2S2ASoU1cFAag==",
"license": "MIT",
"dependencies": {
"@peculiar/asn1-cms": "^2.6.0",
"@peculiar/asn1-csr": "^2.6.0",
"@peculiar/asn1-ecc": "^2.6.0",
"@peculiar/asn1-pkcs9": "^2.6.0",
"@peculiar/asn1-rsa": "^2.6.0",
"@peculiar/asn1-schema": "^2.6.0",
"@peculiar/asn1-x509": "^2.6.0",
"pvtsutils": "^1.3.6",
"reflect-metadata": "^0.2.2",
"tslib": "^2.8.1",
"tsyringe": "^4.10.0"
},
"engines": {
"node": ">=22.0.0"
}
},
"node_modules/@phosphor/algorithm": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/@phosphor/algorithm/-/algorithm-1.2.0.tgz",
@ -5616,6 +5766,20 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/asn1js": {
"version": "3.0.7",
"resolved": "https://registry.npmmirror.com/asn1js/-/asn1js-3.0.7.tgz",
"integrity": "sha512-uLvq6KJu04qoQM6gvBfKFjlh6Gl0vOKQuR5cJMDHQkmwfMOQeN3F3SHCv9SNYSL+CRoHvOGFfllDlVz03GQjvQ==",
"license": "BSD-3-Clause",
"dependencies": {
"pvtsutils": "^1.3.6",
"pvutils": "^1.1.3",
"tslib": "^2.8.1"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/available-typed-arrays": {
"version": "1.0.7",
"resolved": "https://registry.npmmirror.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
@ -8625,6 +8789,24 @@
"node": ">=6"
}
},
"node_modules/pvtsutils": {
"version": "1.3.6",
"resolved": "https://registry.npmmirror.com/pvtsutils/-/pvtsutils-1.3.6.tgz",
"integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==",
"license": "MIT",
"dependencies": {
"tslib": "^2.8.1"
}
},
"node_modules/pvutils": {
"version": "1.1.5",
"resolved": "https://registry.npmmirror.com/pvutils/-/pvutils-1.1.5.tgz",
"integrity": "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==",
"license": "MIT",
"engines": {
"node": ">=16.0.0"
}
},
"node_modules/radash": {
"version": "12.1.1",
"resolved": "https://registry.npmmirror.com/radash/-/radash-12.1.1.tgz",
@ -8766,7 +8948,8 @@
"node_modules/reflect-metadata": {
"version": "0.2.2",
"resolved": "https://registry.npmmirror.com/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
"integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q=="
"integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==",
"license": "Apache-2.0"
},
"node_modules/reflect.getprototypeof": {
"version": "1.0.9",
@ -9596,6 +9779,24 @@
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
},
"node_modules/tsyringe": {
"version": "4.10.0",
"resolved": "https://registry.npmmirror.com/tsyringe/-/tsyringe-4.10.0.tgz",
"integrity": "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==",
"license": "MIT",
"dependencies": {
"tslib": "^1.9.3"
},
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/tsyringe/node_modules/tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
"license": "0BSD"
},
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz",

View File

@ -16,6 +16,7 @@
"@flowgram.ai/document": "0.5.6",
"@flowgram.ai/fixed-layout-editor": "0.5.6",
"@flowgram.ai/minimap-plugin": "0.5.6",
"@peculiar/x509": "^1.14.2",
"@tabler/icons-react": "^3.36.0",
"@uiw/codemirror-extensions-basic-setup": "^4.25.4",
"@uiw/codemirror-theme-vscode": "^4.25.4",
@ -39,6 +40,7 @@
"react-i18next": "^16.5.0",
"react-router": "^7.11.0",
"react-router-dom": "^7.11.0",
"reflect-metadata": "^0.2.2",
"tailwind-merge": "^3.4.0",
"yaml": "^2.8.2",
"zod": "^4.2.1",

View File

@ -1,4 +1,5 @@
import { useEffect, useLayoutEffect, useState } from "react";
import "reflect-metadata";
import { useEffect, useLayoutEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { RouterProvider } from "react-router-dom";
import { Alert, App, ConfigProvider, type ThemeConfig, theme } from "antd";

View File

@ -227,12 +227,12 @@ const BizApplyNodeConfigForm = ({ node, ...props }: BizApplyNodeConfigFormProps)
value: resp.data.keyAlgorithm,
},
]);
} catch (e) {
} catch (err) {
formInst.setFields([
{
name: "keyContent",
value: value,
errors: [getErrMsg(e)],
errors: [getErrMsg(err)],
},
]);
}

View File

@ -13,6 +13,7 @@ import { type WorkflowNodeConfigForBizUpload, defaultNodeConfigForBizUpload } fr
import { useAntdForm } from "@/hooks";
import { getErrMsg } from "@/utils/error";
import { isUrlWithHttpOrHttps } from "@/utils/validator";
import { getSubjectAltNames as getX509SubjectAltNames } from "@/utils/x509";
import { NodeFormContextProvider } from "./_context";
import { NodeType } from "../nodes/typings";
@ -46,42 +47,40 @@ const BizUploadNodeConfigForm = ({ node, ...props }: BizUploadNodeConfigFormProp
});
const fieldSource = Form.useWatch("source", { form: formInst, preserve: true });
const fieldCertificate = Form.useWatch("certificate", { form: formInst, preserve: true });
const fieldName = useMemo(() => {
if (!fieldSource || fieldSource === UPLOAD_SOURCE_FORM) {
return fieldCertificate ? getX509SubjectAltNames(fieldCertificate).join(";") : void 0;
}
return void 0;
}, [fieldSource, fieldCertificate]);
const handleSourceChange = (value: string) => {
if (value === initialValues?.source) {
formInst.resetFields(["certificate", "privateKey", "name"]);
formInst.resetFields(["certificate", "privateKey"]);
} else {
setTimeout(() => {
formInst.setFieldValue("certificate", "");
formInst.setFieldValue("privateKey", "");
formInst.setFieldValue("name", "");
}, 0);
}
};
const handleCertificatePEMChange = async (value: string) => {
try {
const resp = await validateCertificate(value);
await validateCertificate(value);
formInst.setFields([
{
name: "name",
value: resp.data.subjectAltNames,
},
{
name: "certificate",
value: value,
},
]);
} catch (e) {
} catch (err) {
formInst.setFields([
{
name: "name",
value: "",
},
{
name: "certificate",
value: value,
errors: [getErrMsg(e)],
errors: [getErrMsg(err)],
},
]);
}
@ -96,12 +95,12 @@ const BizUploadNodeConfigForm = ({ node, ...props }: BizUploadNodeConfigFormProp
value: value,
},
]);
} catch (e) {
} catch (err) {
formInst.setFields([
{
name: "privateKey",
value: value,
errors: [getErrMsg(e)],
errors: [getErrMsg(err)],
},
]);
}
@ -120,8 +119,8 @@ const BizUploadNodeConfigForm = ({ node, ...props }: BizUploadNodeConfigFormProp
</Form.Item>
<Show when={fieldSource === UPLOAD_SOURCE_FORM}>
<Form.Item name="name" label={t("workflow_node.upload.form.name.label")} rules={[formRule]}>
<Input variant="filled" placeholder={t("workflow_node.upload.form.name.placeholder")} readOnly />
<Form.Item label={t("workflow_node.upload.form.name.label")}>
<Input placeholder={t("workflow_node.upload.form.name.placeholder")} readOnly value={fieldName} variant="filled" />
</Form.Item>
<Form.Item name="certificate" label={t("workflow_node.upload.form.certificate_pem.label")} rules={[formRule]}>

View File

@ -3,6 +3,7 @@ import { FeedbackLevel, Field } from "@flowgram.ai/fixed-layout-editor";
import { IconCloudUpload } from "@tabler/icons-react";
import { newNode } from "@/domain/workflow";
import { getSubjectAltNames as getX509SubjectAltNames } from "@/utils/x509";
import { BaseNode } from "./_shared";
import { NodeKindType, type NodeRegistry, NodeType } from "./typings";
@ -47,8 +48,10 @@ export const BizUploadNodeRegistry: NodeRegistry = {
{({ field: { value: fieldSource } }) => (
<>
{fieldSource == null || fieldSource === "" || fieldSource === "form" ? (
<Field<string> name="config.name">
{({ field: { value: fieldName } }) => <>{fieldName || t("workflow.detail.design.editor.placeholder")}</>}
<Field<string> name="config.certificate">
{({ field: { value: fieldCertificate } }) => (
<>{(fieldCertificate ? getX509SubjectAltNames(fieldCertificate).join(";") : "") || t("workflow.detail.design.editor.placeholder")}</>
)}
</Field>
) : (
<Field<string> name="config.certificate">

View File

@ -125,7 +125,6 @@ export const defaultNodeConfigForBizApply = (): Partial<WorkflowNodeConfigForBiz
export type WorkflowNodeConfigForBizUpload = {
source: string;
domains?: string;
certificate: string;
privateKey: string;
};

23
ui/src/utils/x509.ts Normal file
View File

@ -0,0 +1,23 @@
import { SubjectAlternativeNameExtension, X509Certificate } from "@peculiar/x509";
export const parseX509Certificate = (certPEM: string): X509Certificate => {
const certX509 = new X509Certificate(certPEM);
if (certX509 == null) throw new Error("Could not parse X.509 certificate. Maybe it is not in PEM format?");
return certX509;
};
export const getSubjectAltNames = (cert: string | X509Certificate): string[] => {
const subjectAltNames: string[] = [];
try {
const certX509 = X509Certificate.isAsnEncoded(cert) ? parseX509Certificate(cert) : cert;
if (certX509 == null) return [];
const sanExt = certX509.getExtension(SubjectAlternativeNameExtension);
subjectAltNames.push(...(sanExt?.names?.items?.map((san) => san.value) || []));
} catch (err) {
console.error(err);
}
return subjectAltNames;
};