diff --git a/go.mod b/go.mod index 8221e92..4f4b7c7 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( go.osspkg.com/casecheck v0.3.0 go.osspkg.com/errors v0.4.0 go.osspkg.com/random v0.5.0 + go.osspkg.com/syncing v0.4.3 golang.org/x/crypto v0.43.0 ) diff --git a/go.sum b/go.sum index c084a9b..266ffd3 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ go.osspkg.com/errors v0.4.0 h1:E17+WyUzTXEHCTxGm8lOMPOOojzHG1lsOuQtTVGoATQ= go.osspkg.com/errors v0.4.0/go.mod h1:s75ZovPemYtrCtRPVsbQNq9MgMbmLMK1NEypr+uwjXI= go.osspkg.com/random v0.5.0 h1:6x2CQ5Vb6PVyuGi6Ao3K6Pr2fzVviBPCEEJC5HQNSmg= go.osspkg.com/random v0.5.0/go.mod h1:lsg3FI87PQdjhVWIVo2GXyPBclipljUxjMlWqRl2cck= +go.osspkg.com/syncing v0.4.3 h1:XioXG9zje1LNCsfQhNHkNPCQqPSJZHWTzM8Xig2zvAU= +go.osspkg.com/syncing v0.4.3/go.mod h1:/LBmgCAHFW6nQgVDILpEuo6eRCFK1yyFeNbDs4eVNls= golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= diff --git a/pki/alg_ecdsa.go b/pki/alg_ecdsa.go new file mode 100644 index 0000000..8f06a1c --- /dev/null +++ b/pki/alg_ecdsa.go @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package pki + +import ( + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "fmt" + "reflect" +) + +type _ecdsa struct{} + +func (*_ecdsa) Name() x509.PublicKeyAlgorithm { + return x509.ECDSA +} + +func (*_ecdsa) IsPrivateKey(key crypto.Signer) bool { + _, ok := key.(*ecdsa.PrivateKey) + return ok +} + +func (*_ecdsa) IsCertificate(cert x509.Certificate) bool { + _, ok := cert.PublicKey.(*ecdsa.PublicKey) + return ok +} + +func (*_ecdsa) IsRequest(cert x509.CertificateRequest) bool { + _, ok := cert.PublicKey.(*ecdsa.PublicKey) + return ok +} + +func (*_ecdsa) IsValidPair(key crypto.Signer, cert x509.Certificate) bool { + raw, ok := key.(*ecdsa.PrivateKey) + if !ok { + return false + } + pk, ok := raw.Public().(*ecdsa.PublicKey) + if !ok { + return false + } + ck, ok := cert.PublicKey.(*ecdsa.PublicKey) + if !ok { + return false + } + + return reflect.DeepEqual(pk, ck) +} + +func (*_ecdsa) Generate(ct CertType) (crypto.Signer, error) { + var curve elliptic.Curve + switch ct { + case RootCaCert: + curve = elliptic.P256() + case InterCACert: + curve = elliptic.P384() + case ClientCert: + curve = elliptic.P256() + default: + return nil, fmt.Errorf("unknown certificate curve for '%s'", ct) + } + + return ecdsa.GenerateKey(curve, rand.Reader) +} diff --git a/pki/alg_rsa.go b/pki/alg_rsa.go new file mode 100644 index 0000000..717aa43 --- /dev/null +++ b/pki/alg_rsa.go @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package pki + +import ( + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "fmt" + "reflect" +) + +type _rsa struct{} + +func (*_rsa) Name() x509.PublicKeyAlgorithm { + return x509.RSA +} + +func (*_rsa) IsPrivateKey(key crypto.Signer) bool { + _, ok := key.(*rsa.PrivateKey) + return ok +} + +func (*_rsa) IsCertificate(cert x509.Certificate) bool { + _, ok := cert.PublicKey.(*rsa.PublicKey) + return ok +} + +func (*_rsa) IsRequest(cert x509.CertificateRequest) bool { + _, ok := cert.PublicKey.(*rsa.PublicKey) + return ok +} + +func (*_rsa) IsValidPair(key crypto.Signer, cert x509.Certificate) bool { + raw, ok := key.(*rsa.PrivateKey) + if !ok { + return false + } + pk, ok := raw.Public().(*rsa.PublicKey) + if !ok { + return false + } + ck, ok := cert.PublicKey.(*rsa.PublicKey) + if !ok { + return false + } + + return reflect.DeepEqual(pk, ck) +} + +func (*_rsa) Generate(ct CertType) (crypto.Signer, error) { + var bits int + switch ct { + case RootCaCert: + bits = 4096 + case InterCACert: + bits = 3072 + case ClientCert: + bits = 2048 + default: + return nil, fmt.Errorf("unknown certificate bits for '%s'", ct) + } + + return rsa.GenerateKey(rand.Reader, bits) +} diff --git a/pki/alg_type.go b/pki/alg_type.go new file mode 100644 index 0000000..643ea31 --- /dev/null +++ b/pki/alg_type.go @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package pki + +import ( + "crypto" + "crypto/x509" + + "go.osspkg.com/syncing" +) + +var ( + signatures = syncing.NewMap[x509.SignatureAlgorithm, x509.PublicKeyAlgorithm](5) + algorithms = syncing.NewMap[x509.PublicKeyAlgorithm, Algorithm](5) +) + +func Register(k x509.SignatureAlgorithm, v Algorithm) { + signatures.Set(k, v.Name()) + algorithms.Set(v.Name(), v) +} + +func init() { + Register(x509.SHA256WithRSA, &_rsa{}) + Register(x509.SHA384WithRSA, &_rsa{}) + Register(x509.SHA512WithRSA, &_rsa{}) + Register(x509.ECDSAWithSHA256, &_ecdsa{}) + Register(x509.ECDSAWithSHA384, &_ecdsa{}) + Register(x509.ECDSAWithSHA512, &_ecdsa{}) +} + +type Algorithm interface { + Name() x509.PublicKeyAlgorithm + IsPrivateKey(key crypto.Signer) bool + IsRequest(cert x509.CertificateRequest) bool + IsCertificate(cert x509.Certificate) bool + IsValidPair(key crypto.Signer, cert x509.Certificate) bool + Generate(ct CertType) (crypto.Signer, error) +} + +type CertType string + +const ( + RootCaCert CertType = "root_ca_cert" + InterCACert CertType = "intermediate_ca_cert" + ClientCert CertType = "client_cert" +) diff --git a/x509cert/common.go b/pki/common.go similarity index 96% rename from x509cert/common.go rename to pki/common.go index ddaf919..00736e7 100644 --- a/x509cert/common.go +++ b/pki/common.go @@ -3,7 +3,7 @@ * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ -package x509cert +package pki import ( _ "crypto/md5" diff --git a/pki/config.go b/pki/config.go new file mode 100644 index 0000000..c9bb304 --- /dev/null +++ b/pki/config.go @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package pki + +import ( + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" +) + +type Config struct { + SignatureAlgorithm x509.SignatureAlgorithm `yaml:"signature_algorithm" json:"signature_algorithm"` + + Organization string `yaml:"organization,omitempty" json:"organization,omitempty"` + OrganizationalUnit string `yaml:"organizational_unit,omitempty" json:"organizational_unit,omitempty"` + Country string `yaml:"country,omitempty" json:"country,omitempty"` + Province string `yaml:"province,omitempty" json:"province,omitempty"` + Locality string `yaml:"locality,omitempty" json:"locality,omitempty"` + StreetAddress string `yaml:"street_address,omitempty" json:"street_address,omitempty"` + PostalCode string `yaml:"postal_code,omitempty" json:"postal_code,omitempty"` + CommonName string `yaml:"common_name,omitempty" json:"common_name,omitempty"` + + EmailAddress []string `yaml:"email_address,omitempty" json:"email_address,omitempty"` + OCSPServerURLs []string `yaml:"ocsp_server_ur_ls,omitempty" json:"ocsp_server_ur_ls,omitempty"` + IssuingCertificateURLs []string `yaml:"issuing_certificate_urls,omitempty" json:"issuing_certificate_urls,omitempty"` + CRLDistributionPointURLs []string `yaml:"crl_distribution_point_ur_ls,omitempty" json:"crl_distribution_point_ur_ls,omitempty"` + CertificatePoliciesURLs []string `yaml:"certificate_policies_urls,omitempty" json:"certificate_policies_urls,omitempty"` +} + +func (v Config) Subject() pkix.Name { + result := pkix.Name{} + + if len(v.Country) > 0 { + result.Country = []string{v.Country} + } + if len(v.Organization) > 0 { + result.Organization = []string{v.Organization} + } + if len(v.OrganizationalUnit) > 0 { + result.OrganizationalUnit = []string{v.OrganizationalUnit} + } + if len(v.Locality) > 0 { + result.Locality = []string{v.Locality} + } + if len(v.Province) > 0 { + result.Province = []string{v.Province} + } + if len(v.StreetAddress) > 0 { + result.StreetAddress = []string{v.StreetAddress} + } + if len(v.PostalCode) > 0 { + result.PostalCode = []string{v.PostalCode} + } + if len(v.CommonName) > 0 { + result.CommonName = v.CommonName + } + + return result +} + +func (v Config) ExtraExtensions() []pkix.Extension { + var result []pkix.Extension + + if len(v.IssuingCertificateURLs) > 0 { + for _, value := range stringsPrepare(v.CertificatePoliciesURLs) { + result = append(result, pkix.Extension{ + Id: asn1.ObjectIdentifier{2, 23, 140, 1, 1}, + Critical: true, + Value: []byte(value), + }) + } + + } + + return result +} diff --git a/pki/encoders.go b/pki/encoders.go new file mode 100644 index 0000000..e54de32 --- /dev/null +++ b/pki/encoders.go @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package pki + +import ( + "crypto" + "crypto/x509" + "encoding/pem" + "fmt" + "strings" +) + +var pemEndLine = []byte("\n-----END ") + +type TypePEMBlock string + +const ( + CertificatePEMBlock TypePEMBlock = "CERTIFICATE" + PrivateKeyPEMBlock TypePEMBlock = "PRIVATE KEY" + RevocationListPEMBlock TypePEMBlock = "X509 CRL" + CertificateRequestPEMBlock TypePEMBlock = "CERTIFICATE REQUEST" +) + +func CreatePEMBlock(b []byte, t TypePEMBlock, prefix string) []byte { + s := string(t) + if len(prefix) > 0 { + s = prefix + " " + s + } + + block := &pem.Block{Type: s, Bytes: b} + + return pem.EncodeToMemory(block) +} + +// --------------------------------------------------------------------------------------------------------------------- + +func MarshalKeyDER(key crypto.Signer) ([]byte, error) { + if key == nil { + return nil, fmt.Errorf("no private key provided") + } + + b, err := x509.MarshalPKCS8PrivateKey(key) + if err != nil { + return nil, fmt.Errorf("marshal PKCS#8 private key: %w", err) + } + + return b, nil +} + +func UnmarshalKeyDER(b []byte) (crypto.Signer, error) { + if len(b) == 0 { + return nil, fmt.Errorf("no private key provided") + } + + raw, err := x509.ParsePKCS8PrivateKey(b) + if err != nil { + return nil, fmt.Errorf("unmarshal PKCS#8 private key: %w", err) + } + + key, ok := raw.(crypto.Signer) + if !ok { + return nil, fmt.Errorf("PKCS#8 private key does not implement crypto.Signer") + } + + return key, nil +} + +func MarshalCrtDER(cert x509.Certificate) []byte { + return cert.Raw +} + +func UnmarshalCrtDER(b []byte) (*x509.Certificate, error) { + if len(b) == 0 { + return nil, fmt.Errorf("no certificate provided") + } + + cert, err := x509.ParseCertificate(b) + if err != nil { + return nil, fmt.Errorf("unmarshal PKCS#8 certificate: %w", err) + } + + return cert, nil +} + +func MarshalKeyPEM(key crypto.Signer) ([]byte, error) { + b, err := MarshalKeyDER(key) + if err != nil { + return nil, err + } + + var prefix string + for name, a := range algorithms.Yield() { + if !a.IsPrivateKey(key) { + continue + } + + prefix = name.String() + } + + return CreatePEMBlock(b, PrivateKeyPEMBlock, prefix), nil +} + +func UnmarshalKeyPEM(b []byte) (crypto.Signer, error) { + block, _ := pem.Decode(b) + if block == nil || !strings.HasSuffix(block.Type, string(PrivateKeyPEMBlock)) { + return nil, fmt.Errorf("no private key provided") + } + return UnmarshalKeyDER(block.Bytes) +} + +func MarshalCrtPEM(cert x509.Certificate) ([]byte, error) { + b := MarshalCrtDER(cert) + + return CreatePEMBlock(b, CertificatePEMBlock, ""), nil +} + +func UnmarshalCrtPEM(b []byte) (*x509.Certificate, error) { + block, _ := pem.Decode(b) + if block == nil || !strings.HasSuffix(block.Type, string(CertificatePEMBlock)) { + return nil, fmt.Errorf("no certificate provided") + } + return UnmarshalCrtDER(block.Bytes) +} + +func MarshalCsrDER(cert x509.CertificateRequest) []byte { + return cert.Raw +} + +func UnmarshalCsrDER(b []byte) (*x509.CertificateRequest, error) { + if len(b) == 0 { + return nil, fmt.Errorf("no CSR provided") + } + cert, err := x509.ParseCertificateRequest(b) + if err != nil { + return nil, fmt.Errorf("unmarshal PKCS#8 request: %w", err) + } + return cert, nil +} + +func MarshalCsrPEM(cert x509.CertificateRequest) ([]byte, error) { + b := MarshalCsrDER(cert) + + return CreatePEMBlock(b, CertificateRequestPEMBlock, ""), nil +} + +func UnmarshalCsrPEM(b []byte) (*x509.CertificateRequest, error) { + block, _ := pem.Decode(b) + if block == nil || !strings.HasSuffix(block.Type, string(CertificateRequestPEMBlock)) { + return nil, fmt.Errorf("no certificate provided") + } + return UnmarshalCsrDER(block.Bytes) +} diff --git a/pki/generate_ca.go b/pki/generate_ca.go new file mode 100644 index 0000000..616d711 --- /dev/null +++ b/pki/generate_ca.go @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package pki + +import ( + "crypto/rand" + "crypto/x509" + "fmt" + "math/big" + "time" +) + +func NewCA( + conf Config, + deadline time.Duration, + serialNumber int64, + withIntermediate bool, +) (*Certificate, error) { + + currTime := time.Now() + template := &x509.Certificate{ + IsCA: true, + BasicConstraintsValid: true, + SignatureAlgorithm: conf.SignatureAlgorithm, + SerialNumber: big.NewInt(serialNumber), + Subject: conf.Subject(), + NotBefore: currTime, + NotAfter: currTime.Add(deadline), + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + OCSPServer: stringsPrepare(conf.OCSPServerURLs), + IssuingCertificateURL: stringsPrepare(conf.IssuingCertificateURLs), + CRLDistributionPoints: stringsPrepare(conf.CRLDistributionPointURLs), + ExtraExtensions: conf.ExtraExtensions(), + EmailAddresses: stringsPrepare(conf.EmailAddress), + MaxPathLenZero: false, + MaxPathLen: 1, + } + + if withIntermediate { + template.MaxPathLen = 2 + } + + algName, ok := signatures.Get(conf.SignatureAlgorithm) + if !ok { + return nil, fmt.Errorf("unknown signature algorithm: %s", conf.SignatureAlgorithm.String()) + } + + alg, ok := algorithms.Get(algName) + if !ok { + return nil, fmt.Errorf("unknown signature algorithm: %s", algName.String()) + } + + key, err := alg.Generate(RootCaCert) + if err != nil { + return nil, fmt.Errorf("failed generating private key: %w", err) + } + + b, err := x509.CreateCertificate(rand.Reader, template, template, key.Public(), key) + if err != nil { + return nil, fmt.Errorf("failed generating certificate: %w", err) + } + + cert, err := UnmarshalCrtDER(b) + if err != nil { + return nil, fmt.Errorf("failed parsing certificate: %w", err) + } + + return &Certificate{Key: key, Crt: cert}, nil +} diff --git a/pki/generate_ca_inter.go b/pki/generate_ca_inter.go new file mode 100644 index 0000000..8aee808 --- /dev/null +++ b/pki/generate_ca_inter.go @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package pki + +import ( + "crypto/rand" + "crypto/x509" + "fmt" + "math/big" + "time" +) + +func NewIntermediateCA( + conf Config, + rootCA Certificate, + deadline time.Duration, + serialNumber int64, +) (*Certificate, error) { + + currTime := time.Now() + template := &x509.Certificate{ + IsCA: true, + BasicConstraintsValid: true, + SignatureAlgorithm: rootCA.Crt.SignatureAlgorithm, + SerialNumber: big.NewInt(serialNumber), + Subject: conf.Subject(), + NotBefore: currTime, + NotAfter: currTime.Add(deadline), + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + OCSPServer: stringsPrepare(conf.OCSPServerURLs), + IssuingCertificateURL: stringsPrepare(conf.IssuingCertificateURLs), + CRLDistributionPoints: stringsPrepare(conf.CRLDistributionPointURLs), + ExtraExtensions: conf.ExtraExtensions(), + EmailAddresses: stringsPrepare(conf.EmailAddress), + MaxPathLenZero: false, + MaxPathLen: 1, + } + + if !rootCA.IsValidPair() { + return nil, fmt.Errorf("invalid Root CA certificate") + } + + if !rootCA.IsCA() { + return nil, fmt.Errorf("invalid Root CA certificate: is not CA") + } + + if rootCA.Crt.MaxPathLen != 2 { + return nil, fmt.Errorf("invalid Root CA certificate: not supported Intermediate CA") + } + + if template.NotAfter.After(rootCA.Crt.NotAfter) { + return nil, fmt.Errorf("invalid Root CA certificate: NotAfter cannot be in the future") + } + + algName, ok := signatures.Get(template.SignatureAlgorithm) + if !ok { + return nil, fmt.Errorf("unknown signature algorithm: %s", template.SignatureAlgorithm.String()) + } + + alg, ok := algorithms.Get(algName) + if !ok { + return nil, fmt.Errorf("unknown signature algorithm: %s", algName.String()) + } + + key, err := alg.Generate(InterCACert) + if err != nil { + return nil, fmt.Errorf("failed generating private key: %w", err) + } + + b, err := x509.CreateCertificate(rand.Reader, template, rootCA.Crt, key.Public(), rootCA.Key) + if err != nil { + return nil, fmt.Errorf("failed generating certificate: %w", err) + } + + cert, err := UnmarshalCrtDER(b) + if err != nil { + return nil, fmt.Errorf("failed parsing certificate: %w", err) + } + + return &Certificate{Key: key, Crt: cert}, nil +} diff --git a/x509cert/crl.go b/pki/generate_crl.go similarity index 62% rename from x509cert/crl.go rename to pki/generate_crl.go index 0a7a867..e8dedaf 100644 --- a/x509cert/crl.go +++ b/pki/generate_crl.go @@ -3,12 +3,11 @@ * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ -package x509cert +package pki import ( "crypto/rand" "crypto/x509" - "crypto/x509/pkix" "fmt" "math/big" "time" @@ -19,7 +18,15 @@ type RevocationEntity struct { RevocationTime time.Time `yaml:"revocation_time" json:"revocation_time"` } -func NewCRL(ca Cert, serialNumber int64, updateInterval time.Duration, revs []RevocationEntity) (*RawCRL, error) { +func NewCRL(rootCA Certificate, id int64, updateInterval time.Duration, revs []RevocationEntity) ([]byte, error) { + if !rootCA.IsValidPair() { + return nil, fmt.Errorf("invalid Root CA certificate") + } + + if !rootCA.IsCA() { + return nil, fmt.Errorf("invalid Root CA certificate: is not CA") + } + list := make([]x509.RevocationListEntry, 0, len(revs)) for _, rev := range revs { list = append(list, x509.RevocationListEntry{ @@ -29,19 +36,18 @@ func NewCRL(ca Cert, serialNumber int64, updateInterval time.Duration, revs []Re } template := &x509.RevocationList{ - Number: big.NewInt(serialNumber), - Issuer: ca.Cert.Certificate.Subject, - SignatureAlgorithm: ca.Cert.Certificate.SignatureAlgorithm, + Number: big.NewInt(id), + Issuer: rootCA.Crt.Subject, + SignatureAlgorithm: rootCA.Crt.SignatureAlgorithm, ThisUpdate: time.Now(), NextUpdate: time.Now().Add(updateInterval), RevokedCertificateEntries: list, - ExtraExtensions: []pkix.Extension{}, } - b, err := x509.CreateRevocationList(rand.Reader, template, ca.Cert.Certificate, ca.Key.Key) + b, err := x509.CreateRevocationList(rand.Reader, template, rootCA.Crt, rootCA.Key) if err != nil { return nil, fmt.Errorf("failed create revocation list: %w", err) } - return &RawCRL{b}, nil + return b, nil } diff --git a/pki/generate_crt.go b/pki/generate_crt.go new file mode 100644 index 0000000..5cd68e7 --- /dev/null +++ b/pki/generate_crt.go @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package pki + +import ( + "crypto/rand" + "crypto/x509" + "fmt" + "math/big" + "time" +) + +func NewCRT( + conf Config, + rootCA Certificate, + deadline time.Duration, + serialNumber int64, + domains ...string, +) (*Certificate, error) { + if !rootCA.IsValidPair() { + return nil, fmt.Errorf("invalid Root CA certificate") + } + + if !rootCA.IsCA() { + return nil, fmt.Errorf("invalid Root CA certificate: is not CA") + } + + if rootCA.Crt.MaxPathLen != 1 { + return nil, fmt.Errorf("invalid Root CA certificate: not supported generate client certificate") + } + + currTime := time.Now() + template := &x509.Certificate{ + IsCA: false, + BasicConstraintsValid: true, + SignatureAlgorithm: rootCA.Crt.SignatureAlgorithm, + SerialNumber: big.NewInt(serialNumber), + Subject: conf.Subject(), + NotBefore: currTime, + NotAfter: currTime.Add(deadline), + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + OCSPServer: stringsPrepare(conf.OCSPServerURLs), + IssuingCertificateURL: stringsPrepare(conf.IssuingCertificateURLs), + CRLDistributionPoints: stringsPrepare(conf.CRLDistributionPointURLs), + ExtraExtensions: conf.ExtraExtensions(), + EmailAddresses: stringsPrepare(conf.EmailAddress), + } + + if template.NotAfter.After(rootCA.Crt.NotAfter) { + return nil, fmt.Errorf("invalid deadline: cannot be in the future then NotAfter Root CA certificate") + } + + var err error + template.IPAddresses, template.DNSNames, err = splitDomains(domains) + if err != nil { + return nil, fmt.Errorf("invalid domains: %w", err) + } + + if len(template.DNSNames) > 0 { + template.Subject.CommonName = template.DNSNames[0] + } else if len(template.IPAddresses) > 0 { + template.Subject.CommonName = template.IPAddresses[0].String() + } + + algName, ok := signatures.Get(template.SignatureAlgorithm) + if !ok { + return nil, fmt.Errorf("unknown signature algorithm: %s", template.SignatureAlgorithm.String()) + } + + alg, ok := algorithms.Get(algName) + if !ok { + return nil, fmt.Errorf("unknown signature algorithm: %s", algName.String()) + } + + key, err := alg.Generate(ClientCert) + if err != nil { + return nil, fmt.Errorf("failed generating private key: %w", err) + } + + b, err := x509.CreateCertificate(rand.Reader, template, rootCA.Crt, key.Public(), rootCA.Key) + if err != nil { + return nil, fmt.Errorf("failed generating certificate: %w", err) + } + + cert, err := UnmarshalCrtDER(b) + if err != nil { + return nil, fmt.Errorf("failed parsing certificate: %w", err) + } + + return &Certificate{Key: key, Crt: cert}, nil +} diff --git a/pki/generate_csr.go b/pki/generate_csr.go new file mode 100644 index 0000000..d269799 --- /dev/null +++ b/pki/generate_csr.go @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package pki + +import ( + "crypto/rand" + "crypto/x509" + "fmt" + "math/big" + "time" +) + +func NewCSR(signatureAlgorithm x509.SignatureAlgorithm, domains ...string) (*Request, error) { + if len(domains) == 0 { + return nil, fmt.Errorf("no certificate domains provided") + } + + algName, ok := signatures.Get(signatureAlgorithm) + if !ok { + return nil, fmt.Errorf("unknown signature algorithm: %s", signatureAlgorithm.String()) + } + alg, ok := algorithms.Get(algName) + if !ok { + return nil, fmt.Errorf("unknown signature algorithm: %s", algName.String()) + } + + template := &x509.CertificateRequest{ + SignatureAlgorithm: signatureAlgorithm, + } + + var err error + template.IPAddresses, template.DNSNames, err = splitDomains(domains) + if err != nil { + return nil, fmt.Errorf("invalid domains: %w", err) + } + + if len(template.DNSNames) > 0 { + template.Subject.CommonName = template.DNSNames[0] + } else if len(template.IPAddresses) > 0 { + template.Subject.CommonName = template.IPAddresses[0].String() + } + + key, err := alg.Generate(ClientCert) + if err != nil { + return nil, fmt.Errorf("failed generating private key: %w", err) + } + + b, err := x509.CreateCertificateRequest(rand.Reader, template, key) + if err != nil { + return nil, fmt.Errorf("failed creating certificate request: %w", err) + } + + cert, err := UnmarshalCsrDER(b) + if err != nil { + return nil, fmt.Errorf("failed parsing certificate: %w", err) + } + + return &Request{Key: key, Csr: cert}, nil +} + +func SignCSR( + conf Config, + rootCA Certificate, + csr x509.CertificateRequest, + deadline time.Duration, + serialNumber int64, +) (*x509.Certificate, error) { + if !rootCA.IsValidPair() { + return nil, fmt.Errorf("invalid Root CA certificate") + } + + if !rootCA.IsCA() { + return nil, fmt.Errorf("invalid Root CA certificate: is not CA") + } + + if rootCA.Crt.MaxPathLen != 1 { + return nil, fmt.Errorf("invalid Root CA certificate: not supported generate client certificate") + } + + if _, ok := signatures.Get(csr.SignatureAlgorithm); !ok { + return nil, fmt.Errorf("unknown signature algorithm: %s", csr.SignatureAlgorithm.String()) + } + + currTime := time.Now() + template := &x509.Certificate{ + IsCA: false, + BasicConstraintsValid: true, + SignatureAlgorithm: rootCA.Crt.SignatureAlgorithm, + SerialNumber: big.NewInt(serialNumber), + Subject: csr.Subject, + NotBefore: currTime, + NotAfter: currTime.Add(deadline), + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + OCSPServer: rootCA.Crt.OCSPServer, + IssuingCertificateURL: stringsPrepare(conf.IssuingCertificateURLs), + CRLDistributionPoints: rootCA.Crt.CRLDistributionPoints, + EmailAddresses: rootCA.Crt.EmailAddresses, + DNSNames: csr.DNSNames, + IPAddresses: csr.IPAddresses, + } + + b, err := x509.CreateCertificate(rand.Reader, template, rootCA.Crt, csr.PublicKey, rootCA.Key) + if err != nil { + return nil, fmt.Errorf("failed generating certificate: %w", err) + } + + cert, err := UnmarshalCrtDER(b) + if err != nil { + return nil, fmt.Errorf("failed parsing certificate: %w", err) + } + + return cert, nil +} diff --git a/pki/generate_test.go b/pki/generate_test.go new file mode 100644 index 0000000..4cffbad --- /dev/null +++ b/pki/generate_test.go @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package pki_test + +import ( + "crypto/x509" + "fmt" + "testing" + "time" + + "go.osspkg.com/casecheck" + + "go.osspkg.com/encrypt/pki" +) + +func TestUnit_Generate(t *testing.T) { + rootCa, err := pki.NewCA( + pki.Config{SignatureAlgorithm: x509.ECDSAWithSHA512, CommonName: "Test Root CA"}, + time.Hour*24*365*10, + time.Now().Unix(), + true, + ) + casecheck.NoError(t, err) + dump(t, rootCa) + + subCa, err := pki.NewIntermediateCA( + pki.Config{CommonName: "Test Web CA"}, + *rootCa, + time.Hour*24*365*5, + time.Now().Unix(), + ) + casecheck.NoError(t, err) + dump(t, subCa) + + crt, err := pki.NewCRT( + pki.Config{CommonName: "Test Web CA"}, + *subCa, + time.Hour*24*90, + time.Now().Unix(), + "localhost", + ) + casecheck.NoError(t, err) + dump(t, crt) +} + +func TestUnit_SignCSR(t *testing.T) { + rootCa, err := pki.NewCA( + pki.Config{SignatureAlgorithm: x509.ECDSAWithSHA512, CommonName: "Test Root CA"}, + time.Hour*24*365*10, + time.Now().Unix(), + true, + ) + casecheck.NoError(t, err) + dump(t, rootCa) + + subCa, err := pki.NewIntermediateCA( + pki.Config{CommonName: "Test Web CA"}, + *rootCa, + time.Hour*24*365*5, + time.Now().Unix(), + ) + casecheck.NoError(t, err) + dump(t, subCa) + + csr, err := pki.NewCSR(x509.SHA384WithRSA, "localhost") + casecheck.NoError(t, err) + + crt, err := pki.SignCSR(pki.Config{}, *subCa, *csr.Csr, time.Hour*24*90, time.Now().Unix()) + casecheck.NoError(t, err) + + dump(t, &pki.Certificate{Crt: crt, Key: csr.Key}) +} + +func dump(t *testing.T, crt *pki.Certificate) { + kb, err := pki.MarshalKeyPEM(crt.Key) + casecheck.NoError(t, err) + cb, err := pki.MarshalCrtPEM(*crt.Crt) + casecheck.NoError(t, err) + dumpCertificateInfo(crt.Crt) + fmt.Println(string(kb)) + fmt.Println(string(cb)) +} + +func dumpCertificateInfo(cert *x509.Certificate) { + fmt.Println("------------------------------------------------------------------") + fmt.Println(" ИНФОРМАЦИЯ О СЕРТИФИКАТЕ") + fmt.Println("------------------------------------------------------------------") + + // Субъект и Издатель + fmt.Printf("Субъект (Subject): %v\n", cert.Subject.ToRDNSequence()) + fmt.Printf("Издатель (Issuer): %v\n", cert.Issuer.ToRDNSequence()) + + // Основные идентификаторы + fmt.Printf("Общее имя (Common Name): %s\n", cert.Subject.CommonName) + fmt.Printf("Серийный номер: %s\n", cert.SerialNumber.String()) + + // Срок действия + fmt.Printf("Действителен с: %s\n", cert.NotBefore.Format("2006-01-02 15:04:05 MST")) + fmt.Printf("Действителен до: %s\n", cert.NotAfter.Format("2006-01-02 15:04:05 MST")) + + // SANs (Subject Alternative Names) + if len(cert.DNSNames) > 0 { + fmt.Printf("DNS SANs: %v\n", cert.DNSNames) + } + if len(cert.IPAddresses) > 0 { + fmt.Printf("IP SANs: %v\n", cert.IPAddresses) + } + + // Алгоритмы + fmt.Printf("Алгоритм подписи: %v\n", cert.SignatureAlgorithm.String()) + fmt.Printf("Алгоритм публичного ключа: %v\n", cert.PublicKeyAlgorithm.String()) + + // Использование ключа (Key Usage) + fmt.Printf("Базовое использование ключа: %v\n", cert.KeyUsage) + if len(cert.ExtKeyUsage) > 0 { + fmt.Printf("Расширенное использование ключа: %v\n", cert.ExtKeyUsage) + } + + // Флаги CA + fmt.Printf("Является CA: %t\n", cert.IsCA) + if cert.MaxPathLen > 0 { + fmt.Printf("Макс. длина пути (MaxPathLen): %d\n", cert.MaxPathLen) + } + + fmt.Println("------------------------------------------------------------------") +} diff --git a/pki/model_certificate.go b/pki/model_certificate.go new file mode 100644 index 0000000..76dfdd0 --- /dev/null +++ b/pki/model_certificate.go @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package pki + +import ( + "bytes" + "crypto" + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + "fmt" + "os" +) + +type Certificate struct { + Key crypto.Signer + Crt *x509.Certificate +} + +func (c *Certificate) IsValidPair() bool { + if c == nil || c.Key == nil || c.Crt == nil { + return false + } + + for _, a := range algorithms.Yield() { + if !a.IsPrivateKey(c.Key) { + continue + } + return a.IsValidPair(c.Key, *c.Crt) + } + + return false +} + +func (c *Certificate) IsCA() bool { + if c == nil || c.Crt == nil { + return false + } + return c.Crt.IsCA +} + +func (c *Certificate) FingerPrint(h crypto.Hash) ([]byte, error) { + if c == nil || c.Crt == nil { + return nil, fmt.Errorf("no certificate provided") + } + + if !h.Available() { + return nil, fmt.Errorf("hash algorithm not defined") + } + + w := h.New() + w.Write(c.Crt.Raw) + + return w.Sum(nil), nil +} + +func (c *Certificate) IssuerKeyHash(h crypto.Hash) ([]byte, error) { + if c == nil || c.Crt == nil { + return nil, fmt.Errorf("no certificate provided") + } + + if !h.Available() { + return nil, fmt.Errorf("hash algorithm not defined") + } + + var info struct { + Algorithm pkix.AlgorithmIdentifier + PublicKey asn1.BitString + } + + if _, err := asn1.Unmarshal(c.Crt.RawSubjectPublicKeyInfo, &info); err != nil { + return nil, err + } + + w := h.New() + w.Write(info.PublicKey.RightAlign()) + + return w.Sum(nil), nil +} + +func (c *Certificate) IssuerNameHash(h crypto.Hash) ([]byte, error) { + if c == nil || c.Crt == nil { + return nil, fmt.Errorf("no certificate provided") + } + + if !h.Available() { + return nil, fmt.Errorf("hash algorithm not defined") + } + + w := h.New() + w.Write(c.Crt.RawSubject) + + return w.Sum(nil), nil +} + +func (c *Certificate) SaveKey(filepath string) error { + if c == nil || c.Key == nil { + return fmt.Errorf("no private key provided") + } + b, err := MarshalKeyPEM(c.Key) + if err != nil { + return fmt.Errorf("marshal private key: %w", err) + } + err = os.WriteFile(filepath, b, 0600) + if err != nil { + return fmt.Errorf("save key to '%s': %w", filepath, err) + } + return nil +} + +func (c *Certificate) SaveCert(filepath string) error { + if c == nil || c.Crt == nil { + return fmt.Errorf("no certificate provided") + } + b, err := MarshalCrtPEM(*c.Crt) + if err != nil { + return fmt.Errorf("marshal certificate: %w", err) + } + err = os.WriteFile(filepath, b, 0644) + if err != nil { + return fmt.Errorf("save certificate to '%s': %w", filepath, err) + } + return nil +} + +func (c *Certificate) LoadKey(filepath string) error { + b, err := os.ReadFile(filepath) + if err != nil { + return fmt.Errorf("load private key from '%s': %w", filepath, err) + } + if bytes.Contains(b, pemEndLine) { + c.Key, err = UnmarshalKeyPEM(b) + } else { + c.Key, err = UnmarshalKeyDER(b) + } + return err +} + +func (c *Certificate) LoadCert(filepath string) error { + b, err := os.ReadFile(filepath) + if err != nil { + return fmt.Errorf("load certificate from '%s': %w", filepath, err) + } + if bytes.Contains(b, pemEndLine) { + c.Crt, err = UnmarshalCrtPEM(b) + } else { + c.Crt, err = UnmarshalCrtDER(b) + } + return err +} diff --git a/pki/model_request.go b/pki/model_request.go new file mode 100644 index 0000000..02da597 --- /dev/null +++ b/pki/model_request.go @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package pki + +import ( + "bytes" + "crypto" + "crypto/x509" + "fmt" + "os" +) + +type Request struct { + Key crypto.Signer + Csr *x509.CertificateRequest +} + +func (c *Request) SaveKey(filepath string) error { + if c == nil || c.Key == nil { + return fmt.Errorf("no private key provided") + } + b, err := MarshalKeyPEM(c.Key) + if err != nil { + return fmt.Errorf("marshal private key: %w", err) + } + err = os.WriteFile(filepath, b, 0600) + if err != nil { + return fmt.Errorf("save key to '%s': %w", filepath, err) + } + return nil +} + +func (c *Request) SaveCert(filepath string) error { + if c == nil || c.Csr == nil { + return fmt.Errorf("no certificate request provided") + } + b, err := MarshalCsrPEM(*c.Csr) + if err != nil { + return fmt.Errorf("marshal certificate request: %w", err) + } + err = os.WriteFile(filepath, b, 0644) + if err != nil { + return fmt.Errorf("save certificate request to '%s': %w", filepath, err) + } + return nil +} + +func (c *Request) LoadKey(filepath string) error { + b, err := os.ReadFile(filepath) + if err != nil { + return fmt.Errorf("load private key from '%s': %w", filepath, err) + } + if bytes.Contains(b, pemEndLine) { + c.Key, err = UnmarshalKeyPEM(b) + } else { + c.Key, err = UnmarshalKeyDER(b) + } + return err +} + +func (c *Request) LoadCert(filepath string) error { + b, err := os.ReadFile(filepath) + if err != nil { + return fmt.Errorf("load certificate request from '%s': %w", filepath, err) + } + if bytes.Contains(b, pemEndLine) { + c.Csr, err = UnmarshalCsrPEM(b) + } else { + c.Csr, err = UnmarshalCsrDER(b) + } + return err +} diff --git a/x509cert/ocsp.go b/pki/ocsp.go similarity index 91% rename from x509cert/ocsp.go rename to pki/ocsp.go index c7d4fee..73f1ef6 100644 --- a/x509cert/ocsp.go +++ b/pki/ocsp.go @@ -3,7 +3,7 @@ * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ -package x509cert +package pki import ( "context" @@ -28,7 +28,7 @@ const ( ) type OCSPServer struct { - CA Cert + CA Certificate Resolver OCSPStatusResolver UpdateInterval time.Duration } @@ -61,7 +61,7 @@ func (v *OCSPServer) HTTPHandler(w http.ResponseWriter, r *http.Request) { ProducedAt: time.Now(), } - resp, err := ocsp.CreateResponse(v.CA.Cert.Certificate, v.CA.Cert.Certificate, response, v.CA.Key.Key) + resp, err := ocsp.CreateResponse(v.CA.Crt, v.CA.Crt, response, v.CA.Key) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return diff --git a/pki/utils.go b/pki/utils.go new file mode 100644 index 0000000..69d5555 --- /dev/null +++ b/pki/utils.go @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package pki + +import ( + "fmt" + "net" + "strings" +) + +func splitDomains(commonNames []string) ([]net.IP, []string, error) { + if len(commonNames) == 0 { + return nil, nil, fmt.Errorf("domains is empty") + } + + ips := make([]net.IP, 0, len(commonNames)) + domains := make([]string, 0, len(commonNames)) + + for _, commonName := range stringsPrepare(commonNames) { + if ip, _, err := net.SplitHostPort(commonName); err == nil { + ips = append(ips, net.ParseIP(ip)) + continue + } + + domains = append(domains, strings.TrimSpace(strings.ToLower(commonName))) + } + + return ips, domains, nil +} + +func stringsPrepare(list []string) (out []string) { + for _, s := range list { + s = strings.TrimSpace(s) + if len(s) == 0 { + continue + } + out = append(out, strings.ToLower(s)) + } + return +} diff --git a/x509cert/cert.go b/x509cert/cert.go deleted file mode 100644 index 6799df8..0000000 --- a/x509cert/cert.go +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. - * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. - */ - -package x509cert - -import ( - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "math/big" - "time" - - "go.osspkg.com/errors" -) - -func NewCA(c Config, ca *Cert, bits int, deadline time.Duration, serialNumber int64, commonName string) (Cert, - error) { - key, err := rsa.GenerateKey(rand.Reader, bits) - if err != nil { - return Cert{}, errors.Wrapf(err, "generate private key") - } - - template := &x509.Certificate{ - IsCA: true, - BasicConstraintsValid: true, - SignatureAlgorithm: c.SignatureAlgorithm, - SerialNumber: big.NewInt(serialNumber), - Subject: c.ToSubject(), - NotBefore: time.Now(), - NotAfter: time.Now().Add(deadline), - KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign | x509.KeyUsageCRLSign, - OCSPServer: c.OCSPServer, - IssuingCertificateURL: c.IssuingCertificateURL, - CRLDistributionPoints: c.CRLDistributionPoints, - } - template.Subject.CommonName = commonName - - var crt []byte - if ca != nil && !ca.IsEmpty() { - template.MaxPathLenZero = false - template.MaxPathLen = ca.Cert.Certificate.MaxPathLen + 1 - - if template.NotAfter.After(ca.Cert.Certificate.NotAfter) { - return Cert{}, errors.New("deadline expires after root certificate expires") - } - - crt, err = x509.CreateCertificate(rand.Reader, template, ca.Cert.Certificate, &key.PublicKey, ca.Key.Key) - } else { - template.MaxPathLenZero = true - template.MaxPathLen = 0 - - crt, err = x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key) - } - if err != nil { - return Cert{}, errors.Wrapf(err, "create certificate") - } - - rc := &RawCert{} - if err = rc.DecodeDER(crt); err != nil { - return Cert{}, errors.Wrapf(err, "decode certificate") - } - - return Cert{ - Cert: rc, - Key: &RawKey{Key: key}, - }, nil -} - -func NewCert(c Config, ca Cert, bits int, deadline time.Duration, serialNumber int64, commonNames ...string) (Cert, error) { - if ca.IsEmpty() { - return Cert{}, errors.New("CA cert is empty") - } - ok, err := ca.Cert.IsCa() - if err != nil { - return Cert{}, err - } - if !ok { - return Cert{}, errors.New("CA cert is not valid") - } - - key, err := rsa.GenerateKey(rand.Reader, bits) - if err != nil { - return Cert{}, errors.Wrapf(err, "generate private key") - } - - template := &x509.Certificate{ - IsCA: false, - BasicConstraintsValid: false, - SignatureAlgorithm: c.SignatureAlgorithm, - SerialNumber: big.NewInt(serialNumber), - Subject: c.ToSubject(), - NotBefore: time.Now(), - NotAfter: time.Now().Add(deadline), - KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, - OCSPServer: c.OCSPServer, - IssuingCertificateURL: c.IssuingCertificateURL, - CRLDistributionPoints: c.CRLDistributionPoints, - } - - if template.NotAfter.After(ca.Cert.Certificate.NotAfter) { - return Cert{}, errors.New("deadline expires after root certificate expires") - } - - commonName := "*" - ips, dns, err := splitCommonNames(commonNames) - if err != nil { - return Cert{}, errors.Wrapf(err, "apply common names") - } - if len(dns) > 0 { - commonName = dns[0] - } - template.Subject.CommonName = commonName - template.IPAddresses = ips - template.DNSNames = dns - - crt, err := x509.CreateCertificate(rand.Reader, template, ca.Cert.Certificate, &key.PublicKey, ca.Key.Key) - if err != nil { - return Cert{}, errors.Wrapf(err, "create certificate") - } - - rc := &RawCert{} - if err = rc.DecodeDER(crt); err != nil { - return Cert{}, errors.Wrapf(err, "decode certificate") - } - - return Cert{ - Cert: rc, - Key: &RawKey{Key: key}, - }, nil -} diff --git a/x509cert/cert_test.go b/x509cert/cert_test.go deleted file mode 100644 index deebec6..0000000 --- a/x509cert/cert_test.go +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. - * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. - */ - -package x509cert_test - -import ( - "crypto" - x510 "crypto/x509" - "encoding/hex" - "fmt" - "testing" - "time" - - "go.osspkg.com/casecheck" - "golang.org/x/crypto/ocsp" - - "go.osspkg.com/encrypt/x509cert" -) - -func TestUnit_X509(t *testing.T) { - t.SkipNow() - - fmt.Println(time.Now().Unix(), time.Now().UnixNano()) - - conf := x509cert.Config{ - Organization: "Demo Inc.", - CRLDistributionPoints: []string{"https://crl.demo.com/"}, - OCSPServer: []string{"https://ocsp.demo.com"}, - SignatureAlgorithm: x510.SHA384WithRSA, - } - - ca, err := x509cert.NewCA(conf, nil, 2048, time.Hour*24*365*10, 1, "Root CA") - casecheck.NoError(t, err) - cacpem, err := ca.Cert.EncodePEM() - casecheck.NoError(t, err) - fmt.Println(string(cacpem)) - //cakpem, err := ca.Key.EncodePEM() - //casecheck.NoError(t, err) - //fmt.Println(string(cakpem)) - - ica, err := x509cert.NewCA(conf, &ca, 2048, time.Hour*24*365*5, 1, "Intermediate CA") - casecheck.NoError(t, err) - icacpem, err := ica.Cert.EncodePEM() - casecheck.NoError(t, err) - fmt.Println(string(icacpem)) - - crt, err := x509cert.NewCert(conf, ica, 2048, time.Hour*24*90, time.Now().UnixNano(), "example.com", "*.example.com") - casecheck.NoError(t, err) - crtcpem, err := crt.Cert.EncodePEM() - casecheck.NoError(t, err) - fmt.Println(string(crtcpem)) - //crtkpem, err := crt.Key.EncodePEM() - //casecheck.NoError(t, err) - //fmt.Println(string(crtkpem)) - - algs := []crypto.Hash{ - crypto.MD4, - crypto.MD5, - crypto.SHA1, - crypto.SHA224, - crypto.SHA256, - crypto.SHA384, - crypto.SHA512, - crypto.SHA512_224, - crypto.SHA512_256, - crypto.SHA3_224, - crypto.SHA3_256, - crypto.SHA3_384, - crypto.SHA3_512, - crypto.RIPEMD160, - crypto.BLAKE2s_256, - crypto.BLAKE2b_256, - crypto.BLAKE2b_384, - crypto.BLAKE2b_512, - } - for _, alg := range algs { - fmt.Println(alg.String(), alg.Available()) - - fp, err := crt.Cert.FingerPrint(alg) - casecheck.NoError(t, err) - fmt.Println("FingerPrint", hex.EncodeToString(fp)) - - inh, err := crt.Cert.IssuerNameHash(alg) - casecheck.NoError(t, err) - fmt.Println("IssuerNameHash", hex.EncodeToString(inh)) - - ikh, err := crt.Cert.IssuerKeyHash(alg) - casecheck.NoError(t, err) - fmt.Println("IssuerKeyHash", hex.EncodeToString(ikh)) - } - - req, err := ocsp.CreateRequest(crt.Cert.Certificate, ca.Cert.Certificate, nil) - casecheck.NoError(t, err) - req1, err := ocsp.ParseRequest(req) - casecheck.NoError(t, err) - - fmt.Println(req1.SerialNumber.Int64(), "IssuerNameHash", hex.EncodeToString(req1.IssuerNameHash), - "IssuerKeyHash", hex.EncodeToString(req1.IssuerKeyHash)) - - b, err := ca.Cert.IssuerNameHash(crypto.SHA1) - casecheck.NoError(t, err) - fmt.Println("IssuerNameHash", hex.EncodeToString(b)) - - b, err = ca.Cert.IssuerKeyHash(crypto.SHA1) - casecheck.NoError(t, err) - fmt.Println("IssuerKeyHash", hex.EncodeToString(b)) -} diff --git a/x509cert/config.go b/x509cert/config.go deleted file mode 100644 index 453ff75..0000000 --- a/x509cert/config.go +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. - * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. - */ - -package x509cert - -import ( - "crypto/x509" - "crypto/x509/pkix" -) - -type Config struct { - Organization string - OrganizationalUnit string - Country string - Province string - Locality string - StreetAddress string - PostalCode string - - OCSPServer []string - IssuingCertificateURL []string - CRLDistributionPoints []string - SignatureAlgorithm x509.SignatureAlgorithm -} - -func (v Config) ToSubject() pkix.Name { - result := pkix.Name{} - - if len(v.Country) > 0 { - result.Country = []string{v.Country} - } - if len(v.Organization) > 0 { - result.Organization = []string{v.Organization} - } - if len(v.OrganizationalUnit) > 0 { - result.OrganizationalUnit = []string{v.OrganizationalUnit} - } - if len(v.Locality) > 0 { - result.Locality = []string{v.Locality} - } - if len(v.Province) > 0 { - result.Province = []string{v.Province} - } - if len(v.StreetAddress) > 0 { - result.StreetAddress = []string{v.StreetAddress} - } - if len(v.PostalCode) > 0 { - result.PostalCode = []string{v.PostalCode} - } - - return result -} diff --git a/x509cert/models.go b/x509cert/models.go deleted file mode 100644 index 23abfe8..0000000 --- a/x509cert/models.go +++ /dev/null @@ -1,281 +0,0 @@ -/* - * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. - * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. - */ - -package x509cert - -import ( - "crypto" - "crypto/rsa" - "crypto/x509" - "crypto/x509/pkix" - "encoding/asn1" - "encoding/pem" - "errors" - "os" -) - -var ( - ErrDecodePEMBlock = errors.New("crypto/x509: failed decoding PEM block") - ErrEmptyCertificate = errors.New("certificate is nil") - ErrNotInitedCertificate = errors.New("certificate is not initialized") - ErrEmptyKey = errors.New("key is nil") - ErrNotInitedKey = errors.New("key is not initialized") - ErrHashAlgNotDefined = errors.New("hash algorithm not defined") -) - -type RawCert struct { - Certificate *x509.Certificate -} - -func (v *RawCert) IsCa() (bool, error) { - if v == nil || v.Certificate == nil { - return false, ErrEmptyCertificate - } - - return v.Certificate.IsCA, nil -} - -func (v *RawCert) FingerPrint(h crypto.Hash) ([]byte, error) { - if v == nil || v.Certificate == nil { - return nil, ErrEmptyCertificate - } - - if !h.Available() { - return nil, ErrHashAlgNotDefined - } - - w := h.New() - w.Write(v.Certificate.Raw) - - return w.Sum(nil), nil -} - -func (v *RawCert) IssuerKeyHash(h crypto.Hash) ([]byte, error) { - if v == nil || v.Certificate == nil { - return nil, ErrEmptyCertificate - } - - if !h.Available() { - return nil, ErrHashAlgNotDefined - } - - var publicKeyInfo struct { - Algorithm pkix.AlgorithmIdentifier - PublicKey asn1.BitString - } - - if _, err := asn1.Unmarshal(v.Certificate.RawSubjectPublicKeyInfo, &publicKeyInfo); err != nil { - return nil, err - } - - w := h.New() - w.Write(publicKeyInfo.PublicKey.RightAlign()) - - return w.Sum(nil), nil -} - -func (v *RawCert) IssuerNameHash(h crypto.Hash) ([]byte, error) { - if v == nil || v.Certificate == nil { - return nil, ErrEmptyCertificate - } - - if !h.Available() { - return nil, ErrHashAlgNotDefined - } - - w := h.New() - w.Write(v.Certificate.RawSubject) - - return w.Sum(nil), nil -} - -func (v *RawCert) EncodeDER() ([]byte, error) { - if v == nil || v.Certificate == nil { - return nil, ErrEmptyCertificate - } - return v.Certificate.Raw, nil -} - -func (v *RawCert) EncodePEM() ([]byte, error) { - b, err := v.EncodeDER() - if err != nil { - return nil, err - } - return encodePEM(b, pemTypeCertificate), nil -} - -func (v *RawCert) EncodeDERFile(filename string) error { - b, err := v.EncodeDER() - if err != nil { - return err - } - - return os.WriteFile(filename, b, 0644) -} - -func (v *RawCert) EncodePEMFile(filename string) error { - b, err := v.EncodePEM() - if err != nil { - return err - } - - return os.WriteFile(filename, b, 0644) -} - -func (v *RawCert) DecodeDER(b []byte) (err error) { - if v == nil { - return ErrNotInitedCertificate - } - v.Certificate, err = x509.ParseCertificate(b) - return -} - -func (v *RawCert) DecodePEM(b []byte) error { - if v == nil { - return ErrNotInitedCertificate - } - block, _ := pem.Decode(b) - if block == nil || block.Type != string(pemTypeCertificate) { - return ErrDecodePEMBlock - } - return v.DecodeDER(block.Bytes) -} - -func (v *RawCert) DecodeDERFile(filename string) error { - b, err := os.ReadFile(filename) - if err != nil { - return err - } - - return v.DecodeDER(b) -} - -func (v *RawCert) DecodePEMFile(filename string) error { - b, err := os.ReadFile(filename) - if err != nil { - return err - } - - return v.DecodePEM(b) -} - -// ------------------------------------------------------------------------------------------------------------------- - -type RawKey struct { - Key *rsa.PrivateKey -} - -func (v *RawKey) EncodeDER() ([]byte, error) { - if v == nil || v.Key == nil { - return nil, ErrEmptyKey - } - return x509.MarshalPKCS1PrivateKey(v.Key), nil -} - -func (v *RawKey) EncodePEM() ([]byte, error) { - b, err := v.EncodeDER() - if err != nil { - return nil, err - } - return encodePEM(b, pemTypePrivateKey), nil -} - -func (v *RawKey) EncodeDERFile(filename string) error { - b, err := v.EncodeDER() - if err != nil { - return err - } - - return os.WriteFile(filename, b, 0600) -} - -func (v *RawKey) EncodePEMFile(filename string) error { - b, err := v.EncodePEM() - if err != nil { - return err - } - - return os.WriteFile(filename, b, 0600) -} - -func (v *RawKey) DecodeDER(b []byte) (err error) { - if v == nil { - return ErrNotInitedKey - } - v.Key, err = x509.ParsePKCS1PrivateKey(b) - return -} - -func (v *RawKey) DecodePEM(b []byte) error { - block, _ := pem.Decode(b) - if block == nil || block.Type != string(pemTypePrivateKey) { - return ErrDecodePEMBlock - } - return v.DecodeDER(block.Bytes) -} - -func (v *RawKey) DecodeDERFile(filename string) error { - b, err := os.ReadFile(filename) - if err != nil { - return err - } - - return v.DecodeDER(b) -} - -func (v *RawKey) DecodePEMFile(filename string) error { - b, err := os.ReadFile(filename) - if err != nil { - return err - } - - return v.DecodePEM(b) -} - -// ------------------------------------------------------------------------------------------------------------------- - -type RawCRL struct { - b []byte -} - -func (v *RawCRL) EncodeDER() []byte { - if v == nil { - return nil - } - return v.b -} - -func (v *RawCRL) EncodePEM() []byte { - if v == nil { - return nil - } - return encodePEM(v.b, pemTypeRevocationList) -} - -func (v *RawCRL) EncodeDERFile(filename string) error { - if v == nil { - return nil - } - return os.WriteFile(filename, v.EncodeDER(), 0744) -} - -func (v *RawCRL) EncodePEMFile(filename string) error { - if v == nil { - return nil - } - return os.WriteFile(filename, v.EncodePEM(), 0744) -} - -// ------------------------------------------------------------------------------------------------------------------- - -type Cert struct { - Cert *RawCert - Key *RawKey -} - -func (c Cert) IsEmpty() bool { - return c.Cert == nil || c.Cert.Certificate == nil || - c.Key == nil || c.Key.Key == nil -} diff --git a/x509cert/utils.go b/x509cert/utils.go deleted file mode 100644 index 77e0e2b..0000000 --- a/x509cert/utils.go +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. - * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. - */ - -package x509cert - -import ( - "encoding/pem" - "fmt" - "net" -) - -func splitCommonNames(commonNames []string) ([]net.IP, []string, error) { - if len(commonNames) == 0 { - return nil, nil, fmt.Errorf("no common names specified") - } - - ips := make([]net.IP, 0, len(commonNames)) - domains := make([]string, 0, len(commonNames)) - - for _, commonName := range commonNames { - if ip, _, err := net.SplitHostPort(commonName); err == nil { - ips = append(ips, net.ParseIP(ip)) - continue - } - - domains = append(domains, commonName) - } - - return ips, domains, nil -} - -type pemType string - -const ( - pemTypeCertificate pemType = "CERTIFICATE" - pemTypePrivateKey pemType = "RSA PRIVATE KEY" - pemTypeRevocationList pemType = "X509 CRL" -) - -func encodePEM(b []byte, t pemType) []byte { - block := &pem.Block{ - Type: string(t), - Bytes: b, - } - - return pem.EncodeToMemory(block) -}