diff --git a/docs/zh/modules/gen_cert.md b/docs/zh/modules/gen_cert.md index 44dac6e5..51cc16a8 100644 --- a/docs/zh/modules/gen_cert.md +++ b/docs/zh/modules/gen_cert.md @@ -9,15 +9,19 @@ gen_cert模块允许用户校验或生成证书文件。 | root_key | CA证书的key路径 | 字符串 | 否 | - | | root_cert | CA证书路径 | 字符串 | 否 | - | | date | 证书过期时间 | 字符串 | 否 | 1y | -| policy | 证书生成策略(Always, IfNotPresent) | 字符串 | 否 | IfNotPresent | +| policy | 证书生成策略(Always, IfNotPresent, None) | 字符串 | 否 | IfNotPresent | | sans | Subject Alternative Names. 允许的IP和DNS | 字符串 | 否 | - | | cn | Common Name | 字符串 | 是 | - | | out_key | 生成的证书key路径 | 字符串 | 是 | - | | out_cert | 生成的证书路径 | 字符串 | 是 | - | -证书生成策略: -Always: 无论`out_key`和`out_cert`指向的证书路径是否存在,都会生成新的证书进行覆盖。 -IfNotPresent: 当`out_key`和`out_cert`指向的证书路径存在时,只对该证书进行校验,并不会生成新的证书。 +证书生成策略说明: + +- **Always**:无论`out_key`和`out_cert`指定的证书文件是否已存在,始终重新生成证书并覆盖原有文件。 +- **IfNotPresent**:仅当`out_key`和`out_cert`指定的证书文件不存在时才生成新证书;如果文件已存在,则先对现有证书进行校验,校验不通过时才会重新生成证书。 +- **None**:如果`out_key`和`out_cert`指定的证书文件已存在,仅对其进行校验,不会生成或覆盖证书文件;若文件不存在则不会生成新证书。 + +该策略可灵活控制证书的生成和校验行为,满足不同场景下的需求。 ## 使用示例 diff --git a/pkg/modules/gen_cert.go b/pkg/modules/gen_cert.go index ceec479a..84f689a9 100644 --- a/pkg/modules/gen_cert.go +++ b/pkg/modules/gen_cert.go @@ -13,7 +13,6 @@ import ( "math" "math/big" "net" - "os" "time" "github.com/cockroachdb/errors" @@ -31,28 +30,28 @@ import ( ) /* -The GenCert module generates SSL/TLS certificates for secure communication. -This module can create both self-signed certificates and certificates signed by a root CA. +The GenCert module is designed to generate SSL/TLS certificates for secure communications. +It supports both self-signed certificates and certificates signed by a root Certificate Authority (CA). Configuration: -Users can specify various certificate parameters: +You can customize certificate generation with the following parameters: gen_cert: - cn: example.com # required: Common Name for the certificate - out_key: /path/to/key # required: output path for private key - out_cert: /path/to/cert # required: output path for certificate - root_key: /path/to/ca.key # optional: root CA private key path - root_cert: /path/to/ca.crt # optional: root CA certificate path - sans: # optional: Subject Alternative Names + cn: example.com # required: Common Name for the certificate + out_key: /path/to/key # required: Output path for the private key + out_cert: /path/to/cert # required: Output path for the certificate + root_key: /path/to/ca.key # optional: Path to the root CA private key + root_cert: /path/to/ca.crt # optional: Path to the root CA certificate + sans: # optional: Subject Alternative Names (SANs) - example.com - www.example.com - policy: IfNotPresent # optional: certificate generation policy - date: 8760h # optional: certificate validity period + policy: IfNotPresent # optional: Certificate generation policy + date: 8760h # optional: Certificate validity period Usage Examples in Playbook Tasks: -1. Generate self-signed certificate: +1. Generate a self-signed certificate: ```yaml - - name: Generate self-signed certificate + - name: Generate a self-signed certificate gen_cert: cn: example.com out_key: /etc/ssl/private/example.key @@ -63,9 +62,9 @@ Usage Examples in Playbook Tasks: register: cert_result ``` -2. Generate certificate signed by root CA: +2. Generate a certificate signed by a root CA: ```yaml - - name: Generate signed certificate + - name: Generate a certificate signed by a root CA gen_cert: cn: example.com root_key: /etc/ssl/private/ca.key @@ -76,29 +75,33 @@ Usage Examples in Playbook Tasks: ``` Return Values: -- On success: Returns "Success" in stdout -- On failure: Returns error message in stderr +- On success: "Success" is returned in stdout. +- On failure: An error message is returned in stderr. */ const ( - // DefaultSignCertAfter defines the default timeout for sign certificates. + // defaultSignCertAfter specifies the default validity period for signed certificates (10 years). defaultSignCertAfter = time.Hour * 24 * 365 * 10 - // CertificateBlockType is a possible value for pem.Block.Type. + // certificateBlockType is the PEM block type for certificates. certificateBlockType = "CERTIFICATE" rsaKeySize = 2048 - // policy to generate file - // policyAlways always generate new cert to override exist cert + // Certificate generation policies: + // policyAlways: Always generate a new certificate, overwriting any existing one. policyAlways = "Always" - // policyIfNotPresent if cert is exist, check it.if not generate new cert. + // policyIfNotPresent: If a certificate exists, validate it. If validation fails or it doesn't exist, generate a new one. policyIfNotPresent = "IfNotPresent" + // policyNone: Only validate the certificate; do not generate a new one. + policyNone = "None" ) +// defaultAltName provides default SANs for certificates. var defaultAltName = &cgutilcert.AltNames{ DNSNames: []string{"localhost"}, IPs: []net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback}, } +// genCertArgs holds the arguments for certificate generation. type genCertArgs struct { rootKey string rootCert string @@ -111,96 +114,145 @@ type genCertArgs struct { isCA *bool } -// signedCertificate generates a certificate signed by the root certificate +// signedCertificate generates a certificate signed by the specified root CA. func (gca genCertArgs) signedCertificate(cfg cgutilcert.Config) (string, string, error) { - // Load CA private key + // Load the CA private key. caKey, err := TryLoadKeyFromDisk(gca.rootKey) if err != nil { - return StdoutFailed, "failed to load root key", err + return StdoutFailed, "Failed to load root key", err } - // Load CA certificate - caCert, _, err := TryLoadCertChainFromDisk(gca.rootCert) + // Load the CA certificate chain. + caCert, err := TryLoadCertChainFromDisk(gca.rootCert) if err != nil { - return StdoutFailed, "failed to load root cert", err + return StdoutFailed, "Failed to load root certificate", err } - // Function to generate and write new certificate and key + // Helper function to generate and write a new certificate and key. generateAndWrite := func() (string, string, error) { newKey, err := rsa.GenerateKey(cryptorand.Reader, rsaKeySize) if err != nil { - return StdoutFailed, "failed to generate rsa key", err + return StdoutFailed, "Failed to generate RSA key", err } - newCert, err := NewSignedCert(cfg, gca.date, newKey, caCert, caKey, ptr.Deref(gca.isCA, false)) + newCert, err := NewSignedCert(cfg, gca.date, newKey, caCert[0], caKey, ptr.Deref(gca.isCA, false)) if err != nil { - return StdoutFailed, "failed to generate signed cert", err + return StdoutFailed, "Failed to generate signed certificate", err } if err := WriteKey(gca.outKey, newKey, gca.policy); err != nil { - return StdoutFailed, "failed to write out key", err + return StdoutFailed, "Failed to write private key", err } if err := WriteCert(gca.outCert, newCert, gca.policy); err != nil { - return StdoutFailed, "failed to write out cert", err + return StdoutFailed, "Failed to write certificate", err } return StdoutSuccess, "", nil } - switch gca.policy { - case policyIfNotPresent: - // Check if key exists + // Helper function to verify the existing certificate and key. + verify := func() error { + // Check if the private key exists and is valid. if _, err := TryLoadKeyFromDisk(gca.outKey); err != nil { - klog.V(4).InfoS("Failed to load out key, create it") - return generateAndWrite() + return err } - // Check if certificate exists - existCert, existIntermediates, err := TryLoadCertChainFromDisk(gca.outCert) + // Check if the certificate exists and is valid. + existCert, err := TryLoadCertChainFromDisk(gca.outCert) if err != nil { - klog.V(4).InfoS("Failed to load out cert, create it") + return err + } + // Validate the certificate's validity period. + if err := ValidateCertPeriod(existCert[0], 0); err != nil { + return err + } + // Validate the certificate chain. + if err := VerifyCertChain(existCert[0], existCert[:1], caCert[0]); err != nil { + return err + } + // Validate the certificate's SANs and other configuration. + return validateCertificateWithConfig(existCert[0], gca.outCert, cfg) + } + + switch gca.policy { + case policyAlways: + // For all other cases (including policyAlways), always generate a new certificate and key. + return generateAndWrite() + case policyIfNotPresent: + if err := verify(); err != nil { + klog.V(4).ErrorS(err, "Certificate or key verification failed, will regenerate", "outKey", gca.outKey, "outCert", gca.outCert) return generateAndWrite() } - // Validate certificate period - if err := ValidateCertPeriod(existCert, 0); err != nil { - return StdoutFailed, "failed to validate cert period", err + // Existing certificate and key are valid; skip generation. + return StdoutSkip, "", nil + case policyNone: + if err := verify(); err != nil { + return StdoutFailed, "Certificate validation failed", err } - // Validate certificate chain - if err := VerifyCertChain(existCert, existIntermediates, caCert); err != nil { - return StdoutFailed, "failed to validate cert chain", err - } - // Validate certificate SAN and other config - if err := validateCertificateWithConfig(existCert, gca.outCert, cfg); err != nil { - return StdoutFailed, "failed to validate cert config", err - } - // Existing certificate and key are valid, skip generation return StdoutSkip, "", nil default: - // Otherwise, always generate new certificate and key - return generateAndWrite() + return StdoutFailed, "unsupport policy", errors.New("unsupport policy") } } -// selfSignedCertificate generate Self-signed certificate +// selfSignedCertificate creates a self-signed certificate and writes it to disk according to the specified policy. +// It returns a status string, an optional message, and an error if one occurred. func (gca genCertArgs) selfSignedCertificate(cfg cgutilcert.Config) (string, string, error) { - newKey, err := rsa.GenerateKey(cryptorand.Reader, rsaKeySize) - if err != nil { - return StdoutFailed, "failed to generate rsa key", err + // Generates a new self-signed certificate and writes both the key and certificate to their respective files. + generateAndWrite := func() (string, string, error) { + newKey, err := rsa.GenerateKey(cryptorand.Reader, rsaKeySize) + if err != nil { + return StdoutFailed, "Unable to generate RSA private key", err + } + + newCert, err := NewSelfSignedCACert(cfg, gca.date, newKey) + if err != nil { + return StdoutFailed, "Unable to generate self-signed certificate", err + } + + // Persist the private key and certificate to disk. + if err := WriteKey(gca.outKey, newKey, gca.policy); err != nil { + return StdoutFailed, "Unable to write private key to file", err + } + if err := WriteCert(gca.outCert, newCert, gca.policy); err != nil { + return StdoutFailed, "Unable to write certificate to file", err + } + + return StdoutSuccess, "", nil } - newCert, err := NewSelfSignedCACert(cfg, gca.date, newKey) - if err != nil { - return StdoutFailed, "failed to generate ca cert", err - } - // write key and cert to file - if err := WriteKey(gca.outKey, newKey, gca.policy); err != nil { - return StdoutFailed, "failed to write out key", err - } - if err := WriteCert(gca.outCert, newCert, gca.policy); err != nil { - return StdoutFailed, "failed to write out cert", err + // Verifies that both the private key and certificate exist and are valid. + verify := func() error { + if _, err := TryLoadKeyFromDisk(gca.outKey); err != nil { + return err + } + if _, err := TryLoadCertChainFromDisk(gca.outCert); err != nil { + return err + } + return nil } - return StdoutSuccess, "", nil + switch gca.policy { + case policyAlways: + // Always generate a new certificate and key, regardless of existing files. + return generateAndWrite() + case policyIfNotPresent: + // If verification fails, log and regenerate; otherwise, skip generation. + if err := verify(); err != nil { + klog.V(4).ErrorS(err, "Existing self-signed certificate or key is invalid or missing, regenerating", "outKey", gca.outKey, "outCert", gca.outCert) + return generateAndWrite() + } + return StdoutSkip, "", nil + case policyNone: + // Only verify the presence and validity of the certificate and key. + if err := verify(); err != nil { + return StdoutFailed, "Self-signed certificate or key validation failed", err + } + return StdoutSkip, "", nil + default: + return StdoutFailed, "unsupported policy", errors.New("unsupported policy") + } } +// newGenCertArgs parses and validates the arguments for certificate generation. func newGenCertArgs(_ context.Context, raw runtime.RawExtension, vars map[string]any) (*genCertArgs, error) { gca := &genCertArgs{} - // args + // Parse arguments. args := variable.Extension2Variables(raw) gca.rootKey, _ = variable.StringVar(vars, args, "root_key") gca.rootCert, _ = variable.StringVar(vars, args, "root_cert") @@ -211,23 +263,23 @@ func newGenCertArgs(_ context.Context, raw runtime.RawExtension, vars map[string gca.outKey, _ = variable.StringVar(vars, args, "out_key") gca.outCert, _ = variable.StringVar(vars, args, "out_cert") gca.isCA, _ = variable.BoolVar(vars, args, "is_ca") - // check args - if gca.policy != policyAlways && gca.policy != policyIfNotPresent { - return nil, errors.New("\"policy\" should be one of [Always, IfNotPresent]") + // Validate arguments. + if gca.policy != policyAlways && gca.policy != policyIfNotPresent && gca.policy != policyNone { + return nil, errors.New("\"policy\" must be one of [Always, IfNotPresent, None]") } if gca.outKey == "" || gca.outCert == "" { - return nil, errors.New("\"out_key\" or \"out_cert\" in args should be string") + return nil, errors.New("\"out_key\" and \"out_cert\" must be specified as strings") } if gca.cn == "" { - return nil, errors.New("\"cn\" in args should be string") + return nil, errors.New("\"cn\" must be specified as a string") } return gca, nil } -// ModuleGenCert handles the "gen_cert" module, generating SSL/TLS certificates +// ModuleGenCert is the entry point for the "gen_cert" module, responsible for generating SSL/TLS certificates. func ModuleGenCert(ctx context.Context, options ExecOptions) (string, string, error) { - // get host variable + // Retrieve all host variables. ha, err := options.getAllVariables() if err != nil { return StdoutFailed, StderrGetHostVariable, err @@ -252,12 +304,8 @@ func ModuleGenCert(ctx context.Context, options ExecOptions) (string, string, er } } -// WriteKey stores the given key at the given location +// WriteKey writes the given private key to the specified file path. func WriteKey(outKey string, key crypto.Signer, policy string) error { - if _, err := os.Stat(outKey); err == nil && policy == policyIfNotPresent { - // skip - return nil - } if key == nil { return errors.New("private key cannot be nil when writing to file") } @@ -273,12 +321,8 @@ func WriteKey(outKey string, key crypto.Signer, policy string) error { return nil } -// WriteCert stores the given certificate at the given location +// WriteCert writes the given certificate to the specified file path. func WriteCert(outCert string, cert *x509.Certificate, policy string) error { - if _, err := os.Stat(outCert); err == nil && policy == policyIfNotPresent { - // skip - return nil - } if cert == nil { return errors.New("certificate cannot be nil when writing to file") } @@ -290,7 +334,7 @@ func WriteCert(outCert string, cert *x509.Certificate, policy string) error { return nil } -// EncodeCertPEM returns PEM-endcoded certificate data +// EncodeCertPEM encodes the given certificate into PEM format. func EncodeCertPEM(cert *x509.Certificate) []byte { block := pem.Block{ Type: certificateBlockType, @@ -300,15 +344,15 @@ func EncodeCertPEM(cert *x509.Certificate) []byte { return pem.EncodeToMemory(&block) } -// TryLoadKeyFromDisk tries to load the key from the disk and validates that it is valid +// TryLoadKeyFromDisk attempts to load and validate a private key from disk. func TryLoadKeyFromDisk(rootKey string) (crypto.Signer, error) { - // Parse the private key from a file + // Parse the private key from the specified file. privKey, err := keyutil.PrivateKeyFromFile(rootKey) if err != nil { return nil, errors.Wrapf(err, "failed to load the private key file %s", rootKey) } - // Allow RSA and ECDSA formats only + // Only RSA and ECDSA private keys are supported. var key crypto.Signer switch k := privKey.(type) { case *rsa.PrivateKey: @@ -322,25 +366,14 @@ func TryLoadKeyFromDisk(rootKey string) (crypto.Signer, error) { return key, nil } -// TryLoadCertChainFromDisk tries to load the cert chain from the disk -func TryLoadCertChainFromDisk(rootCert string) (*x509.Certificate, []*x509.Certificate, error) { - certs, err := cgutilcert.CertsFromFile(rootCert) - if err != nil { - return nil, nil, errors.Wrapf(err, "failed to load the certificate file %s", rootCert) - } - - cert := certs[0] - intermediates := certs[1:] - - return cert, intermediates, nil +// TryLoadCertChainFromDisk loads a certificate chain from the specified file. +func TryLoadCertChainFromDisk(rootCert string) ([]*x509.Certificate, error) { + return cgutilcert.CertsFromFile(rootCert) } -// appendSANsToAltNames parses SANs from as list of strings and adds them to altNames for use on a specific cert -// altNames is passed in with a pointer, and the struct is modified -// valid IP address strings are parsed and added to altNames.IPs as net.IP's -// RFC-1123 compliant DNS strings are added to altNames.DNSNames as strings -// RFC-1123 compliant wildcard DNS strings are added to altNames.DNSNames as strings -// certNames is used to print user facing warnings and should be the name of the cert the altNames will be used for +// appendSANsToAltNames parses SANs from a list of strings and adds them to altNames for use in a certificate. +// Valid IP addresses are added to altNames.IPs, and valid DNS names (including wildcards) are added to altNames.DNSNames. +// Invalid entries are logged as warnings. func appendSANsToAltNames(altNames *cgutilcert.AltNames, sans []string) cgutilcert.AltNames { for _, altname := range sans { if ip := netutils.ParseIPSloppy(altname); ip != nil { @@ -351,7 +384,7 @@ func appendSANsToAltNames(altNames *cgutilcert.AltNames, sans []string) cgutilce altNames.DNSNames = append(altNames.DNSNames, altname) } else { klog.V(4).Infof( - "[certificates] WARNING: Added to the '%s' SAN failed, because it is not a valid IP or RFC-1123 compliant DNS entry\n", + "[certificates] WARNING: Failed to add '%s' to the SAN list, as it is not a valid IP or RFC-1123-compliant DNS entry\n", altname, ) } @@ -360,13 +393,13 @@ func appendSANsToAltNames(altNames *cgutilcert.AltNames, sans []string) cgutilce return *altNames } -// NewSelfSignedCACert creates a CA certificate +// NewSelfSignedCACert creates a new self-signed CA certificate. func NewSelfSignedCACert(cfg cgutilcert.Config, after time.Duration, key crypto.Signer) (*x509.Certificate, error) { now := time.Now() - // returns a uniform random value in [0, max-1), then add 1 to serial to make it a uniform random value in [1, max). + // Generate a random serial number in the range [1, MaxInt64). serial, err := cryptorand.Int(cryptorand.Reader, new(big.Int).SetInt64(math.MaxInt64-1)) if err != nil { - return nil, errors.Wrap(err, "failed to generate certificate's SerialNumber number") + return nil, errors.Wrap(err, "failed to generate certificate serial number") } serial = new(big.Int).Add(serial, big.NewInt(1)) @@ -374,7 +407,7 @@ func NewSelfSignedCACert(cfg cgutilcert.Config, after time.Duration, key crypto. if !cfg.NotBefore.IsZero() { notBefore = cfg.NotBefore.UTC() } - if after == 0 { // default 10 year + if after == 0 { // Default validity: 10 years. after = defaultSignCertAfter } @@ -400,17 +433,17 @@ func NewSelfSignedCACert(cfg cgutilcert.Config, after time.Duration, key crypto. return x509.ParseCertificate(certDERBytes) } -// NewSignedCert creates a signed certificate using the given CA certificate and key +// NewSignedCert creates a certificate signed by the given CA certificate and key. func NewSignedCert(cfg cgutilcert.Config, after time.Duration, key crypto.Signer, caCert *x509.Certificate, caKey crypto.Signer, isCA bool) (*x509.Certificate, error) { - // returns a uniform random value in [0, max-1), then add 1 to serial to make it a uniform random value in [1, max). + // Generate a random serial number in the range [1, MaxInt64). serial, err := cryptorand.Int(cryptorand.Reader, new(big.Int).SetInt64(math.MaxInt64-1)) if err != nil { - return nil, errors.Wrap(err, "failed to generate certificate's SerialNumber number") + return nil, errors.Wrap(err, "failed to generate certificate serial number") } serial = new(big.Int).Add(serial, big.NewInt(1)) if cfg.CommonName == "" { - return nil, errors.New("must specify a CommonName") + return nil, errors.New("commonName must be specified") } keyUsage := x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature @@ -448,7 +481,7 @@ func NewSignedCert(cfg cgutilcert.Config, after time.Duration, key crypto.Signer return x509.ParseCertificate(certDERBytes) } -// RemoveDuplicateAltNames removes duplicate items in altNames. +// RemoveDuplicateAltNames eliminates duplicate entries from the AltNames struct. func RemoveDuplicateAltNames(altNames *cgutilcert.AltNames) { if altNames == nil { return @@ -469,8 +502,7 @@ func RemoveDuplicateAltNames(altNames *cgutilcert.AltNames) { altNames.IPs = ips } -// ValidateCertPeriod checks if the certificate is valid relative to the current time -// (+/- offset) +// ValidateCertPeriod checks whether the certificate is currently valid, considering the given offset. func ValidateCertPeriod(cert *x509.Certificate, offset time.Duration) error { period := fmt.Sprintf("NotBefore: %v, NotAfter: %v", cert.NotBefore, cert.NotAfter) now := time.Now().Add(offset) @@ -484,8 +516,7 @@ func ValidateCertPeriod(cert *x509.Certificate, offset time.Duration) error { return nil } -// VerifyCertChain verifies that a certificate has a valid chain of -// intermediate CAs back to the root CA +// VerifyCertChain ensures that a certificate has a valid chain of trust back to the root CA. func VerifyCertChain(cert *x509.Certificate, intermediates []*x509.Certificate, root *x509.Certificate) error { rootPool := x509.NewCertPool() rootPool.AddCert(root) @@ -508,8 +539,7 @@ func VerifyCertChain(cert *x509.Certificate, intermediates []*x509.Certificate, return nil } -// validateCertificateWithConfig makes sure that a given certificate is valid at -// least for the SANs defined in the configuration. +// validateCertificateWithConfig ensures that the certificate is valid for all SANs specified in the configuration. func validateCertificateWithConfig(cert *x509.Certificate, baseName string, cfg cgutilcert.Config) error { for _, dnsName := range cfg.AltNames.DNSNames { if err := cert.VerifyHostname(dnsName); err != nil {