From 6d21fda86ac37e7d7a0fa8b276f2a7bb0cd4b1f1 Mon Sep 17 00:00:00 2001 From: Dustin Schoenbrun Date: Fri, 10 Apr 2026 15:25:31 -0400 Subject: [PATCH] Create a mechanism for analyzing certificates for PQC-readiness This patch adds functionality to the tls module where we can now generate an analysis for a given certificate and determine its readiness for PQC and compliance for TLS 1.3. Tests were also added to ensure this all works. We will need to go back and add more to this when the new algorithms become available in the crypto libraries for true, quantum-safe instead of quantum-resistant solutions. Co-Author: Claude --- TLS_ANALYSIS_EXAMPLE.md | 217 ++++++++++++++ modules/common/test/helpers/certgen.go | 306 +++++++++++++++++++ modules/common/tls/tls.go | 236 +++++++++++++++ modules/common/tls/tls_test.go | 398 +++++++++++++++++++++++++ 4 files changed, 1157 insertions(+) create mode 100644 TLS_ANALYSIS_EXAMPLE.md create mode 100644 modules/common/test/helpers/certgen.go 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")) + } + }) + } +}