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() }