feat: enhance certificate model with subjectName, issuerName, and validationPolicy fields

This commit is contained in:
Fu Diwei
2026-06-09 09:54:58 +08:00
committed by RHQYZ
parent d38eb37998
commit 1cea69d1d2
6 changed files with 399 additions and 119 deletions
+52 -20
View File
@@ -17,30 +17,35 @@ const CollectionNameCertificate = "certificate"
type Certificate struct {
Meta
Source CertificateSourceType `db:"source" json:"source"`
SubjectAltNames string `db:"subjectAltNames" json:"subjectAltNames"`
SerialNumber string `db:"serialNumber" json:"serialNumber"`
Certificate string `db:"certificate" json:"certificate"`
PrivateKey string `db:"privateKey" json:"privateKey"`
IssuerOrg string `db:"issuerOrg" json:"issuerOrg"`
IssuerCertificate string `db:"issuerCertificate" json:"issuerCertificate"`
KeyAlgorithm CertificateKeyAlgorithmType `db:"keyAlgorithm" json:"keyAlgorithm"`
ValidityNotBefore time.Time `db:"validityNotBefore" json:"validityNotBefore"`
ValidityNotAfter time.Time `db:"validityNotAfter" json:"validityNotAfter"`
ValidityInterval int32 `db:"validityInterval" json:"validityInterval"`
ACMEAcctUrl string `db:"acmeAcctUrl" json:"acmeAcctUrl"`
ACMECertUrl string `db:"acmeCertUrl" json:"acmeCertUrl"`
IsRenewed bool `db:"isRenewed" json:"isRenewed"`
IsRevoked bool `db:"isRevoked" json:"isRevoked"`
WorkflowId string `db:"workflowRef" json:"workflowId"`
WorkflowRunId string `db:"workflowRunRef" json:"workflowRunId"`
WorkflowNodeId string `db:"workflowNodeId" json:"workflowNodeId"`
DeletedAt *time.Time `db:"deleted" json:"deleted"`
Source CertificateSourceType `db:"source" json:"source"`
Certificate string `db:"certificate" json:"certificate"`
PrivateKey string `db:"privateKey" json:"privateKey"`
SerialNumber string `db:"serialNumber" json:"serialNumber"`
SubjectName string `db:"subjectName" json:"subjectName"`
SubjectAltNames string `db:"subjectAltNames" json:"subjectAltNames"`
IssuerName string `db:"issuerName" json:"issuerName"`
IssuerOrg string `db:"issuerOrg" json:"issuerOrg"`
IssuerCertificate string `db:"issuerCertificate" json:"issuerCertificate"`
KeyAlgorithm CertificateKeyAlgorithmType `db:"keyAlgorithm" json:"keyAlgorithm"`
ValidationPolicy CertificateValidationPolicyType `db:"validationPolicy" json:"validationPolicy"`
ValidityNotBefore time.Time `db:"validityNotBefore" json:"validityNotBefore"`
ValidityNotAfter time.Time `db:"validityNotAfter" json:"validityNotAfter"`
ValidityInterval int32 `db:"validityInterval" json:"validityInterval"`
ACMEAcctUrl string `db:"acmeAcctUrl" json:"acmeAcctUrl"`
ACMECertUrl string `db:"acmeCertUrl" json:"acmeCertUrl"`
IsRenewed bool `db:"isRenewed" json:"isRenewed"`
IsRevoked bool `db:"isRevoked" json:"isRevoked"`
WorkflowId string `db:"workflowRef" json:"workflowId"`
WorkflowRunId string `db:"workflowRunRef" json:"workflowRunId"`
WorkflowNodeId string `db:"workflowNodeId" json:"workflowNodeId"`
DeletedAt *time.Time `db:"deleted" json:"deleted"`
}
func (c *Certificate) PopulateFromX509(certX509 *x509.Certificate) *Certificate {
c.SubjectAltNames = strings.Join(xcertx509.GetSubjectAltNames(certX509), ";")
c.SerialNumber = strings.ToUpper(certX509.SerialNumber.Text(16))
c.SubjectName = certX509.Subject.CommonName
c.SubjectAltNames = strings.Join(xcertx509.GetSubjectAltNames(certX509), ";")
c.IssuerName = certX509.Issuer.CommonName
c.IssuerOrg = strings.Join(certX509.Issuer.Organization, ";")
c.ValidityNotBefore = certX509.NotBefore
c.ValidityNotAfter = certX509.NotAfter
@@ -58,6 +63,20 @@ func (c *Certificate) PopulateFromX509(certX509 *x509.Certificate) *Certificate
c.KeyAlgorithm = CertificateKeyAlgorithmType("")
}
validationType := xcertx509.GetValidationType(certX509)
switch validationType {
case xcertx509.ExtendedValidation:
c.ValidationPolicy = CertificateValidationPolicyTypeEV
case xcertx509.DomainValidated:
c.ValidationPolicy = CertificateValidationPolicyTypeDV
case xcertx509.OrganizationalValidated:
c.ValidationPolicy = CertificateValidationPolicyTypeOV
case xcertx509.IndividualValidated:
c.ValidationPolicy = CertificateValidationPolicyTypeIV
default:
c.ValidationPolicy = CertificateValidationPolicyType("")
}
return c
}
@@ -106,6 +125,19 @@ const (
CertificateKeyAlgorithmTypeEC384 = CertificateKeyAlgorithmType(certcrypto.EC384)
)
type CertificateValidationPolicyType string
func (t CertificateValidationPolicyType) String() string {
return string(t)
}
const (
CertificateValidationPolicyTypeEV = CertificateValidationPolicyType("EV")
CertificateValidationPolicyTypeDV = CertificateValidationPolicyType("DV")
CertificateValidationPolicyTypeOV = CertificateValidationPolicyType("OV")
CertificateValidationPolicyTypeIV = CertificateValidationPolicyType("IV")
)
type CertificateFormatType string
func (t CertificateFormatType) String() string {
+10 -4
View File
@@ -94,13 +94,16 @@ func (r *CertificateRepository) Save(ctx context.Context, certificate *domain.Ce
}
record.Set("source", certificate.Source.String())
record.Set("subjectAltNames", certificate.SubjectAltNames)
record.Set("serialNumber", certificate.SerialNumber)
record.Set("certificate", certificate.Certificate)
record.Set("privateKey", certificate.PrivateKey)
record.Set("serialNumber", certificate.SerialNumber)
record.Set("subjectName", certificate.SubjectName)
record.Set("subjectAltNames", certificate.SubjectAltNames)
record.Set("issuerOrg", certificate.IssuerOrg)
record.Set("issuerName", certificate.IssuerName)
record.Set("issuerCertificate", certificate.IssuerCertificate)
record.Set("keyAlgorithm", certificate.KeyAlgorithm.String())
record.Set("validationPolicy", certificate.ValidationPolicy.String())
record.Set("validityNotBefore", certificate.ValidityNotBefore)
record.Set("validityNotAfter", certificate.ValidityNotAfter)
record.Set("validityInterval", certificate.ValidityInterval)
@@ -156,13 +159,16 @@ func (r *CertificateRepository) castRecordToModel(record *core.Record) (*domain.
UpdatedAt: record.GetDateTime("updated").Time(),
},
Source: domain.CertificateSourceType(record.GetString("source")),
SubjectAltNames: record.GetString("subjectAltNames"),
SerialNumber: record.GetString("serialNumber"),
Certificate: record.GetString("certificate"),
PrivateKey: record.GetString("privateKey"),
SerialNumber: record.GetString("serialNumber"),
SubjectName: record.GetString("subjectName"),
SubjectAltNames: record.GetString("subjectAltNames"),
IssuerName: record.GetString("issuerName"),
IssuerOrg: record.GetString("issuerOrg"),
IssuerCertificate: record.GetString("issuerCertificate"),
KeyAlgorithm: domain.CertificateKeyAlgorithmType(record.GetString("keyAlgorithm")),
ValidationPolicy: domain.CertificateValidationPolicyType(record.GetString("validationPolicy")),
ValidityNotBefore: record.GetDateTime("validityNotBefore").Time(),
ValidityNotAfter: record.GetDateTime("validityNotAfter").Time(),
ValidityInterval: int32(record.GetInt("validityInterval")),
-92
View File
@@ -1,92 +0,0 @@
package migrations
import (
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
tracer := NewTracer("v0.4.25")
tracer.Printf("go ...")
// update collection `access`
// - modify field `config` schema
{
collection, err := app.FindCollectionByNameOrId("4yzbv8urny5ja1e")
if err != nil {
return err
}
records, err := app.FindAllRecords(collection)
if err != nil {
return err
}
for _, record := range records {
changed := false
provider := record.GetString("provider")
config := make(map[string]any)
if err := record.UnmarshalJSONField("config", &config); err != nil {
return err
}
switch provider {
case "cloudflare":
{
if _, ok := config["dnsApiToken"]; ok {
config["apiToken"] = config["dnsApiToken"]
delete(config, "dnsApiToken")
record.Set("config", config)
changed = true
}
if _, ok := config["zoneApiToken"]; ok {
config["apiTokenForZone"] = config["zoneApiToken"]
delete(config, "zoneApiToken")
record.Set("config", config)
changed = true
}
}
case "byteplus":
{
if _, ok := config["accessKey"]; ok {
config["accessKeyId"] = config["accessKey"]
delete(config, "accessKey")
record.Set("config", config)
changed = true
}
if _, ok := config["secretKey"]; ok {
config["secretAccessKey"] = config["secretKey"]
delete(config, "secretKey")
record.Set("config", config)
changed = true
}
}
case "volcengine":
{
if _, ok := config["accessKeySecret"]; ok {
config["secretAccessKey"] = config["accessKeySecret"]
delete(config, "accessKeySecret")
record.Set("config", config)
changed = true
}
}
}
if changed {
if err := app.Save(record); err != nil {
return err
}
tracer.Printf("record #%s in collection '%s' updated", record.Id, collection.Name)
}
}
}
tracer.Printf("done")
return nil
}, func(app core.App) error {
return nil
})
}
+259
View File
@@ -0,0 +1,259 @@
package migrations
import (
"errors"
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
xcert "github.com/certimate-go/certimate/pkg/utils/cert"
xcertx509 "github.com/certimate-go/certimate/pkg/utils/cert/x509"
)
func init() {
m.Register(func(app core.App) error {
tracer := NewTracer("v0.4.25")
tracer.Printf("go ...")
// update collection `access`
// - modify field `config` schema
{
collection, err := app.FindCollectionByNameOrId("4yzbv8urny5ja1e")
if err != nil {
return err
}
records, err := app.FindAllRecords(collection)
if err != nil {
return err
}
for _, record := range records {
changed := false
provider := record.GetString("provider")
config := make(map[string]any)
if err := record.UnmarshalJSONField("config", &config); err != nil {
return err
}
switch provider {
case "cloudflare":
{
if _, ok := config["dnsApiToken"]; ok {
config["apiToken"] = config["dnsApiToken"]
delete(config, "dnsApiToken")
record.Set("config", config)
changed = true
}
if _, ok := config["zoneApiToken"]; ok {
config["apiTokenForZone"] = config["zoneApiToken"]
delete(config, "zoneApiToken")
record.Set("config", config)
changed = true
}
}
case "byteplus":
{
if _, ok := config["accessKey"]; ok {
config["accessKeyId"] = config["accessKey"]
delete(config, "accessKey")
record.Set("config", config)
changed = true
}
if _, ok := config["secretKey"]; ok {
config["secretAccessKey"] = config["secretKey"]
delete(config, "secretKey")
record.Set("config", config)
changed = true
}
}
case "volcengine":
{
if _, ok := config["accessKeySecret"]; ok {
config["secretAccessKey"] = config["accessKeySecret"]
delete(config, "accessKeySecret")
record.Set("config", config)
changed = true
}
}
}
if changed {
if err := app.Save(record); err != nil {
return err
}
tracer.Printf("record #%s in collection '%s' updated", record.Id, collection.Name)
}
}
}
// update collection `certificate`
// - add field `subjectName`
// - add field `issuerName`
// - add field `validationPolicy`
{
collection, err := app.FindCollectionByNameOrId("4szxr9x43tpj6np")
if err != nil {
return err
}
if err := collection.Fields.AddMarshaledJSONAt(2, []byte(`{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "plmambpz",
"max": 100000,
"min": 0,
"name": "certificate",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": true,
"system": false,
"type": "text"
}`)); err != nil {
return err
}
if err := collection.Fields.AddMarshaledJSONAt(3, []byte(`{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "49qvwxcg",
"max": 100000,
"min": 0,
"name": "privateKey",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": true,
"system": false,
"type": "text"
}`)); err != nil {
return err
}
if err := collection.Fields.AddMarshaledJSONAt(4, []byte(`{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text2069360702",
"max": 0,
"min": 0,
"name": "serialNumber",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
}`)); err != nil {
return err
}
if err := collection.Fields.AddMarshaledJSONAt(5, []byte(`{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text2876278798",
"max": 0,
"min": 0,
"name": "subjectName",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
}`)); err != nil {
return err
}
if err := collection.Fields.AddMarshaledJSONAt(7, []byte(`{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text2678583873",
"max": 0,
"min": 0,
"name": "issuerName",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
}`)); err != nil {
return err
}
if err := collection.Fields.AddMarshaledJSONAt(11, []byte(`{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text2516249007",
"max": 0,
"min": 0,
"name": "validationPolicy",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
}`)); err != nil {
return err
}
if err := app.Save(collection); err != nil {
return err
}
tracer.Printf("collection '%s' updated", collection.Name)
records, err := app.FindAllRecords(collection)
if err != nil {
return err
}
for _, record := range records {
changed := false
if certX509, err := xcert.ParseCertificateFromPEM(record.GetString("certificate")); err == nil {
record.Set("subjectName", certX509.Subject.CommonName)
record.Set("issuerName", certX509.Issuer.CommonName)
switch xcertx509.GetValidationType(certX509) {
case xcertx509.ExtendedValidation:
record.Set("validationPolicy", "EV")
case xcertx509.DomainValidated:
record.Set("validationPolicy", "DV")
case xcertx509.OrganizationalValidated:
record.Set("validationPolicy", "OV")
case xcertx509.IndividualValidated:
record.Set("validationPolicy", "IV")
default:
record.Set("validationPolicy", "")
}
changed = true
}
if changed {
if err := app.Save(record); err != nil {
return err
}
tracer.Printf("record #%s in collection '%s' updated", record.Id, collection.Name)
}
}
}
tracer.Printf("done")
return nil
}, func(app core.App) error {
return errors.ErrUnsupported
})
}
+73 -1
View File
@@ -6,7 +6,14 @@ import (
"net"
)
var oidSubjectAlternativeNameExtension = asn1.ObjectIdentifier{2, 5, 29, 17}
var (
oidSubjectAlternativeNameExtension = asn1.ObjectIdentifier{2, 5, 29, 17}
oidValidationTypeEV = asn1.ObjectIdentifier{2, 23, 140, 1, 1}
oidValidationTypeDV = asn1.ObjectIdentifier{2, 23, 140, 1, 2, 1}
oidValidationTypeOV = asn1.ObjectIdentifier{2, 23, 140, 1, 2, 2}
oidValidationTypeIV = asn1.ObjectIdentifier{2, 23, 140, 1, 2, 3}
)
const (
sanGeneralNameTagEmail = 1
@@ -15,6 +22,16 @@ const (
sanGeneralNameTagIP = 7
)
type ValidationType int
const (
UnknownValidation ValidationType = iota
ExtendedValidation
DomainValidated
OrganizationalValidated
IndividualValidated
)
// 返回指定 x509.Certificate 对象的主题名称。
// 如果主题名称为空,则返回第一个主题替代名称。
//
@@ -84,3 +101,58 @@ func GetSubjectAltNames(cert *x509.Certificate) []string {
return sans
}
// 返回指定 x509.Certificate 对象的证书验证类型。
//
// 入参:
// - cert: x509.Certificate 对象。
//
// 出参:
// - 证书验证类型。
func GetValidationType(cert *x509.Certificate) ValidationType {
// 同一证书可能有多个符合的策略,按 EV > OV > IV > DV 顺序判断
if HasPolicy(cert, oidValidationTypeEV) {
return ExtendedValidation
} else if HasPolicy(cert, oidValidationTypeOV) {
return OrganizationalValidated
} else if HasPolicy(cert, oidValidationTypeIV) {
return IndividualValidated
} else if HasPolicy(cert, oidValidationTypeDV) {
return DomainValidated
}
return UnknownValidation
}
// 检查指定 x509.Certificate 对象是否包含指定的证书策略。
//
// 入参:
// - cert: x509.Certificate 对象。
// - policy: 证书策略 OID。
//
// 出参:
// - 是否包含指定的证书策略。
func HasPolicy(cert *x509.Certificate, policy asn1.ObjectIdentifier) bool {
for _, p := range cert.PolicyIdentifiers {
if p.Equal(policy) {
return true
}
}
return false
}
// 检查指定 x509.Certificate 对象是否包含指定的证书策略。
//
// 入参:
// - cert: x509.Certificate 对象。
// - policy: 证书策略 OID 字符串。
//
// 出参:
// - 是否包含指定的证书策略。
func HasPolicyString(cert *x509.Certificate, policy string) bool {
for _, p := range cert.PolicyIdentifiers {
if p.String() == policy {
return true
}
}
return false
}
+5 -2
View File
@@ -2,12 +2,15 @@ import { type WorkflowModel } from "./workflow";
export interface CertificateModel extends BaseModel {
source: string;
subjectAltNames: string;
serialNumber: string;
certificate: string;
privateKey: string;
serialNumber: string;
subjectName: string;
subjectAltNames: string;
issuerName: string;
issuerOrg: string;
keyAlgorithm: string;
validationPolicy: string;
validityNotBefore: ISO8601String;
validityNotAfter: ISO8601String;
isRenewed: boolean;