Skip to content
This repository has been archived by the owner on Feb 27, 2023. It is now read-only.

Commit

Permalink
feature: add dfdaemon sub command 'gen-ca' for CA generation
Browse files Browse the repository at this point in the history
Signed-off-by: SataQiu <[email protected]>
  • Loading branch information
SataQiu committed Mar 19, 2020
1 parent 393bde4 commit e6a20fb
Show file tree
Hide file tree
Showing 4 changed files with 362 additions and 0 deletions.
124 changes: 124 additions & 0 deletions cmd/dfdaemon/app/cert.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
* Copyright The Dragonfly Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package app

import (
"fmt"
"os"
"time"

"github.com/dragonflyoss/Dragonfly/pkg/certutils"

"github.com/pkg/errors"
"github.com/spf13/cobra"
)

const (
duration10years = time.Hour * 24 * 365 * 10
)

// GenCACommand is used to implement 'gen-ca' command.
type GenCACommand struct {
cmd *cobra.Command

// config contains the basic fields required for creating a certificate
config certutils.CertConfig

// keyOutputPath is the destination path of generated ca.key
keyOutputPath string
// certOutputPath is the destination path of generated ca.crt
certOutputPath string
// overwrite is a flag to control whether to overwrite the existing CA files
overwrite bool
}

// NewGenCACommand returns cobra.Command for "gen-ca" command
func NewGenCACommand() *cobra.Command {
genCACommand := &GenCACommand{}
genCACommand.cmd = &cobra.Command{
Use: "gen-ca",
Short: fmt.Sprintf("generate CA files, including ca.key and ca.crt"),
Args: cobra.NoArgs,
SilenceErrors: true,
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
return genCACommand.runGenCA(args)
},
}

genCACommand.addFlags()
return genCACommand.cmd
}

// addFlags adds flags for specific command.
func (g *GenCACommand) addFlags() {
flagSet := g.cmd.Flags()

flagSet.StringVarP(&g.config.CommonName, "common-name", "", "", "subject common name of the certificate, if not specified, the hostname will be used")
flagSet.DurationVarP(&g.config.ExpireDuration, "expire-duration", "", duration10years, "expire duration of the certificate")
flagSet.StringVarP(&g.keyOutputPath, "key-output", "", "/tmp/ca.key", "destination path of generated ca.key file")
flagSet.StringVarP(&g.certOutputPath, "cert-output", "", "/tmp/ca.crt", "destination path of generated ca.crt file")
flagSet.BoolVarP(&g.overwrite, "overwrite", "", false, "whether to overwrite the existing CA files")
}

// complete completes all the required options.
func (g *GenCACommand) complete() error {
if g.config.CommonName == "" {
hostname, err := os.Hostname()
if err != nil {
return errors.Wrap(err, "failed to read hostname")
}
g.config.CommonName = hostname
}
return nil
}

// validate validates the provided options.
func (g *GenCACommand) validate() error {
if _, err := os.Stat(g.keyOutputPath); !os.IsNotExist(err) && !g.overwrite {
return fmt.Errorf("path %q already exists, please remove it before generating the newer one or pass --overwrite flag explicitly", g.keyOutputPath)
}
if _, err := os.Stat(g.certOutputPath); !os.IsNotExist(err) && !g.overwrite {
return fmt.Errorf("path %q already exists, please remove it before generating the newer one or pass --overwrite flag explicitly", g.certOutputPath)
}
return nil
}

