package auth import ( "context" "crypto/sha256" "encoding/base64" "encoding/json" "fmt" "time" "northwest.io/muxstr/internal/nostr" ) // NostrCredentials implements credentials.PerRPCCredentials for NIP-98 authentication. // It automatically signs each gRPC request with a Nostr event (kind 27235) and // attaches it to the Authorization header. type NostrCredentials struct { key *nostr.Key includePayload bool } // NewNostrCredentials creates credentials using the provided key. // Each RPC call will be authenticated with a freshly signed NIP-98 event. // The key must have a private key (CanSign() must return true). func NewNostrCredentials(key *nostr.Key) *NostrCredentials { return &NostrCredentials{ key: key, includePayload: false, } } // NewNostrCredentialsWithPayload creates credentials that include payload hashes. // When enabled, a SHA256 hash of the request body is included in the auth event. func NewNostrCredentialsWithPayload(key *nostr.Key) *NostrCredentials { return &NostrCredentials{ key: key, includePayload: true, } } // GetRequestMetadata implements credentials.PerRPCCredentials. // It creates and signs a NIP-98 auth event for each request. func (n *NostrCredentials) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) { if len(uri) == 0 { return nil, fmt.Errorf("no URI provided") } // Create kind 27235 event (NIP-98 HTTP Auth) event := &nostr.Event{ PubKey: n.key.Public(), CreatedAt: time.Now().Unix(), Kind: 27235, // NIP-98 HTTP Auth Tags: nostr.Tags{}, Content: "", } // Add URL tag event.Tags = append(event.Tags, nostr.Tag{"u", uri[0]}) // Add method tag - default to POST for gRPC // The URI contains the method name, e.g., /nostr.v1.NostrRelay/PublishEvent event.Tags = append(event.Tags, nostr.Tag{"method", "POST"}) // TODO: Add payload hash if includePayload is true // This requires access to the request body, which isn't available in GetRequestMetadata // We could use a context key to pass the payload hash from the application // Sign the event if err := n.key.Sign(event); err != nil { return nil, fmt.Errorf("failed to sign auth event: %w", err) } // Encode event as base64 JSON eventJSON, err := json.Marshal(event) if err != nil { return nil, fmt.Errorf("failed to marshal auth event: %w", err) } authHeader := "Nostr " + base64.StdEncoding.EncodeToString(eventJSON) return map[string]string{ "authorization": authHeader, }, nil } // RequireTransportSecurity implements credentials.PerRPCCredentials. // Returns false to allow usage over insecure connections (for development). // In production, use TLS and set this to true. func (n *NostrCredentials) RequireTransportSecurity() bool { return false } // SetRequireTLS configures whether TLS is required. // When true, the credentials will only work over TLS connections. type NostrCredentialsWithTLS struct { *NostrCredentials } // NewNostrCredentialsWithTLS creates credentials that require TLS. func NewNostrCredentialsWithTLS(key *nostr.Key) *NostrCredentialsWithTLS { return &NostrCredentialsWithTLS{ NostrCredentials: NewNostrCredentials(key), } } // RequireTransportSecurity returns true to enforce TLS. func (n *NostrCredentialsWithTLS) RequireTransportSecurity() bool { return true } // HashPayload creates a SHA256 hash of the payload for inclusion in auth events. // This can be used to verify request integrity. func HashPayload(payload []byte) string { hash := sha256.Sum256(payload) return fmt.Sprintf("%x", hash) }