فهرست منبع

更新 Go 模块和工具链版本并添加新功能

更新 go.mod 文件中的 Go 版本至 1.23.0 并调整依赖项,新增全局证书信息处理函数和文件读写相关工具函数,改进错误输出格式,并增加对证书主题的读取和处理功能。
SongZihuan 4 هفته پیش
والد
کامیت
bbb464fdec

+ 10 - 5
go.mod

@@ -1,11 +1,16 @@
 module github.com/SongZihuan/MyCA
 
-go 1.22
+go 1.23.0
 
-require github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78
+toolchain go1.23.2
 
 require (
-	golang.org/x/crypto v0.33.0 // indirect
-	golang.org/x/sys v0.30.0 // indirect
-	golang.org/x/term v0.29.0 // indirect
+	github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78
+	golang.org/x/term v0.30.0
+	software.sslmate.com/src/go-pkcs12 v0.5.0
+)
+
+require (
+	golang.org/x/crypto v0.36.0 // indirect
+	golang.org/x/sys v0.31.0 // indirect
 )

+ 8 - 6
go.sum

@@ -1,8 +1,10 @@
 github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
 github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
-golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
-golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
-golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
-golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
-golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
+golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
+golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
+golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
+golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
+golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
+software.sslmate.com/src/go-pkcs12 v0.5.0 h1:EC6R394xgENTpZ4RltKydeDUjtlM5drOYIG9c6TVj2M=
+software.sslmate.com/src/go-pkcs12 v0.5.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=

+ 42 - 23
src/cert/cert.go

