yao/openapi/oauth/signing.go
Max 966e0cfd00 Enhance OAuth test setup with reusable certificate management
- Introduced global test certificate paths to avoid redundant certificate generation across tests, improving efficiency.
- Implemented a function to create temporary certificates once for all tests, ensuring consistent usage of signing certificates.
- Updated test configurations to utilize the new certificate management, enhancing clarity and maintainability.
- Added cleanup functionality for global test certificates to ensure proper resource management after tests.
2025-07-21 19:44:00 +08:00

429 lines
12 KiB
Go

package oauth
import (
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"math/big"
"os"
"path/filepath"
"time"
"github.com/yaoapp/yao/openapi/oauth/types"
"github.com/yaoapp/yao/share"
)
// SigningCertificates holds the loaded signing certificates and keys
type SigningCertificates struct {
// Primary signing certificate and private key
SigningCert *x509.Certificate `json:"-"`
SigningKey interface{} `json:"-"` // *rsa.PrivateKey, *ecdsa.PrivateKey, etc.
SigningKeyPair *tls.Certificate `json:"-"`
// Verification certificates for token validation
VerificationCerts []*x509.Certificate `json:"-"`
// mTLS CA certificate for client validation
MTLSClientCACert *x509.Certificate `json:"-"`
// Signing algorithm
Algorithm string `json:"algorithm"`
// Certificate paths for reference
SigningCertPath string `json:"signing_cert_path"`
SigningKeyPath string `json:"signing_key_path"`
// Auto-generated flag
IsAutoGenerated bool `json:"is_auto_generated"`
}
// LoadSigningCertificates loads or generates signing certificates based on configuration
func LoadSigningCertificates(config *types.SigningConfig) (*SigningCertificates, error) {
certs := &SigningCertificates{
Algorithm: config.SigningAlgorithm,
}
// Check if certificate files exist
signingCertExists := fileExists(config.SigningCertPath)
signingKeyExists := fileExists(config.SigningKeyPath)
// If both files exist, try to load them
if signingCertExists && signingKeyExists {
err := loadExistingCertificates(certs, config)
if err != nil {
// If loading fails, log warning and generate new certificates
fmt.Printf("Warning: Failed to load existing certificates (%v), generating new temporary certificates\n", err)
return generateTemporaryCertificates(config)
}
return certs, nil
}
// If certificates don't exist, generate temporary ones
return generateTemporaryCertificates(config)
}
// loadExistingCertificates loads certificates from the configured paths
func loadExistingCertificates(certs *SigningCertificates, config *types.SigningConfig) error {
// Load signing certificate
certPEM, err := os.ReadFile(config.SigningCertPath)
if err != nil {
return fmt.Errorf("failed to read signing certificate: %w", err)
}
certBlock, _ := pem.Decode(certPEM)
if certBlock == nil {
return fmt.Errorf("failed to decode signing certificate PEM")
}
signingCert, err := x509.ParseCertificate(certBlock.Bytes)
if err != nil {
return fmt.Errorf("failed to parse signing certificate: %w", err)
}
// Load signing key
keyPEM, err := os.ReadFile(config.SigningKeyPath)
if err != nil {
return fmt.Errorf("failed to read signing key: %w", err)
}
keyBlock, _ := pem.Decode(keyPEM)
if keyBlock == nil {
return fmt.Errorf("failed to decode signing key PEM")
}
var signingKey interface{}
if config.SigningKeyPassword != "" {
// Decrypt encrypted key
keyBytes, err := x509.DecryptPEMBlock(keyBlock, []byte(config.SigningKeyPassword))
if err != nil {
return fmt.Errorf("failed to decrypt signing key: %w", err)
}
signingKey, err = parsePrivateKey(keyBytes)
if err != nil {
return fmt.Errorf("failed to parse decrypted signing key: %w", err)
}
} else {
// Parse unencrypted key
var err error
signingKey, err = parsePrivateKey(keyBlock.Bytes)
if err != nil {
return fmt.Errorf("failed to parse signing key: %w", err)
}
}
// Create TLS certificate pair
keyPair, err := tls.X509KeyPair(certPEM, keyPEM)
if err != nil {
return fmt.Errorf("failed to create key pair: %w", err)
}
certs.SigningCert = signingCert
certs.SigningKey = signingKey
certs.SigningKeyPair = &keyPair
certs.SigningCertPath = config.SigningCertPath
certs.SigningKeyPath = config.SigningKeyPath
certs.IsAutoGenerated = false
// Load verification certificates if configured
if len(config.VerificationCerts) > 0 {
verificationCerts, err := loadVerificationCertificates(config.VerificationCerts)
if err != nil {
return fmt.Errorf("failed to load verification certificates: %w", err)
}
certs.VerificationCerts = verificationCerts
}
// Load mTLS CA certificate if configured
if config.MTLSClientCACertPath != "" {
mtlsCACert, err := loadCertificateFromFile(config.MTLSClientCACertPath)
if err != nil {
return fmt.Errorf("failed to load mTLS CA certificate: %w", err)
}
certs.MTLSClientCACert = mtlsCACert
}
return nil
}
// generateTemporaryCertificates generates temporary self-signed certificates
func generateTemporaryCertificates(config *types.SigningConfig) (*SigningCertificates, error) {
// Generate RSA private key
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, fmt.Errorf("failed to generate private key: %w", err)
}
// Create certificate template
template := x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
Organization: []string{share.App.Name},
Country: []string{"US"},
Province: []string{""},
Locality: []string{""},
StreetAddress: []string{""},
PostalCode: []string{""},
CommonName: fmt.Sprintf("%s OAuth Signing Certificate", share.App.Name),
},
NotBefore: time.Now(),
NotAfter: time.Now().Add(365 * 24 * time.Hour), // Valid for 1 year
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
BasicConstraintsValid: true,
}
// Generate certificate
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
if err != nil {
return nil, fmt.Errorf("failed to create certificate: %w", err)
}
// Parse the generated certificate
cert, err := x509.ParseCertificate(certDER)
if err != nil {
return nil, fmt.Errorf("failed to parse generated certificate: %w", err)
}
// Create PEM blocks
certPEM := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: certDER,
})
keyDER, err := x509.MarshalPKCS8PrivateKey(privateKey)
if err != nil {
return nil, fmt.Errorf("failed to marshal private key: %w", err)
}
keyPEM := pem.EncodeToMemory(&pem.Block{
Type: "PRIVATE KEY",
Bytes: keyDER,
})
// Create TLS certificate pair
keyPair, err := tls.X509KeyPair(certPEM, keyPEM)
if err != nil {
return nil, fmt.Errorf("failed to create key pair: %w", err)
}
// Determine system directory for storing temporary certificates
systemDir := getSystemCertificateDirectory()
// Create directory if it doesn't exist
if err := os.MkdirAll(systemDir, 0755); err != nil {
return nil, fmt.Errorf("failed to create system certificate directory: %w", err)
}
// Generate unique filenames
timestamp := time.Now().Format("20060102150405")
certPath := filepath.Join(systemDir, fmt.Sprintf("oauth_signing_cert_%s.pem", timestamp))
keyPath := filepath.Join(systemDir, fmt.Sprintf("oauth_signing_key_%s.pem", timestamp))
// Save certificate and key to system directory
if err := os.WriteFile(certPath, certPEM, 0644); err != nil {
return nil, fmt.Errorf("failed to write certificate file: %w", err)
}
if err := os.WriteFile(keyPath, keyPEM, 0600); err != nil {
return nil, fmt.Errorf("failed to write key file: %w", err)
}
fmt.Printf("Generated temporary OAuth signing certificate at: %s\n", certPath)
fmt.Printf("Generated temporary OAuth signing key at: %s\n", keyPath)
return &SigningCertificates{
SigningCert: cert,
SigningKey: privateKey,
SigningKeyPair: &keyPair,
Algorithm: config.SigningAlgorithm,
SigningCertPath: certPath,
SigningKeyPath: keyPath,
IsAutoGenerated: true,
}, nil
}
// loadVerificationCertificates loads additional verification certificates
func loadVerificationCertificates(certPaths []string) ([]*x509.Certificate, error) {
var certs []*x509.Certificate
for _, certPath := range certPaths {
cert, err := loadCertificateFromFile(certPath)
if err != nil {
return nil, fmt.Errorf("failed to load verification certificate %s: %w", certPath, err)
}
certs = append(certs, cert)
}
return certs, nil
}
// loadCertificateFromFile loads a certificate from a PEM file
func loadCertificateFromFile(certPath string) (*x509.Certificate, error) {
certPEM, err := os.ReadFile(certPath)
if err != nil {
return nil, fmt.Errorf("failed to read certificate file: %w", err)
}
certBlock, _ := pem.Decode(certPEM)
if certBlock == nil {
return nil, fmt.Errorf("failed to decode certificate PEM")
}
cert, err := x509.ParseCertificate(certBlock.Bytes)
if err != nil {
return nil, fmt.Errorf("failed to parse certificate: %w", err)
}
return cert, nil
}
// parsePrivateKey parses a private key from DER bytes
func parsePrivateKey(der []byte) (interface{}, error) {
// Try PKCS#8 first
if key, err := x509.ParsePKCS8PrivateKey(der); err == nil {
return key, nil
}
// Try PKCS#1 RSA
if key, err := x509.ParsePKCS1PrivateKey(der); err == nil {
return key, nil
}
// Try EC private key
if key, err := x509.ParseECPrivateKey(der); err == nil {
return key, nil
}
return nil, fmt.Errorf("unable to parse private key")
}
// fileExists checks if a file exists
func fileExists(filename string) bool {
_, err := os.Stat(filename)
return !os.IsNotExist(err)
}
// getSystemCertificateDirectory returns the appropriate system directory for storing certificates
func getSystemCertificateDirectory() string {
// Use different directories based on the operating system
homeDir, err := os.UserHomeDir()
if err != nil {
// Fallback to temporary directory
return filepath.Join(os.TempDir(), "yao-oauth-certs")
}
// Create a hidden directory in user's home
return filepath.Join(homeDir, ".yao", "oauth", "certs")
}
// ValidateCertificate validates a certificate for OAuth signing
func (c *SigningCertificates) ValidateCertificate() error {
if c.SigningCert == nil {
return fmt.Errorf("signing certificate is nil")
}
// Check if certificate is expired
now := time.Now()
if now.Before(c.SigningCert.NotBefore) {
return fmt.Errorf("signing certificate is not yet valid")
}
if now.After(c.SigningCert.NotAfter) {
return fmt.Errorf("signing certificate has expired")
}
// Check if certificate has appropriate key usage
if c.SigningCert.KeyUsage&x509.KeyUsageDigitalSignature == 0 {
return fmt.Errorf("signing certificate does not have digital signature key usage")
}
return nil
}
// GetPublicKey returns the public key from the signing certificate
func (c *SigningCertificates) GetPublicKey() interface{} {
if c.SigningCert == nil {
return nil
}
return c.SigningCert.PublicKey
}
// GetKeyID returns a key identifier for the signing certificate
func (c *SigningCertificates) GetKeyID() string {
if c.SigningCert == nil {
return ""
}
// Use the certificate's serial number as key ID
return c.SigningCert.SerialNumber.String()
}
// CleanupTemporaryCertificates removes auto-generated temporary certificates
func (c *SigningCertificates) CleanupTemporaryCertificates() error {
if !c.IsAutoGenerated {
return nil // Don't delete user-provided certificates
}
var errs []error
if c.SigningCertPath != "" && fileExists(c.SigningCertPath) {
if err := os.Remove(c.SigningCertPath); err != nil {
errs = append(errs, fmt.Errorf("failed to remove certificate file %s: %w", c.SigningCertPath, err))
}
}
if c.SigningKeyPath != "" && fileExists(c.SigningKeyPath) {
if err := os.Remove(c.SigningKeyPath); err != nil {
errs = append(errs, fmt.Errorf("failed to remove key file %s: %w", c.SigningKeyPath, err))
}
}
if len(errs) > 0 {
return fmt.Errorf("cleanup errors: %v", errs)
}
return nil
}
// Service signing certificate methods
// GetSigningCertificates returns the signing certificates for the service
func (s *Service) GetSigningCertificates() *SigningCertificates {
return s.signingCerts
}
// GetSigningKey returns the signing private key
func (s *Service) GetSigningKey() interface{} {
if s.signingCerts == nil {
return nil
}
return s.signingCerts.SigningKey
}
// GetSigningCertificate returns the signing certificate
func (s *Service) GetSigningCertificate() interface{} {
if s.signingCerts == nil {
return nil
}
return s.signingCerts.SigningCert
}
// GetSigningAlgorithm returns the signing algorithm
func (s *Service) GetSigningAlgorithm() string {
if s.signingCerts == nil {
return "RS256" // default
}
return s.signingCerts.Algorithm
}
// GetKeyID returns the key identifier for JWT token signing
func (s *Service) GetKeyID() string {
if s.signingCerts == nil {
return ""
}
return s.signingCerts.GetKeyID()
}