package auth import ( "context" "crypto/sha256" "crypto/x509" "encoding/hex" "fmt" "log" "strings" "armature/models" "armature/store" ) // ParseDN parses an RFC 2253 / RFC 4514 distinguished name into key-value pairs. // // Examples: // // "CN=Jeff Smith,O=Acme Corp,OU=Engineering" // "CN=Jane Doe,emailAddress=jane@acme.com,O=Acme Corp" // // Handles simple comma-separated key=value pairs. Does NOT handle // escaped commas in values (\,) or multi-valued RDNs (+). Sufficient // for typical X.509 client cert DNs. func ParseDN(dn string) map[string]string { result := make(map[string]string) parts := strings.Split(dn, ",") for _, part := range parts { part = strings.TrimSpace(part) idx := strings.Index(part, "=") if idx < 1 { continue } key := strings.TrimSpace(part[:idx]) val := strings.TrimSpace(part[idx+1:]) result[key] = val } return result } // FingerprintCert returns the hex-encoded SHA-256 hash of a certificate's // raw DER encoding. This is used as the stable external_id for mTLS users. func FingerprintCert(cert *x509.Certificate) string { h := sha256.Sum256(cert.Raw) return hex.EncodeToString(h[:]) } // resolveOrProvision looks up an existing user by auth_source=mtls and external_id, // or auto-provisions a new user from the certificate's CN and fingerprint. // // Shared by both MTLSProxyProvider and MTLSNativeProvider. func resolveOrProvision( ctx context.Context, stores store.Stores, cn string, dnFields map[string]string, fingerprint string, autoActivate bool, defaultTeam string, ) (*Result, error) { // ── Look up existing user ────────────────────────────────────── user, err := stores.Users.GetByExternalID(ctx, string(ModeMTLS), fingerprint) if err == nil && user != nil { if !user.IsActive { return nil, ErrInactive } log.Printf("[auth/mtls] existing user %s (%s)", user.Username, cn) return &Result{User: user, IsNewUser: false, VaultHint: ""}, nil } // ── Auto-provision ───────────────────────────────────────────── if !autoActivate { return nil, fmt.Errorf("%w: auto-provision disabled", ErrInvalidCreds) } email := dnFields["emailAddress"] if email == "" { email = models.HandleFromName(cn) + "@mtls.local" } handle := UniqueHandle(ctx, stores.Users, models.HandleFromName(cn)) user = &models.User{ Username: handle, Email: email, DisplayName: cn, IsActive: true, AuthSource: string(ModeMTLS), ExternalID: &fingerprint, Handle: handle, } if err := stores.Users.Create(ctx, user); err != nil { return nil, fmt.Errorf("auto-provision failed: %w", err) } log.Printf("[auth/mtls] auto-provisioned user %s from CN=%s", user.ID, cn) EnsureEveryoneGroup(ctx, stores, user.ID) if defaultTeam != "" { if err := stores.Teams.AddMember(ctx, defaultTeam, user.ID, "member"); err != nil { log.Printf("[auth/mtls] warn: could not add %s to default team: %v", user.ID, err) } } return &Result{User: user, IsNewUser: true, VaultHint: ""}, nil }