@@ -6,10 +6,9 @@ import (
 	"crypto/elliptic"
 	"crypto/rsa"
 	"crypto/x509"
-	"crypto/x509/pkix"
 	"encoding/gob"
 	"fmt"
-	"github.com/SongZihuan/MyCA/src/sysinfo"
+	"github.com/SongZihuan/MyCA/src/global"
 	"github.com/SongZihuan/MyCA/src/utils"
 	"math/big"
 	"net"
@@ -19,7 +18,7 @@ import (
 )
 
 type CAInfo interface {
-	NewCert() *big.Int
+	NewCertSerialNumber() (*big.Int, error)
 	GetIssuingCertificateURL() []string
 	GetOCSPServer() []string
 	GetCRLDistributionPoints() []string
@@ -85,9 +84,8 @@ func (info *CertInfo) SaveCertInfo() error {
 	return nil
 }
 
-func (info *CertInfo) GetSerialNumber() *big.Int {
-	info.SerialNumber = info.CA.NewCert()
-	return info.SerialNumber
+func (info *CertInfo) GetSerialNumber() (*big.Int, error) {
+	return info.CA.NewCertSerialNumber()
 }
 
 func (info *CertInfo) NewCert() *big.Int {
@@ -99,15 +97,26 @@ func (info *CertInfo) GetIssuingCertificateURL() []string {
 }
 
 // CreateCert 创建由CA签名的IP、域名证书
-func CreateCert(infoFilePath string, caInfo CAInfo, cryptoType utils.CryptoType, keyLength int, org string, cn string, domains []string, ips []net.IP, emails []string, urls []*url.URL, notBefore time.Time, notAfter time.Time, ca *x509.Certificate, caKey crypto.PrivateKey) (*x509.Certificate, crypto.PrivateKey, *CertInfo, error) {
+func CreateCert(infoFilePath string, caInfo CAInfo, cryptoType utils.CryptoType, keyLength int, subject *global.CertSubject, keyUsage x509.KeyUsage, extKeyUsage []x509.ExtKeyUsage, domains []string, ips []net.IP, emails []string, urls []*url.URL, notBefore time.Time, notAfter time.Time, ca *x509.Certificate, caKey crypto.PrivateKey) (*x509.Certificate, crypto.PrivateKey, *CertInfo, error) {
 	var privKey crypto.PrivateKey
-	var pubKey interface{}
+	var pubKey crypto.PublicKey
 
 	info, err := NewCertInfo(infoFilePath, caInfo)
 	if err != nil {
 		return nil, nil, nil, err
 	}
 
+	err = subject.SetCNIfEmpty() // 兜底,确保CN被设置
+	if err != nil {
+		return nil, nil, nil, err
+	}
+
+	if extKeyUsage == nil {
+		extKeyUsage = make([]x509.ExtKeyUsage, 0, 0)
+	} else {
+		extKeyUsage = utils.CopySlice(extKeyUsage)
+	}
+
 	switch cryptoType {
 	case utils.CryptoTypeRsa:
 		if keyLength != 2048 && keyLength != 4096 {
@@ -153,25 +162,35 @@ func CreateCert(infoFilePath string, caInfo CAInfo, cryptoType utils.CryptoType,
 		notAfter = notBefore.Add(time.Hour * 24 * 365 * 5) // 5年
 	}
 
-	org, cn = sysinfo.CreateCASubjectLong(org, cn, domains, ips, emails, urls)
+	ski, err := utils.CalculateSubjectKeyIdentifier(pubKey)
+	if err != nil {
+		return nil, nil, nil, fmt.Errorf("get subject key indentifier failed: %s", err.Error())
+	}
+
+	serialNumber, err := info.GetSerialNumber()
+	if err != nil {
+		return nil, nil, nil, fmt.Errorf("get new serial number failed: %s", err.Error())
+	}
 
 	template := &x509.Certificate{
-		SerialNumber: info.GetSerialNumber(),
-		Subject: pkix.Name{
-			Organization: []string{org},
-			CommonName:   cn,
-		},
-		NotBefore: notBefore,
-		NotAfter:  notAfter,
-
-		KeyUsage:              x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
-		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, // 允许全部扩展用途
+		SerialNumber: serialNumber,
+		Subject:      subject.ToPkixName(),
+		NotBefore:    notBefore,
+		NotAfter:     notAfter,
+
+		KeyUsage:    keyUsage,
+		ExtKeyUsage: extKeyUsage, // 允许全部扩展用途
+
 		BasicConstraintsValid: true,
 		IsCA:                  false,
-		DNSNames:              domains,
-		IPAddresses:           ips,
-		EmailAddresses:        emails,
-		URIs:                  urls,
+
+		DNSNames:       domains,
+		IPAddresses:    ips,
+		EmailAddresses: emails,
+		URIs:           urls,
+
+		SubjectKeyId:   []byte(ski),
+		AuthorityKeyId: ca.SubjectKeyId,
 
 		// 此处CA证书和终端证书不同,CA显示自己的吊销列表,终端证书显示对于CA的吊销列表
 		OCSPServer:            info.CA.GetOCSPServer(),

+ 44 - 27
src/cert/selfcert.go

@@ -8,12 +8,12 @@ import (
 	"crypto"
 	"crypto/ecdsa"
 	"crypto/elliptic"
+	"crypto/rand"
 	"crypto/rsa"
 	"crypto/x509"
-	"crypto/x509/pkix"
 	"encoding/gob"
 	"fmt"
-	"github.com/SongZihuan/MyCA/src/sysinfo"
+	"github.com/SongZihuan/MyCA/src/global"
 	"github.com/SongZihuan/MyCA/src/utils"
 	"math/big"
 	"net"
@@ -45,7 +45,7 @@ func NewSelfCertInfo(filepath string, ocsp []string, issuerURL []string, crlURL
 	return info, nil
 }
 
-func GetRCAInfo(filepath string) (*SelfCertInfo, error) {
+func GetSelfCertInfo(filepath string) (*SelfCertInfo, error) {
 	file, err := os.Open(filepath)
 	if err != nil {
 		return nil, err
@@ -84,8 +84,8 @@ func (info *SelfCertInfo) SaveSelfCert() error {
 	return nil
 }
 
-func (info *SelfCertInfo) NewCert() *big.Int {
-	return big.NewInt(1)
+func (info *SelfCertInfo) NewCertSerialNumber() (*big.Int, error) {
+	return rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), uint(40)))
 }
 
 func (info *SelfCertInfo) GetIssuingCertificateURL() []string {
@@ -101,17 +101,26 @@ func (info *SelfCertInfo) GetCRLDistributionPoints() []string {
 }
 
 // CreateSelfCert 创建自签名域名、IP证书
-func CreateSelfCert(infoFilePath string, cryptoType utils.CryptoType, keyLength int, org string, cn string, domains []string, ips []net.IP, emails []string, urls []*url.URL, ocsp []string, selfURL []string, crlURL []string, notBefore time.Time, notAfter time.Time) (*x509.Certificate, crypto.PrivateKey, *SelfCertInfo, error) {
+func CreateSelfCert(infoFilePath string, cryptoType utils.CryptoType, keyLength int, subject *global.CertSubject, keyUsage x509.KeyUsage, extKeyUsage []x509.ExtKeyUsage, domains []string, ips []net.IP, emails []string, urls []*url.URL, ocsp []string, selfURL []string, crlURL []string, notBefore time.Time, notAfter time.Time) (*x509.Certificate, crypto.PrivateKey, *SelfCertInfo, error) {
 	var privKey crypto.PrivateKey
-	var pubKey interface{}
-
-	org, cn = sysinfo.CreateCASubjectLong(org, cn, domains, ips, emails, urls)
+	var pubKey crypto.PublicKey
 
 	info, err := NewSelfCertInfo(infoFilePath, ocsp, selfURL, crlURL)
 	if err != nil {
 		return nil, nil, nil, err
 	}
 
+	err = subject.SetCNIfEmpty() // 兜底,确保CN被设置
+	if err != nil {
+		return nil, nil, nil, err
+	}
+
+	if extKeyUsage == nil {
+		extKeyUsage = make([]x509.ExtKeyUsage, 0, 0)
+	} else {
+		extKeyUsage = utils.CopySlice(extKeyUsage)
+	}
+
 	switch cryptoType {
 	case utils.CryptoTypeRsa:
 		if keyLength != 2048 && keyLength != 4096 {
@@ -149,10 +158,6 @@ func CreateSelfCert(infoFilePath string, cryptoType utils.CryptoType, keyLength
 		return nil, nil, nil, fmt.Errorf("unsupported crypto type: %s", cryptoType)
 	}
 
-	if org == "" {
-		org = "MyOrg"
-	}
-
 	if notBefore.Equal(time.Time{}) {
 		notBefore = time.Now()
 	}
@@ -161,23 +166,35 @@ func CreateSelfCert(infoFilePath string, cryptoType utils.CryptoType, keyLength
 		notAfter = notBefore.Add(time.Hour * 24 * 365 * 5) // 5年
 	}
 
+	ski, err := utils.CalculateSubjectKeyIdentifier(pubKey)
+	if err != nil {
+		return nil, nil, nil, fmt.Errorf("get subject key indentifier failed: %s", err.Error())
+	}
+
+	serialNumber, err := info.NewCertSerialNumber()
+	if err != nil {
+		return nil, nil, nil, fmt.Errorf("get new serial number failed: %s", err.Error())
+	}
+
 	template := &x509.Certificate{
-		SerialNumber: big.NewInt(1),
-		Subject: pkix.Name{
-			Organization: []string{org},
-			CommonName:   cn,
-		},
-		NotBefore: notBefore,
-		NotAfter:  notAfter,
-
-		KeyUsage:              x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
-		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, // 允许全部扩展用途
+		SerialNumber: serialNumber,
+		Subject:      subject.ToPkixName(),
+		NotBefore:    notBefore,
+		NotAfter:     notAfter,
+
+		KeyUsage:    keyUsage,
+		ExtKeyUsage: extKeyUsage, // 允许全部扩展用途
+
 		BasicConstraintsValid: true,
 		IsCA:                  false,
-		DNSNames:              domains,
-		IPAddresses:           ips,
-		EmailAddresses:        emails,
-		URIs:                  urls,
+
+		DNSNames:       domains,
+		IPAddresses:    ips,
+		EmailAddresses: emails,
+		URIs:           urls,
+
+		SubjectKeyId:   []byte(ski),
+		AuthorityKeyId: []byte(ski),
 
 		OCSPServer:            info.OCSPServer,
 		IssuingCertificateURL: info.IssuingCertificateURL,

+ 0 - 21
src/flagparser/flagparser.go

@@ -4,7 +4,6 @@ import (
 	"flag"
 	"fmt"
 	resource "github.com/SongZihuan/MyCA"
-	"os"
 	"os/user"
 	"path"
 )
@@ -39,25 +38,5 @@ func InitFlagParser() error {
 		return StopRun
 	}
 
-	err = os.MkdirAll(Home, 0600)
-	if err != nil {
-		return err
-	}
-
-	err = os.MkdirAll(path.Join(Home, "rca"), 0600)
-	if err != nil {
-		return err
-	}
-
-	err = os.MkdirAll(path.Join(Home, "ica"), 0600)
-	if err != nil {
-		return err
-	}
-
-	err = os.MkdirAll(path.Join(Home, "cert"), 0600)
-	if err != nil {
-		return err
-	}
-
 	return nil
 }

+ 146 - 0
src/global/cert.go

@@ -0,0 +1,146 @@
+// Copyright 2025 MyCA Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+// Package global 包含证书方面的信息
+package global
+
+import (
+	"crypto/x509/pkix"
+	"fmt"
+	"github.com/SongZihuan/MyCA/src/sysinfo"
+	"github.com/SongZihuan/MyCA/src/utils"
+	"net"
+	"net/url"
+	"strings"
+	"unicode"
+)
+
+type CertSubject struct {
+	C  []string // 国家(仅限2字母大写)
+	ST []string // 省份
+	L  []string // 城市
+	O  []string // 组织
+	OU []string // 组织单位
+	SA []string // 街道
+	PC []string // 邮编
+	CN string   // 名称
+
+	ItemList []string
+	ItemMap  map[string]string
+}
+
+func NewCertSubject() *CertSubject {
+	return &CertSubject{
+		ItemList: make([]string, 0, 6),
+		ItemMap:  make(map[string]string, 6),
+	}
+}
+
+func (c *CertSubject) Set(name string, value []string) error {
+	name = strings.ToUpper(name)
+
+	if value == nil {
+		value = make([]string, 0, 0)
+	}
+
+	value = utils.CleanStringSlice(value)
+
+	switch name {
+	case "C":
+		if len(value) == 0 || c.checkCountryList(value) {
+			c.C = utils.CopySlice(value)
+			break
+		}
+		return fmt.Errorf("not a valid country name")
+	case "ST":
+		c.ST = utils.CopySlice(value)
+	case "L":
+		c.L = utils.CopySlice(value)
+	case "O":
+		c.O = utils.CopySlice(value)
+	case "OU":
+		c.OU = utils.CopySlice(value)
+	case "SA":
+		c.SA = utils.CopySlice(value)
+	case "PC":
+		c.PC = utils.CopySlice(value)
+	case "CN":
+		if len(value) == 0 {
+			c.CN = ""
+		} else if len(value) == 1 {
+			c.CN = value[0]
+		} else {
+			return fmt.Errorf("too many CN")
+		}
+	default:
+		return fmt.Errorf("not a valid nname: %s", name)
+	}
+
+	if len(value) != 0 && value[0] != "" {
+		c.ItemList = append(c.ItemList, value[0])
+		c.ItemMap[name] = value[0]
+	}
+
+	return nil
+}
+
+func (c *CertSubject) checkCountryList(countryList []string) bool {
+	for _, country := range countryList {
+		if !c.checkCountry(country) {
+			return false
+		}
+	}
+	return true
+}
+
+func (c *CertSubject) checkCountry(country string) bool {
+	return len(country) == 2 && unicode.IsUpper(rune(country[0])) && unicode.IsUpper(rune(country[1]))
+}
+
+func (c *CertSubject) SetCNIfEmpty(args ...any) error {
+	if c.CN != "" {
+		return nil
+	} else if len(args) == 0 {
+		c.CN = fmt.Sprintf("%s-%02d", sysinfo.Username, utils.RandIntn(98)+1) // 数字范围1-99
+	} else if len(args) == 4 {
+		domains, ok1 := args[0].([]string)
+		ips, ok2 := args[1].([]net.IP)
+		emails, ok3 := args[3].([]string)
+		urls, ok4 := args[4].([]*url.URL)
+
+		if !ok1 || !ok2 || !ok3 || !ok4 {
+			return fmt.Errorf("args error")
+		}
+
+		if len(domains) != 0 {
+			c.CN = domains[0]
+		} else if len(ips) != 0 {
+			c.CN = ips[0].String()
+		} else if len(emails) != 0 {
+			c.CN = emails[0]
+		} else if len(urls) != 0 {
+			c.CN = urls[0].String()
+		} else {
+			c.CN = utils.RandStr(6 + utils.RandIntn(3)) // 6-8位随机字符串
+		}
+	} else {
+		return fmt.Errorf("args error")
+	}
+
+	return nil
+}
+
+func (c *CertSubject) ToPkixName() pkix.Name {
+	return pkix.Name{
+		Country:            utils.CopySlice(c.C),
+		Organization:       utils.CopySlice(c.C),
+		OrganizationalUnit: utils.CopySlice(c.C),
+		Locality:           utils.CopySlice(c.C),
+		Province:           utils.CopySlice(c.C),
+		StreetAddress:      utils.CopySlice(c.C),
+		PostalCode:         utils.CopySlice(c.C),
+		CommonName:         c.CN,
+		SerialNumber:       "", // 与证书的`SerialNumber`不同,默认可以不设置
+	}
+}

+ 9 - 0
src/global/readme.go

@@ -0,0 +1,9 @@
+// Copyright 2025 MyCA Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+// Package global 全局依赖包
+// 仅能被其他文件依赖,不能依赖项目内其他文件
+package global
+
+// 空文件

+ 54 - 22
src/ica/ica.go

@@ -4,12 +4,12 @@ import (
 	"crypto"
 	"crypto/ecdsa"
 	"crypto/elliptic"
+	"crypto/rand"
 	"crypto/rsa"
 	"crypto/x509"
-	"crypto/x509/pkix"
 	"encoding/gob"
 	"fmt"
-	"github.com/SongZihuan/MyCA/src/sysinfo"
+	"github.com/SongZihuan/MyCA/src/global"
 	"github.com/SongZihuan/MyCA/src/utils"
 	"math/big"
 	"os"
@@ -34,8 +34,14 @@ func init() {
 }
 
 func NewICAInfo(filepath string, ca UpstreamCAInfo, ocsp []string, issuerURL []string, crlURL []string) (*ICAInfo, error) {
+	randMax := new(big.Int).Lsh(big.NewInt(1), uint(40))
+	randSerialNumber, err := rand.Int(rand.Reader, randMax)
+	if err != nil {
+		return nil, fmt.Errorf("error generating random number: %s", err.Error())
+	}
+
 	info := &ICAInfo{
-		SerialNumber:          big.NewInt(0),
+		SerialNumber:          randSerialNumber,
 		OCSPServer:            ocsp,
 		IssuingCertificateURL: issuerURL,
 		CRLDistributionPoints: crlURL,
@@ -85,8 +91,14 @@ func (info *ICAInfo) SaveICAInfo() error {
 	return nil
 }
 
-func (info *ICAInfo) NewCert() *big.Int {
-	return info.SerialNumber.Add(info.SerialNumber, big.NewInt(1))
+func (info *ICAInfo) NewCertSerialNumber() (*big.Int, error) {
+	randMax := new(big.Int).Lsh(big.NewInt(1), uint(40))
+	addSerialNumber, err := rand.Int(rand.Reader, randMax)
+	if err != nil {
+		return nil, fmt.Errorf("error generating random number: %s", err.Error())
+	}
+
+	return info.SerialNumber.Add(info.SerialNumber, addSerialNumber), nil
 }
 
 func (info *ICAInfo) GetIssuingCertificateURL() []string {
@@ -102,15 +114,26 @@ func (info *ICAInfo) GetCRLDistributionPoints() []string {
 }
 
 // CreateICA 创建中间CA证书
-func CreateICA(infoFilePath string, caInfo UpstreamCAInfo, cryptoType utils.CryptoType, keyLength int, org string, cn string, selfOSCP []string, selfURL []string, crlURL []string, notBefore time.Time, notAfter time.Time, rootCert *x509.Certificate, rootKey crypto.PrivateKey) (*x509.Certificate, crypto.PrivateKey, *ICAInfo, error) {
+func CreateICA(infoFilePath string, caInfo UpstreamCAInfo, cryptoType utils.CryptoType, keyLength int, subject *global.CertSubject, keyUsage x509.KeyUsage, extKeyUsage []x509.ExtKeyUsage, maxPathLen int, selfOSCP []string, selfURL []string, crlURL []string, notBefore time.Time, notAfter time.Time, ca *x509.Certificate, caKey crypto.PrivateKey) (*x509.Certificate, crypto.PrivateKey, *ICAInfo, error) {
 	var privKey crypto.PrivateKey
-	var pubKey interface{}
+	var pubKey crypto.PublicKey
 
 	info, err := NewICAInfo(infoFilePath, caInfo, selfOSCP, selfURL, crlURL)
 	if err != nil {
 		return nil, nil, nil, err
 	}
 
+	err = subject.SetCNIfEmpty() // 兜底,确保CN被设置
+	if err != nil {
+		return nil, nil, nil, err
+	}
+
+	if extKeyUsage == nil {
+		extKeyUsage = make([]x509.ExtKeyUsage, 0, 0)
+	} else {
+		extKeyUsage = utils.CopySlice(extKeyUsage)
+	}
+
 	switch cryptoType {
 	case utils.CryptoTypeRsa:
 		if keyLength != 2048 && keyLength != 4096 {
@@ -148,8 +171,6 @@ func CreateICA(infoFilePath string, caInfo UpstreamCAInfo, cryptoType utils.Cryp
 		return nil, nil, nil, fmt.Errorf("unsupported crypto type: %s", cryptoType)
 	}
 
-	org, cn = sysinfo.CreateCASubject(org, cn)
-
 	if notBefore.Equal(time.Time{}) {
 		notBefore = time.Now()
 	}
@@ -158,21 +179,32 @@ func CreateICA(infoFilePath string, caInfo UpstreamCAInfo, cryptoType utils.Cryp
 		notAfter = notBefore.Add(time.Hour * 24 * 365 * 5) // 5年
 	}
 
+	ski, err := utils.CalculateSubjectKeyIdentifier(pubKey)
+	if err != nil {
+		return nil, nil, nil, fmt.Errorf("get subject key indentifier failed: %s", err.Error())
+	}
+
+	serialNumber, err := info.NewCertSerialNumber()
+	if err != nil {
+		return nil, nil, nil, fmt.Errorf("get new serial number failed: %s", err.Error())
+	}
+
 	template := &x509.Certificate{
-		SerialNumber: info.NewCert(),
-		Subject: pkix.Name{
-			Organization: []string{org},
-			CommonName:   cn,
-		},
-		NotBefore: notBefore,
-		NotAfter:  notAfter,
-
-		KeyUsage:              x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
-		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, // 允许全部扩展用途
+		SerialNumber: serialNumber,
+		Subject:      subject.ToPkixName(),
+		NotBefore:    notBefore,
+		NotAfter:     notAfter,
+
+		KeyUsage:    keyUsage,
+		ExtKeyUsage: extKeyUsage, // 允许全部扩展用途
+
 		BasicConstraintsValid: true,
 		IsCA:                  true,
-		MaxPathLen:            -1,
-		MaxPathLenZero:        false,
+		MaxPathLen:            maxPathLen,
+		MaxPathLenZero:        true,
+
+		SubjectKeyId:   []byte(ski),
+		AuthorityKeyId: ca.SubjectKeyId,
 
 		// 此处CA证书和终端证书不同,CA显示自己的吊销列表,终端证书显示对于CA的吊销列表
 		OCSPServer:            info.OCSPServer,
@@ -180,7 +212,7 @@ func CreateICA(infoFilePath string, caInfo UpstreamCAInfo, cryptoType utils.Cryp
 		CRLDistributionPoints: info.CRLDistributionPoints,
 	}
 
-	derBytes, err := x509.CreateCertificate(utils.Rander(), template, rootCert, pubKey, rootKey)
+	derBytes, err := x509.CreateCertificate(utils.Rander(), template, ca, pubKey, caKey)
 	if err != nil {
 		return nil, nil, nil, err
 	}

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 435 - 243
src/mainfunc/myca/mycav1/action.go


+ 30 - 1
src/mainfunc/myca/mycav1/mycav1.go

@@ -7,6 +7,7 @@ import (
 	"github.com/SongZihuan/MyCA/src/flagparser"
 	"os"
 	"os/signal"
+	"path"
 	"syscall"
 	"time"
 )
@@ -18,13 +19,41 @@ func MainV1() (exitcode int) {
 			return 0
 		}
 
-		fmt.Println("Error:", err)
+		fmt.Printf("Error: %s\n", err.Error())
 		return 1
 	}
 
 	home = flagparser.Home
+	homeRCA = path.Join(home, "rca")
+	homeICA = path.Join(home, "ica")
+	homeCert = path.Join(home, "cert")
+
 	stdinReader = bufio.NewReader(os.Stdin)
 
+	err = os.MkdirAll(home, 0600)
+	if err != nil {
+		fmt.Printf("Error: %s\n", err.Error())
+		return 1
+	}
+
+	err = os.MkdirAll(path.Join(home, "rca"), 0600)
+	if err != nil {
+		fmt.Printf("Error: %s\n", err.Error())
+		return 1
+	}
+
+	err = os.MkdirAll(path.Join(home, "ica"), 0600)
+	if err != nil {
+		fmt.Printf("Error: %s\n", err.Error())
+		return 1
+	}
+
+	err = os.MkdirAll(path.Join(home, "cert"), 0600)
+	if err != nil {
+		fmt.Printf("Error: %s\n", err.Error())
+		return 1
+	}
+
 	stopChan := make(chan int, 2)
 	sigChan := make(chan os.Signal, 10)
 	signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)

+ 443 - 4
src/mainfunc/myca/mycav1/read.go

@@ -1,10 +1,15 @@
 package mycav1
 
 import (
+	"crypto/x509"
+	"errors"
 	"fmt"
+	"github.com/SongZihuan/MyCA/src/global"
+	"github.com/SongZihuan/MyCA/src/sysinfo"
 	"github.com/SongZihuan/MyCA/src/utils"
 	"golang.org/x/term"
 	"os"
+	"path"
 	"strconv"
 	"strings"
 	"time"
@@ -18,7 +23,7 @@ func ReadNumber() int {
 
 	input, err := stdinReader.ReadString('\n')
 	if err != nil {
-		fmt.Println("Error:", err)
+		fmt.Printf("Error: %s\n", err.Error())
 		return 0
 	}
 
@@ -31,7 +36,7 @@ func ReadNumber() int {
 
 	m, err := strconv.ParseInt(input, 10, 64)
 	if err != nil {
-		fmt.Println("Error:", err)
+		fmt.Printf("Error: %s\n", err.Error())
 		return 0
 	}
 
@@ -46,7 +51,7 @@ func ReadString() string {
 
 	input, err := stdinReader.ReadString('\n')
 	if err != nil {
-		fmt.Println("Error:", err)
+		fmt.Printf("Error: %s\n", err.Error())
 		return ""
 	}
 
@@ -69,7 +74,7 @@ func ReadPassword() string {
 	fmt.Printf("[note: the password you type will not be echoed] ")
 	pw, err := term.ReadPassword(int(os.Stdin.Fd()))
 	if err != nil {
-		fmt.Println("Error:", err)
+		fmt.Printf("Error: %s\n", err.Error())
 	}
 
 	password := string(pw)
@@ -112,3 +117,437 @@ func ReadBoolDefaultNo() bool {
 	input := strings.ToLower(ReadString())
 	return input == "yes" || input == "y" || input == "ok"
 }
+
+func ReadSubject() (*global.CertSubject, error) {
+	res := global.NewCertSubject()
+
+	err := res.Set("C", ReadMoreString("Enter the country name [only two capital letters]"))
+	if err != nil {
+		return nil, err
+	}
+
+	err = res.Set("ST", ReadMoreString("Enter the Province or State"))
+	if err != nil {
+		return nil, err
+	}
+
+	err = res.Set("L", ReadMoreString("Enter the City"))
+	if err != nil {
+		return nil, err
+	}
+
+	_o := ReadMoreString("Enter the organization or company name")
+	if len(_o) != 0 {
+		err = res.Set("O", _o)
+		if err != nil {
+			return nil, err
+		}
+	} else if sysinfo.Hostname != "" {
+		fmt.Printf("Do you want to use the hostname (%s) to be the organization or company name?", sysinfo.Hostname)
+		if ReadBoolDefaultYesPrint() {
+			err = res.Set("O", []string{sysinfo.Hostname})
+			if err != nil {
+				return nil, err
+			}
+		}
+	}
+
+	err = res.Set("SA", ReadMoreString("Enter the StreetAddress"))
+	if err != nil {
+		return nil, err
+	}
+
+	err = res.Set("PC", ReadMoreString("Enter the PostalCode"))
+	if err != nil {
+		return nil, err
+	}
+
+	fmt.Printf("Enter the common name: ")
+	_cn := ReadString()
+	if _cn != "" {
+		err = res.Set("CN", []string{_cn})
+		if err != nil {
+			return nil, err
+		}
+	} else if sysinfo.Username != "" {
+		fmt.Printf("Do you want to use the username (%s) to be the common name?", sysinfo.Username)
+		if ReadBoolDefaultYesPrint() {
+			err = res.Set("CN", []string{sysinfo.Username})
+			if err != nil {
+				return nil, err
+			}
+		}
+	}
+
+	return res, nil
+}
+
+func processDirName(name string) string {
+	for _, k := range " \t@#$¥&()|\\/:*?\"<>" {
+		name = strings.Replace(name, string(k), "", -1) // 删除错误字符
+	}
+
+	name = strings.TrimRight(name, ".")
+	return name
+}
+
+func processAllDefaultName(name1, name2, name3, name4 string) (string, string, string, string) {
+	return processDirName(name1),
+		processDirName(name2),
+		processDirName(name3),
+		processDirName(name4)
+}
+
+func ReadDir(basePath string, defaultPrefix string, subject *global.CertSubject) (string, string, error) {
+	if subject.CN == "" {
+		return "", "", fmt.Errorf("not common name")
+	}
+
+	fmt.Printf("The files are stored in: %s\n", basePath)
+	showFileOnPath(basePath)
+
+	name1 := fmt.Sprintf("%s%s", defaultPrefix, strings.Join(subject.ItemList, "-"))
+	name2 := fmt.Sprintf("%s", strings.Join(subject.ItemList, "-"))
+	name3 := fmt.Sprintf("%s%s", defaultPrefix, subject.CN)
+	name4 := fmt.Sprintf("%s", subject.CN)
+
+	name1, name2, name3, name4 = processAllDefaultName(name1, name2, name3, name4)
+
+	if !utils.IsValidFilename(name1) || !utils.IsValidFilename(name2) || !utils.IsValidFilename(name3) || !utils.IsValidFilename(name4) {
+		return "", "", fmt.Errorf("bad default name: bad subject")
+	}
+
+	fmt.Printf(`Here are some default file names:
+1) %s
+2) %s
+3) %s
+4) %s
+5/other) Customize a file name
+Please choose a name: `, name1, name2, name3, name4)
+	nameChoose := ReadNumber()
+	switch nameChoose {
+	case 1:
+		return name1, path.Join(basePath, name1), nil
+	case 2:
+		return name2, path.Join(basePath, name2), nil
+	case 3:
+		return name3, path.Join(basePath, name3), nil
+	case 4:
+		return name4, path.Join(basePath, name4), nil
+	}
+
+	fmt.Printf("Please enter your custom name: ")
+	name := ReadString()
+
+	if utils.IsValidFilename(name) {
+		return name, path.Join(basePath, name), nil
+	}
+
+	fmt.Printf("The name you enter is not valid, Do you need us to edit it?")
+	if ReadBoolDefaultYesPrint() {
+		nameAfterEdit := processDirName(name)
+		if utils.IsValidFilename(nameAfterEdit) {
+			fmt.Printf("The new name (%s) is valid, Are you sure you want to use it?")
+			if ReadBoolDefaultYesPrint() {
+				return nameAfterEdit, path.Join(basePath, nameAfterEdit), nil
+			}
+			return "", "", fmt.Errorf("not a valid name")
+		}
+		fmt.Printf("Sorry, edit failed.")
+		return "", "", fmt.Errorf("not a valid name")
+	}
+
+	return "", "", fmt.Errorf("not a valid name")
+}
+
+type Warning struct {
+	Msg string
+}
+
+func NewWarning(msg string) *Warning {
+	return &Warning{
+		Msg: msg,
+	}
+}
+
+func NewWarningF(format string, args ...any) *Warning {
+	return NewWarning(fmt.Sprintf(format, args...))
+}
+
+func (w *Warning) Error() string {
+	return w.Msg
+}
+
+func ReadMoreString(tips string) []string {
+	resList := make([]string, 0, 10)
+
+	for {
+		fmt.Printf("%s [empty to stop]: ", tips)
+		res := ReadString()
+		if res == "" {
+			break
+		}
+		resList = append(resList, res)
+	}
+
+	return resList
+}
+
+func ReadMoreStringWithPolicy[T any](tips string, checker func(string) (T, error)) ([]T, error) {
+	resList := make([]T, 0, 10)
+
+	for {
+		fmt.Printf("%s [empty to stop]: ", tips)
+		res := ReadString()
+		if res == "" {
+			break
+		} else {
+			newRes, err := checker(res)
+			if err != nil {
+				var warn *Warning
+				if errors.As(err, &warn) {
+					fmt.Printf("Error: %s\n", warn.Error())
+				} else {
+					return nil, err
+				}
+			} else {
+				resList = append(resList, newRes)
+			}
+		}
+	}
+
+	return resList, nil
+}
+
+func ReadMoreStringWithProcess(tips string, checker func(string) error) error {
+	for {
+		fmt.Printf("%s [empty to stop]: ", tips)
+		res := ReadString()
+		if res == "" {
+			break
+		} else {
+			err := checker(res)
+			if err != nil {
+				var warn *Warning
+				if errors.As(err, &warn) {
+					fmt.Printf("Error: %s\n", warn.Error())
+				} else {
+					return err
+				}
+			}
+		}
+	}
+
+	return nil
+}
+
+func ReadMoreNumberWithPolicy[T any](tips string, checker func(int) (T, error)) ([]T, error) {
+	resList := make([]T, 0, 10)
+
+	for {
+		fmt.Printf("%s [zero to stop]: ", tips)
+		res := ReadNumber()
+		if res == 0 {
+			break
+		} else {
+			newRes, err := checker(res)
+			if err != nil {
+				var warn *Warning
+				if errors.As(err, &warn) {
+					fmt.Printf("Error: %s\n", warn.Error())
+				} else {
+					return nil, err
+				}
+			} else {
+				resList = append(resList, newRes)
+			}
+		}
+	}
+
+	return resList, nil
+}
+
+var KeyUsageList = []x509.KeyUsage{
+	x509.KeyUsageDigitalSignature,  // 数字签名
+	x509.KeyUsageContentCommitment, // 公钥可加密数据
+	x509.KeyUsageKeyEncipherment,   // 加密密钥 (RSA交换)
+	x509.KeyUsageDataEncipherment,  // 公钥可解密数据
+	x509.KeyUsageKeyAgreement,      // 可用于密钥协商
+	x509.KeyUsageCertSign,          // 签发证书
+	x509.KeyUsageCRLSign,           // 签发CRL
+}
+
+var KeyUsageMap = map[x509.KeyUsage]string{
+	x509.KeyUsageDigitalSignature:  "KeyUsageDigitalSignature",
+	x509.KeyUsageContentCommitment: "KeyUsageContentCommitment",
+	x509.KeyUsageKeyEncipherment:   "KeyUsageKeyEncipherment",
+	x509.KeyUsageDataEncipherment:  "KeyUsageDataEncipherment",
+	x509.KeyUsageKeyAgreement:      "KeyUsageKeyAgreement",
+	x509.KeyUsageCertSign:          "KeyUsageCertSign",
+	x509.KeyUsageCRLSign:           "KeyUsageCRLSign",
+}
+
+func ReadKeyUsage(certType string) (x509.KeyUsage, error) {
+	var res x509.KeyUsage
+	var addRecord = make(map[x509.KeyUsage]bool, len(KeyUsageList))
+
+	fmt.Printf("Now we should setting the key usage.")
+	switch strings.ToLower(certType) {
+	case "ica":
+		fallthrough
+	case "rca":
+		fmt.Printf("The KeyUsageCertSign and KeyUsageCRLSign must be choose.")
+		res |= x509.KeyUsageCertSign
+		res |= x509.KeyUsageCRLSign
+
+		addRecord[x509.KeyUsageCertSign] = true
+		addRecord[x509.KeyUsageCRLSign] = true
+	case "new_cert":
+		fmt.Printf("The KeyUsageDigitalSignature must be choose.")
+		res |= x509.KeyUsageDigitalSignature
+
+		addRecord[x509.KeyUsageDigitalSignature] = true
+	case "old_cert":
+		fmt.Printf("The KeyUsageKeyEncipherment must be choose.")
+		res |= x509.KeyUsageKeyEncipherment
+
+		addRecord[x509.KeyUsageKeyEncipherment] = true
+	case "cert":
+		fmt.Printf("The KeyUsageDigitalSignature and KeyUsageKeyEncipherment must be choose.")
+		res |= x509.KeyUsageDigitalSignature
+		res |= x509.KeyUsageKeyEncipherment
+
+		addRecord[x509.KeyUsageDigitalSignature] = true
+		addRecord[x509.KeyUsageKeyEncipherment] = true
+	}
+
+	fmt.Printf("There will show the other Key Usage that you can add to you cert: \n")
+	for i, usage := range KeyUsageList {
+		fmt.Printf(" %d %s\n", i+1, KeyUsageMap[usage])
+	}
+
+	_, err := ReadMoreNumberWithPolicy("Choose the other KeyUsage", func(i int) (x509.KeyUsage, error) {
+		index := i - 1
+
+		if index < 0 || index >= len(KeyUsageList) {
+			return 0, NewWarning("invalid number")
+		}
+
+		usage := KeyUsageList[index]
+
+		if yes, ok := addRecord[usage]; ok && yes {
+			return 0, NewWarning("Please do not enter repeatedly")
+		}
+
+		res |= usage
+		addRecord[usage] = true
+
+		return usage, nil
+	})
+	if err != nil {
+		return 0, err
+	}
+
+	fmt.Printf("The key usage you add: \n")
+	for usage, yes := range addRecord {
+		if yes {
+			fmt.Printf(" %s\n", KeyUsageMap[usage])
+		}
+	}
+
+	return res, nil
+}
+
+var ExtKeyUsageList = []x509.ExtKeyUsage{
+	x509.ExtKeyUsageServerAuth,
+	x509.ExtKeyUsageClientAuth,
+	x509.ExtKeyUsageCodeSigning,
+	x509.ExtKeyUsageEmailProtection,
+	x509.ExtKeyUsageIPSECEndSystem,
+	x509.ExtKeyUsageIPSECTunnel,
+	x509.ExtKeyUsageIPSECUser,
+	x509.ExtKeyUsageTimeStamping,
+	x509.ExtKeyUsageOCSPSigning,
+	x509.ExtKeyUsageMicrosoftServerGatedCrypto,
+	x509.ExtKeyUsageNetscapeServerGatedCrypto,
+	x509.ExtKeyUsageMicrosoftCommercialCodeSigning,
+	x509.ExtKeyUsageMicrosoftKernelCodeSigning,
+}
+
+var ExtKeyUsageMap = map[x509.ExtKeyUsage]string{
+	x509.ExtKeyUsageServerAuth:                     "ExtKeyUsageServerAuth",
+	x509.ExtKeyUsageClientAuth:                     "ExtKeyUsageClientAuth",
+	x509.ExtKeyUsageCodeSigning:                    "ExtKeyUsageCodeSigning",
+	x509.ExtKeyUsageEmailProtection:                "ExtKeyUsageEmailProtection",
+	x509.ExtKeyUsageIPSECEndSystem:                 "ExtKeyUsageIPSECEndSystem",
+	x509.ExtKeyUsageIPSECTunnel:                    "ExtKeyUsageIPSECTunnel",
+	x509.ExtKeyUsageIPSECUser:                      "ExtKeyUsageIPSECUser",
+	x509.ExtKeyUsageTimeStamping:                   "ExtKeyUsageTimeStamping",
+	x509.ExtKeyUsageOCSPSigning:                    "ExtKeyUsageOCSPSigning",
+	x509.ExtKeyUsageMicrosoftServerGatedCrypto:     "ExtKeyUsageMicrosoftServerGatedCrypto",
+	x509.ExtKeyUsageNetscapeServerGatedCrypto:      "ExtKeyUsageNetscapeServerGatedCrypto",
+	x509.ExtKeyUsageMicrosoftCommercialCodeSigning: "ExtKeyUsageMicrosoftCommercialCodeSigning",
+	x509.ExtKeyUsageMicrosoftKernelCodeSigning:     "ExtKeyUsageMicrosoftKernelCodeSigning",
+}
+
+func ReadExtKeyUsage() ([]x509.ExtKeyUsage, error) {
+	var res = make([]x509.ExtKeyUsage, 0, len(ExtKeyUsageList))
+	var addRecord = make(map[x509.ExtKeyUsage]bool, len(ExtKeyUsageList))
+
+	fmt.Printf("Dou you want to add extUsage? [default=all/no/choose]: ")
+	switch strings.ToLower(ReadString()) {
+	case "n":
+		fallthrough
+	case "no":
+		return res, nil
+
+	case "all":
+		fallthrough
+	case "a":
+		fallthrough
+	default:
+		res = append(res, x509.ExtKeyUsageAny)
+		return res, nil
+
+	case "choose":
+		fallthrough
+	case "c":
+		// pass
+	}
+
+	fmt.Printf("There will show the Ext Key Usage that you can add to you cert: \n")
+	for i, usage := range ExtKeyUsageList {
+		fmt.Printf(" %d %s\n", i+1, ExtKeyUsageMap[usage])
+	}
+
+	_, err := ReadMoreNumberWithPolicy("Choose the other ExtKeyUsage", func(i int) (x509.ExtKeyUsage, error) {
+		index := i - 1
+
+		if index < 0 || index >= len(ExtKeyUsageList) {
+			return 0, NewWarning("invalid number")
+		}
+
+		extUsage := ExtKeyUsageList[index]
+
+		if yes, ok := addRecord[extUsage]; ok && yes {
+			return 0, NewWarning("Please do not enter repeatedly")
+		}
+
+		res = append(res, extUsage)
+		addRecord[extUsage] = true
+
+		return extUsage, nil
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	fmt.Printf("The ext key usage you add: \n")
+	for extUsage, yes := range addRecord {
+		if yes {
+			fmt.Printf(" %s\n", ExtKeyUsageMap[extUsage])
+		}
+	}
+
+	return res, nil
+}

+ 33 - 2
src/mainfunc/myca/mycav1/show.go

@@ -3,6 +3,7 @@ package mycav1
 import (
 	"fmt"
 	"github.com/SongZihuan/MyCA/src/utils"
+	"os"
 	"path"
 )
 
@@ -13,7 +14,7 @@ func PrintMenu() {
 func showAllRCA() []string {
 	rca, err := utils.ReadDirOnlyDir(path.Join(home, "rca"))
 	if err != nil {
-		fmt.Println("Error:", err)
+		fmt.Printf("Error: %s\n", err.Error())
 	}
 
 	fmt.Println("总计: ", len(rca))
@@ -28,7 +29,7 @@ func showAllRCA() []string {
 func showAllICA() []string {
 	ica, err := utils.ReadDirOnlyDir(path.Join(home, "ica"))
 	if err != nil {
-		fmt.Println("Error:", err)
+		fmt.Printf("Error: %s\n", err.Error())
 	}
 
 	fmt.Println("总计: ", len(ica))
@@ -39,3 +40,33 @@ func showAllICA() []string {
 
 	return ica
 }
+
+func showFileOnPath(basePath string) []os.DirEntry {
+	fileList, err := os.ReadDir(basePath)
+	if err != nil {
+		fmt.Printf("Error: %s\n", err.Error())
+	}
+
+	fmt.Printf("地址 %s 下文件列表 总计: %d\n", basePath, len(fileList))
+
+	var dirCount = 0
+	var fileCount = 0
+
+	for i, v := range fileList {
+		if v.IsDir() {
+			fmt.Printf(" %d. 文件夹 %s\n", i+1, v.Name())
+			dirCount += 1
+		} else {
+			fmt.Printf(" %d. 文件 %s\n", i+1, v.Name())
+			fileCount += 1
+		}
+	}
+
+	if fileCount+dirCount != len(fileList) {
+		panic("file count add dir count not equal the total count")
+	}
+
+	fmt.Printf("文件:%d 文件夹:%d  总计:%d\n", fileCount, dirCount, len(fileList))
+
+	return fileList
+}

+ 5 - 5
src/mainfunc/myca/mycav1/string.go

@@ -12,8 +12,8 @@ const menu = `Menu:
   9) Exit`
 
 const cryptoMenu = `Crypto Menu:
- 1) RSA 2048
- 2) RSA 4096
- 3) ECDSA P256
- 4) ECDSA P384
- 5) ECDSA P521`
+ 1) RSA 2048 (Good compatibility)
+ 2) RSA 4096 (Compatible and safe but more complex)
+ 3) ECDSA P256 (Safe and recommend)
+ 4) ECDSA P384 (Safe and recommend)
+ 5) ECDSA P521 (Bad compatibility and not recommend)`

+ 3 - 0
src/mainfunc/myca/mycav1/variable.go

@@ -4,3 +4,6 @@ import "bufio"
 
 var stdinReader *bufio.Reader
 var home string
+var homeRCA string
+var homeICA string
+var homeCert string

+ 53 - 21
src/rootca/rootca.go

@@ -4,12 +4,12 @@ import (
 	"crypto"
 	"crypto/ecdsa"
 	"crypto/elliptic"
+	"crypto/rand"
 	"crypto/rsa"
 	"crypto/x509"
-	"crypto/x509/pkix"
 	"encoding/gob"
 	"fmt"
-	"github.com/SongZihuan/MyCA/src/sysinfo"
+	"github.com/SongZihuan/MyCA/src/global"
 	"github.com/SongZihuan/MyCA/src/utils"
 	"math/big"
 	"os"
@@ -30,8 +30,14 @@ func init() {
 }
 
 func NewRCAInfo(filepath string, ocsp []string, issuerURL []string, crlURL []string) (*RCAInfo, error) {
+	randMax := new(big.Int).Lsh(big.NewInt(1), uint(40))
+	randSerialNumber, err := rand.Int(rand.Reader, randMax)
+	if err != nil {
+		return nil, fmt.Errorf("error generating random number: %s", err.Error())
+	}
+
 	info := &RCAInfo{
-		SerialNumber:          big.NewInt(0),
+		SerialNumber:          randSerialNumber,
 		OCSPServer:            ocsp,
 		IssuingCertificateURL: issuerURL,
 		CRLDistributionPoints: crlURL,
@@ -80,8 +86,14 @@ func (info *RCAInfo) SaveRCAInfo() error {
 	return nil
 }
 
-func (info *RCAInfo) NewCert() *big.Int {
-	return info.SerialNumber.Add(info.SerialNumber, big.NewInt(1))
+func (info *RCAInfo) NewCertSerialNumber() (*big.Int, error) {
+	randMax := new(big.Int).Lsh(big.NewInt(1), uint(40))
+	addSerialNumber, err := rand.Int(rand.Reader, randMax)
+	if err != nil {
+		return nil, fmt.Errorf("error generating random number: %s", err.Error())
+	}
+
+	return info.SerialNumber.Add(info.SerialNumber, addSerialNumber), nil
 }
 
 func (info *RCAInfo) GetIssuingCertificateURL() []string {
@@ -97,15 +109,26 @@ func (info *RCAInfo) GetCRLDistributionPoints() []string {
 }
 
 // CreateRCA 创建根CA证书
-func CreateRCA(infoFilePath string, cryptoType utils.CryptoType, keyLength int, org string, cn string, ocsp []string, selfURL []string, crlURL []string, notBefore time.Time, notAfter time.Time) (*x509.Certificate, crypto.PrivateKey, *RCAInfo, error) {
+func CreateRCA(infoFilePath string, cryptoType utils.CryptoType, keyLength int, subject *global.CertSubject, keyUsage x509.KeyUsage, extKeyUsage []x509.ExtKeyUsage, maxPathLen int, ocsp []string, selfURL []string, crlURL []string, notBefore time.Time, notAfter time.Time) (*x509.Certificate, crypto.PrivateKey, *RCAInfo, error) {
 	var privKey crypto.PrivateKey
-	var pubKey interface{}
+	var pubKey crypto.PublicKey
 
 	info, err := NewRCAInfo(infoFilePath, ocsp, selfURL, crlURL)
 	if err != nil {
 		return nil, nil, nil, err
 	}
 
+	err = subject.SetCNIfEmpty() // 兜底,确保CN被设置
+	if err != nil {
+		return nil, nil, nil, err
+	}
+
+	if extKeyUsage == nil {
+		extKeyUsage = make([]x509.ExtKeyUsage, 0, 0)
+	} else {
+		extKeyUsage = utils.CopySlice(extKeyUsage)
+	}
+
 	switch cryptoType {
 	case utils.CryptoTypeRsa:
 		if keyLength != 2048 && keyLength != 4096 {
@@ -143,8 +166,6 @@ func CreateRCA(infoFilePath string, cryptoType utils.CryptoType, keyLength int,
 		return nil, nil, nil, fmt.Errorf("unsupported crypto type: %s", cryptoType)
 	}
 
-	org, cn = sysinfo.CreateCASubject(org, cn)
-
 	if notBefore.Equal(time.Time{}) {
 		notBefore = time.Now()
 	}
@@ -153,21 +174,32 @@ func CreateRCA(infoFilePath string, cryptoType utils.CryptoType, keyLength int,
 		notAfter = notBefore.Add(time.Hour * 24 * 365 * 10) // 10年
 	}
 
+	ski, err := utils.CalculateSubjectKeyIdentifier(pubKey)
+	if err != nil {
+		return nil, nil, nil, fmt.Errorf("get subject key indentifier failed: %s", err.Error())
+	}
+
+	serialNumber, err := info.NewCertSerialNumber()
+	if err != nil {
+		return nil, nil, nil, fmt.Errorf("get new serial number failed: %s", err.Error())
+	}
+
 	template := &x509.Certificate{
-		SerialNumber: info.NewCert(),
-		Subject: pkix.Name{
-			Organization: []string{org},
-			CommonName:   cn,
-		},
-		NotBefore: notBefore,
-		NotAfter:  notAfter,
-
-		KeyUsage:              x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
-		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, // 允许全部扩展用途
+		SerialNumber: serialNumber,
+		Subject:      subject.ToPkixName(),
+		NotBefore:    notBefore,
+		NotAfter:     notAfter,
+
+		KeyUsage:    keyUsage,
+		ExtKeyUsage: extKeyUsage, // 允许全部扩展用途
+
 		BasicConstraintsValid: true,
 		IsCA:                  true,
-		MaxPathLen:            -1,
-		MaxPathLenZero:        false,
+		MaxPathLen:            maxPathLen,
+		MaxPathLenZero:        true,
+
+		SubjectKeyId:   []byte(ski),
+		AuthorityKeyId: []byte(ski),
 
 		OCSPServer:            info.OCSPServer,
 		IssuingCertificateURL: info.IssuingCertificateURL,

+ 4 - 12
src/sysinfo/subject.go

@@ -11,23 +11,15 @@ import (
 	"net/url"
 )
 
-func CreateCASubject(org string, cn string) (string, string) {
-	if org == "" {
-		org = Hostname
-	}
-
+func CreateCASubject(cn string) string {
 	if cn == "" {
 		cn = fmt.Sprintf("%s%02d", Username, utils.RandIntn(98)+1) // 数字范围1-99
 	}
 
-	return org, cn
+	return cn
 }
 
-func CreateCASubjectLong(org string, cn string, domains []string, ips []net.IP, emails []string, urls []*url.URL) (string, string) {
-	if org == "" {
-		org = Hostname
-	}
-
+func CreateCASubjectLong(cn string, domains []string, ips []net.IP, emails []string, urls []*url.URL) string {
 	if cn == "" {
 		if len(domains) != 0 {
 			cn = domains[0]
@@ -42,5 +34,5 @@ func CreateCASubjectLong(org string, cn string, domains []string, ips []net.IP,
 		}
 	}
 
-	return org, cn
+	return cn
 }

+ 40 - 0
src/utils/file.go

@@ -3,6 +3,8 @@ package utils
 import (
 	"errors"
 	"os"
+	"regexp"
+	"strings"
 )
 
 func IsExists(path string) bool {
@@ -85,3 +87,41 @@ func ReadDirOnlyDir(dirPath string) ([]string, error) {
 
 	return res, nil
 }
+
+func IsValidFilename(filename string) bool {
+	// 文件名不能为空
+	if len(strings.TrimSpace(filename)) == 0 {
+		return false
+	}
+
+	// 定义非法字符
+	// Windows 非法字符: \ / : * ? " < > |
+	// Linux 非法字符: /
+	illegalChars := `[\\/:*?"<>|]`
+	matched, err := regexp.MatchString(illegalChars, filename)
+	if err != nil || matched {
+		return false
+	}
+
+	// Windows 中不允许以空格或点结尾
+	if strings.HasSuffix(filename, " ") || strings.HasSuffix(filename, ".") {
+		return false
+	}
+
+	// Windows 保留文件名检查
+	reservedNames := map[string]bool{
+		"CON": true, "PRN": true, "AUX": true, "NUL": true,
+		"COM1": true, "COM2": true, "COM3": true, "COM4": true, "COM5": true,
+		"COM6": true, "COM7": true, "COM8": true, "COM9": true,
+		"LPT1": true, "LPT2": true, "LPT3": true, "LPT4": true, "LPT5": true,
+		"LPT6": true, "LPT7": true, "LPT8": true, "LPT9": true,
+	}
+
+	// 提取文件名部分(不包括扩展名)
+	name := strings.Split(filename, ".")[0]
+	if reservedNames[strings.ToUpper(name)] {
+		return false
+	}
+
+	return true
+}

+ 25 - 0
src/utils/slice.go

@@ -0,0 +1,25 @@
+// Copyright 2025 MyCA Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package utils
+
+import "strings"
+
+func CopySlice[T any](src []T) []T {
+	dest := make([]T, len(src))
+	copy(dest, src)
+	return dest
+}
+
+func CleanStringSlice(lst []string) []string {
+	res := make([]string, 0, len(lst))
+
+	for _, l := range lst {
+		if str := strings.TrimSpace(l); str != "" {
+			res = append(res, str)
+		}
+	}
+
+	return res
+}

+ 100 - 0
src/utils/x509.go

@@ -3,13 +3,18 @@ package utils
 import (
 	"crypto"
 	"crypto/ecdsa"
+	"crypto/rand"
 	"crypto/rsa"
+	"crypto/sha1"
 	"crypto/x509"
+	"encoding/hex"
 	"encoding/pem"
 	"fmt"
 	"github.com/youmark/pkcs8"
+	"log"
 	"os"
 	"regexp"
+	"software.sslmate.com/src/go-pkcs12"
 	"strings"
 )
 
@@ -94,6 +99,85 @@ func SavePrivateKey(key crypto.PrivateKey, pwStr string, savePath string) error
 	return nil
 }
 
+func SaveSPX(key crypto.PrivateKey, priPasswordStr string, cert *x509.Certificate, caFullchain []byte, savePath string) error {
+	var priPassword []byte = nil
+	var priType = PemTypePrivateKeyNotPassword
+	priPasswordStr = strings.TrimSpace(priPasswordStr)
+
+	certPEM := pem.EncodeToMemory(&pem.Block{
+		Type:  PemTypeCertificate,
+		Bytes: cert.Raw,
+	})
+
+	if len(priPasswordStr) != 0 {
+		if !isValidPassword(priPasswordStr) {
+			return fmt.Errorf("password is invalid")
+		}
+
+		priType = PemTypePrivateKeyWithPassword
+		priPassword = []byte(priPasswordStr)
+	}
+
+	priData, err := pkcs8.MarshalPrivateKey(key, priPassword, pkcs8.DefaultOpts)
+	if err != nil {
+		return err
+	}
+
+	priPEM := pem.EncodeToMemory(&pem.Block{
+		Type:  priType,
+		Bytes: priData,
+	})
+
+	spxData := make([]byte, len(certPEM), len(certPEM)+len(caFullchain))
+	copy(spxData, certPEM)
+	spxData = append(spxData, caFullchain...)
+	spxData = append(spxData, priPEM...)
+
+	err = os.WriteFile(savePath, spxData, 0600)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func SavePFX(key crypto.PrivateKey, priPasswordStr string, cert *x509.Certificate, caFullchain []byte, savePath string) error {
+	priPasswordStr = strings.TrimSpace(priPasswordStr)
+
+	if len(priPasswordStr) != 0 && !isValidPassword(priPasswordStr) {
+		return fmt.Errorf("password is invalid")
+	}
+
+	var chainCerts = make([]*x509.Certificate, 0)
+	for len(caFullchain) > 0 {
+		var block *pem.Block
+		block, caFullchain = pem.Decode(caFullchain)
+		if block == nil {
+			break
+		} else if block.Type != "CERTIFICATE" {
+			return fmt.Errorf("full chain block type error: %s", block.Type)
+		}
+
+		cert, err := x509.ParseCertificate(block.Bytes)
+		if err != nil {
+			return err
+		}
+		chainCerts = append(chainCerts, cert)
+	}
+
+	pfxData, err := pkcs12.LegacyRC2.WithRand(rand.Reader).Encode(key, cert, chainCerts, priPasswordStr)
+	if err != nil {
+		log.Fatalf("Failed to create PKCS #12 data: %v", err)
+	}
+
+	err = os.WriteFile(savePath, pfxData, 0600)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
 func ReadPemBlock(filePath string) (*pem.Block, error) {
 	data, err := os.ReadFile(filePath)
 	if err != nil {
@@ -141,3 +225,19 @@ func isValidPassword(password string) bool {
 	matched, _ := regexp.MatchString(`^[a-zA-Z0-9!@#$%^&*_]+$`, password)
 	return matched
 }
+
+func CalculateSubjectKeyIdentifier(publicKey crypto.PublicKey) (string, error) {
+	// 将公钥序列化为 PKIX 格式(DER 编码)
+	publicKeyBytes, err := x509.MarshalPKIXPublicKey(publicKey)
+	if err != nil {
+		return "", fmt.Errorf("failed to marshal public key: %w", err)
+	}
+
+	// 对公钥的 DER 编码计算 SHA-1 哈希值
+	hash := sha1.Sum(publicKeyBytes)
+
+	// 转换为十六进制字符串
+	skiHex := hex.EncodeToString(hash[:])
+
+	return skiHex, nil
+}

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است