diff --git a/TLS_ANALYSIS_EXAMPLE.md b/TLS_ANALYSIS_EXAMPLE.md new file mode 100644 index 00000000..fdb662bc --- /dev/null +++ b/TLS_ANALYSIS_EXAMPLE.md @@ -0,0 +1,217 @@ +# TLS Analysis Feature - Usage Examples + +This document demonstrates how to use the TLS 1.3 detection and PQC-safe algorithm checking features. + +## Overview + +The TLS library now provides functionality to: +- Detect if certificates support TLS 1.3 +- Check if certificates use Post-Quantum Cryptography (PQC) safe algorithms +- Analyze certificate properties (key algorithm, key size, signature algorithm) +- Get recommended cipher suites based on certificate strength + +## Usage Examples + +### 1. Basic Certificate Analysis + +```go +import ( + "context" + "fmt" + + "github.com/openstack-k8s-operators/lib-common/modules/common/tls" + "k8s.io/apimachinery/pkg/types" +) + +// Analyze a certificate from PEM bytes +func analyzeCertFromPEM(certPEM []byte) error { + analysis, err := tls.AnalyzeCertificate(certPEM) + if err != nil { + return fmt.Errorf("failed to analyze certificate: %w", err) + } + + fmt.Printf("TLS 1.3 Support: %v\n", analysis.SupportsTLS13) + fmt.Printf("PQC-Safe: %v\n", analysis.IsPQCSafe) + fmt.Printf("Key Algorithm: %s\n", analysis.KeyAlgorithm) + fmt.Printf("Key Size: %d bits\n", analysis.KeySize) + fmt.Printf("Signature Algorithm: %s\n", analysis.SignatureAlgorithm) + fmt.Printf("Recommended Cipher Suites: %v\n", analysis.CipherSuites) + + return nil +} +``` + +### 2. Analyze Certificate from Kubernetes Secret + +```go +// Analyze a certificate stored in a Kubernetes secret +func analyzeCertFromSecret(ctx context.Context, client client.Client, namespace, secretName string) error { + analysis, err := tls.AnalyzeCertSecret( + ctx, + client, + types.NamespacedName{Name: secretName, Namespace: namespace}, + ) + if err != nil { + return fmt.Errorf("failed to analyze certificate secret: %w", err) + } + + if !analysis.SupportsTLS13 { + fmt.Printf("Warning: Certificate does not support TLS 1.3\n") + } + + if !analysis.IsPQCSafe { + fmt.Printf("Warning: Certificate is not PQC-safe. Consider using:\n") + fmt.Printf(" - RSA with >= 3072 bits\n") + fmt.Printf(" - ECDSA with P-384 or P-521 curves\n") + } + + return nil +} +``` + +### 3. Using Service Methods in an Operator + +```go +import ( + "github.com/openstack-k8s-operators/lib-common/modules/common/helper" + "github.com/openstack-k8s-operators/lib-common/modules/common/tls" + ctrl "sigs.k8s.io/controller-runtime" +) + +func (r *MyServiceReconciler) reconcileTLS( + ctx context.Context, + h *helper.Helper, + instance *myv1.MyService, +) (ctrl.Result, error) { + + tlsService := &tls.Service{ + SecretName: instance.Spec.TLS.SecretName, + } + + // Check if TLS 1.3 is enabled + isTLS13, err := tlsService.IsTLS13Enabled(ctx, h, instance.Namespace) + if err != nil { + return ctrl.Result{}, err + } + + // Check if using PQC-safe algorithms + isPQC, err := tlsService.IsPQCSafe(ctx, h, instance.Namespace) + if err != nil { + return ctrl.Result{}, err + } + + // Get full analysis + analysis, err := tlsService.GetTLSAnalysis(ctx, h, instance.Namespace) + if err != nil { + return ctrl.Result{}, err + } + + // Log the findings + r.Log.Info("TLS Configuration Analysis", + "service", instance.Name, + "tls13Enabled", isTLS13, + "pqcSafe", isPQC, + "keyAlgorithm", analysis.KeyAlgorithm, + "keySize", analysis.KeySize, + ) + + // Update status or conditions based on findings + if !isPQC { + // Set a warning condition or status + r.Log.Info("Certificate is not PQC-safe", + "recommendation", "Consider upgrading to RSA-3072+ or ECDSA P-384+") + } + + return ctrl.Result{}, nil +} +``` + +### 4. Validation During Certificate Creation + +```go +func validateCertificateRequirements(ctx context.Context, client client.Client, certSecret types.NamespacedName, requirePQC bool) error { + analysis, err := tls.AnalyzeCertSecret(ctx, client, certSecret) + if err != nil { + return fmt.Errorf("failed to analyze certificate: %w", err) + } + + if !analysis.SupportsTLS13 { + return fmt.Errorf("certificate does not support TLS 1.3") + } + + if requirePQC && !analysis.IsPQCSafe { + return fmt.Errorf("certificate is not PQC-safe (current: %s-%d, required: RSA-3072+ or ECDSA P-384+)", + analysis.KeyAlgorithm, analysis.KeySize) + } + + return nil +} +``` + +## PQC-Safe Algorithm Requirements + +Based on NIST SP 800-57 guidelines for transitional quantum resistance: + +### RSA +- **Not PQC-Safe**: RSA-1024, RSA-2048 +- **PQC-Safe**: RSA-3072, RSA-4096 + +### ECDSA +- **Not PQC-Safe**: P-256 +- **PQC-Safe**: P-384, P-521 + +### Ed25519 +- **Not PQC-Safe**: Ed25519 (256-bit) + +### TLS 1.3 Compatibility + +All modern key algorithms are compatible with TLS 1.3: +- RSA >= 2048 bits +- ECDSA with P-256, P-384, or P-521 +- Ed25519 + +## Testing + +The implementation includes comprehensive unit tests covering: +- Certificate analysis for all key types (RSA, ECDSA, Ed25519) +- Different key sizes +- PQC-safety validation +- TLS 1.3 compatibility checks +- Error handling for invalid certificates +- Cipher suite recommendations + +Run tests with: +```bash +cd modules/common +go test -v ./tls -run TestAnalyzeCertificate +go test -v ./tls -run TestPQCSafe +go test -v ./tls -run TestTLS13Compatibility +``` + +## Certificate Generation for Testing + +Use the test helpers to generate certificates: + +```go +import ( + helpers "github.com/openstack-k8s-operators/lib-common/modules/common/test/helpers" +) + +// Generate RSA 3072-bit certificate (PQC-safe) +cert, err := helpers.GenerateCertificate(helpers.RSA3072CertConfig()) + +// Generate ECDSA P-384 certificate (PQC-safe) +cert, err := helpers.GenerateCertificate(helpers.ECDSAP384CertConfig()) + +// Generate custom certificate +config := &helpers.CertConfig{ + KeyType: "rsa", + KeySize: 4096, + CommonName: "my-service.example.com", + DNSNames: []string{"my-service.example.com", "localhost"}, + Organization: "My Organization", + NotBefore: time.Now(), + NotAfter: time.Now().Add(365 * 24 * time.Hour), +} +cert, err := helpers.GenerateCertificate(config) +``` diff --git a/modules/common/test/helpers/certgen.go b/modules/common/test/helpers/certgen.go new file mode 100644 index 00000000..fee2bf0e --- /dev/null +++ b/modules/common/test/helpers/certgen.go @@ -0,0 +1,306 @@ +/* +Copyright 2026 Red hat +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 specific language governing permissions and +limitations under the License. +*/ + +package helpers + +import ( + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "time" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" +) + +// CertConfig defines the configuration used to generate a test certificate +type CertConfig struct { + // KeyType is the type of key to generate: "rsa", "ecdsa", or "ed25519" + KeyType string + // KeySize is the size of the key in bits (for RSA: 2048, 3072, 4096; for ECDSA: 256, 384, 521) + KeySize int + // CommonName for the certificate + CommonName string + // DNSNames for Subject Alternative Names + DNSNames []string + // Organization name + Organization string + // NotBefore time (defaults to now) + NotBefore time.Time + // NotAfter time (defaults to now + 1 year) + NotAfter time.Time +} + +// GeneratedCert contains the generated certificate and key in PEM format +type GeneratedCert struct { + CertPEM []byte + KeyPEM []byte + CAPEM []byte +} + +// DefaultCertConfig returns a default certificate configuration with RSA-2048 and some sane +// defaults +func DefaultCertConfig() *CertConfig { + return &CertConfig{ + KeyType: "rsa", + KeySize: 2048, + CommonName: "test.example.com", + DNSNames: []string{"test.example.com", "localhost"}, + Organization: "Test Org", + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(1, 0, 0), + } +} + +// RSA2048CertConfig returns a configuration for an RSA 2048-bit certificate +// This certificate is TLS 1.3 compatible but is not PQC-safe +func RSA2048CertConfig() *CertConfig { + cfg := DefaultCertConfig() + cfg.KeyType = "rsa" + cfg.KeySize = 2048 + return cfg +} + +// RSA3072CertConfig returns a configuration for an RSA 3072-bit certificate +// This certificate is TLS 1.3 compatible and is quantum-resistant +func RSA3072CertConfig() *CertConfig { + cfg := DefaultCertConfig() + cfg.KeyType = "rsa" + cfg.KeySize = 3072 + return cfg +} + +// RSA4096CertConfig returns a configuration for an RSA 4096-bit certificate +// This certificate is TLS 1.3 compatible and is quantum-resistant +func RSA4096CertConfig() *CertConfig { + cfg := DefaultCertConfig() + cfg.KeyType = "rsa" + cfg.KeySize = 4096 + return cfg +} + +// ECDSAP256CertConfig returns a configuration for an ECDSA P-256 certificate +// This certificate is TLS 1.3 compatible but is not PQC-safe +func ECDSAP256CertConfig() *CertConfig { + cfg := DefaultCertConfig() + cfg.KeyType = "ecdsa" + cfg.KeySize = 256 + return cfg +} + +// ECDSAP384CertConfig returns a configuration for an ECDSA P-384 certificate +// This certificate is TLS 1.3 compatible and is quantum-resistant +func ECDSAP384CertConfig() *CertConfig { + cfg := DefaultCertConfig() + cfg.KeyType = "ecdsa" + cfg.KeySize = 384 + return cfg +} + +// ECDSAP521CertConfig returns a configuration for an ECDSA P-521 certificate +// This certificate is TLS 1.3 compatible and is quantum-resistant +func ECDSAP521CertConfig() *CertConfig { + cfg := DefaultCertConfig() + cfg.KeyType = "ecdsa" + cfg.KeySize = 521 + return cfg +} + +// Ed25519CertConfig returns a configuration for an Ed25519 certificate +// This certificate is TLS 1.3 compatible but is not PQC-safe +func Ed25519CertConfig() *CertConfig { + cfg := DefaultCertConfig() + cfg.KeyType = "ed25519" + cfg.KeySize = 0 // Ed25519 has a fixed key size + return cfg +} + +// GenerateCertificate generates a self-signed certificate from the provided configuration +func GenerateCertificate(config *CertConfig) (*GeneratedCert, error) { + // Generate the private key based on type + var privateKey interface{} + var err error + + switch config.KeyType { + case "rsa": + privateKey, err = rsa.GenerateKey(rand.Reader, config.KeySize) + case "ecdsa": + var curve elliptic.Curve + switch config.KeySize { + case 256: + curve = elliptic.P256() + case 384: + curve = elliptic.P384() + case 521: + curve = elliptic.P521() + default: + curve = elliptic.P256() + } + privateKey, err = ecdsa.GenerateKey(curve, rand.Reader) + case "ed25519": + _, privateKey, err = ed25519.GenerateKey(rand.Reader) + default: + privateKey, err = rsa.GenerateKey(rand.Reader, 2048) + } + + if err != nil { + return nil, err + } + + // Create certificate template + serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) + if err != nil { + return nil, err + } + + template := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + CommonName: config.CommonName, + Organization: []string{config.Organization}, + }, + DNSNames: config.DNSNames, + NotBefore: config.NotBefore, + NotAfter: config.NotAfter, + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, + BasicConstraintsValid: true, + IsCA: true, + } + + // Create the certificate + var publicKey interface{} + switch key := privateKey.(type) { + case *rsa.PrivateKey: + publicKey = &key.PublicKey + case *ecdsa.PrivateKey: + publicKey = &key.PublicKey + case ed25519.PrivateKey: + publicKey = key.Public() + } + + certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, publicKey, privateKey) + if err != nil { + return nil, err + } + + // Encode the certificate with PEM + certPEM := pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: certDER, + }) + + // Encode private key with PEM + var keyPEM []byte + switch key := privateKey.(type) { + case *rsa.PrivateKey: + keyPEM = pem.EncodeToMemory(&pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(key), + }) + case *ecdsa.PrivateKey: + keyBytes, err := x509.MarshalECPrivateKey(key) + if err != nil { + return nil, err + } + keyPEM = pem.EncodeToMemory(&pem.Block{ + Type: "EC PRIVATE KEY", + Bytes: keyBytes, + }) + case ed25519.PrivateKey: + keyBytes, err := x509.MarshalPKCS8PrivateKey(key) + if err != nil { + return nil, err + } + keyPEM = pem.EncodeToMemory(&pem.Block{ + Type: "PRIVATE KEY", + Bytes: keyBytes, + }) + } + + return &GeneratedCert{ + CertPEM: certPEM, + KeyPEM: keyPEM, + CAPEM: certPEM, // Self-signed, so the CA is the same as the certificate itself + }, nil +} + +// CreateCertSecretWithConfig creates a Kubernetes secret with a generated, self-signed certificate +func (tc *TestHelper) CreateCertSecretWithConfig( + name types.NamespacedName, + config *CertConfig, +) (*corev1.Secret, *GeneratedCert, error) { + cert, err := GenerateCertificate(config) + if err != nil { + return nil, nil, err + } + + data := map[string][]byte{ + "tls.crt": cert.CertPEM, + "tls.key": cert.KeyPEM, + "ca.crt": cert.CAPEM, + } + + secret := tc.CreateSecret(name, data) + return secret, cert, nil +} + +// CreateRSA2048CertSecret creates a Kubernetes secret with an RSA 2048-bit certificate +func (tc *TestHelper) CreateRSA2048CertSecret(name types.NamespacedName) (*corev1.Secret, *GeneratedCert, error) { + return tc.CreateCertSecretWithConfig(name, RSA2048CertConfig()) +} + +// CreateRSA3072CertSecret creates a Kubernetes secret with an RSA 3072-bit certificate +// (quantum-resistant) +func (tc *TestHelper) CreateRSA3072CertSecret(name types.NamespacedName) (*corev1.Secret, + *GeneratedCert, error) { + return tc.CreateCertSecretWithConfig(name, RSA3072CertConfig()) +} + +// CreateRSA4096CertSecret creates a Kubernetes secret with an RSA 4096-bit certificate +// (quantum-resistant) +func (tc *TestHelper) CreateRSA4096CertSecret(name types.NamespacedName) (*corev1.Secret, + *GeneratedCert, error) { + return tc.CreateCertSecretWithConfig(name, RSA4096CertConfig()) +} + +// CreateECDSAP256CertSecret creates a Kubernetes secret with an ECDSA P-256 certificate +func (tc *TestHelper) CreateECDSAP256CertSecret(name types.NamespacedName) (*corev1.Secret, + *GeneratedCert, error) { + return tc.CreateCertSecretWithConfig(name, ECDSAP256CertConfig()) +} + +// CreateECDSAP384CertSecret creates a Kubernetes secret with an ECDSA P-384 certificate +// (quantum-resistant) +func (tc *TestHelper) CreateECDSAP384CertSecret(name types.NamespacedName) (*corev1.Secret, + *GeneratedCert, error) { + return tc.CreateCertSecretWithConfig(name, ECDSAP384CertConfig()) +} + +// CreateECDSAP521CertSecret creates a Kubernetes secret with an ECDSA P-521 certificate +// (quantum-resistant) +func (tc *TestHelper) CreateECDSAP521CertSecret(name types.NamespacedName) (*corev1.Secret, + *GeneratedCert, error) { + return tc.CreateCertSecretWithConfig(name, ECDSAP521CertConfig()) +} + +// CreateEd25519CertSecret creates a Kubernetes secret with an Ed25519 certificate +func (tc *TestHelper) CreateEd25519CertSecret(name types.NamespacedName) (*corev1.Secret, *GeneratedCert, error) { + return tc.CreateCertSecretWithConfig(name, Ed25519CertConfig()) +} diff --git a/modules/common/tls/tls.go b/modules/common/tls/tls.go index 1b9742b3..bca7de3b 100644 --- a/modules/common/tls/tls.go +++ b/modules/common/tls/tls.go @@ -21,7 +21,13 @@ package tls import ( "context" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/rsa" + "crypto/tls" + "crypto/x509" "encoding/json" + "encoding/pem" "fmt" "time" @@ -443,3 +449,233 @@ func (c *Ca) CreateVolume() corev1.Volume { return volume } + +type TLSAnalysis struct { + // SupportsTLS13 indicates if TLS 1.3 is supported + SupportsTLS13 bool + // MinTLSVersion is the minimum TLS version supported + MinTLSVersion uint16 + // MaxTLSVersion is the maximum TLS version supported + MaxTLSVersion uint16 + // IsPQCSafe indicates whether the certificate uses post-quantum safe algorithms + IsPQCSafe bool + // SignatureAlgorithm is the signature algorithm used by the certificate + SignatureAlgorithm string + // KeyAlgorithm is the public key algorithm used + KeyAlgorithm string + // KeySize is the size of the public key in bits + KeySize int + // CipherSuites lists the cipher suites that would be used + CipherSuites []string +} + +var pqcSafeAlgorithms = map[x509.SignatureAlgorithm]bool{ + x509.SHA256WithRSA: false, // Depends on key size + x509.SHA384WithRSA: false, // Depends on key size + x509.SHA512WithRSA: false, // Depends on key size + x509.ECDSAWithSHA256: false, // Depends on key size + x509.ECDSAWithSHA384: true, // P-384 is transitionally safe + x509.ECDSAWithSHA512: true, // P521 is transitionally safe + x509.SHA256WithRSAPSS: false, // Depends on key size + x509.SHA384WithRSAPSS: false, // Depends on key size + x509.SHA512WithRSAPSS: false, // Depends on key size + x509.PureEd25519: false, // Ed25519 is not PQC-safe +} + +// Minimum key sizes required for transitional PQC safety (see NIST SP 800-57) +const ( + minPQCSafeRSAKeySize = 3072 + minPQCSafeECDSAKeySize = 384 // P-384 curve + minTLS13RSAKeySize = 2048 + minTLS13ECDSAKeySize = 256 +) + +// AnalyzeCertificate analyzes a certificate for TLS 1.3 enablement and PQC-safe algorithm usage +func AnalyzeCertificate(certPEM []byte) (*TLSAnalysis, error) { + // Decode the PEM block + block, _ := pem.Decode(certPEM) + if block == nil { + return nil, fmt.Errorf("failed to decode PEM block") + } + + // Parse the certificate + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, fmt.Errorf("failed to parse certificate: %w", err) + } + + analysis := &TLSAnalysis{ + SignatureAlgorithm: cert.SignatureAlgorithm.String(), + } + + // Determine if we're PQC-safe based on algorithm and key size + analysis.IsPQCSafe = isPQCSafe(cert) + + // Determine algorithm and key size + switch pubKey := cert.PublicKey.(type) { + case *rsa.PublicKey: + analysis.KeyAlgorithm = "RSA" + analysis.KeySize = pubKey.N.BitLen() + case *ecdsa.PublicKey: + analysis.KeyAlgorithm = "ECDSA" + analysis.KeySize = pubKey.Curve.Params().BitSize + case ed25519.PublicKey: + analysis.KeyAlgorithm = "Ed25519" + analysis.KeySize = ed25519.PublicKeySize * 8 // PublicKeySize is in bytes + default: + analysis.KeyAlgorithm = "Unknown" + } + + // Check TLS 1.3 support + // The Certificate itself doesn't dictate TLS version, but we can see if it's compatible with + // TLS 1.3 requirements. + analysis.SupportsTLS13 = isTLS13Compatible(cert) + + // Set version info (these typically come from server config, not the certificate) + analysis.MinTLSVersion = tls.VersionTLS12 + analysis.MaxTLSVersion = tls.VersionTLS13 + + // Get the recommended cipher suites + analysis.CipherSuites = getRecommendedCipherSuites(analysis.IsPQCSafe) + + return analysis, nil +} + +// isPQCSafe determines if a certificate is PQC-safe through using a PQC-safe algorithm or a +// large enough key size +func isPQCSafe(cert *x509.Certificate) bool { + // Check the signature algorithm + baseSafe, exists := pqcSafeAlgorithms[cert.SignatureAlgorithm] + + // For algorithms where PQC-safety depends on key length + if exists && !baseSafe { + switch pubKey := cert.PublicKey.(type) { + case *rsa.PublicKey: + // RSA keys need to be >= 3072 bits for PQC transitional safety + return pubKey.N.BitLen() >= minPQCSafeRSAKeySize + case *ecdsa.PublicKey: + // ECDSA needs a P-384 or P-521 curve + return pubKey.Curve.Params().BitSize >= minPQCSafeECDSAKeySize + } + } + + return baseSafe +} + +// isTLS13Compatible checks if a certificate is compatible with TLS 1.3 +func isTLS13Compatible(cert *x509.Certificate) bool { + // TLS 1.3 removed support for RSA-PSS and requires specific signature algorithms. + // Generally, certificates with RSA >= 2048, ECDSA with curves P-256+, + // or Ed25519 are compatible. + // NOTE: TLS 1.3 compatibility does NOT necessarily mean that it's PQC-safe! + switch pubKey := cert.PublicKey.(type) { + case *rsa.PublicKey: + return pubKey.N.BitLen() >= minTLS13RSAKeySize + case *ecdsa.PublicKey: + return pubKey.Curve.Params().BitSize >= minTLS13ECDSAKeySize + case ed25519.PublicKey: + return true + default: + return false + } +} + +// getRecommendedCipherSuites returns the recommended cipher suites depending on whether we want +// PQC ciphers or not. +func getRecommendedCipherSuites(pqcSafe bool) []string { + // TLS 1.3 cipher suites (these are always used for TLS 1.3) + tls13Suites := []string{ + "TLS_AES_128_GCM_SHA256", + "TLS_AES_256_GCM_SHA384", + "TLS_CHACHA20_POLY1305_SHA256", + } + + if pqcSafe { + // For PQC-safe configs, prefer stronger ciphers + return append([]string{ + "TLS_AES_256_GCM_SHA384", + }, tls13Suites...) + } + + return tls13Suites +} + +// AnalyzeCertSecret analyzes a certificate stored in a Kubernetes secret +func AnalyzeCertSecret( + ctx context.Context, + c client.Client, + secretName types.NamespacedName, +) (*TLSAnalysis, error) { + // Get the secret + certSecret := &corev1.Secret{} + err := c.Get(ctx, secretName, certSecret) + if err != nil { + return nil, fmt.Errorf("failed to get certificate secret: %w", err) + } + + // Get the certificate's data + certData, exists := certSecret.Data[CertKey] + if !exists { + return nil, fmt.Errorf("certificate data not found in secret") + } + + // Analyze the certificate + return AnalyzeCertificate(certData) +} + +// IsTLS13Enabled checks if TLS 1.3 is enabled for a service +func (s *Service) IsTLS13Enabled( + ctx context.Context, + h *helper.Helper, + namespace string, +) (bool, error) { + if s.SecretName == "" { + return false, fmt.Errorf("no certificate configured") + } + + analysis, err := AnalyzeCertSecret( + ctx, + h.GetClient(), + types.NamespacedName{Name: s.SecretName, Namespace: namespace}, + ) + if err != nil { + return false, err + } + + return analysis.SupportsTLS13, nil +} + +// IsPQCSafe checks if the certificate uses PQC-safe algorithms/key lengths +func (s *Service) IsPQCSafe(ctx context.Context, h *helper.Helper, namespace string) (bool, error) { + if s.SecretName == "" { + return false, fmt.Errorf("no certificate configured") + } + + analysis, err := AnalyzeCertSecret( + ctx, + h.GetClient(), + types.NamespacedName{Name: s.SecretName, Namespace: namespace}, + ) + if err != nil { + return false, err + } + + return analysis.IsPQCSafe, nil +} + +// GetTLSAnalysis returns a comprehensive TLS analysis for a service +func (s *Service) GetTLSAnalysis( + ctx context.Context, + h *helper.Helper, + namespace string, +) (*TLSAnalysis, error) { + if s.SecretName == "" { + return nil, fmt.Errorf("no certificate configured") + } + + return AnalyzeCertSecret( + ctx, + h.GetClient(), + types.NamespacedName{Name: s.SecretName, Namespace: namespace}, + ) +} diff --git a/modules/common/tls/tls_test.go b/modules/common/tls/tls_test.go index d8f5f739..bc74a2bf 100644 --- a/modules/common/tls/tls_test.go +++ b/modules/common/tls/tls_test.go @@ -18,12 +18,14 @@ package tls import ( "testing" + "time" corev1 "k8s.io/api/core/v1" "k8s.io/utils/ptr" . "github.com/onsi/gomega" // nolint:revive "github.com/openstack-k8s-operators/lib-common/modules/common/service" + helpers "github.com/openstack-k8s-operators/lib-common/modules/common/test/helpers" ) func TestAPIEnabled(t *testing.T) { @@ -343,3 +345,399 @@ func TestCaCreateVolume(t *testing.T) { }) } } + +func TestAnalyzeCertificate(t *testing.T) { + tests := []struct { + name string + certGenerator func() ([]byte, error) + wantTLS13 bool + wantPQCSafe bool + wantKeyAlgorithm string + wantKeySize int + wantMinCipherCount int + }{ + { + name: "RSA 2048-bit certificate (TLS 1.3 compatible, not PQC-safe)", + certGenerator: func() ([]byte, error) { + cfg := &helpers.CertConfig{ + KeyType: "rsa", + KeySize: 2048, + CommonName: "test-rsa-2048.example.com", + DNSNames: []string{"test-rsa-2048.example.com"}, + Organization: "Test Org", + NotBefore: ptr.To(time.Now()).Add(-1 * time.Hour), + NotAfter: ptr.To(time.Now()).Add(24 * time.Hour), + } + cert, err := helpers.GenerateCertificate(cfg) + if err != nil { + return nil, err + } + return cert.CertPEM, nil + }, + wantTLS13: true, + wantPQCSafe: false, + wantKeyAlgorithm: "RSA", + wantKeySize: 2048, + wantMinCipherCount: 1, + }, + { + name: "RSA 3072-bit certificate (TLS 1.3 compatible, PQC-safe)", + certGenerator: func() ([]byte, error) { + cfg := &helpers.CertConfig{ + KeyType: "rsa", + KeySize: 3072, + CommonName: "test-rsa-3072.example.com", + DNSNames: []string{"test-rsa-3072.example.com"}, + Organization: "Test Org", + NotBefore: ptr.To(time.Now()).Add(-1 * time.Hour), + NotAfter: ptr.To(time.Now()).Add(24 * time.Hour), + } + cert, err := helpers.GenerateCertificate(cfg) + if err != nil { + return nil, err + } + return cert.CertPEM, nil + }, + wantTLS13: true, + wantPQCSafe: true, + wantKeyAlgorithm: "RSA", + wantKeySize: 3072, + wantMinCipherCount: 1, + }, + { + name: "RSA 4096-bit certificate (TLS 1.3 compatible, PQC-safe)", + certGenerator: func() ([]byte, error) { + cfg := &helpers.CertConfig{ + KeyType: "rsa", + KeySize: 4096, + CommonName: "test-rsa-4096.example.com", + DNSNames: []string{"test-rsa-4096.example.com"}, + Organization: "Test Org", + NotBefore: ptr.To(time.Now()).Add(-1 * time.Hour), + NotAfter: ptr.To(time.Now()).Add(24 * time.Hour), + } + cert, err := helpers.GenerateCertificate(cfg) + if err != nil { + return nil, err + } + return cert.CertPEM, nil + }, + wantTLS13: true, + wantPQCSafe: true, + wantKeyAlgorithm: "RSA", + wantKeySize: 4096, + wantMinCipherCount: 1, + }, + { + name: "ECDSA P-256 certificate (TLS 1.3 compatible, not PQC-safe)", + certGenerator: func() ([]byte, error) { + cfg := &helpers.CertConfig{ + KeyType: "ecdsa", + KeySize: 256, + CommonName: "test-ecdsa-p256.example.com", + DNSNames: []string{"test-ecdsa-p256.example.com"}, + Organization: "Test Org", + NotBefore: ptr.To(time.Now()).Add(-1 * time.Hour), + NotAfter: ptr.To(time.Now()).Add(24 * time.Hour), + } + cert, err := helpers.GenerateCertificate(cfg) + if err != nil { + return nil, err + } + return cert.CertPEM, nil + }, + wantTLS13: true, + wantPQCSafe: false, + wantKeyAlgorithm: "ECDSA", + wantKeySize: 256, + wantMinCipherCount: 1, + }, + { + name: "ECDSA P-384 certificate (TLS 1.3 compatible, PQC-safe)", + certGenerator: func() ([]byte, error) { + cfg := &helpers.CertConfig{ + KeyType: "ecdsa", + KeySize: 384, + CommonName: "test-ecdsa-p384.example.com", + DNSNames: []string{"test-ecdsa-p384.example.com"}, + Organization: "Test Org", + NotBefore: ptr.To(time.Now()).Add(-1 * time.Hour), + NotAfter: ptr.To(time.Now()).Add(24 * time.Hour), + } + cert, err := helpers.GenerateCertificate(cfg) + if err != nil { + return nil, err + } + return cert.CertPEM, nil + }, + wantTLS13: true, + wantPQCSafe: true, + wantKeyAlgorithm: "ECDSA", + wantKeySize: 384, + wantMinCipherCount: 1, + }, + { + name: "ECDSA P-521 certificate (TLS 1.3 compatible, PQC-safe)", + certGenerator: func() ([]byte, error) { + cfg := &helpers.CertConfig{ + KeyType: "ecdsa", + KeySize: 521, + CommonName: "test-ecdsa-p521.example.com", + DNSNames: []string{"test-ecdsa-p521.example.com"}, + Organization: "Test Org", + NotBefore: ptr.To(time.Now()).Add(-1 * time.Hour), + NotAfter: ptr.To(time.Now()).Add(24 * time.Hour), + } + cert, err := helpers.GenerateCertificate(cfg) + if err != nil { + return nil, err + } + return cert.CertPEM, nil + }, + wantTLS13: true, + wantPQCSafe: true, + wantKeyAlgorithm: "ECDSA", + wantKeySize: 521, + wantMinCipherCount: 1, + }, + { + name: "Ed25519 certificate (TLS 1.3 compatible, not PQC-safe)", + certGenerator: func() ([]byte, error) { + cfg := &helpers.CertConfig{ + KeyType: "ed25519", + KeySize: 0, + CommonName: "test-ed25519.example.com", + DNSNames: []string{"test-ed25519.example.com"}, + Organization: "Test Org", + NotBefore: ptr.To(time.Now()).Add(-1 * time.Hour), + NotAfter: ptr.To(time.Now()).Add(24 * time.Hour), + } + cert, err := helpers.GenerateCertificate(cfg) + if err != nil { + return nil, err + } + return cert.CertPEM, nil + }, + wantTLS13: true, + wantPQCSafe: false, + wantKeyAlgorithm: "Ed25519", + wantKeySize: 256, + wantMinCipherCount: 1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + certPEM, err := tt.certGenerator() + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(certPEM).NotTo(BeEmpty()) + + analysis, err := AnalyzeCertificate(certPEM) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(analysis).NotTo(BeNil()) + + g.Expect(analysis.SupportsTLS13).To(Equal(tt.wantTLS13), + "TLS 1.3 support should be %v", tt.wantTLS13) + g.Expect(analysis.IsPQCSafe).To(Equal(tt.wantPQCSafe), + "PQC safety should be %v", tt.wantPQCSafe) + g.Expect(analysis.KeyAlgorithm).To(Equal(tt.wantKeyAlgorithm), + "Key algorithm should be %s", tt.wantKeyAlgorithm) + g.Expect(analysis.KeySize).To(Equal(tt.wantKeySize), + "Key size should be %d", tt.wantKeySize) + g.Expect(analysis.SignatureAlgorithm).NotTo(BeEmpty(), + "Signature algorithm should not be empty") + g.Expect(len(analysis.CipherSuites)).To(BeNumerically(">=", tt.wantMinCipherCount), + "Should have at least %d cipher suites", tt.wantMinCipherCount) + }) + } +} + +func TestAnalyzeCertificateErrors(t *testing.T) { + tests := []struct { + name string + certPEM []byte + wantError bool + }{ + { + name: "Empty PEM data", + certPEM: []byte{}, + wantError: true, + }, + { + name: "Invalid PEM data", + certPEM: []byte("not a valid PEM"), + wantError: true, + }, + { + name: "Valid PEM but invalid certificate", + certPEM: []byte(`-----BEGIN CERTIFICATE----- +invalid certificate data +-----END CERTIFICATE-----`), + wantError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + analysis, err := AnalyzeCertificate(tt.certPEM) + + if tt.wantError { + g.Expect(err).To(HaveOccurred()) + g.Expect(analysis).To(BeNil()) + } else { + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(analysis).NotTo(BeNil()) + } + }) + } +} + +func TestPQCSafeAlgorithms(t *testing.T) { + tests := []struct { + name string + keyType string + keySize int + wantPQCSafe bool + }{ + {"RSA 1024 not PQC-safe", "rsa", 1024, false}, + {"RSA 2048 not PQC-safe", "rsa", 2048, false}, + {"RSA 3072 PQC-safe", "rsa", 3072, true}, + {"RSA 4096 PQC-safe", "rsa", 4096, true}, + {"ECDSA P-256 not PQC-safe", "ecdsa", 256, false}, + {"ECDSA P-384 PQC-safe", "ecdsa", 384, true}, + {"ECDSA P-521 PQC-safe", "ecdsa", 521, true}, + {"Ed25519 not PQC-safe", "ed25519", 0, false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + cfg := &helpers.CertConfig{ + KeyType: tt.keyType, + KeySize: tt.keySize, + CommonName: "pqc-test.example.com", + DNSNames: []string{"pqc-test.example.com"}, + Organization: "Test Org", + NotBefore: ptr.To(time.Now()).Add(-1 * time.Hour), + NotAfter: ptr.To(time.Now()).Add(24 * time.Hour), + } + + cert, err := helpers.GenerateCertificate(cfg) + g.Expect(err).NotTo(HaveOccurred()) + + analysis, err := AnalyzeCertificate(cert.CertPEM) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(analysis.IsPQCSafe).To(Equal(tt.wantPQCSafe), + "%s with key size %d should have PQC-safe=%v", tt.keyType, tt.keySize, tt.wantPQCSafe) + }) + } +} + +func TestTLS13Compatibility(t *testing.T) { + tests := []struct { + name string + keyType string + keySize int + wantTLS13Compat bool + }{ + {"RSA 1024 not TLS 1.3 compatible", "rsa", 1024, false}, + {"RSA 2048 TLS 1.3 compatible", "rsa", 2048, true}, + {"RSA 3072 TLS 1.3 compatible", "rsa", 3072, true}, + {"RSA 4096 TLS 1.3 compatible", "rsa", 4096, true}, + {"ECDSA P-256 TLS 1.3 compatible", "ecdsa", 256, true}, + {"ECDSA P-384 TLS 1.3 compatible", "ecdsa", 384, true}, + {"ECDSA P-521 TLS 1.3 compatible", "ecdsa", 521, true}, + {"Ed25519 TLS 1.3 compatible", "ed25519", 0, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + cfg := &helpers.CertConfig{ + KeyType: tt.keyType, + KeySize: tt.keySize, + CommonName: "tls13-test.example.com", + DNSNames: []string{"tls13-test.example.com"}, + Organization: "Test Org", + NotBefore: ptr.To(time.Now()).Add(-1 * time.Hour), + NotAfter: ptr.To(time.Now()).Add(24 * time.Hour), + } + + cert, err := helpers.GenerateCertificate(cfg) + g.Expect(err).NotTo(HaveOccurred()) + + analysis, err := AnalyzeCertificate(cert.CertPEM) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(analysis.SupportsTLS13).To(Equal(tt.wantTLS13Compat), + "%s with key size %d should have TLS 1.3 compatibility=%v", + tt.keyType, tt.keySize, tt.wantTLS13Compat) + }) + } +} + +func TestCipherSuiteRecommendations(t *testing.T) { + tests := []struct { + name string + keyType string + keySize int + wantPQCSafe bool + expectStronger bool + }{ + { + name: "RSA 2048 should get standard cipher suites", + keyType: "rsa", + keySize: 2048, + wantPQCSafe: false, + expectStronger: false, + }, + { + name: "RSA 3072 should get stronger cipher suites", + keyType: "rsa", + keySize: 3072, + wantPQCSafe: true, + expectStronger: true, + }, + { + name: "ECDSA P-384 should get stronger cipher suites", + keyType: "ecdsa", + keySize: 384, + wantPQCSafe: true, + expectStronger: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + cfg := &helpers.CertConfig{ + KeyType: tt.keyType, + KeySize: tt.keySize, + CommonName: "cipher-test.example.com", + DNSNames: []string{"cipher-test.example.com"}, + Organization: "Test Org", + NotBefore: ptr.To(time.Now()).Add(-1 * time.Hour), + NotAfter: ptr.To(time.Now()).Add(24 * time.Hour), + } + + cert, err := helpers.GenerateCertificate(cfg) + g.Expect(err).NotTo(HaveOccurred()) + + analysis, err := AnalyzeCertificate(cert.CertPEM) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(analysis.IsPQCSafe).To(Equal(tt.wantPQCSafe)) + g.Expect(len(analysis.CipherSuites)).To(BeNumerically(">", 0)) + + if tt.expectStronger { + // PQC-safe configs should prefer TLS_AES_256_GCM_SHA384 + g.Expect(analysis.CipherSuites[0]).To(Equal("TLS_AES_256_GCM_SHA384")) + } + }) + } +}