From 609a6640e4ef4b60ea60f5bd1f56ca21326c4917 Mon Sep 17 00:00:00 2001 From: Benjamin Cane Date: Sun, 19 May 2024 07:20:02 -0700 Subject: [PATCH] Adding KeyPair Config for more options --- gencerts.go | 55 +++++++++++++++++ gencerts_test.go | 147 ++++++++++++++++++++++++++++++++++++++++++++++ kpconfig.go | 41 +++++++++++++ kpconfig_test.go | 94 +++++++++++++++++++++++++++++ testcerts.go | 60 ++----------------- testcerts_test.go | 140 ------------------------------------------- 6 files changed, 343 insertions(+), 194 deletions(-) create mode 100644 gencerts.go create mode 100644 gencerts_test.go create mode 100644 kpconfig.go create mode 100644 kpconfig_test.go diff --git a/gencerts.go b/gencerts.go new file mode 100644 index 0000000..2801f2d --- /dev/null +++ b/gencerts.go @@ -0,0 +1,55 @@ +package testcerts + +// GenerateCerts generates a x509 certificate and key. +// It returns the certificate and key as byte slices, and any error that occurred. +// +// cert, key, err := GenerateCerts() +// if err != nil { +// // handle error +// } +func GenerateCerts(domains ...string) ([]byte, []byte, error) { + ca := NewCA() + + // Returning CA for backwards compatibility + if len(domains) == 0 { + return ca.PublicKey(), ca.PrivateKey(), nil + } + + // If domains exist return a regular cert + kp, err := ca.NewKeyPair(domains...) + if err != nil { + return nil, nil, err + } + return kp.PublicKey(), kp.PrivateKey(), nil +} + +// GenerateCertsToFile creates a x509 certificate and key and writes it to the specified file paths. +// +// err := GenerateCertsToFile("/path/to/cert", "/path/to/key") +// if err != nil { +// // handle error +// } +// +// If the specified file paths already exist, it will overwrite the existing files. +func GenerateCertsToFile(certFile, keyFile string) error { + // Create Certs using CA for backwards compatibility + return NewCA().ToFile(certFile, keyFile) +} + +// GenerateCertsToTempFile will create a temporary x509 certificate and key in a randomly generated file using the +// directory path provided. If no directory is specified, the default directory for temporary files as returned by +// os.TempDir will be used. +// +// cert, key, err := GenerateCertsToTempFile("/tmp/") +// if err != nil { +// // handle error +// } +func GenerateCertsToTempFile(dir string) (string, string, error) { + // Create Certs using CA for backwards compatibility + cert, key, err := NewCA().ToTempFile(dir) + if err != nil { + return "", "", err + } + + return cert.Name(), key.Name(), nil +} diff --git a/gencerts_test.go b/gencerts_test.go new file mode 100644 index 0000000..0dc187f --- /dev/null +++ b/gencerts_test.go @@ -0,0 +1,147 @@ +package testcerts + +import ( + "os" + "path/filepath" + "testing" +) + +func TestGeneratingCerts(t *testing.T) { + _, _, err := GenerateCerts() + if err != nil { + t.Errorf("Error while generating certificates - %s", err) + } +} + +func TestGeneratingCertsToFile(t *testing.T) { + t.Run("Test the happy path", func(t *testing.T) { + tempDir, err := os.MkdirTemp("", "") + if err != nil { + t.Errorf("Error creating temporary directory: %s", err) + } + defer os.RemoveAll(tempDir) + + certPath := filepath.Join(tempDir, "cert") + keyPath := filepath.Join(tempDir, "key") + + err = GenerateCertsToFile(certPath, keyPath) + if err != nil { + t.Errorf("Error while generating certificates to files - %s", err) + } + + // Check if Cert file exists + _, err = os.Stat(certPath) + if err != nil { + t.Errorf("Error while generating certificates to files file error - %s", err) + } + + // Check if Key file exists + _, err = os.Stat(keyPath) + if err != nil { + t.Errorf("Error while generating certificates to files file error - %s", err) + } + }) + + t.Run("Testing the unhappy path for cert files", func(t *testing.T) { + tempDir, err := os.MkdirTemp("", "") + if err != nil { + t.Errorf("Error creating temporary directory: %s", err) + } + defer os.RemoveAll(tempDir) + + certPath := filepath.Join(tempDir, "doesntexist", "cert") + keyPath := filepath.Join(tempDir, "key") + + err = GenerateCertsToFile(certPath, keyPath) + if err == nil { + t.Errorf("Expected error when generating a certificate with a bad path got nil") + } + }) + + t.Run("Testing the unhappy path for key files", func(t *testing.T) { + tempDir, err := os.MkdirTemp("", "") + if err != nil { + t.Errorf("Error creating temporary directory: %s", err) + } + defer os.RemoveAll(tempDir) + + certPath := filepath.Join(tempDir, "cert") + keyPath := filepath.Join(tempDir, "doesntexist", "key") + + err = GenerateCertsToFile(certPath, keyPath) + if err == nil { + t.Errorf("Expected error when generating a key with a bad path got nil") + } + }) + + t.Run("Testing the unhappy path for insufficient permissions", func(t *testing.T) { + dir, err := os.MkdirTemp("", "permission-test") + if err != nil { + t.Errorf("Error creating temp directory - %s", err) + } + defer os.RemoveAll(dir) + + // Change permissions of the temp directory so that it can't be written to + err = os.Chmod(dir, 0444) + if err != nil { + t.Errorf("Error changing permissions of temp directory - %s", err) + } + + certPath := filepath.Join(dir, "cert") + keyPath := filepath.Join(dir, "key") + + err = GenerateCertsToFile(certPath, keyPath) + if err == nil { + t.Errorf("Expected error when generating certificate with insufficient permissions, got nil") + } + }) +} + +func TestGenerateCertsToTempFile(t *testing.T) { + t.Run("Test the happy path", func(t *testing.T) { + certFile, keyFile, err := GenerateCertsToTempFile("/tmp") + if err != nil { + t.Errorf("Error while generating certificates to temp files - %s", err) + } + + // Check if Cert file exists + _, err = os.Stat(certFile) + if err != nil { + t.Errorf("Error while generating certificates to temp files file error - %s", err) + } + _ = os.Remove(certFile) + + // Check if Key file exists + _, err = os.Stat(keyFile) + if err != nil { + t.Errorf("Error while generating certificates to temp files file error - %s", err) + } + _ = os.Remove(keyFile) + }) + + t.Run("Testing the unhappy path when creating cert temp file", func(t *testing.T) { + _, _, err := GenerateCertsToTempFile("/doesnotexist") + if err == nil { + t.Errorf("Expected error when generating a certificate with a bad directory path got nil") + } + }) + + t.Run("Testing the unhappy path for insufficient permissions when creating temp file", func(t *testing.T) { + dir, err := os.MkdirTemp("", "permission-test") + if err != nil { + t.Errorf("Error creating temp directory - %s", err) + } + defer os.RemoveAll(dir) + + // Change permissions of the temp directory so that it can't be written to + err = os.Chmod(dir, 0444) + if err != nil { + t.Errorf("Error changing permissions of temp directory - %s", err) + } + + _, _, err = GenerateCertsToTempFile(dir) + if err == nil { + t.Errorf("Expected error when generating a key with a bad directory path got nil") + } + }) +} diff --git a/kpconfig.go b/kpconfig.go new file mode 100644 index 0000000..15ed3c3 --- /dev/null +++ b/kpconfig.go @@ -0,0 +1,41 @@ +package testcerts + +import ( + "errors" + "net" +) + +var ( + // ErrEmptyConfig is returned when a KeyPairConfig is empty. + ErrEmptyConfig = errors.New("empty KeyPairConfig") + + // ErrInvalidIP is returned when an IP address is invalid. + ErrInvalidIP = errors.New("invalid IP address") +) + +// KeyPairConfig represents a configuration for generating an X509 KeyPair. +type KeyPairConfig struct { + // Domains is a list of domains to include in the certificate. + Domains []string + + // IPAddresses is a list of IP addresses to include in the certificate. + IPAddresses []string +} + +// Validate validates the KeyPairConfig ensuring that it is not empty and that +// provided values are valid. +func (c *KeyPairConfig) Validate() error { + // Check if the config is empty. + if len(c.Domains) == 0 && len(c.IPAddresses) == 0 { + return ErrEmptyConfig + } + + // Validate IP addresses. + for _, ip := range c.IPAddresses { + if net.ParseIP(ip) == nil { + return ErrInvalidIP + } + } + + return nil +} diff --git a/kpconfig_test.go b/kpconfig_test.go new file mode 100644 index 0000000..d47bc7b --- /dev/null +++ b/kpconfig_test.go @@ -0,0 +1,94 @@ +package testcerts + +import ( + "testing" +) + +type KPConfigTestCase struct { + name string + cfg KeyPairConfig + err error +} + +func TestKPConfigs(t *testing.T) { + tc := []KPConfigTestCase{ + { + name: "Happy Path - Simple Domain", + cfg: KeyPairConfig{ + Domains: []string{"example.com"}, + }, + err: nil, + }, + { + name: "Happy Path - Multiple Domains", + cfg: KeyPairConfig{ + Domains: []string{"example.com", "example.org"}, + }, + err: nil, + }, + { + name: "Happy Path - Multiple Domains with Wildcard", + cfg: KeyPairConfig{ + Domains: []string{"example.com", "*.example.com"}, + }, + err: nil, + }, + { + name: "Empty Config", + cfg: KeyPairConfig{}, + err: ErrEmptyConfig, + }, + { + name: "Happy Path - Valid IP", + cfg: KeyPairConfig{ + IPAddresses: []string{"127.0.0.1"}, + }, + err: nil, + }, + { + name: "Happy Path - Multiple Valid IPs", + cfg: KeyPairConfig{ + IPAddresses: []string{"127.0.0.1", "10.0.0.0"}, + }, + err: nil, + }, + { + name: "Happy Path - IPv6 Localhost", + cfg: KeyPairConfig{ + IPAddresses: []string{"::1"}, + }, + err: nil, + }, + { + name: "Happy Path - Multiple IPv6 Addresses", + cfg: KeyPairConfig{ + IPAddresses: []string{"::1", "2001:0db8:85a3:0000:0000:8a2e:0370:7334"}, + }, + err: nil, + }, + { + name: "Happy Path - Valid IP and Domain", + cfg: KeyPairConfig{ + IPAddresses: []string{"127.0.0.1", "10.0.0.0"}, + Domains: []string{"example.com", "localhost"}, + }, + err: nil, + }, + { + name: "Invalid IP", + cfg: KeyPairConfig{ + IPAddresses: []string{"127.0.0.1", "not an IP"}, + }, + err: ErrInvalidIP, + }, + } + + for _, c := range tc { + t.Run(c.name, func(t *testing.T) { + err := c.cfg.Validate() + if err != c.err { + t.Errorf("Validation failed, expected error return of %v, got %v", c.err, err) + } + }) + } +} diff --git a/testcerts.go b/testcerts.go index d267694..a594a8a 100644 --- a/testcerts.go +++ b/testcerts.go @@ -163,6 +163,12 @@ func (ca *CertificateAuthority) NewKeyPair(domains ...string) (*KeyPair, error) return kp, nil } +// NewKeyPairWithConfig generates a new KeyPair signed by the CertificateAuthority for the given configuration. +// The configuration is used to populate the Subject Alternative Name field of the certificate. +func (ca *CertificateAuthority) NewKeyPairWithConfig(config KeyPairConfig) (*KeyPair, error) { + return nil, nil +} + // CertPool returns a Certificate Pool of the CertificateAuthority Certificate. func (ca *CertificateAuthority) CertPool() *x509.CertPool { return ca.certPool @@ -315,60 +321,6 @@ func (kp *KeyPair) ConfigureTLSConfig(tlsConfig *tls.Config) *tls.Config { return tlsConfig } -// GenerateCerts generates a x509 certificate and key. -// It returns the certificate and key as byte slices, and any error that occurred. -// -// cert, key, err := GenerateCerts() -// if err != nil { -// // handle error -// } -func GenerateCerts(domains ...string) ([]byte, []byte, error) { - ca := NewCA() - - // Returning CA for backwards compatibility - if len(domains) == 0 { - return ca.PublicKey(), ca.PrivateKey(), nil - } - - // If domains exist return a regular cert - kp, err := ca.NewKeyPair(domains...) - if err != nil { - return nil, nil, err - } - return kp.PublicKey(), kp.PrivateKey(), nil -} - -// GenerateCertsToFile creates a x509 certificate and key and writes it to the specified file paths. -// -// err := GenerateCertsToFile("/path/to/cert", "/path/to/key") -// if err != nil { -// // handle error -// } -// -// If the specified file paths already exist, it will overwrite the existing files. -func GenerateCertsToFile(certFile, keyFile string) error { - // Create Certs using CA for backwards compatibility - return NewCA().ToFile(certFile, keyFile) -} - -// GenerateCertsToTempFile will create a temporary x509 certificate and key in a randomly generated file using the -// directory path provided. If no directory is specified, the default directory for temporary files as returned by -// os.TempDir will be used. -// -// cert, key, err := GenerateCertsToTempFile("/tmp/") -// if err != nil { -// // handle error -// } -func GenerateCertsToTempFile(dir string) (string, string, error) { - // Create Certs using CA for backwards compatibility - cert, key, err := NewCA().ToTempFile(dir) - if err != nil { - return "", "", err - } - - return cert.Name(), key.Name(), nil -} - // genSelfSignedKeyPair will generate a key and self-signed certificate from the provided Certificate. func genSelfSignedKeyPair(cert *x509.Certificate) (*pem.Block, *ecdsa.PrivateKey, error) { // Create a Private Key diff --git a/testcerts_test.go b/testcerts_test.go index bd50f76..c703d95 100644 --- a/testcerts_test.go +++ b/testcerts_test.go @@ -196,146 +196,6 @@ func TestCertsUsage(t *testing.T) { } } -func TestGeneratingCerts(t *testing.T) { - _, _, err := GenerateCerts() - if err != nil { - t.Errorf("Error while generating certificates - %s", err) - } -} - -func TestGeneratingCertsToFile(t *testing.T) { - t.Run("Test the happy path", func(t *testing.T) { - tempDir, err := os.MkdirTemp("", "") - if err != nil { - t.Errorf("Error creating temporary directory: %s", err) - } - defer os.RemoveAll(tempDir) - - certPath := filepath.Join(tempDir, "cert") - keyPath := filepath.Join(tempDir, "key") - - err = GenerateCertsToFile(certPath, keyPath) - if err != nil { - t.Errorf("Error while generating certificates to files - %s", err) - } - - // Check if Cert file exists - _, err = os.Stat(certPath) - if err != nil { - t.Errorf("Error while generating certificates to files file error - %s", err) - } - - // Check if Key file exists - _, err = os.Stat(keyPath) - if err != nil { - t.Errorf("Error while generating certificates to files file error - %s", err) - } - }) - - t.Run("Testing the unhappy path for cert files", func(t *testing.T) { - tempDir, err := os.MkdirTemp("", "") - if err != nil { - t.Errorf("Error creating temporary directory: %s", err) - } - defer os.RemoveAll(tempDir) - - certPath := filepath.Join(tempDir, "doesntexist", "cert") - keyPath := filepath.Join(tempDir, "key") - - err = GenerateCertsToFile(certPath, keyPath) - if err == nil { - t.Errorf("Expected error when generating a certificate with a bad path got nil") - } - }) - - t.Run("Testing the unhappy path for key files", func(t *testing.T) { - tempDir, err := os.MkdirTemp("", "") - if err != nil { - t.Errorf("Error creating temporary directory: %s", err) - } - defer os.RemoveAll(tempDir) - - certPath := filepath.Join(tempDir, "cert") - keyPath := filepath.Join(tempDir, "doesntexist", "key") - - err = GenerateCertsToFile(certPath, keyPath) - if err == nil { - t.Errorf("Expected error when generating a key with a bad path got nil") - } - }) - - t.Run("Testing the unhappy path for insufficient permissions", func(t *testing.T) { - dir, err := os.MkdirTemp("", "permission-test") - if err != nil { - t.Errorf("Error creating temp directory - %s", err) - } - defer os.RemoveAll(dir) - - // Change permissions of the temp directory so that it can't be written to - err = os.Chmod(dir, 0444) - if err != nil { - t.Errorf("Error changing permissions of temp directory - %s", err) - } - - certPath := filepath.Join(dir, "cert") - keyPath := filepath.Join(dir, "key") - - err = GenerateCertsToFile(certPath, keyPath) - if err == nil { - t.Errorf("Expected error when generating certificate with insufficient permissions, got nil") - } - }) -} - -func TestGenerateCertsToTempFile(t *testing.T) { - t.Run("Test the happy path", func(t *testing.T) { - certFile, keyFile, err := GenerateCertsToTempFile("/tmp") - if err != nil { - t.Errorf("Error while generating certificates to temp files - %s", err) - } - - // Check if Cert file exists - _, err = os.Stat(certFile) - if err != nil { - t.Errorf("Error while generating certificates to temp files file error - %s", err) - } - _ = os.Remove(certFile) - - // Check if Key file exists - _, err = os.Stat(keyFile) - if err != nil { - t.Errorf("Error while generating certificates to temp files file error - %s", err) - } - _ = os.Remove(keyFile) - }) - - t.Run("Testing the unhappy path when creating cert temp file", func(t *testing.T) { - _, _, err := GenerateCertsToTempFile("/doesnotexist") - if err == nil { - t.Errorf("Expected error when generating a certificate with a bad directory path got nil") - } - }) - - t.Run("Testing the unhappy path for insufficient permissions when creating temp file", func(t *testing.T) { - dir, err := os.MkdirTemp("", "permission-test") - if err != nil { - t.Errorf("Error creating temp directory - %s", err) - } - defer os.RemoveAll(dir) - - // Change permissions of the temp directory so that it can't be written to - err = os.Chmod(dir, 0444) - if err != nil { - t.Errorf("Error changing permissions of temp directory - %s", err) - } - - _, _, err = GenerateCertsToTempFile(dir) - if err == nil { - t.Errorf("Expected error when generating a key with a bad directory path got nil") - } - }) -} - func TestFullFlow(t *testing.T) { // Create a signed Certificate and Key for "localhost" ca := NewCA()