diff --git a/LICENSE.md b/LICENSE.md index e21d537..09d334f 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,4 +1,4 @@ -Copyright 2022 OnPointCoding +Copyright 2023 MrMelon54 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/README.md b/README.md index a84fb00..6c9302f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -certgen +CertGen ======= Some certificate generation utilities diff --git a/ca.go b/ca.go index a047783..d64bb99 100644 --- a/ca.go +++ b/ca.go @@ -6,42 +6,57 @@ import ( "crypto/tls" "crypto/x509" "crypto/x509/pkix" - "log" + "fmt" "math/big" "time" ) -func MakeCaTls(name pkix.Name, serialNumber *big.Int) (*CertGen, error) { +// MakeCaTls generates a CA TLS certificate +func MakeCaTls(bits int, name pkix.Name, serialNumber *big.Int, future Future) (*CertGen, error) { + // base certificate data + now := time.Now() ca := &x509.Certificate{ SerialNumber: serialNumber, Subject: name, - NotBefore: time.Now(), - NotAfter: time.Now().AddDate(10, 0, 0), + NotBefore: now, + NotAfter: future(now), IsCA: true, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, BasicConstraintsValid: true, } - caPrivKey, err := rsa.GenerateKey(rand.Reader, 4096) + // generate rsa private key + caPrivKey, err := rsa.GenerateKey(rand.Reader, bits) if err != nil { - log.Fatalln("Failed to generate CA private key:", err) + return nil, fmt.Errorf("Failed to generate CA private key: %w", err) } + // create certificate bytes caBytes, err := x509.CreateCertificate(rand.Reader, ca, ca, caPrivKey.Public(), caPrivKey) if err != nil { - log.Fatalln("Failed to generate CA certificate bytes:", err) + return nil, fmt.Errorf("Failed to generate CA certificate bytes: %w", err) } + + // add the raw certificate bytes so `*x509.Certificate.Equal(*x509.Certificate)` is valid + ca.Raw = caBytes + + // get private key bytes privKeyBytes := x509.MarshalPKCS1PrivateKey(caPrivKey) gen := &CertGen{cert: ca, certBytes: caBytes, key: caPrivKey, keyBytes: privKeyBytes} + + // generate pem blocks err = gen.generatePem() if err != nil { - return nil, err + return nil, fmt.Errorf("Failed to generate PEM encoding: %w", err) } + + // generate key pair caKeyPair, err := tls.X509KeyPair(gen.certPem, gen.keyPem) if err != nil { - log.Fatalln("Failed to generate CA key pair:", err) + return nil, fmt.Errorf("Failed to generate CA key pair: %w", err) } + gen.tlsCert = caKeyPair return gen, nil } diff --git a/ca_test.go b/ca_test.go new file mode 100644 index 0000000..200950f --- /dev/null +++ b/ca_test.go @@ -0,0 +1,20 @@ +package certgen + +import ( + "crypto/x509/pkix" + "github.com/stretchr/testify/assert" + "math/big" + "testing" +) + +func TestMakeCaTls(t *testing.T) { + ca, err := MakeCaTls(2048, pkix.Name{ + Country: []string{"GB"}, + Organization: []string{"certgen"}, + OrganizationalUnit: []string{"test"}, + SerialNumber: "1", + CommonName: "certgen.test", + }, big.NewInt(1)) + assert.NoError(t, err) + assert.Equal(t, "certgen.test", ca.cert.Subject.CommonName) +} diff --git a/client.go b/client.go index 06b2bbf..44c69da 100644 --- a/client.go +++ b/client.go @@ -3,47 +3,70 @@ package certgen import ( "crypto/rand" "crypto/rsa" + "crypto/sha1" "crypto/tls" "crypto/x509" "crypto/x509/pkix" - "log" + "fmt" "math/big" "time" ) -func MakeClientTls(ca *CertGen, name pkix.Name, serialNumber *big.Int) (*CertGen, error) { +// MakeClientTls generates a client TLS certificate using a CA to sign it +// If ca is nil then the client will sign its own certificate +func MakeClientTls(ca *CertGen, bits int, name pkix.Name, serialNumber *big.Int, future Future) (*CertGen, error) { + // generate rsa private key + clientPrivKey, err := rsa.GenerateKey(rand.Reader, bits) + if err != nil { + return nil, fmt.Errorf("failed to generate client private key: %w", err) + } + + // generate SubjectKeyId from sha1 hash of public key bytes + pubKeyBytes := x509.MarshalPKCS1PublicKey(&clientPrivKey.PublicKey) + pubKeyHash := sha1.Sum(pubKeyBytes) + + // base certificate data + now := time.Now() cert := &x509.Certificate{ SerialNumber: serialNumber, Subject: name, - NotBefore: time.Now(), - NotAfter: time.Now().AddDate(10, 0, 0), - SubjectKeyId: []byte{1, 2, 3, 4, 6}, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + NotBefore: now, + NotAfter: future(now), + SubjectKeyId: pubKeyHash[:], + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, KeyUsage: x509.KeyUsageDigitalSignature, } - clientPrivKey, err := rsa.GenerateKey(rand.Reader, 4096) - if err != nil { - log.Fatalln("Failed to generate client private key:", err) - } - + // use current certificate as CA if nil if ca == nil { ca = &CertGen{cert: cert, key: clientPrivKey} } + + // create certificate bytes clientBytes, err := x509.CreateCertificate(rand.Reader, cert, ca.cert, clientPrivKey.Public(), ca.key) if err != nil { - log.Fatalln("Failed to generate client certificate bytes:", err) + return nil, fmt.Errorf("failed to generate client certificate bytes: %w", err) } + + // add the raw certificate bytes so `*x509.Certificate.Equal(*x509.Certificate)` is valid + cert.Raw = clientBytes + + // get private key bytes privKeyBytes := x509.MarshalPKCS1PrivateKey(clientPrivKey) gen := &CertGen{cert: cert, certBytes: clientBytes, key: clientPrivKey, keyBytes: privKeyBytes} + + // generate pem blocks err = gen.generatePem() if err != nil { - return nil, err + return nil, fmt.Errorf("failed to generate PEM encoding: %w", err) } + + // generate key pair caKeyPair, err := tls.X509KeyPair(gen.certPem, gen.keyPem) if err != nil { - log.Fatalln("Failed to generate client key pair:", err) + return nil, fmt.Errorf("failed to generate client key pair: %w", err) } + gen.tlsCert = caKeyPair return gen, nil } diff --git a/client_test.go b/client_test.go new file mode 100644 index 0000000..cb3e5ed --- /dev/null +++ b/client_test.go @@ -0,0 +1,23 @@ +package certgen + +import ( + "crypto/x509/pkix" + "github.com/stretchr/testify/assert" + "math/big" + "testing" + "time" +) + +func TestMakeClientTls(t *testing.T) { + client, err := MakeClientTls(nil, 2048, pkix.Name{ + Country: []string{"GB"}, + Organization: []string{"certgen"}, + OrganizationalUnit: []string{"test"}, + SerialNumber: "2", + CommonName: "certgen.client", + }, big.NewInt(2), func(now time.Time) time.Time { + return now.AddDate(10, 0, 0) + }) + assert.NoError(t, err) + assert.Equal(t, "certgen.client", client.cert.Subject.CommonName) +} diff --git a/future.go b/future.go new file mode 100644 index 0000000..2a817b1 --- /dev/null +++ b/future.go @@ -0,0 +1,6 @@ +package certgen + +import "time" + +// Future is a function for converting the current time to a future time +type Future func(now time.Time) time.Time diff --git a/go.mod b/go.mod index b86ce71..823f955 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,11 @@ -module code.mrmelon54.com/melon/certgen +module github.com/MrMelon54/certgen go 1.18 + +require github.com/stretchr/testify v1.8.4 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum index e69de29..fa4b6e6 100644 --- a/go.sum +++ b/go.sum @@ -0,0 +1,10 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/server.go b/server.go index e5ad2f1..fd02098 100644 --- a/server.go +++ b/server.go @@ -3,50 +3,74 @@ package certgen import ( "crypto/rand" "crypto/rsa" + "crypto/sha1" "crypto/tls" "crypto/x509" "crypto/x509/pkix" - "log" + "fmt" "math/big" "net" "time" ) -func MakeServerTls(ca *CertGen, name pkix.Name, serialNumber *big.Int, dnsNames []string, ipAddresses []net.IP) (*CertGen, error) { +// MakeServerTls generates a server TLS certificate using a CA to sign it +// If ca is nil then the server will sign its own certificate +// dnsNames and ipAddresses can be nil if they are not required on the certificate +func MakeServerTls(ca *CertGen, bits int, name pkix.Name, serialNumber *big.Int, future Future, dnsNames []string, ipAddresses []net.IP) (*CertGen, error) { + // generate rsa private key + serverPrivKey, err := rsa.GenerateKey(rand.Reader, bits) + if err != nil { + return nil, fmt.Errorf("failed to generate server private key: %w", err) + } + + // generate SubjectKeyId from sha1 hash of public key bytes + pubKeyBytes := x509.MarshalPKCS1PublicKey(&serverPrivKey.PublicKey) + pubKeyHash := sha1.Sum(pubKeyBytes) + + // base certificate data + now := time.Now() cert := &x509.Certificate{ SerialNumber: serialNumber, Subject: name, DNSNames: dnsNames, IPAddresses: ipAddresses, - NotBefore: time.Now(), - NotAfter: time.Now().AddDate(10, 0, 0), - SubjectKeyId: []byte{1, 2, 3, 4, 6}, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + NotBefore: now, + NotAfter: future(now), + SubjectKeyId: pubKeyHash[:], + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, KeyUsage: x509.KeyUsageDigitalSignature, } - serverPrivKey, err := rsa.GenerateKey(rand.Reader, 4096) - if err != nil { - log.Fatalln("Failed to generate server private key:", err) - } - + // use current certificate as CA if nil if ca == nil { ca = &CertGen{cert: cert, key: serverPrivKey} } + + // create certificate bytes serverBytes, err := x509.CreateCertificate(rand.Reader, cert, ca.cert, serverPrivKey.Public(), ca.key) if err != nil { - log.Fatalln("Failed to generate server certificate bytes:", err) + return nil, fmt.Errorf("failed to generate server certificate bytes: %w", err) } + + // add the raw certificate bytes so `*x509.Certificate.Equal(*x509.Certificate)` is valid + cert.Raw = serverBytes + + // get private key bytes privKeyBytes := x509.MarshalPKCS1PrivateKey(serverPrivKey) gen := &CertGen{cert: cert, certBytes: serverBytes, key: serverPrivKey, keyBytes: privKeyBytes} + + // generate pem blocks err = gen.generatePem() if err != nil { - return nil, err + return nil, fmt.Errorf("failed to generate PEM encoding: %w", err) } + + // generate key pair caKeyPair, err := tls.X509KeyPair(gen.certPem, gen.keyPem) if err != nil { - log.Fatalln("Failed to generate server key pair:", err) + return nil, fmt.Errorf("failed to generate server key pair: %w", err) } + gen.tlsCert = caKeyPair return gen, nil } diff --git a/server_test.go b/server_test.go new file mode 100644 index 0000000..178a591 --- /dev/null +++ b/server_test.go @@ -0,0 +1,25 @@ +package certgen + +import ( + "crypto/x509/pkix" + "github.com/stretchr/testify/assert" + "math/big" + "net" + "testing" + "time" +) + +func TestMakeServerTls(t *testing.T) { + server, err := MakeServerTls(nil, 2048, pkix.Name{ + Country: []string{"GB"}, + Organization: []string{"certgen"}, + OrganizationalUnit: []string{"test"}, + SerialNumber: "2", + CommonName: "certgen.server", + }, big.NewInt(2), func(now time.Time) time.Time { + return now.AddDate(10, 0, 0) + }, []string{"certgen.server", "*.certgen.server"}, []net.IP{net.IPv4(1, 1, 1, 1), net.IPv6loopback}) + assert.NoError(t, err) + assert.Equal(t, "certgen.server", server.cert.Subject.CommonName) + assert.Equal(t, []string{"certgen.server", "*.certgen.server"}, server.cert.DNSNames) +} diff --git a/tls-leaf.go b/tls-leaf.go index 5067941..7cab307 100644 --- a/tls-leaf.go +++ b/tls-leaf.go @@ -6,10 +6,13 @@ import ( ) func TlsLeaf(cert *tls.Certificate) *x509.Certificate { + // return the existing leaf if cert.Leaf != nil { return cert.Leaf } + if len(cert.Certificate) >= 1 { + // if there is a certificate then validate, parse and set the leaf if a, err := x509.ParseCertificate(cert.Certificate[0]); err == nil { cert.Leaf = a } diff --git a/tls-leaf_test.go b/tls-leaf_test.go new file mode 100644 index 0000000..9d6f6ca --- /dev/null +++ b/tls-leaf_test.go @@ -0,0 +1,22 @@ +package certgen + +import ( + "crypto/x509/pkix" + "github.com/stretchr/testify/assert" + "math/big" + "testing" +) + +func TestTlsLeaf(t *testing.T) { + ca, err := MakeCaTls(2048, pkix.Name{ + Country: []string{"GB"}, + Organization: []string{"certgen"}, + OrganizationalUnit: []string{"test"}, + SerialNumber: "1", + CommonName: "certgen.test", + }, big.NewInt(1)) + assert.NoError(t, err) + + leaf := TlsLeaf(&ca.tlsCert) + assert.True(t, leaf.Equal(ca.cert)) +}