Skip to content

Commit

Permalink
Add create-user command to generate user certificates [DB-594] (#23)
Browse files Browse the repository at this point in the history
* Add create-user command to generate user certificates

* Refactor and cleanup

* Update README

* Adjust assertion text

---------

Co-authored-by: Joseph Cummings <[email protected]>
  • Loading branch information
shaan1337 and josephcummings authored Mar 1, 2024
1 parent 506236c commit 9b364b1
Show file tree
Hide file tree
Showing 13 changed files with 469 additions and 124 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,4 @@ ca/

es-gencert-cli
certs.yml
.DS_Store
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions certificates/boring_linux_test.go
Original file line number Diff line number Diff line change
@@ -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())
}
45 changes: 42 additions & 3 deletions certificates/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ package certificates
import (
"bytes"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/pem"
"fmt"
"math/big"
"os"
Expand Down Expand Up @@ -49,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")

Expand All @@ -68,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
Expand All @@ -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
}
44 changes: 37 additions & 7 deletions certificates/common_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
6 changes: 3 additions & 3 deletions certificates/create_ca.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand All @@ -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).")
Expand Down
14 changes: 5 additions & 9 deletions certificates/create_ca_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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)

Expand All @@ -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)
})
}
4 changes: 2 additions & 2 deletions certificates/create_certs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.")
Expand Down
46 changes: 5 additions & 41 deletions certificates/create_node.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {
Expand All @@ -120,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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -277,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
}
Expand All @@ -288,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).")
Expand Down
Loading

0 comments on commit 9b364b1

Please sign in to comment.