func (g *GenCACommand) runGenCA(args []string) error {
if err := g.complete(); err != nil {
return err
}

if err := g.validate(); err != nil {
return err
}

caCert, caKey, err := certutils.NewCertificateAuthority(&g.config)
if err != nil {
return errors.Wrapf(err, "failed to generate CA certificate")
}

if err := certutils.WriteKey(g.keyOutputPath, caKey); err != nil {
return errors.Wrapf(err, "failed to write ca.key to %v", g.keyOutputPath)
}

if err := certutils.WriteCert(g.certOutputPath, caCert); err != nil {
return errors.Wrapf(err, "failed to write ca.crt to %v", g.certOutputPath)
}

return nil
}
1 change: 1 addition & 0 deletions cmd/dfdaemon/app/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ func init() {
exitOnError(bindRootFlags(viper.GetViper()), "bind root command flags")

// add sub commands
rootCmd.AddCommand(NewGenCACommand())
rootCmd.AddCommand(cmd.NewGenDocCommand("dfdaemon"))
rootCmd.AddCommand(cmd.NewVersionCommand("dfdaemon"))
rootCmd.AddCommand(cmd.NewConfigCommand("dfdaemon", getDefaultConfig))
Expand Down
159 changes: 159 additions & 0 deletions pkg/certutils/cert_util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/*
* Copyright The Dragonfly Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package certutils

import (
"crypto"
cryptorand "crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"io/ioutil"
"math/big"
"os"
"path/filepath"
"time"

"github.com/pkg/errors"
)

const (
privateKeyBlockType = "PRIVATE KEY"
certificateBlockType = "CERTIFICATE"
rsaKeySize = 2048
)

var organization = []string{"dragonfly"}

// CertConfig contains the basic fields required for creating a certificate
type CertConfig struct {
// CommonName is the subject name of the certificate
CommonName string
// ExpireDuration is the duration the certificate can be valid
ExpireDuration time.Duration
}

// NewCertificateAuthority creates new certificate and private key for the certificate authority
func NewCertificateAuthority(config *CertConfig) (*x509.Certificate, crypto.Signer, error) {
key, err := NewPrivateKey()
if err != nil {
return nil, nil, errors.Wrap(err, "unable to create private key while generating CA certificate")
}

cert, err := NewSelfSignedCACert(key, config)
if err != nil {
return nil, nil, errors.Wrap(err, "unable to create self-signed CA certificate")
}

return cert, key, nil
}

// NewPrivateKey creates an RSA private key
func NewPrivateKey() (crypto.Signer, error) {
return rsa.GenerateKey(cryptorand.Reader, rsaKeySize)
}

// NewSelfSignedCACert creates a CA certificate
func NewSelfSignedCACert(key crypto.Signer, config *CertConfig) (*x509.Certificate, error) {
now := time.Now()
tmpl := x509.Certificate{
SerialNumber: new(big.Int).SetInt64(0),
Subject: pkix.Name{
CommonName: config.CommonName,
Organization: organization,
},
NotBefore: now.UTC(),
NotAfter: now.Add(config.ExpireDuration).UTC(),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
BasicConstraintsValid: true,
IsCA: true,
}

certDERBytes, err := x509.CreateCertificate(cryptorand.Reader, &tmpl, &tmpl, key.Public(), key)
if err != nil {
return nil, err
}
return x509.ParseCertificate(certDERBytes)
}

// WriteKey stores the given key at the given location
func WriteKey(path string, key crypto.Signer) error {
if key == nil {
return errors.New("private key cannot be nil when writing to file")
}

if err := writeKeyToDisk(path, encodeKeyPEM(key)); err != nil {
return errors.Wrapf(err, "unable to write private key to file %s", path)
}

return nil
}

// WriteCert stores the given certificate at the given location
func WriteCert(path string, cert *x509.Certificate) error {
if cert == nil {
return errors.New("certificate cannot be nil when writing to file")
}

if err := writeCertToDisk(path, encodeCertPEM(cert)); err != nil {
return errors.Wrapf(err, "unable to write certificate to file %s", path)
}

return nil
}

// writeKeyToDisk writes the pem-encoded key data to keyPath.
// The key file will be created with file mode 0600.
// If the key file already exists, it will be overwritten.
// The parent directory of the keyPath will be created as needed with file mode 0755.
func writeKeyToDisk(keyPath string, data []byte) error {
if err := os.MkdirAll(filepath.Dir(keyPath), os.FileMode(0755)); err != nil {
return err
}
return ioutil.WriteFile(keyPath, data, os.FileMode(0600))
}

// writeCertToDisk writes the pem-encoded certificate data to certPath.
// The certificate file will be created with file mode 0644.
// If the certificate file already exists, it will be overwritten.
// The parent directory of the certPath will be created as needed with file mode 0755.
func writeCertToDisk(certPath string, data []byte) error {
if err := os.MkdirAll(filepath.Dir(certPath), os.FileMode(0755)); err != nil {
return err
}
return ioutil.WriteFile(certPath, data, os.FileMode(0644))
}

// encodeKeyPEM returns PEM-endcoded key data
func encodeKeyPEM(privateKey crypto.PrivateKey) []byte {
t := privateKey.(*rsa.PrivateKey)
block := pem.Block{
Type: privateKeyBlockType,
Bytes: x509.MarshalPKCS1PrivateKey(t),
}
return pem.EncodeToMemory(&block)
}

// encodeCertPEM returns PEM-endcoded certificate data
func encodeCertPEM(cert *x509.Certificate) []byte {
block := pem.Block{
Type: certificateBlockType,
Bytes: cert.Raw,
}
return pem.EncodeToMemory(&block)
}
78 changes: 78 additions & 0 deletions pkg/certutils/cert_util_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Copyright The Dragonfly Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package certutils

import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"fmt"
"io/ioutil"
"os"
"testing"
"time"

"github.com/go-check/check"
)

func Test(t *testing.T) {
check.TestingT(t)
}

type CertUtilTestSuite struct{}

func init() {
check.Suite(&CertUtilTestSuite{})
}

func (suite *CertUtilTestSuite) TestNewCertificateAuthority(c *check.C) {
config := &CertConfig{
CommonName: "dfdaemon",
ExpireDuration: time.Hour * 24,
}
cert, key, err := NewCertificateAuthority(config)
c.Assert(err, check.IsNil)
c.Assert(cert, check.NotNil)
c.Assert(key, check.NotNil)
c.Assert(cert.IsCA, check.Equals, true)
}

func (suite *CertUtilTestSuite) TestWriteKey(c *check.C) {
tmpdir, err := ioutil.TempDir("", "")
c.Assert(err, check.IsNil)

defer os.RemoveAll(tmpdir)

caKey, err := rsa.GenerateKey(rand.Reader, 2048)
c.Assert(err, check.IsNil)

path := fmt.Sprintf("%s/ca.key", tmpdir)
err = WriteKey(path, caKey)
c.Assert(err, check.IsNil)
}

func (suite *CertUtilTestSuite) TestWriteCert(c *check.C) {
tmpdir, err := ioutil.TempDir("", "")
c.Assert(err, check.IsNil)

defer os.RemoveAll(tmpdir)

caCert := &x509.Certificate{}
path := fmt.Sprintf("%s/ca.crt", tmpdir)
err = WriteCert(path, caCert)
c.Assert(err, check.IsNil)
}

0 comments on commit e6a20fb

Please sign in to comment.