From 5ae6ad95bf5bda97233a3d2e52bd889efe19c573 Mon Sep 17 00:00:00 2001 From: Joseph Cummings Date: Tue, 27 Feb 2024 15:08:10 +0000 Subject: [PATCH 1/4] Add create-user command to generate user certificates --- certificates/common.go | 39 ++++++ certificates/create_node.go | 40 +----- certificates/create_node_test.go | 23 +++ certificates/create_user.go | 231 +++++++++++++++++++++++++++++++ certificates/create_user_test.go | 77 +++++++++++ main.go | 8 ++ 6 files changed, 380 insertions(+), 38 deletions(-) create mode 100644 certificates/create_user.go create mode 100644 certificates/create_user_test.go diff --git a/certificates/common.go b/certificates/common.go index 204e7b4..44d632f 100644 --- a/certificates/common.go +++ b/certificates/common.go @@ -9,6 +9,9 @@ import ( "os" "path" "text/tabwriter" + "crypto/rsa" + "crypto/x509" + "encoding/pem" ) const defaultKeySize = 2048 @@ -85,3 +88,39 @@ func fileExists(path string, force bool) bool { } return false } + +func readCertificateFromFile(path string) (*x509.Certificate, error) { + pemBytes, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("error reading file: %s", err.Error()) + } + + block, _ := pem.Decode(pemBytes) + if block == nil { + return nil, fmt.Errorf("error decoding PEM data from file: %s", path) + } + + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, fmt.Errorf("error parsing certificate from ASN.1 DER data in file: %s", path) + } + return cert, nil +} + +func readRSAKeyFromFile(path string) (*rsa.PrivateKey, error) { + keyBytes, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("error reading file: %s", err.Error()) + } + + block, _ := pem.Decode(keyBytes) + if block == nil { + return nil, fmt.Errorf("error decoding PEM data from file: %s", path) + } + + key, err := x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + return nil, fmt.Errorf("error parsing RSA key from ASN.1 DER data in file: %s", path) + } + return key, nil +} diff --git a/certificates/create_node.go b/certificates/create_node.go index d746095..e484367 100644 --- a/certificates/create_node.go +++ b/certificates/create_node.go @@ -37,42 +37,6 @@ type CreateNodeArguments struct { Force bool `yaml:"force"` } -func readCertificateFromFile(path string) (*x509.Certificate, error) { - pemBytes, err := os.ReadFile(path) - if err != nil { - return nil, fmt.Errorf("error reading file: %s", err.Error()) - } - - block, _ := pem.Decode(pemBytes) - if block == nil { - return nil, fmt.Errorf("error decoding PEM data from file: %s", path) - } - - cert, err := x509.ParseCertificate(block.Bytes) - if err != nil { - return nil, fmt.Errorf("error parsing certificate from ASN.1 DER data in file: %s", path) - } - return cert, nil -} - -func readRSAKeyFromFile(path string) (*rsa.PrivateKey, error) { - keyBytes, err := os.ReadFile(path) - if err != nil { - return nil, fmt.Errorf("error reading file: %s", err.Error()) - } - - block, _ := pem.Decode(keyBytes) - if block == nil { - return nil, fmt.Errorf("error decoding PEM data from file: %s", path) - } - - key, err := x509.ParsePKCS1PrivateKey(block.Bytes) - if err != nil { - return nil, fmt.Errorf("error parsing RSA key from ASN.1 DER data in file: %s", path) - } - return key, nil -} - func parseIPAddresses(ipAddresses string) ([]net.IP, error) { if len(ipAddresses) == 0 { return []net.IP{}, nil @@ -98,7 +62,7 @@ func parseDNSNames(dnsNames string) ([]string, error) { return dns, nil } -func getOutputDirectory() (string, error) { +func getNodeOutputDirectory() (string, error) { for i := 1; i <= 100; i++ { dir := "node" + strconv.Itoa(i) if _, err := os.Stat(dir); os.IsNotExist(err) { @@ -177,7 +141,7 @@ func (c *CreateNode) Run(args []string) int { outputBaseFileName := "node" if len(outputDir) == 0 { - outputDir, err = getOutputDirectory() + outputDir, err = getNodeOutputDirectory() if err != nil { c.Ui.Error(err.Error()) return 1 diff --git a/certificates/create_node_test.go b/certificates/create_node_test.go index e2d9001..0869c3c 100644 --- a/certificates/create_node_test.go +++ b/certificates/create_node_test.go @@ -66,6 +66,29 @@ func TestGenerateNodeCertificate(t *testing.T) { nodeKeyPath := path.Join(OutputDirNode, NodeCertFileName+".key") assertFilesExist(t, nodeCertPath, nodeKeyPath) + nodeCertificate, err := readCertificateFromFile(nodeCertPath) + assert.NoError(t, err) + + // verify the subject + assert.Equal(t, "CN=EventStoreDB", nodeCertificate.Subject.String()) + + // verify the issuer + assert.Equal(t, caCertificate.Issuer.String(), nodeCertificate.Issuer.String()) + + // verify the EKUs + assert.Equal(t, 2, len(nodeCertificate.ExtKeyUsage)) + assert.Equal(t, x509.ExtKeyUsageClientAuth, nodeCertificate.ExtKeyUsage[0]) + assert.Equal(t, x509.ExtKeyUsageServerAuth, nodeCertificate.ExtKeyUsage[1]) + assert.Equal(t, 0, len(nodeCertificate.UnknownExtKeyUsage)) + + // verify the IP SANs + assert.Equal(t, 1, len(nodeCertificate.IPAddresses)) + assert.Equal(t, "127.0.0.1", nodeCertificate.IPAddresses[0].String()) + + // verify the DNS SANs + assert.Equal(t, 1, len(nodeCertificate.DNSNames)) + assert.Equal(t, "localhost", nodeCertificate.DNSNames[0]) + cleanup() }) diff --git a/certificates/create_user.go b/certificates/create_user.go new file mode 100644 index 0000000..1d13064 --- /dev/null +++ b/certificates/create_user.go @@ -0,0 +1,231 @@ +package certificates + +import ( + "bytes" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "errors" + "flag" + "fmt" + "os" + "path" + "strconv" + "strings" + "time" + + multierror "github.com/hashicorp/go-multierror" + "github.com/mitchellh/cli" +) + +type CreateUser struct { + Ui cli.Ui +} + +type CreateUserArguments struct { + Username string `yaml:"username"` + CACertificatePath string `yaml:"ca-certificate"` + CAKeyPath string `yaml:"ca-key"` + Days int `yaml:"days"` + OutputDir string `yaml:"out"` +} + +func getUserOutputDirectory(username string) (string, error) { + dir := "user-" + username + if _, err := os.Stat(dir); os.IsNotExist(err) { + return dir, nil + } + + for i := 1; i <= 100; i++ { + dir = "user-" + username + strconv.Itoa(i) + if _, err := os.Stat(dir); os.IsNotExist(err) { + return dir, nil + } + } + + return "", fmt.Errorf("could not obtain a proper name for output directory") +} + +func (c *CreateUser) Run(args []string) int { + var config CreateUserArguments + + flags := flag.NewFlagSet("create_user", flag.ContinueOnError) + flags.Usage = func() { c.Ui.Info(c.Help()) } + flags.StringVar(&config.Username, "username", "", "the EventStoreDB user") + flags.StringVar(&config.CACertificatePath, "ca-certificate", "./ca/ca.crt", "the path to the CA certificate file") + flags.StringVar(&config.CAKeyPath, "ca-key", "./ca/ca.key", "the path to the CA key file") + flags.IntVar(&config.Days, "days", 0, "the validity period of the certificate in days") + flags.StringVar(&config.OutputDir, "out", "", "The output directory") + + if err := flags.Parse(args); err != nil { + return 1 + } + + validationErrors := new(multierror.Error) + + if len(config.Username) == 0 { + multierror.Append(validationErrors, errors.New("username is a required field")) + } + + if len(config.CACertificatePath) == 0 { + multierror.Append(validationErrors, errors.New("ca-certificate is a required field")) + } + + if len(config.CAKeyPath) == 0 { + multierror.Append(validationErrors, errors.New("ca-key is a required field")) + } + + if config.Days < 0 { + multierror.Append(validationErrors, errors.New("days must be positive")) + } + + if validationErrors.ErrorOrNil() != nil { + c.Ui.Error(validationErrors.Error()) + return 1 + } + + caCert, err := readCertificateFromFile(config.CACertificatePath) + if err != nil { + c.Ui.Error(err.Error()) + return 1 + } + + caKey, err := readRSAKeyFromFile(config.CAKeyPath) + if err != nil { + err := fmt.Errorf("error: %s. please note that only RSA keys are currently supported", err.Error()) + c.Ui.Error(err.Error()) + return 1 + } + + outputDir := config.OutputDir + outputBaseFileName := "user-" + config.Username + + if len(outputDir) == 0 { + outputDir, err = getUserOutputDirectory(config.Username) + if err != nil { + c.Ui.Error(err.Error()) + return 1 + } + outputBaseFileName = outputDir + } + + /*default validity period*/ + years := 1 + days := 0 + + if config.Days != 0 { + days = config.Days + years = 0 + } + + err = generateUserCertificate(config.Username, caCert, caKey, years, days, outputDir, outputBaseFileName) + if err != nil { + c.Ui.Error(err.Error()) + return 1 + } + + if isBoringEnabled() { + c.Ui.Output(fmt.Sprintf("A user certificate & key file have been generated in the '%s' directory (FIPS mode enabled).", outputDir)) + } else { + c.Ui.Output(fmt.Sprintf("A user certificate & key file have been generated in the '%s' directory.", outputDir)) + } + + return 0 +} + +func generateUserCertificate(username string, caCert *x509.Certificate, caPrivateKey *rsa.PrivateKey, years int, days int, outputDir string, outputBaseFileName string) error { + serialNumber, err := generateSerialNumber(128) + if err != nil { + return fmt.Errorf("could not generate 128-bit serial number: %s", err.Error()) + } + + privateKey, err := rsa.GenerateKey(rand.Reader, defaultKeySize) + if err != nil { + return fmt.Errorf("could not generate RSA private key: %s", err.Error()) + } + + subjectKeyID := generateKeyIDFromRSAPublicKey(privateKey.N, privateKey.E) + authorityKeyID := generateKeyIDFromRSAPublicKey(caPrivateKey.N, caPrivateKey.E) + + cert := &x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + CommonName: username, + }, + IsCA: false, + BasicConstraintsValid: true, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(years, 0, days), + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + SubjectKeyId: subjectKeyID, + AuthorityKeyId: authorityKeyID, + } + + privateKeyPem := new(bytes.Buffer) + pem.Encode(privateKeyPem, &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(privateKey), + }) + if err != nil { + return fmt.Errorf("could not encode private key to PEM format: %s", err.Error()) + } + + certBytes, err := x509.CreateCertificate(rand.Reader, cert, caCert, &privateKey.PublicKey, caPrivateKey) + if err != nil { + return fmt.Errorf("could not generate certificate: %s", err.Error()) + } + + certPem := new(bytes.Buffer) + err = pem.Encode(certPem, &pem.Block{ + Type: "CERTIFICATE", + Bytes: certBytes, + }) + if err != nil { + return fmt.Errorf("could not encode certificate to PEM format: %s", err.Error()) + } + + err = os.Mkdir(outputDir, 0755) + if err != nil { + if !os.IsExist(err) { + return fmt.Errorf("could not create directory %s: %s", outputDir, err.Error()) + } + if os.IsExist(err) { + return fmt.Errorf("output directory: %s already exists. please delete it and try again", outputDir) + } + } + + certFile := fmt.Sprintf("%s.crt", outputBaseFileName) + err = os.WriteFile(path.Join(outputDir, certFile), certPem.Bytes(), 0444) + if err != nil { + return fmt.Errorf("error writing certificate to %s: %s", certFile, err.Error()) + } + + keyFile := fmt.Sprintf("%s.key", outputBaseFileName) + err = os.WriteFile(path.Join(outputDir, keyFile), privateKeyPem.Bytes(), 0400) + if err != nil { + return fmt.Errorf("error writing private key to %s: %s", keyFile, err.Error()) + } + + return nil + +} +func (c *CreateUser) Help() string { + helpText := ` +Usage: create_user [options] + Generate a user TLS certificate to be used with EventStoreDB clients +Options: + -username The name of the EventStoreDB user + -ca-certificate The path to the CA certificate file (default: ./ca/ca.crt) + -ca-key The path to the CA key file (default: ./ca/ca.key) + -days The validity period of the certificates in days (default: 1 year) + -out The output directory (default: ./user-) +` + return strings.TrimSpace(helpText) +} + +func (c *CreateUser) Synopsis() string { + return "Generate a user TLS certificate to be used with EventStoreDB clients" +} diff --git a/certificates/create_user_test.go b/certificates/create_user_test.go new file mode 100644 index 0000000..a969f63 --- /dev/null +++ b/certificates/create_user_test.go @@ -0,0 +1,77 @@ +package certificates + +import ( + "crypto/rsa" + "crypto/x509" + "os" + "path" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGenerateUserCertificate(t *testing.T) { + + t.Run("nominal-case", func(t *testing.T) { + + years := 1 + days := 0 + username := "bob" + outputDirCA := "./ca" + outputDirUser := "./user-bob" + var caCert *x509.Certificate + var caKey *rsa.PrivateKey + var userCertFileName = "user-bob" + + // Clean up from previous runs + os.RemoveAll(outputDirCA) + os.RemoveAll(outputDirUser) + + certificateError := generateCACertificate(years, days, outputDirCA, caCert, caKey, false) + + certFilePath := path.Join(outputDirCA, "ca.crt") + keyFilePath := path.Join(outputDirCA, "ca.key") + + _, certPathError := os.Stat(certFilePath) + _, keyPathError := os.Stat(keyFilePath) + + assert.NoError(t, certificateError) + assert.False(t, os.IsNotExist(certPathError)) + assert.False(t, os.IsNotExist(keyPathError)) + + caCertificate, err := readCertificateFromFile(certFilePath) + assert.NoError(t, err) + caPrivateKey, err := readRSAKeyFromFile(keyFilePath) + assert.NoError(t, err) + + certificateError = generateUserCertificate(username, caCertificate, caPrivateKey, years, days, outputDirUser, userCertFileName) + + userCertPath := path.Join(outputDirUser, "user-bob.crt") + userKeyPath := path.Join(outputDirUser, "user-bob.key") + + _, userCertPathError := os.Stat(userCertPath) + _, userKeyPathError := os.Stat(userKeyPath) + + assert.NoError(t, certificateError) + assert.False(t, os.IsNotExist(userCertPathError)) + assert.False(t, os.IsNotExist(userKeyPathError)) + + userCertificate, err := readCertificateFromFile(userCertPath) + assert.NoError(t, err) + + // verify the subject + assert.Equal(t, "CN=bob", userCertificate.Subject.String()) + + // verify the issuer + assert.Equal(t, caCertificate.Issuer.String(), userCertificate.Issuer.String()) + + // verify the EKUs + assert.Equal(t, 1, len(userCertificate.ExtKeyUsage)) + assert.Equal(t, x509.ExtKeyUsageClientAuth, userCertificate.ExtKeyUsage[0]) + assert.Equal(t, 0, len(userCertificate.UnknownExtKeyUsage)) + + // Clean up + os.RemoveAll(outputDirCA) + os.RemoveAll(outputDirUser) + }) +} diff --git a/main.go b/main.go index f8ed919..54b3e7a 100644 --- a/main.go +++ b/main.go @@ -61,6 +61,14 @@ func main() { }, }, nil }, + "create-user": func() (cli.Command, error) { + return &certificates.CreateUser{ + Ui: &cli.ColoredUi{ + Ui: ui, + OutputColor: cli.UiColorBlue, + }, + }, nil + }, } c.HelpFunc = createGeneralHelpFunc(appName, flags) From 6aa6698ebe6a697596ab347a547499102e84cf7c Mon Sep 17 00:00:00 2001 From: Joseph Cummings Date: Thu, 29 Feb 2024 15:42:42 +0000 Subject: [PATCH 2/4] Refactor and cleanup --- .gitignore | 1 + certificates/boring_linux_test.go | 15 +++++ certificates/common.go | 12 ++-- certificates/common_test.go | 44 ++++++++++--- certificates/create_ca.go | 6 +- certificates/create_ca_test.go | 14 ++--- certificates/create_certs.go | 4 +- certificates/create_node.go | 6 +- certificates/create_node_test.go | 84 ++++++++----------------- certificates/create_user.go | 98 +++++++++++------------------ certificates/create_user_test.go | 101 ++++++++++++++++-------------- 11 files changed, 191 insertions(+), 194 deletions(-) create mode 100644 certificates/boring_linux_test.go diff --git a/.gitignore b/.gitignore index 846f1f2..2630087 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,4 @@ ca/ es-gencert-cli certs.yml +.DS_Store diff --git a/certificates/boring_linux_test.go b/certificates/boring_linux_test.go new file mode 100644 index 0000000..e726b4b --- /dev/null +++ b/certificates/boring_linux_test.go @@ -0,0 +1,15 @@ +//go:build linux +// +build linux + +package certificates + +import ( + "crypto/boring" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestBoringCryptoAvailable(t *testing.T) { + assert.True(t, boring.Enabled()) +} diff --git a/certificates/common.go b/certificates/common.go index 44d632f..1c5d498 100644 --- a/certificates/common.go +++ b/certificates/common.go @@ -3,15 +3,15 @@ package certificates import ( "bytes" "crypto/rand" + "crypto/rsa" "crypto/sha256" + "crypto/x509" + "encoding/pem" "fmt" "math/big" "os" "path" "text/tabwriter" - "crypto/rsa" - "crypto/x509" - "encoding/pem" ) const defaultKeySize = 2048 @@ -52,7 +52,7 @@ func writeHelpOption(w *tabwriter.Writer, title string, description string) { fmt.Fprintf(w, "\t-%s\t%s\n", title, description) } -func writeCACertAndKey(outputDir string, fileName string, certPem, privateKeyPem *bytes.Buffer, force bool) error { +func writeCertAndKey(outputDir string, fileName string, certPem, privateKeyPem *bytes.Buffer, force bool) error { certFile := path.Join(outputDir, fileName+".crt") keyFile := path.Join(outputDir, fileName+".key") @@ -71,12 +71,12 @@ func writeCACertAndKey(outputDir string, fileName string, certPem, privateKeyPem err := writeFileWithDir(certFile, certPem.Bytes(), 0444) if err != nil { - return fmt.Errorf("error writing CA certificate to %s: %s", certFile, err.Error()) + return fmt.Errorf("error writing certificate to %s: %s", certFile, err.Error()) } err = writeFileWithDir(keyFile, privateKeyPem.Bytes(), 0400) if err != nil { - return fmt.Errorf("error writing CA's private key to %s: %s", keyFile, err.Error()) + return fmt.Errorf("error writing certificate private key to %s: %s", keyFile, err.Error()) } return nil diff --git a/certificates/common_test.go b/certificates/common_test.go index 377eea4..1c4d9f9 100644 --- a/certificates/common_test.go +++ b/certificates/common_test.go @@ -1,14 +1,44 @@ -//go:build linux - package certificates import ( - "crypto/boring" - "testing" - + "crypto/rsa" + "crypto/x509" "github.com/stretchr/testify/assert" + "os" + "path" + "testing" ) -func TestBoringCryptoAvailable(t *testing.T) { - assert.True(t, boring.Enabled()) +func assertFilesExist(t *testing.T, files ...string) { + for _, file := range files { + _, err := os.Stat(file) + assert.False(t, os.IsNotExist(err)) + } +} + +func generateAndAssertCACert(t *testing.T, years int, days int, outputDirCa string, force bool) (*x509.Certificate, *rsa.PrivateKey) { + certificateError := generateCACertificate(years, days, outputDirCa, nil, nil, force) + assert.NoError(t, certificateError) + + certFilePath := path.Join(outputDirCa, "ca.crt") + keyFilePath := path.Join(outputDirCa, "ca.key") + assertFilesExist(t, certFilePath, keyFilePath) + + caCertificate, err := readCertificateFromFile(certFilePath) + assert.NoError(t, err) + caPrivateKey, err := readRSAKeyFromFile(keyFilePath) + assert.NoError(t, err) + + return caCertificate, caPrivateKey +} + +func cleanupDirsForTest(t *testing.T, dirs ...string) { + cleanupDirs := func() { + for _, dir := range dirs { + os.RemoveAll(dir) + } + } + + cleanupDirs() + t.Cleanup(cleanupDirs) } diff --git a/certificates/create_ca.go b/certificates/create_ca.go index d132ce3..a7d3337 100644 --- a/certificates/create_ca.go +++ b/certificates/create_ca.go @@ -40,7 +40,7 @@ func (c *CreateCA) Run(args []string) int { flags.StringVar(&config.OutputDir, "out", "./ca", "The output directory") flags.StringVar(&config.CACertificatePath, "ca-certificate", "", "the path to a CA certificate file") flags.StringVar(&config.CAKeyPath, "ca-key", "", "the path to a CA key file") - flags.BoolVar(&config.Force, "force", false, "Force overwrite of existing files without prompting") + flags.BoolVar(&config.Force, "force", false, forceOption) if err := flags.Parse(args); err != nil { return 1 @@ -184,7 +184,7 @@ func generateCACertificate(years int, days int, outputDir string, caCert *x509.C return fmt.Errorf("could not encode certificate to PEM format: %s", err.Error()) } - err = writeCACertAndKey(outputDir, "ca", certPem, privateKeyPem, force) + err = writeCertAndKey(outputDir, "ca", certPem, privateKeyPem, force) return err } @@ -195,7 +195,7 @@ func (c *CreateCA) Help() string { w := tabwriter.NewWriter(&buffer, 0, 0, 2, ' ', 0) fmt.Fprintln(w, "Usage: create_ca [options]") - fmt.Fprintln(w, "Generate a root/intermediate CA TLS certificate to be used with EventStoreDB.") + fmt.Fprintln(w, c.Synopsis()) fmt.Fprintln(w, "Options:") writeHelpOption(w, "days", "The validity period of the certificate in days (default: 5 years).") diff --git a/certificates/create_ca_test.go b/certificates/create_ca_test.go index d6aad96..36ab422 100644 --- a/certificates/create_ca_test.go +++ b/certificates/create_ca_test.go @@ -3,24 +3,20 @@ package certificates import ( "crypto/rsa" "crypto/x509" - "os" "path" "testing" "github.com/stretchr/testify/assert" ) -func setupTestEnvironment(t *testing.T, override bool) (years int, days int, outputDir string, caCert *x509.Certificate, caKey *rsa.PrivateKey) { +func setupTestEnvironmentForCaTests(t *testing.T) (years int, days int, outputDir string, caCert *x509.Certificate, caKey *rsa.PrivateKey) { years = 1 days = 0 outputDir = "./ca" caCert = nil caKey = nil - t.Cleanup(func() { - os.RemoveAll(outputDir) - }) - + cleanupDirsForTest(t, outputDir) return } @@ -59,7 +55,7 @@ func testGenerateCACertificate(t *testing.T, years int, days int, outputDir stri func TestGenerateCACertificate(t *testing.T) { t.Run("nominal-case", func(t *testing.T) { - years, days, outputDir, caCert, caKey := setupTestEnvironment(t, false) + years, days, outputDir, caCert, caKey := setupTestEnvironmentForCaTests(t) err := generateCACertificate(years, days, outputDir, caCert, caKey, false) @@ -70,12 +66,12 @@ func TestGenerateCACertificate(t *testing.T) { }) t.Run("directory-exists", func(t *testing.T) { - years, days, outputDir, caCert, caKey := setupTestEnvironment(t, false) + years, days, outputDir, caCert, caKey := setupTestEnvironmentForCaTests(t) testGenerateCACertificate(t, years, days, outputDir, caCert, caKey, false) }) t.Run("directory-exists-force", func(t *testing.T) { - years, days, outputDir, caCert, caKey := setupTestEnvironment(t, true) + years, days, outputDir, caCert, caKey := setupTestEnvironmentForCaTests(t) testGenerateCACertificate(t, years, days, outputDir, caCert, caKey, true) }) } diff --git a/certificates/create_certs.go b/certificates/create_certs.go index fde5fac..4594eeb 100644 --- a/certificates/create_certs.go +++ b/certificates/create_certs.go @@ -36,7 +36,7 @@ func (c *CreateCertificates) Run(args []string) int { flags := flag.NewFlagSet("create_certs", flag.ContinueOnError) flags.Usage = func() { c.Ui.Info(c.Help()) } flags.StringVar(&arguments.ConfigPath, "config-file", "./certs.yml", "The config yml file") - flags.BoolVar(&arguments.Force, "force", false, "Force overwrite of existing files without prompting") + flags.BoolVar(&arguments.Force, "force", false, forceOption) if err := flags.Parse(args); err != nil { return 1 @@ -162,7 +162,7 @@ func (c *CreateCertificates) Help() string { w := tabwriter.NewWriter(&buffer, 0, 0, 2, ' ', 0) fmt.Fprintln(w, "Usage: create_certs [options]") - fmt.Fprintln(w, "Generate ca and node Certificates from an yml configuration file.") + fmt.Fprintln(w, c.Synopsis()) fmt.Fprintln(w, "Options:") writeHelpOption(w, "config-file", "The path to the yml config file.") diff --git a/certificates/create_node.go b/certificates/create_node.go index e484367..bf05ea6 100644 --- a/certificates/create_node.go +++ b/certificates/create_node.go @@ -84,7 +84,7 @@ func (c *CreateNode) Run(args []string) int { flags.StringVar(&config.DNSNames, "dns-names", "", "comma-separated list of DNS names of the node") flags.IntVar(&config.Days, "days", 0, "the validity period of the certificate in days") flags.StringVar(&config.OutputDir, "out", "", "The output directory") - flags.BoolVar(&config.Force, "force", false, "Force overwrite of existing files without prompting") + flags.BoolVar(&config.Force, "force", false, forceOption) if err := flags.Parse(args); err != nil { return 1 @@ -241,7 +241,7 @@ func generateNodeCertificate(caCert *x509.Certificate, caPrivateKey *rsa.Private return fmt.Errorf("could not encode certificate to PEM format: %s", err.Error()) } - err = writeCACertAndKey(outputDir, outputBaseFileName, certPem, privateKeyPem, force) + err = writeCertAndKey(outputDir, outputBaseFileName, certPem, privateKeyPem, force) return err } @@ -252,7 +252,7 @@ func (c *CreateNode) Help() string { w := tabwriter.NewWriter(&buffer, 0, 0, 2, ' ', 0) // 2 spaces minimum gap between columns fmt.Fprintln(w, "Usage: create_node [options]") - fmt.Fprintln(w, "Generate a node/server TLS certificate to be used with EventStoreDB.") + fmt.Fprintln(w, c.Synopsis()) fmt.Fprintln(w, "Options:") writeHelpOption(w, "ca-certificate", "The path to the CA certificate file (default: ./ca/ca.crt).") diff --git a/certificates/create_node_test.go b/certificates/create_node_test.go index 0869c3c..feeacd4 100644 --- a/certificates/create_node_test.go +++ b/certificates/create_node_test.go @@ -1,69 +1,41 @@ package certificates import ( - "crypto/rsa" "crypto/x509" - "os" "path" "testing" "github.com/stretchr/testify/assert" ) -const ( - Years = 1 - Days = 0 - OutputDirCA = "./ca" - OutputDirNode = "./node" - NodeCertFileName = "node" - IPAddresses = "127.0.0.1" - CommonName = "EventStoreDB" -) - -var DnsNames = []string{"localhost"} - -func cleanup() { - os.RemoveAll(OutputDirCA) - os.RemoveAll(OutputDirNode) -} - -func assertFilesExist(t *testing.T, files ...string) { - for _, file := range files { - _, err := os.Stat(file) - assert.False(t, os.IsNotExist(err)) - } -} - -func generateAndAssertCACert(t *testing.T, forceFlag bool) (*x509.Certificate, *rsa.PrivateKey) { - certificateError := generateCACertificate(Years, Days, OutputDirCA, nil, nil, forceFlag) - assert.NoError(t, certificateError) - - certFilePath := path.Join(OutputDirCA, "ca.crt") - keyFilePath := path.Join(OutputDirCA, "ca.key") - assertFilesExist(t, certFilePath, keyFilePath) - - caCertificate, err := readCertificateFromFile(certFilePath) - assert.NoError(t, err) - caPrivateKey, err := readRSAKeyFromFile(keyFilePath) - assert.NoError(t, err) - - return caCertificate, caPrivateKey +func setupTestEnvironmentForNodeTests(t *testing.T) (years int, days int, outputDirCa string, outputDirNode string, nodeCertFileName string, ipAddresses string, commonName string, dnsNames []string) { + years = 1 + days = 0 + outputDirCa = "./ca" + outputDirNode = "./node" + nodeCertFileName = "node" + ipAddresses = "127.0.0.1" + commonName = "EventStoreDB" + dnsNames = []string{"localhost"} + + cleanupDirsForTest(t, outputDirCa, outputDirNode) + return } func TestGenerateNodeCertificate(t *testing.T) { t.Run("nominal-case", func(t *testing.T) { - cleanup() + years, days, outputDirCa, outputDirNode, nodeCertFileName, ipAddresses, commonName, dnsNames := setupTestEnvironmentForNodeTests(t) - caCertificate, caPrivateKey := generateAndAssertCACert(t, false) - ips, err := parseIPAddresses(IPAddresses) + caCertificate, caPrivateKey := generateAndAssertCACert(t, years, days, outputDirCa, false) + ips, err := parseIPAddresses(ipAddresses) assert.NoError(t, err) - certificateError := generateNodeCertificate(caCertificate, caPrivateKey, ips, DnsNames, Years, Days, OutputDirNode, NodeCertFileName, CommonName, false) + certificateError := generateNodeCertificate(caCertificate, caPrivateKey, ips, dnsNames, years, days, outputDirNode, nodeCertFileName, commonName, false) assert.NoError(t, certificateError) - nodeCertPath := path.Join(OutputDirNode, NodeCertFileName+".crt") - nodeKeyPath := path.Join(OutputDirNode, NodeCertFileName+".key") + nodeCertPath := path.Join(outputDirNode, nodeCertFileName+".crt") + nodeKeyPath := path.Join(outputDirNode, nodeCertFileName+".key") assertFilesExist(t, nodeCertPath, nodeKeyPath) nodeCertificate, err := readCertificateFromFile(nodeCertPath) @@ -88,28 +60,26 @@ func TestGenerateNodeCertificate(t *testing.T) { // verify the DNS SANs assert.Equal(t, 1, len(nodeCertificate.DNSNames)) assert.Equal(t, "localhost", nodeCertificate.DNSNames[0]) - - cleanup() }) t.Run("force-flag", func(t *testing.T) { - cleanup() + years, days, outputDirCa, outputDirNode, nodeCertFileName, ipAddresses, commonName, dnsNames := setupTestEnvironmentForNodeTests(t) - caCertificate, caPrivateKey := generateAndAssertCACert(t, false) - ips, err := parseIPAddresses(IPAddresses) + caCertificate, caPrivateKey := generateAndAssertCACert(t, years, days, outputDirCa, false) + ips, err := parseIPAddresses(ipAddresses) assert.NoError(t, err) - nodeCertFilePath := path.Join(OutputDirNode, NodeCertFileName+".crt") - nodeKeyFilePath := path.Join(OutputDirNode, NodeCertFileName+".key") + nodeCertFilePath := path.Join(outputDirNode, nodeCertFileName+".crt") + nodeKeyFilePath := path.Join(outputDirNode, nodeCertFileName+".key") - generateNodeCertificate(caCertificate, caPrivateKey, ips, DnsNames, Years, Days, OutputDirNode, NodeCertFileName, CommonName, false) + generateNodeCertificate(caCertificate, caPrivateKey, ips, dnsNames, years, days, outputDirNode, nodeCertFileName, commonName, false) nodeCertFile, err := readCertificateFromFile(nodeCertFilePath) assert.NoError(t, err) nodeKeyFile, err := readRSAKeyFromFile(nodeKeyFilePath) assert.NoError(t, err) // try to generate again without force - err = generateNodeCertificate(caCertificate, caPrivateKey, ips, DnsNames, Years, Days, OutputDirNode, NodeCertFileName, CommonName, false) + err = generateNodeCertificate(caCertificate, caPrivateKey, ips, dnsNames, years, days, outputDirNode, nodeCertFileName, commonName, false) assert.Error(t, err) nodeCertFileAfter, err := readCertificateFromFile(nodeCertFilePath) assert.NoError(t, err) @@ -119,7 +89,7 @@ func TestGenerateNodeCertificate(t *testing.T) { assert.Equal(t, nodeKeyFile, nodeKeyFileAfter, "Expected node key to be the same") // try to generate again with force - err = generateNodeCertificate(caCertificate, caPrivateKey, ips, DnsNames, Years, Days, OutputDirNode, NodeCertFileName, CommonName, true) + err = generateNodeCertificate(caCertificate, caPrivateKey, ips, dnsNames, years, days, outputDirNode, nodeCertFileName, commonName, true) assert.NoError(t, err) nodeCertFileAfterWithForce, err := readCertificateFromFile(nodeCertFilePath) assert.NoError(t, err) @@ -127,7 +97,5 @@ func TestGenerateNodeCertificate(t *testing.T) { assert.NoError(t, err) assert.NotEqual(t, nodeCertFileAfter, nodeCertFileAfterWithForce, "Expected node certificate to be different") assert.NotEqual(t, nodeKeyFileAfter, nodeKeyFileAfterWithForce, "Expected node key key to be different") - - cleanup() }) } diff --git a/certificates/create_user.go b/certificates/create_user.go index 1d13064..903e915 100644 --- a/certificates/create_user.go +++ b/certificates/create_user.go @@ -10,10 +10,9 @@ import ( "errors" "flag" "fmt" - "os" "path" - "strconv" "strings" + "text/tabwriter" "time" multierror "github.com/hashicorp/go-multierror" @@ -30,22 +29,7 @@ type CreateUserArguments struct { CAKeyPath string `yaml:"ca-key"` Days int `yaml:"days"` OutputDir string `yaml:"out"` -} - -func getUserOutputDirectory(username string) (string, error) { - dir := "user-" + username - if _, err := os.Stat(dir); os.IsNotExist(err) { - return dir, nil - } - - for i := 1; i <= 100; i++ { - dir = "user-" + username + strconv.Itoa(i) - if _, err := os.Stat(dir); os.IsNotExist(err) { - return dir, nil - } - } - - return "", fmt.Errorf("could not obtain a proper name for output directory") + Force bool `yaml:"force"` } func (c *CreateUser) Run(args []string) int { @@ -58,6 +42,7 @@ func (c *CreateUser) Run(args []string) int { flags.StringVar(&config.CAKeyPath, "ca-key", "./ca/ca.key", "the path to the CA key file") flags.IntVar(&config.Days, "days", 0, "the validity period of the certificate in days") flags.StringVar(&config.OutputDir, "out", "", "The output directory") + flags.BoolVar(&config.Force, "force", false, forceOption) if err := flags.Parse(args); err != nil { return 1 @@ -103,12 +88,18 @@ func (c *CreateUser) Run(args []string) int { outputBaseFileName := "user-" + config.Username if len(outputDir) == 0 { - outputDir, err = getUserOutputDirectory(config.Username) - if err != nil { - c.Ui.Error(err.Error()) - return 1 - } - outputBaseFileName = outputDir + outputDir = outputBaseFileName + } + + // check if user certificates already exist + if fileExists(path.Join(outputDir, outputBaseFileName+".crt"), config.Force) { + c.Ui.Error(ErrFileExists) + return 1 + } + + if fileExists(path.Join(outputDir, outputBaseFileName+".key"), config.Force) { + c.Ui.Error(ErrFileExists) + return 1 } /*default validity period*/ @@ -120,7 +111,7 @@ func (c *CreateUser) Run(args []string) int { years = 0 } - err = generateUserCertificate(config.Username, caCert, caKey, years, days, outputDir, outputBaseFileName) + err = generateUserCertificate(config.Username, outputBaseFileName, caCert, caKey, years, days, outputDir, config.Force) if err != nil { c.Ui.Error(err.Error()) return 1 @@ -135,7 +126,7 @@ func (c *CreateUser) Run(args []string) int { return 0 } -func generateUserCertificate(username string, caCert *x509.Certificate, caPrivateKey *rsa.PrivateKey, years int, days int, outputDir string, outputBaseFileName string) error { +func generateUserCertificate(username string, outputBaseFileName string, caCert *x509.Certificate, caPrivateKey *rsa.PrivateKey, years int, days int, outputDir string, force bool) error { serialNumber, err := generateSerialNumber(128) if err != nil { return fmt.Errorf("could not generate 128-bit serial number: %s", err.Error()) @@ -159,7 +150,7 @@ func generateUserCertificate(username string, caCert *x509.Certificate, caPrivat NotBefore: time.Now(), NotAfter: time.Now().AddDate(years, 0, days), KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, SubjectKeyId: subjectKeyID, AuthorityKeyId: authorityKeyID, } @@ -187,43 +178,30 @@ func generateUserCertificate(username string, caCert *x509.Certificate, caPrivat return fmt.Errorf("could not encode certificate to PEM format: %s", err.Error()) } - err = os.Mkdir(outputDir, 0755) - if err != nil { - if !os.IsExist(err) { - return fmt.Errorf("could not create directory %s: %s", outputDir, err.Error()) - } - if os.IsExist(err) { - return fmt.Errorf("output directory: %s already exists. please delete it and try again", outputDir) - } - } + err = writeCertAndKey(outputDir, outputBaseFileName, certPem, privateKeyPem, force) - certFile := fmt.Sprintf("%s.crt", outputBaseFileName) - err = os.WriteFile(path.Join(outputDir, certFile), certPem.Bytes(), 0444) - if err != nil { - return fmt.Errorf("error writing certificate to %s: %s", certFile, err.Error()) - } + return err +} - keyFile := fmt.Sprintf("%s.key", outputBaseFileName) - err = os.WriteFile(path.Join(outputDir, keyFile), privateKeyPem.Bytes(), 0400) - if err != nil { - return fmt.Errorf("error writing private key to %s: %s", keyFile, err.Error()) - } +func (c *CreateUser) Help() string { + var buffer bytes.Buffer - return nil + w := tabwriter.NewWriter(&buffer, 0, 0, 2, ' ', 0) -} -func (c *CreateUser) Help() string { - helpText := ` -Usage: create_user [options] - Generate a user TLS certificate to be used with EventStoreDB clients -Options: - -username The name of the EventStoreDB user - -ca-certificate The path to the CA certificate file (default: ./ca/ca.crt) - -ca-key The path to the CA key file (default: ./ca/ca.key) - -days The validity period of the certificates in days (default: 1 year) - -out The output directory (default: ./user-) -` - return strings.TrimSpace(helpText) + fmt.Fprintln(w, "Usage: create_user [options]") + fmt.Fprintln(w, c.Synopsis()) + fmt.Fprintln(w, "Options:") + + writeHelpOption(w, "username", "The name of the EventStoreDB user to generate a certificate for.") + writeHelpOption(w, "ca-certificate", "The path to the CA certificate file (default: ./ca/ca.crt).") + writeHelpOption(w, "ca-key", "The path to the CA key file (default: ./ca/ca.key).") + writeHelpOption(w, "days", "The validity period of the certificates in days (default: 1 year).") + writeHelpOption(w, "out", "The output directory (default: ./user-).") + writeHelpOption(w, "force", forceOption) + + w.Flush() + + return strings.TrimSpace(buffer.String()) } func (c *CreateUser) Synopsis() string { diff --git a/certificates/create_user_test.go b/certificates/create_user_test.go index a969f63..7283ee2 100644 --- a/certificates/create_user_test.go +++ b/certificates/create_user_test.go @@ -1,66 +1,43 @@ package certificates import ( - "crypto/rsa" "crypto/x509" - "os" "path" "testing" "github.com/stretchr/testify/assert" ) -func TestGenerateUserCertificate(t *testing.T) { - - t.Run("nominal-case", func(t *testing.T) { - - years := 1 - days := 0 - username := "bob" - outputDirCA := "./ca" - outputDirUser := "./user-bob" - var caCert *x509.Certificate - var caKey *rsa.PrivateKey - var userCertFileName = "user-bob" +func setupTestEnvironmentForUserTests(t *testing.T) (years int, days int, username string, userCertFileName string, outputDirCa string, outputDirUser string) { + years = 1 + days = 0 + username = "bob" + userCertFileName = "user-" + username + outputDirCa = "./ca" + outputDirUser = "./" + userCertFileName - // Clean up from previous runs - os.RemoveAll(outputDirCA) - os.RemoveAll(outputDirUser) - - certificateError := generateCACertificate(years, days, outputDirCA, caCert, caKey, false) + cleanupDirsForTest(t, outputDirCa, outputDirUser) + return +} - certFilePath := path.Join(outputDirCA, "ca.crt") - keyFilePath := path.Join(outputDirCA, "ca.key") +func TestGenerateUserCertificate(t *testing.T) { - _, certPathError := os.Stat(certFilePath) - _, keyPathError := os.Stat(keyFilePath) + t.Run("nominal-case", func(t *testing.T) { + years, days, username, userCertFileName, outputDirCa, outputDirUser := setupTestEnvironmentForUserTests(t) - assert.NoError(t, certificateError) - assert.False(t, os.IsNotExist(certPathError)) - assert.False(t, os.IsNotExist(keyPathError)) + caCertificate, caPrivateKey := generateAndAssertCACert(t, years, days, outputDirCa, false) - caCertificate, err := readCertificateFromFile(certFilePath) - assert.NoError(t, err) - caPrivateKey, err := readRSAKeyFromFile(keyFilePath) + err := generateUserCertificate(username, userCertFileName, caCertificate, caPrivateKey, years, days, outputDirUser, false) assert.NoError(t, err) - certificateError = generateUserCertificate(username, caCertificate, caPrivateKey, years, days, outputDirUser, userCertFileName) - - userCertPath := path.Join(outputDirUser, "user-bob.crt") - userKeyPath := path.Join(outputDirUser, "user-bob.key") + userCertPath := path.Join(outputDirUser, userCertFileName+".crt") + userKeyPath := path.Join(outputDirUser, userCertFileName+".key") + assertFilesExist(t, userCertPath, userKeyPath) - _, userCertPathError := os.Stat(userCertPath) - _, userKeyPathError := os.Stat(userKeyPath) - - assert.NoError(t, certificateError) - assert.False(t, os.IsNotExist(userCertPathError)) - assert.False(t, os.IsNotExist(userKeyPathError)) - - userCertificate, err := readCertificateFromFile(userCertPath) - assert.NoError(t, err) + userCertificate, _ := readCertificateFromFile(userCertPath) // verify the subject - assert.Equal(t, "CN=bob", userCertificate.Subject.String()) + assert.Equal(t, "CN="+username, userCertificate.Subject.String()) // verify the issuer assert.Equal(t, caCertificate.Issuer.String(), userCertificate.Issuer.String()) @@ -69,9 +46,41 @@ func TestGenerateUserCertificate(t *testing.T) { assert.Equal(t, 1, len(userCertificate.ExtKeyUsage)) assert.Equal(t, x509.ExtKeyUsageClientAuth, userCertificate.ExtKeyUsage[0]) assert.Equal(t, 0, len(userCertificate.UnknownExtKeyUsage)) + }) + + t.Run("force-flag", func(t *testing.T) { + years, days, username, userCertFileName, outputDirCa, outputDirUser := setupTestEnvironmentForUserTests(t) + + caCertificate, caPrivateKey := generateAndAssertCACert(t, years, days, outputDirCa, false) + + err := generateUserCertificate(username, userCertFileName, caCertificate, caPrivateKey, years, days, outputDirUser, false) + assert.NoError(t, err) + + userCertPath := path.Join(outputDirUser, userCertFileName+".crt") + userKeyPath := path.Join(outputDirUser, userCertFileName+".key") + assertFilesExist(t, userCertPath, userKeyPath) + + userCertificate, _ := readCertificateFromFile(userCertPath) + userCertificateKey, _ := readRSAKeyFromFile(userKeyPath) - // Clean up - os.RemoveAll(outputDirCA) - os.RemoveAll(outputDirUser) + // try to generate again without force + err = generateUserCertificate(username, userCertFileName, caCertificate, caPrivateKey, years, days, outputDirUser, false) + assert.Error(t, err) + userCertificateAfter, err := readCertificateFromFile(userCertPath) + assert.NoError(t, err) + userCertificateKeyAfter, err := readRSAKeyFromFile(userKeyPath) + assert.NoError(t, err) + assert.Equal(t, userCertificate, userCertificateAfter, "Expected user certificate to be the same") + assert.Equal(t, userCertificateKey, userCertificateKeyAfter, "Expected user key to be the same") + + // try to generate again with force + err = generateUserCertificate(username, userCertFileName, caCertificate, caPrivateKey, years, days, outputDirUser, true) + assert.NoError(t, err) + userCertificateAfterWithForce, err := readCertificateFromFile(userCertPath) + assert.NoError(t, err) + userCertificateKeyAfterWithForce, err := readRSAKeyFromFile(userKeyPath) + assert.NoError(t, err) + assert.NotEqual(t, userCertificate, userCertificateAfterWithForce, "Expected user certificate to be different") + assert.NotEqual(t, userCertificateKey, userCertificateKeyAfterWithForce, "Expected user key key to be different") }) } From 07c0138e729f0f1ac4cd8b4c471094cbc31f78dc Mon Sep 17 00:00:00 2001 From: Joseph Cummings Date: Thu, 29 Feb 2024 15:52:40 +0000 Subject: [PATCH 3/4] Update README --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a5bbdac..fac3410 100644 --- a/README.md +++ b/README.md @@ -52,10 +52,16 @@ Generating a certificate authority: Generating a certificate for an EventStoreDB node: -``` +```bash ./es-gencert-cli create-node -ca-certificate ./es-ca/ca.crt -ca-key ./es-ca/ca.key -out ./node1 -ip-addresses 127.0.0.1,172.20.240.1 -dns-names localhost,eventstore-node1.localhost.com ``` +Generating a certification for user authentication: + +```bash +./es-gencert-cli create-user -username ouro -days 10 -ca-certificate ./es-ca/ca.crt -ca-key ./es-ca/ca.key +``` + Generating certificates using config file: ``` ./es-gencert-cli create-certs --config-file ./certs.yml From 409a6b93c9cc0a55da4df5338803aa3ed2a366ff Mon Sep 17 00:00:00 2001 From: Joseph Cummings Date: Thu, 29 Feb 2024 17:00:43 +0000 Subject: [PATCH 4/4] Adjust assertion text --- certificates/create_node_test.go | 2 +- certificates/create_user_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/certificates/create_node_test.go b/certificates/create_node_test.go index feeacd4..278cc01 100644 --- a/certificates/create_node_test.go +++ b/certificates/create_node_test.go @@ -96,6 +96,6 @@ func TestGenerateNodeCertificate(t *testing.T) { nodeKeyFileAfterWithForce, err := readRSAKeyFromFile(nodeKeyFilePath) assert.NoError(t, err) assert.NotEqual(t, nodeCertFileAfter, nodeCertFileAfterWithForce, "Expected node certificate to be different") - assert.NotEqual(t, nodeKeyFileAfter, nodeKeyFileAfterWithForce, "Expected node key key to be different") + assert.NotEqual(t, nodeKeyFileAfter, nodeKeyFileAfterWithForce, "Expected node key to be different") }) } diff --git a/certificates/create_user_test.go b/certificates/create_user_test.go index 7283ee2..f015fdc 100644 --- a/certificates/create_user_test.go +++ b/certificates/create_user_test.go @@ -81,6 +81,6 @@ func TestGenerateUserCertificate(t *testing.T) { userCertificateKeyAfterWithForce, err := readRSAKeyFromFile(userKeyPath) assert.NoError(t, err) assert.NotEqual(t, userCertificate, userCertificateAfterWithForce, "Expected user certificate to be different") - assert.NotEqual(t, userCertificateKey, userCertificateKeyAfterWithForce, "Expected user key key to be different") + assert.NotEqual(t, userCertificateKey, userCertificateKeyAfterWithForce, "Expected user key to be different") }) }