Compare commits
24 Commits
652ca4b524
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| e77a67a3de | |||
| d815b12719 | |||
| 7660cc57fd | |||
| 25526ac993 | |||
| 0bf26d2019 | |||
| b049c6a779 | |||
| 0bf7b63286 | |||
| 89136cae59 | |||
| 72ffe8456d | |||
| 8b1d063ffe | |||
| bb9df2fe8d | |||
| ef2112534c | |||
| 8d9c1542dd | |||
| 99c2ebe055 | |||
| 9893e625b5 | |||
| d37336d578 | |||
| 03272b363d | |||
| 6088b91dbd | |||
| 9a470510a4 | |||
| 34e22c5338 | |||
| 868ebbf79d | |||
| e73874986e | |||
| 762ba66de8 | |||
| b7ae9b2fd9 |
3
.env.example
Normal file
3
.env.example
Normal file
@@ -0,0 +1,3 @@
|
||||
VIPER_CONFIG=your-viper-file-name-without-extension
|
||||
VIPER_CONFIG_TYPE=yaml
|
||||
ENV=dev
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,3 +1,5 @@
|
||||
.env
|
||||
viper.default.yaml
|
||||
.vscode
|
||||
.vscode
|
||||
certs
|
||||
coverage
|
||||
8
LICENSE
Normal file
8
LICENSE
Normal file
@@ -0,0 +1,8 @@
|
||||
---- Definitions ----
|
||||
license means right to use
|
||||
|
||||
|
||||
Everybody is invited to contribute to improve this project and the main idea.
|
||||
This idea which is to help the community to develop more secure code.
|
||||
|
||||
By the grace of YAHWEH
|
||||
13
Makefile
Normal file
13
Makefile
Normal file
@@ -0,0 +1,13 @@
|
||||
COVERAGE_DIR=coverage
|
||||
|
||||
lint:
|
||||
golangci-lint run ./...
|
||||
goreportcard:
|
||||
goreportcard-cli -v
|
||||
test:
|
||||
go test ./...
|
||||
test-coverage:
|
||||
rm -rf ${COVERAGE_DIR}
|
||||
mkdir ${COVERAGE_DIR}
|
||||
go test -v -coverprofile ${COVERAGE_DIR}/cover.out ./...
|
||||
go tool cover -html ${COVERAGE_DIR}/cover.out -o ${COVERAGE_DIR}/cover.html
|
||||
95
README.md
Normal file
95
README.md
Normal file
@@ -0,0 +1,95 @@
|
||||
# go-gen-cert
|
||||
|
||||
## Preamble
|
||||
I've decided to create this project based on [this example](https://github.com/yasushi-saito/grpc-ssl-example/blob/master/go/main.go) but with some improvements, which I would like to give thanks.
|
||||
|
||||
I had some trouble during TLS communication between both of my gRPC server and client. I've decided to create a tool to generate SSL certificates following a little of this [guide](https://jamielinux.com/docs/openssl-certificate-authority/create-the-intermediate-pair.html).
|
||||
|
||||
## TODO:
|
||||
- [ ] Create intermediate authority to sign certificates on behalf CA to add more security. If intermediate is hacked then you can revoke from CA and generate new intermediates keeping CA isolated from beeing hacked.
|
||||
|
||||
- ~~[x] Complete tests~~
|
||||
|
||||
## Configuration
|
||||
If you are on `dev` environment, like I've been doing, you must create `.env` file similar as `.env.example` in this repo:
|
||||
|
||||
```bash
|
||||
VIPER_CONFIG=your-viper-file-name-without-extension
|
||||
VIPER_CONFIG_TYPE=yaml
|
||||
ENV=dev
|
||||
```
|
||||
|
||||
Then add viper configuration file, yaml for example, in your root directory:
|
||||
```yaml
|
||||
export_dir: "/home"
|
||||
ca:
|
||||
serial_number: 12152 # serial number
|
||||
subject:
|
||||
organization: "yourdomain.com"
|
||||
common_name: "*.yourdomain.com"
|
||||
key_usage: 1
|
||||
ext_key_usage:
|
||||
- 1
|
||||
- 2
|
||||
duration: "8760h0m0s" #1 year
|
||||
client:
|
||||
serial_number: 12151232 # serial number
|
||||
subject:
|
||||
organization: "yourdomain.com"
|
||||
country: "RM"
|
||||
province: "REML"
|
||||
locality: ""
|
||||
street_address: ""
|
||||
postal_code: ""
|
||||
subject_key_id:
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
- 4
|
||||
- 6
|
||||
key_usage: 1
|
||||
ext_key_usage:
|
||||
- 1
|
||||
- 2
|
||||
duration: "8760h0m0s"
|
||||
```
|
||||
## Execution
|
||||
Then you can just run
|
||||
```bash
|
||||
go run main.go
|
||||
```
|
||||
|
||||
## tests
|
||||
Just simply run make command and watch coverage results on `cover.html` within `coverage`
|
||||
```shell
|
||||
make test-coverage
|
||||
rm -rf coverage
|
||||
mkdir coverage
|
||||
go test -v -coverprofile coverage/cover.out ./...
|
||||
=== RUN TestCredentialsFromKeyWithPasswd
|
||||
--- PASS: TestCredentialsFromKeyWithPasswd (0.37s)
|
||||
=== RUN TestCredentialsFromKeyWithPasswdError
|
||||
--- PASS: TestCredentialsFromKeyWithPasswdError (0.46s)
|
||||
PASS
|
||||
coverage: 90.9% of statements
|
||||
ok gitea.urkob.com/urko/go-grpc-certificate/pkg/credentials 0.839s coverage: 90.9% of statements
|
||||
go tool cover -html coverage/cover.out -o coverage/cover.html
|
||||
```
|
||||
|
||||
## goreportcard
|
||||
```bash
|
||||
make goreportcard
|
||||
```
|
||||
output:
|
||||
```bash
|
||||
➜ go-cert-gen git:(main) goreportcard-cli -v
|
||||
Grade .......... A+ 100.0%
|
||||
Files ................. 12
|
||||
Issues ................. 0
|
||||
gofmt ............... 100%
|
||||
go_vet .............. 100%
|
||||
gocyclo ............. 100%
|
||||
ineffassign ......... 100%
|
||||
license ............. 100%
|
||||
misspell ............ 100%
|
||||
```
|
||||
51
cmd/main.go
51
cmd/main.go
@@ -27,9 +27,9 @@ var envConfig struct {
|
||||
|
||||
var writer pkgio.WriterIface
|
||||
|
||||
func intEnvConfig(isProd bool) {
|
||||
if !isProd {
|
||||
err := godotenv.Load(util.RootDir() + "/.env")
|
||||
func intEnvConfig(envFilePath string) {
|
||||
if envFilePath != "" {
|
||||
err := godotenv.Load(envFilePath)
|
||||
if err != nil {
|
||||
log.Fatalf("environment variable ENV is empty and an error occurred while loading the .env file: %s\n", err)
|
||||
}
|
||||
@@ -87,32 +87,59 @@ var rootCmd = &cobra.Command{
|
||||
log.Fatalf("rootCA.WithClientCert: %s", err)
|
||||
}
|
||||
|
||||
exportPem("root-ca.pem", rootCA.PEM())
|
||||
exportPem("root-key.pem", rootCA.Key())
|
||||
outputPath, err := exportPem("root-ca.pem", rootCA.PEM())
|
||||
if err != nil {
|
||||
log.Fatalf("exportPem: %s\n", err)
|
||||
}
|
||||
log.Printf("file created successfully: %s\n", outputPath)
|
||||
|
||||
exportPem("client-cert.pem", clientCert.PEM())
|
||||
exportPem("client-key.pem", clientCert.Key())
|
||||
outputPath, err = exportPem("root-key.pem", rootCA.Key())
|
||||
if err != nil {
|
||||
log.Fatalf("exportPem: %s\n", err)
|
||||
}
|
||||
log.Printf("file created successfully: %s\n", outputPath)
|
||||
|
||||
outputPath, err = exportPem("client-cert.pem", clientCert.PEM())
|
||||
if err != nil {
|
||||
log.Fatalf("exportPem: %s\n", err)
|
||||
}
|
||||
log.Printf("file created successfully: %s\n", outputPath)
|
||||
|
||||
outputPath, err = exportPem("client-key.pem", clientCert.Key())
|
||||
if err != nil {
|
||||
log.Fatalf("exportPem: %s\n", err)
|
||||
}
|
||||
log.Printf("file created successfully: %s\n", outputPath)
|
||||
},
|
||||
}
|
||||
|
||||
func getExtKeyUsage(intKeyUsageSlice []int) []x509.ExtKeyUsage {
|
||||
extKeyUsage := make([]x509.ExtKeyUsage, len(intKeyUsageSlice))
|
||||
if intKeyUsageSlice == nil || len(intKeyUsageSlice) <= 0 {
|
||||
return []x509.ExtKeyUsage{}
|
||||
}
|
||||
|
||||
extKeyUsage := make([]x509.ExtKeyUsage, 0, len(intKeyUsageSlice))
|
||||
for _, v := range intKeyUsageSlice {
|
||||
extKeyUsage = append(extKeyUsage, x509.ExtKeyUsage(v))
|
||||
}
|
||||
return extKeyUsage
|
||||
}
|
||||
|
||||
func exportPem(filename string, data []byte) {
|
||||
func exportPem(filename string, data []byte) (string, error) {
|
||||
outputPath, err := writer.WriteFile(filename, data)
|
||||
if err != nil {
|
||||
log.Fatalf("rootCA.WithClientCert: %s", err)
|
||||
return "", fmt.Errorf("rootCA.WithClientCert: %s", err)
|
||||
}
|
||||
log.Printf("file created successfuly: %s\n", outputPath)
|
||||
return outputPath, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
intEnvConfig(false)
|
||||
envFile := ""
|
||||
if os.Getenv("ENV") != "prod" {
|
||||
envFile = "./.env"
|
||||
}
|
||||
|
||||
intEnvConfig(envFile)
|
||||
cobra.OnInitialize(initConfig)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,62 @@
|
||||
package cmd
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"crypto/x509"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gitlab.com/urkob/go-cert-gen/internal/io"
|
||||
)
|
||||
|
||||
func TestExecute(t *testing.T) {
|
||||
Execute()
|
||||
}
|
||||
|
||||
func Test_getExtKeyUsage(t *testing.T) {
|
||||
intKeyUsageSlice := make([]int, 0, 1)
|
||||
intKeyUsageSlice = append(intKeyUsageSlice, int(x509.ExtKeyUsageClientAuth))
|
||||
|
||||
keyUsage := getExtKeyUsage(intKeyUsageSlice)
|
||||
assert.Len(t, keyUsage, len(intKeyUsageSlice))
|
||||
assert.Equal(t, keyUsage[0], x509.ExtKeyUsageClientAuth)
|
||||
|
||||
intKeyUsageSlice = make([]int, 0)
|
||||
keyUsage = getExtKeyUsage(intKeyUsageSlice)
|
||||
assert.Len(t, keyUsage, 0)
|
||||
|
||||
keyUsage = getExtKeyUsage(nil)
|
||||
assert.Len(t, keyUsage, 0)
|
||||
}
|
||||
|
||||
var testFile = "test-file.txt"
|
||||
|
||||
func init() {
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
log.Fatalf("os.Getwd: %s\n", err)
|
||||
}
|
||||
writer = io.NewWriter(wd)
|
||||
}
|
||||
|
||||
func Test_exportPem(t *testing.T) {
|
||||
defer func() {
|
||||
os.Remove(testFile)
|
||||
err := os.Remove(testFile)
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
|
||||
data := []byte("test data")
|
||||
outputPath, err := exportPem(testFile, data)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, outputPath)
|
||||
}
|
||||
|
||||
func Test_exportPemError(t *testing.T) {
|
||||
data := []byte("test data")
|
||||
outputPath, err := exportPem("", data)
|
||||
require.Error(t, err)
|
||||
require.Empty(t, outputPath)
|
||||
}
|
||||
|
||||
3
go.mod
3
go.mod
@@ -7,15 +7,18 @@ require (
|
||||
github.com/kelseyhightower/envconfig v1.4.0
|
||||
github.com/spf13/cobra v1.6.1
|
||||
github.com/spf13/viper v1.15.0
|
||||
github.com/stretchr/testify v1.8.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/spf13/afero v1.9.3 // indirect
|
||||
github.com/spf13/cast v1.5.0 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
|
||||
80
internal/cert/client_cert.go
Normal file
80
internal/cert/client_cert.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package cert
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"gitlab.com/urkob/go-cert-gen/pkg/client"
|
||||
)
|
||||
|
||||
type clientCert struct {
|
||||
certPEM []byte
|
||||
keyPEM []byte
|
||||
}
|
||||
|
||||
func (c *clientCert) Key() []byte {
|
||||
return c.keyPEM
|
||||
}
|
||||
|
||||
func (c *clientCert) PEM() []byte {
|
||||
return c.certPEM
|
||||
}
|
||||
|
||||
func newClientCert(config *client.ClientCertConfig, rootCA *x509.Certificate, rootKeyPEM []byte) ([]byte, []byte, error) {
|
||||
template := &x509.Certificate{
|
||||
SerialNumber: config.Serial,
|
||||
Subject: pkix.Name{
|
||||
Organization: []string{config.Subject.Organization},
|
||||
Country: []string{config.Subject.Country},
|
||||
Province: []string{config.Subject.Province},
|
||||
Locality: []string{config.Subject.Locality},
|
||||
StreetAddress: []string{config.Subject.StreetAddress},
|
||||
PostalCode: []string{config.Subject.PostalCode},
|
||||
},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().Add(config.Duration),
|
||||
SubjectKeyId: config.SubjectKeyId,
|
||||
ExtKeyUsage: config.ExtKeyUsage,
|
||||
KeyUsage: config.KeyUsage,
|
||||
}
|
||||
|
||||
block, _ := pem.Decode(rootKeyPEM)
|
||||
if block == nil {
|
||||
return nil, nil, errors.New("pem.Decode")
|
||||
}
|
||||
|
||||
caPrivKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("x509.ParsePKCS8PrivateKey: %s", err)
|
||||
}
|
||||
|
||||
priv, err := newPrivateKey()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("newPrivateKey: %s", err)
|
||||
}
|
||||
|
||||
der, err := x509.CreateCertificate(rand.Reader, template, rootCA, &priv.PublicKey, caPrivKey)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("x509.CreateCertificate: %s", err)
|
||||
}
|
||||
|
||||
out := &bytes.Buffer{}
|
||||
err = pem.Encode(out, &pem.Block{Type: CERTIFICATE, Bytes: der})
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("pem.Encode: %s", err)
|
||||
}
|
||||
|
||||
certPEM := out.Bytes()
|
||||
keyPEM, err := encodePrivateKey(priv)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("encodePrivateKey: %s", err)
|
||||
}
|
||||
|
||||
return certPEM, keyPEM, nil
|
||||
}
|
||||
36
internal/cert/client_cert_test.go
Normal file
36
internal/cert/client_cert_test.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package cert
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_newClientCert(t *testing.T) {
|
||||
ca, err := NewRootCA(&rootTestConfig)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, ca)
|
||||
|
||||
require.NotNil(t, ca.Key())
|
||||
require.Greater(t, len(ca.Key()), 0)
|
||||
|
||||
require.NotNil(t, ca.PEM())
|
||||
require.Greater(t, len(ca.PEM()), 0)
|
||||
|
||||
x509RootCA, err := parseCertificate(ca.PEM())
|
||||
require.NoError(t, err)
|
||||
|
||||
pem, key, err := newClientCert(&clientTestConfig, x509RootCA, ca.Key())
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NotNil(t, pem)
|
||||
require.Greater(t, len(pem), 0)
|
||||
|
||||
require.NotNil(t, key)
|
||||
require.Greater(t, len(key), 0)
|
||||
}
|
||||
|
||||
func Test_newClientCertErrr(t *testing.T) {
|
||||
_, _, err := newClientCert(&clientTestConfig, nil, []byte{})
|
||||
require.Error(t, err)
|
||||
}
|
||||
@@ -8,15 +8,104 @@ import (
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"gitlab.com/urkob/go-cert-gen/pkg/ca"
|
||||
|
||||
"gitlab.com/urkob/go-cert-gen/pkg/client"
|
||||
)
|
||||
|
||||
const (
|
||||
CERTIFICATE = "CERTIFICATE"
|
||||
PRIVATE_KEY = "PRIVATE KEY"
|
||||
)
|
||||
|
||||
type rootCA struct {
|
||||
caPEM []byte
|
||||
keyPEM []byte
|
||||
}
|
||||
|
||||
func (r *rootCA) Key() []byte {
|
||||
return r.keyPEM
|
||||
}
|
||||
|
||||
func (r *rootCA) PEM() []byte {
|
||||
return r.caPEM
|
||||
}
|
||||
|
||||
func (r *rootCA) WithClientCert(config *client.ClientCertConfig) (client.ClientCertIface, error) {
|
||||
x509RootCA, err := parseCertificate(r.caPEM)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parseCertificate: %s", err)
|
||||
}
|
||||
|
||||
clientCertPEM, clientKeyPEM, err := newClientCert(config, x509RootCA, r.keyPEM)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("newClientCert: %s", err)
|
||||
}
|
||||
|
||||
return &clientCert{
|
||||
certPEM: clientCertPEM,
|
||||
keyPEM: clientKeyPEM,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Create a self-signed certificate.
|
||||
func newRootCA(config *ca.CaConfig) ([]byte, []byte, error) {
|
||||
if config == nil {
|
||||
return nil, nil, errors.New("ca.CaConfig config is nil")
|
||||
}
|
||||
priv, err := newPrivateKey()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("newPrivateKey: %s", err)
|
||||
}
|
||||
|
||||
template := x509.Certificate{
|
||||
SerialNumber: config.SerialNumber,
|
||||
Subject: pkix.Name{
|
||||
Organization: []string{config.Subject.Organization},
|
||||
CommonName: config.Subject.CommonName,
|
||||
},
|
||||
NotBefore: time.Now().Add(-time.Minute),
|
||||
NotAfter: time.Now().Add(config.Duration),
|
||||
IsCA: true,
|
||||
KeyUsage: config.KeyUsage,
|
||||
ExtKeyUsage: config.ExtKeyUsage,
|
||||
BasicConstraintsValid: true,
|
||||
}
|
||||
der, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("x509.CreateCertificate: %s", err)
|
||||
}
|
||||
|
||||
out := &bytes.Buffer{}
|
||||
err = pem.Encode(out, &pem.Block{Type: CERTIFICATE, Bytes: der})
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("pem.Encode: %s", err)
|
||||
}
|
||||
|
||||
caPEM := out.Bytes()
|
||||
keyPEM, err := encodePrivateKey(priv)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("encodePrivateKey: %s", err)
|
||||
}
|
||||
|
||||
return caPEM, keyPEM, nil
|
||||
}
|
||||
|
||||
func NewRootCA(config *ca.CaConfig) (ca.RootCACertificateIface, error) {
|
||||
caPEM, keyPEM, err := newRootCA(config)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("newRootCA: %s", err)
|
||||
}
|
||||
|
||||
return &rootCA{
|
||||
caPEM: caPEM,
|
||||
keyPEM: keyPEM,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Creates a new 512bit private key.
|
||||
func newPrivateKey() (*ecdsa.PrivateKey, error) {
|
||||
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
@@ -45,147 +134,16 @@ func encodePrivateKey(priv *ecdsa.PrivateKey) ([]byte, error) {
|
||||
out := &bytes.Buffer{}
|
||||
privBytes, err := x509.MarshalPKCS8PrivateKey(priv)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal: %s", err)
|
||||
return nil, fmt.Errorf("x509.MarshalPKCS8PrivateKey: %s", err)
|
||||
}
|
||||
pem.Encode(out, &pem.Block{
|
||||
Type: "PRIVATE KEY",
|
||||
|
||||
err = pem.Encode(out, &pem.Block{
|
||||
Type: PRIVATE_KEY,
|
||||
Bytes: privBytes,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return out.Bytes(), nil
|
||||
}
|
||||
|
||||
// Create a self-signed certificate.
|
||||
func newRootCA(config *ca.CaConfig) ([]byte, []byte, error) {
|
||||
priv, err := newPrivateKey()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("newPrivateKey: %s", err)
|
||||
}
|
||||
|
||||
template := x509.Certificate{
|
||||
SerialNumber: config.SerialNumber,
|
||||
Subject: pkix.Name{
|
||||
Organization: []string{config.Subject.Organization},
|
||||
CommonName: config.Subject.CommonName,
|
||||
},
|
||||
NotBefore: time.Now().Add(-time.Minute),
|
||||
NotAfter: time.Now().Add(config.Duration),
|
||||
IsCA: true,
|
||||
KeyUsage: config.KeyUsage,
|
||||
ExtKeyUsage: config.ExtKeyUsage,
|
||||
BasicConstraintsValid: true,
|
||||
}
|
||||
der, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("x509.CreateCertificate: %s", err)
|
||||
}
|
||||
|
||||
out := &bytes.Buffer{}
|
||||
pem.Encode(out, &pem.Block{Type: "CERTIFICATE", Bytes: der})
|
||||
caPEM := out.Bytes()
|
||||
keyPEM, err := encodePrivateKey(priv)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("encodePrivateKey: %s", err)
|
||||
}
|
||||
|
||||
return caPEM, keyPEM, nil
|
||||
}
|
||||
|
||||
func newClientCert(config *client.ClientCertConfig, rootCA *x509.Certificate, rootKeyPEM []byte) ([]byte, []byte, error) {
|
||||
template := &x509.Certificate{
|
||||
SerialNumber: config.Serial,
|
||||
Subject: pkix.Name{
|
||||
Organization: []string{config.Subject.Organization},
|
||||
Country: []string{config.Subject.Country},
|
||||
Province: []string{config.Subject.Province},
|
||||
Locality: []string{config.Subject.Locality},
|
||||
StreetAddress: []string{config.Subject.StreetAddress},
|
||||
PostalCode: []string{config.Subject.PostalCode},
|
||||
},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().Add(config.Duration),
|
||||
SubjectKeyId: config.SubjectKeyId,
|
||||
ExtKeyUsage: config.ExtKeyUsage,
|
||||
KeyUsage: config.KeyUsage,
|
||||
}
|
||||
|
||||
block, _ := pem.Decode(rootKeyPEM)
|
||||
caPrivKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
log.Fatalf("x509.ParsePKCS8PrivateKey: %s", err)
|
||||
}
|
||||
|
||||
priv, err := newPrivateKey()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("newPrivateKey: %s", err)
|
||||
}
|
||||
|
||||
der, err := x509.CreateCertificate(rand.Reader, template, rootCA, &priv.PublicKey, caPrivKey)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("x509.CreateCertificate: %s", err)
|
||||
}
|
||||
|
||||
out := &bytes.Buffer{}
|
||||
pem.Encode(out, &pem.Block{Type: "CERTIFICATE", Bytes: der})
|
||||
certPEM := out.Bytes()
|
||||
keyPEM, err := encodePrivateKey(priv)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("encodePrivateKey: %s", err)
|
||||
}
|
||||
|
||||
return certPEM, keyPEM, nil
|
||||
}
|
||||
|
||||
type rootCA struct {
|
||||
caPEM []byte
|
||||
keyPEM []byte
|
||||
}
|
||||
|
||||
type clientCert struct {
|
||||
certPEM []byte
|
||||
keyPEM []byte
|
||||
}
|
||||
|
||||
func (c *clientCert) Key() []byte {
|
||||
return c.keyPEM
|
||||
}
|
||||
|
||||
func (c *clientCert) PEM() []byte {
|
||||
return c.certPEM
|
||||
}
|
||||
|
||||
func (r *rootCA) WithClientCert(config *client.ClientCertConfig) (client.ClientCertIface, error) {
|
||||
x509RootCA, err := parseCertificate(r.caPEM)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parseCertificate: %s", err)
|
||||
}
|
||||
|
||||
clientCertPEM, clientKeyPEM, err := newClientCert(config, x509RootCA, r.keyPEM)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("newClientCert: %s", err)
|
||||
}
|
||||
|
||||
return &clientCert{
|
||||
certPEM: clientCertPEM,
|
||||
keyPEM: clientKeyPEM,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *rootCA) Key() []byte {
|
||||
return r.keyPEM
|
||||
}
|
||||
|
||||
func (r *rootCA) PEM() []byte {
|
||||
return r.caPEM
|
||||
}
|
||||
|
||||
func NewRootCA(config *ca.CaConfig) (ca.RootCACertificateIface, error) {
|
||||
caPEM, keyPEM, err := newRootCA(config)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("newRootCA: %s", err)
|
||||
}
|
||||
|
||||
return &rootCA{
|
||||
caPEM: caPEM,
|
||||
keyPEM: keyPEM,
|
||||
}, nil
|
||||
}
|
||||
143
internal/cert/root_ca_test.go
Normal file
143
internal/cert/root_ca_test.go
Normal file
@@ -0,0 +1,143 @@
|
||||
package cert
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/x509"
|
||||
"math/big"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"gitlab.com/urkob/go-cert-gen/pkg/ca"
|
||||
"gitlab.com/urkob/go-cert-gen/pkg/client"
|
||||
)
|
||||
|
||||
const year = time.Hour * 24 * 365
|
||||
|
||||
var rootTestConfig = ca.CaConfig{
|
||||
SerialNumber: big.NewInt(12321),
|
||||
Subject: ca.CaSubject{
|
||||
Organization: "test-organization",
|
||||
CommonName: "test-organization",
|
||||
},
|
||||
KeyUsage: x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{
|
||||
x509.ExtKeyUsageServerAuth,
|
||||
x509.ExtKeyUsageClientAuth,
|
||||
},
|
||||
Duration: year,
|
||||
}
|
||||
|
||||
var clientTestConfig = client.ClientCertConfig{
|
||||
Serial: big.NewInt(12321),
|
||||
Subject: client.Subject{
|
||||
Organization: rootTestConfig.Subject.Organization,
|
||||
Country: "REML",
|
||||
Province: "REML",
|
||||
Locality: "REML",
|
||||
StreetAddress: "c/o Sovereign 7 rural free delivery",
|
||||
PostalCode: "[Near 777]",
|
||||
},
|
||||
Duration: year,
|
||||
SubjectKeyId: []byte{1, 2, 3, 4, 6},
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{
|
||||
x509.ExtKeyUsageServerAuth,
|
||||
x509.ExtKeyUsageClientAuth,
|
||||
},
|
||||
KeyUsage: x509.KeyUsageDigitalSignature,
|
||||
}
|
||||
|
||||
func Test_newPrivateKey(t *testing.T) {
|
||||
privKey, err := newPrivateKey()
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NotEmpty(t, privKey.PublicKey.Params().Name)
|
||||
require.Equal(t, elliptic.P256().Params().Name, privKey.PublicKey.Params().Name)
|
||||
}
|
||||
|
||||
func Test_encodePrivateKey(t *testing.T) {
|
||||
privKey, err := newPrivateKey()
|
||||
require.NoError(t, err)
|
||||
|
||||
bytes, err := encodePrivateKey(privKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NotNil(t, bytes)
|
||||
require.Greater(t, len(bytes), 0)
|
||||
}
|
||||
|
||||
func Test_encodePrivateKeyError(t *testing.T) {
|
||||
key := ecdsa.PrivateKey{}
|
||||
_, err := encodePrivateKey(&key)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func Test_newRootCA(t *testing.T) {
|
||||
caPEM, keyPEM, err := newRootCA(&rootTestConfig)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, caPEM)
|
||||
require.Greater(t, len(caPEM), 0)
|
||||
require.NotNil(t, keyPEM)
|
||||
require.Greater(t, len(keyPEM), 0)
|
||||
}
|
||||
|
||||
func Test_newRootCAError(t *testing.T) {
|
||||
_, _, err := newRootCA(&ca.CaConfig{})
|
||||
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func Test_parseCertificate(t *testing.T) {
|
||||
caPEM, _, err := newRootCA(&rootTestConfig)
|
||||
require.NoError(t, err)
|
||||
|
||||
rootCert, err := parseCertificate(caPEM)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, rootCert)
|
||||
require.Equal(t, rootCert.SignatureAlgorithm, x509.ECDSAWithSHA256)
|
||||
require.Equal(t, rootCert.Issuer.Organization, []string{rootTestConfig.Subject.Organization})
|
||||
require.Equal(t, rootCert.Issuer.CommonName, rootTestConfig.Subject.CommonName)
|
||||
}
|
||||
|
||||
func Test_parseCertificateError(t *testing.T) {
|
||||
_, err := parseCertificate([]byte{})
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestNewRootCA(t *testing.T) {
|
||||
rootCert, err := NewRootCA(&rootTestConfig)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, rootCert)
|
||||
}
|
||||
|
||||
func TestNewRootCAERror(t *testing.T) {
|
||||
_, err := NewRootCA(nil)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func Test_rootCA_WithClientCert(t *testing.T) {
|
||||
rootCert, err := NewRootCA(&rootTestConfig)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, rootCert)
|
||||
|
||||
clientSrv, err := rootCert.WithClientCert(&clientTestConfig)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, clientSrv)
|
||||
|
||||
require.NotNil(t, clientSrv.Key())
|
||||
require.Greater(t, len(clientSrv.Key()), 0)
|
||||
|
||||
require.NotNil(t, clientSrv.PEM())
|
||||
require.Greater(t, len(clientSrv.PEM()), 0)
|
||||
}
|
||||
|
||||
func Test_rootCA_WithClientCertEror(t *testing.T) {
|
||||
rootCert := rootCA{
|
||||
caPEM: nil,
|
||||
}
|
||||
|
||||
_, err := rootCert.WithClientCert(&clientTestConfig)
|
||||
require.Error(t, err)
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package io
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"gitlab.com/urkob/go-cert-gen/pkg/io"
|
||||
@@ -11,22 +12,33 @@ type writer struct {
|
||||
dirPath string
|
||||
}
|
||||
|
||||
// WriteFile writes file into `w writer` directory.
|
||||
// Returns outputPath and error
|
||||
func (w writer) WriteFile(filename string, data []byte) (string, error) {
|
||||
if filename == "" {
|
||||
return "", fmt.Errorf("filename cannot be empty")
|
||||
}
|
||||
if w.dirPath == "" {
|
||||
return "", fmt.Errorf("export directory cannot be empty")
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(w.dirPath, 0o755); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
log.Println("to write file")
|
||||
outputPath := w.dirPath + "/" + filename
|
||||
if err := os.WriteFile(outputPath, data, 0o600); err != nil {
|
||||
return "", err
|
||||
}
|
||||
log.Println("file written")
|
||||
return outputPath, nil
|
||||
}
|
||||
|
||||
func NewWriter(dirPath string) io.WriterIface {
|
||||
return newWriter(dirPath)
|
||||
}
|
||||
|
||||
func newWriter(dirPath string) *writer {
|
||||
return &writer{
|
||||
dirPath: dirPath,
|
||||
}
|
||||
|
||||
94
internal/io/writer_test.go
Normal file
94
internal/io/writer_test.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package io
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var (
|
||||
testWriterPath = "testPath"
|
||||
testWriterPathError = "test Path, @:ººººº\\/.Ç*⁺´+"
|
||||
testFileContent = []byte("test data")
|
||||
testFileName = "test-file.txt"
|
||||
workingDir = ""
|
||||
)
|
||||
|
||||
func deleteAllDirs() error {
|
||||
return os.RemoveAll(testWriterPath)
|
||||
}
|
||||
|
||||
func init() {
|
||||
err := deleteAllDirs()
|
||||
if err != nil {
|
||||
log.Fatalln("deleteAllDirs: ", err)
|
||||
}
|
||||
workingDir, err = os.Getwd()
|
||||
if err != nil {
|
||||
log.Fatalln("os.Getwd: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_newWriter(t *testing.T) {
|
||||
w := newWriter(testWriterPath)
|
||||
require.NotNil(t, w)
|
||||
}
|
||||
|
||||
func TestNewWriter(t *testing.T) {
|
||||
w := NewWriter(testWriterPath)
|
||||
require.NotNil(t, w)
|
||||
}
|
||||
|
||||
func Test_writer_WriteFile(t *testing.T) {
|
||||
err := deleteAllDirs()
|
||||
require.NoError(t, err)
|
||||
|
||||
defer func(t *testing.T) {
|
||||
err := deleteAllDirs()
|
||||
require.NoError(t, err)
|
||||
}(t)
|
||||
|
||||
w := newWriter(testWriterPath)
|
||||
wgot, err := w.WriteFile(testFileName, testFileContent)
|
||||
require.NoError(t, err)
|
||||
|
||||
btsReaded, err := os.ReadFile(wgot)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, btsReaded)
|
||||
require.Equal(t, string(btsReaded), string(testFileContent))
|
||||
}
|
||||
|
||||
func Test_writer_WriteFileError(t *testing.T) {
|
||||
err := deleteAllDirs()
|
||||
require.NoError(t, err)
|
||||
|
||||
defer func(t *testing.T) {
|
||||
err := deleteAllDirs()
|
||||
require.NoError(t, err)
|
||||
}(t)
|
||||
|
||||
w := newWriter("")
|
||||
_, err = w.WriteFile(testFileName, testFileContent)
|
||||
assert.Error(t, err)
|
||||
|
||||
w = newWriter(testWriterPath)
|
||||
_, err = w.WriteFile("", testFileContent)
|
||||
require.Error(t, err)
|
||||
|
||||
err = os.MkdirAll(testWriterPath, fs.ModeDir)
|
||||
require.NoError(t, err, "mkdir should not throw error")
|
||||
|
||||
err = os.MkdirAll(testWriterPath+"/"+testFileName, fs.ModeDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = w.WriteFile(testFileName, nil)
|
||||
assert.Error(t, err)
|
||||
|
||||
w = newWriter(testWriterPathError)
|
||||
_, err = w.WriteFile(testFileName, testFileContent)
|
||||
require.Error(t, err)
|
||||
}
|
||||
@@ -29,26 +29,10 @@ type Subject struct {
|
||||
PostalCode string
|
||||
}
|
||||
|
||||
/*
|
||||
var (
|
||||
subjectKeyId = []byte{1, 2, 3, 4, 6}
|
||||
extKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}
|
||||
keyUsage = x509.KeyUsageDigitalSignature
|
||||
)
|
||||
|
||||
func NewDefaultConfig() *ClientCertConfig {
|
||||
return &ClientCertConfig{
|
||||
Serial: big.NewInt(12321),
|
||||
Subject: Subject{
|
||||
Organization: "",
|
||||
Country: "",
|
||||
Province: "",
|
||||
Locality: "",
|
||||
StreetAddress: "",
|
||||
PostalCode: "",
|
||||
},
|
||||
Duration: time.Duration(time.Hour * 24 * 365),
|
||||
SubjectKeyId: subjectKeyId,
|
||||
ExtKeyUsage: extKeyUsage,
|
||||
KeyUsage: keyUsage,
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package io
|
||||
|
||||
type WriterIface interface {
|
||||
// WriteFile writes file into `w writer` directory.
|
||||
// Returns outputPath and error
|
||||
WriteFile(filename string, data []byte) (string, error)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ ca:
|
||||
ext_key_usage:
|
||||
- 1
|
||||
- 2
|
||||
duration: 518400 #1 year
|
||||
duration: "8760h0m0s" #1 year
|
||||
client:
|
||||
serial_number: 12151232 # serial number
|
||||
subject:
|
||||
@@ -28,4 +28,4 @@ client:
|
||||
ext_key_usage:
|
||||
- 1
|
||||
- 2
|
||||
duration: 518400
|
||||
duration: "8760h0m0s"
|
||||
Reference in New Issue
Block a user