This repository has been archived by the owner on Feb 27, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 773
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feature: add dfdaemon sub command 'gen-ca' for CA generation
Signed-off-by: SataQiu <[email protected]>
- Loading branch information
Showing
4 changed files
with
362 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |