Add Cashu blind signature access tokens (NIP-XX draft)
Implements privacy-preserving bearer tokens for relay access control using Cashu-style blind signatures. Tokens prove whitelist membership without linking issuance to usage. Features: - BDHKE crypto primitives (HashToCurve, Blind, Sign, Unblind, Verify) - Keyset management with weekly rotation - Token format with kind permissions and scope isolation - Generic issuer/verifier with pluggable authorization - HTTP endpoints: POST /cashu/mint, GET /cashu/keysets, GET /cashu/info - ACL adapter bridging ORLY's access control to Cashu AuthzChecker - Stateless revocation via ACL re-check on each token use - Two-token rotation for seamless renewal (max 2 weeks after blacklist) Configuration: - ORLY_CASHU_ENABLED: Enable Cashu tokens - ORLY_CASHU_TOKEN_TTL: Token validity (default: 1 week) - ORLY_CASHU_SCOPES: Allowed scopes (relay, nip46, blossom, api) - ORLY_CASHU_REAUTHORIZE: Re-check ACL on each verification Files: - pkg/cashu/bdhke/: Core blind signature cryptography - pkg/cashu/keyset/: Keyset management and rotation - pkg/cashu/token/: Token format with kind permissions - pkg/cashu/issuer/: Token issuance with authorization - pkg/cashu/verifier/: Token verification with middleware - pkg/interfaces/cashu/: AuthzChecker, KeysetStore interfaces - pkg/bunker/acl_adapter.go: ORLY ACL integration - app/handle-cashu.go: HTTP endpoints - docs/NIP-XX-CASHU-ACCESS-TOKENS.md: Full specification 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
144
app/handle-cashu.go
Normal file
144
app/handle-cashu.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"lol.mleku.dev/chk"
|
||||
"lol.mleku.dev/log"
|
||||
|
||||
"git.mleku.dev/mleku/nostr/httpauth"
|
||||
"next.orly.dev/pkg/cashu/issuer"
|
||||
"next.orly.dev/pkg/cashu/keyset"
|
||||
"next.orly.dev/pkg/cashu/token"
|
||||
)
|
||||
|
||||
// CashuMintRequest is the request body for token issuance.
|
||||
type CashuMintRequest struct {
|
||||
BlindedMessage string `json:"blinded_message"` // Hex-encoded blinded point B_
|
||||
Scope string `json:"scope"` // Token scope (e.g., "relay", "nip46")
|
||||
Kinds []int `json:"kinds,omitempty"` // Permitted event kinds
|
||||
KindRanges [][]int `json:"kind_ranges,omitempty"` // Permitted kind ranges
|
||||
}
|
||||
|
||||
// CashuMintResponse is the response body for token issuance.
|
||||
type CashuMintResponse struct {
|
||||
BlindedSignature string `json:"blinded_signature"` // Hex-encoded blinded signature C_
|
||||
KeysetID string `json:"keyset_id"` // Keyset ID used
|
||||
Expiry int64 `json:"expiry"` // Token expiration timestamp
|
||||
MintPubkey string `json:"mint_pubkey"` // Hex-encoded mint public key
|
||||
}
|
||||
|
||||
// handleCashuMint handles POST /cashu/mint - issues a new token.
|
||||
func (s *Server) handleCashuMint(w http.ResponseWriter, r *http.Request) {
|
||||
// Check if Cashu is enabled
|
||||
if s.CashuIssuer == nil {
|
||||
http.Error(w, "Cashu tokens not enabled", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
// Require NIP-98 authentication
|
||||
valid, pubkey, err := httpauth.CheckAuth(r)
|
||||
if chk.E(err) || !valid {
|
||||
http.Error(w, "NIP-98 authentication required", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse request body
|
||||
var req CashuMintRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Decode blinded message from hex
|
||||
blindedMsg, err := hex.DecodeString(req.BlindedMessage)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid blinded_message: must be hex", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Default scope
|
||||
if req.Scope == "" {
|
||||
req.Scope = token.ScopeRelay
|
||||
}
|
||||
|
||||
// Issue token
|
||||
issueReq := &issuer.IssueRequest{
|
||||
BlindedMessage: blindedMsg,
|
||||
Pubkey: pubkey,
|
||||
Scope: req.Scope,
|
||||
Kinds: req.Kinds,
|
||||
KindRanges: req.KindRanges,
|
||||
}
|
||||
|
||||
resp, err := s.CashuIssuer.Issue(r.Context(), issueReq, r.RemoteAddr)
|
||||
if err != nil {
|
||||
log.W.F("Cashu mint failed for %x: %v", pubkey[:8], err)
|
||||
http.Error(w, err.Error(), http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
log.D.F("Cashu token issued for %x, scope=%s, keyset=%s", pubkey[:8], req.Scope, resp.KeysetID)
|
||||
|
||||
// Return response
|
||||
mintResp := CashuMintResponse{
|
||||
BlindedSignature: hex.EncodeToString(resp.BlindedSignature),
|
||||
KeysetID: resp.KeysetID,
|
||||
Expiry: resp.Expiry,
|
||||
MintPubkey: hex.EncodeToString(resp.MintPubkey),
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(mintResp)
|
||||
}
|
||||
|
||||
// handleCashuKeysets handles GET /cashu/keysets - returns available keysets.
|
||||
func (s *Server) handleCashuKeysets(w http.ResponseWriter, r *http.Request) {
|
||||
if s.CashuIssuer == nil {
|
||||
http.Error(w, "Cashu tokens not enabled", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
infos := s.CashuIssuer.GetKeysetInfo()
|
||||
|
||||
type KeysetsResponse struct {
|
||||
Keysets []keyset.KeysetInfo `json:"keysets"`
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(KeysetsResponse{Keysets: infos})
|
||||
}
|
||||
|
||||
// handleCashuInfo handles GET /cashu/info - returns mint information.
|
||||
func (s *Server) handleCashuInfo(w http.ResponseWriter, r *http.Request) {
|
||||
if s.CashuIssuer == nil {
|
||||
http.Error(w, "Cashu tokens not enabled", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
info := s.CashuIssuer.GetMintInfo(s.Config.AppName)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(info)
|
||||
}
|
||||
|
||||
// CashuTokenTTL returns the configured token TTL.
|
||||
func (s *Server) CashuTokenTTL() time.Duration {
|
||||
enabled, tokenTTL, _, _, _, _ := s.Config.GetCashuConfigValues()
|
||||
if !enabled {
|
||||
return 0
|
||||
}
|
||||
return tokenTTL
|
||||
}
|
||||
|
||||
// CashuKeysetTTL returns the configured keyset TTL.
|
||||
func (s *Server) CashuKeysetTTL() time.Duration {
|
||||
enabled, _, keysetTTL, _, _, _ := s.Config.GetCashuConfigValues()
|
||||
if !enabled {
|
||||
return 0
|
||||
}
|
||||
return keysetTTL
|
||||
}
|
||||
Reference in New Issue
Block a user