diff --git a/internal/domain/certificate.go b/internal/domain/certificate.go index 190583dc2..f61cd3993 100644 --- a/internal/domain/certificate.go +++ b/internal/domain/certificate.go @@ -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 { diff --git a/internal/repository/certificate.go b/internal/repository/certificate.go index b54404645..57f687d98 100644 --- a/internal/repository/certificate.go +++ b/internal/repository/certificate.go @@ -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")), diff --git a/migrations/1780545600_upgrade_v0.4.25.go b/migrations/1780545600_upgrade_v0.4.25.go deleted file mode 100644 index bd83e7066..000000000 --- a/migrations/1780545600_upgrade_v0.4.25.go +++ /dev/null @@ -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 - }) -} diff --git a/migrations/1780704000_upgrade_v0.4.25.go b/migrations/1780704000_upgrade_v0.4.25.go new file mode 100644 index 000000000..1842ed3b0 --- /dev/null +++ b/migrations/1780704000_upgrade_v0.4.25.go @@ -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 + }) +} diff --git a/pkg/utils/cert/x509/x509.go b/pkg/utils/cert/x509/x509.go index d6d3376f7..fddfa0be2 100644 --- a/pkg/utils/cert/x509/x509.go +++ b/pkg/utils/cert/x509/x509.go @@ -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 +} diff --git a/ui/src/domain/certificate.ts b/ui/src/domain/certificate.ts index 7b5298a91..d91fd73a3 100644 --- a/ui/src/domain/certificate.ts +++ b/ui/src/domain/certificate.ts @@ -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;