Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add create-user command to generate user certificates [DB-594] #23

Merged
merged 4 commits into from
Mar 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading