Files
next.orly.dev/pkg/cashu/keyset/keyset_test.go
mleku ea4a54c5e7 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>
2025-12-28 11:30:11 +02:00

279 lines
6.3 KiB
Go

package keyset
import (
"testing"
"time"
)
func TestNewKeyset(t *testing.T) {
k, err := New()
if err != nil {
t.Fatalf("New() failed: %v", err)
}
// Check ID is 14 characters (7 bytes hex)
if len(k.ID) != 14 {
t.Errorf("ID length = %d, want 14", len(k.ID))
}
// Check keys are set
if k.PrivateKey == nil {
t.Error("PrivateKey is nil")
}
if k.PublicKey == nil {
t.Error("PublicKey is nil")
}
// Check times are set
if k.CreatedAt.IsZero() {
t.Error("CreatedAt is zero")
}
if !k.IsActiveForSigning() {
t.Error("New keyset should be active for signing")
}
if !k.IsValidForVerification() {
t.Error("New keyset should be valid for verification")
}
}
func TestKeysetIDDeterministic(t *testing.T) {
// Same private key should produce same ID
privKeyBytes := make([]byte, 32)
for i := range privKeyBytes {
privKeyBytes[i] = byte(i)
}
k1, err := NewFromPrivateKey(privKeyBytes, time.Now(), DefaultActiveWindow, DefaultVerifyWindow)
if err != nil {
t.Fatalf("NewFromPrivateKey failed: %v", err)
}
k2, err := NewFromPrivateKey(privKeyBytes, time.Now(), DefaultActiveWindow, DefaultVerifyWindow)
if err != nil {
t.Fatalf("NewFromPrivateKey failed: %v", err)
}
if k1.ID != k2.ID {
t.Errorf("IDs should match: %s != %s", k1.ID, k2.ID)
}
}
func TestKeysetExpiration(t *testing.T) {
// Create keyset with very short TTL
k, err := NewWithTTL(100*time.Millisecond, 200*time.Millisecond)
if err != nil {
t.Fatalf("NewWithTTL failed: %v", err)
}
// Should be active initially
if !k.IsActiveForSigning() {
t.Error("New keyset should be active for signing")
}
// Wait for signing to expire
time.Sleep(150 * time.Millisecond)
if k.IsActiveForSigning() {
t.Error("Keyset should not be active for signing after expiry")
}
if !k.IsValidForVerification() {
t.Error("Keyset should still be valid for verification")
}
// Wait for verification to expire
time.Sleep(100 * time.Millisecond)
if k.IsValidForVerification() {
t.Error("Keyset should not be valid for verification after verify expiry")
}
}
func TestKeysetDeactivate(t *testing.T) {
k, _ := New()
if !k.Active {
t.Error("New keyset should be active")
}
k.Deactivate()
if k.Active {
t.Error("Keyset should not be active after Deactivate()")
}
if k.IsActiveForSigning() {
t.Error("Deactivated keyset should not be active for signing")
}
}
func TestKeysetInfo(t *testing.T) {
k, _ := New()
info := k.Info()
if info.ID != k.ID {
t.Errorf("Info ID = %s, want %s", info.ID, k.ID)
}
if len(info.PublicKey) != 66 { // 33 bytes * 2 hex chars
t.Errorf("Info PublicKey length = %d, want 66", len(info.PublicKey))
}
if !info.Active {
t.Error("Info Active should be true for new keyset")
}
}
func TestManager(t *testing.T) {
store := NewMemoryStore()
manager := NewManager(store, DefaultActiveWindow, DefaultVerifyWindow)
if err := manager.Init(); err != nil {
t.Fatalf("Init failed: %v", err)
}
// Should have a signing keyset
signing := manager.GetSigningKeyset()
if signing == nil {
t.Fatal("GetSigningKeyset returned nil")
}
// Should have at least one verification keyset
verification := manager.GetVerificationKeysets()
if len(verification) == 0 {
t.Error("GetVerificationKeysets returned empty")
}
// Should find keyset by ID
found := manager.FindByID(signing.ID)
if found == nil {
t.Error("FindByID returned nil for signing keyset")
}
if found.ID != signing.ID {
t.Errorf("FindByID returned wrong keyset: %s != %s", found.ID, signing.ID)
}
}
func TestManagerRotation(t *testing.T) {
store := NewMemoryStore()
manager := NewManager(store, 50*time.Millisecond, 200*time.Millisecond)
if err := manager.Init(); err != nil {
t.Fatalf("Init failed: %v", err)
}
initialID := manager.GetSigningKeyset().ID
// Rotation should not happen yet
rotated, err := manager.RotateIfNeeded()
if err != nil {
t.Fatalf("RotateIfNeeded failed: %v", err)
}
if rotated {
t.Error("Should not rotate when keyset is still active")
}
// Wait for signing to expire
time.Sleep(60 * time.Millisecond)
// Now rotation should happen
rotated, err = manager.RotateIfNeeded()
if err != nil {
t.Fatalf("RotateIfNeeded failed: %v", err)
}
if !rotated {
t.Error("Should rotate when keyset is expired")
}
newID := manager.GetSigningKeyset().ID
if newID == initialID {
t.Error("New keyset should have different ID")
}
// Old keyset should still be valid for verification
old := manager.FindByID(initialID)
if old == nil {
t.Error("Old keyset should still be found for verification")
}
}
func TestManagerPersistence(t *testing.T) {
store := NewMemoryStore()
// First manager creates keyset
m1 := NewManager(store, DefaultActiveWindow, DefaultVerifyWindow)
if err := m1.Init(); err != nil {
t.Fatalf("Init failed: %v", err)
}
id := m1.GetSigningKeyset().ID
// Second manager should load existing keyset
m2 := NewManager(store, DefaultActiveWindow, DefaultVerifyWindow)
if err := m2.Init(); err != nil {
t.Fatalf("Init failed: %v", err)
}
if m2.GetSigningKeyset().ID != id {
t.Error("Second manager should use same keyset as first")
}
}
func TestManagerListKeysetInfo(t *testing.T) {
store := NewMemoryStore()
manager := NewManager(store, DefaultActiveWindow, DefaultVerifyWindow)
manager.Init()
infos := manager.ListKeysetInfo()
if len(infos) == 0 {
t.Error("ListKeysetInfo returned empty")
}
for _, info := range infos {
if info.ID == "" {
t.Error("KeysetInfo has empty ID")
}
if info.PublicKey == "" {
t.Error("KeysetInfo has empty PublicKey")
}
}
}
func TestMemoryStore(t *testing.T) {
store := NewMemoryStore()
k, _ := New()
// Save
if err := store.SaveKeyset(k); err != nil {
t.Fatalf("SaveKeyset failed: %v", err)
}
// Load
loaded, err := store.LoadKeyset(k.ID)
if err != nil {
t.Fatalf("LoadKeyset failed: %v", err)
}
if loaded == nil {
t.Fatal("LoadKeyset returned nil")
}
if loaded.ID != k.ID {
t.Errorf("Loaded ID = %s, want %s", loaded.ID, k.ID)
}
// List active
active, err := store.ListActiveKeysets()
if err != nil {
t.Fatalf("ListActiveKeysets failed: %v", err)
}
if len(active) != 1 {
t.Errorf("ListActiveKeysets returned %d, want 1", len(active))
}
// Delete
if err := store.DeleteKeyset(k.ID); err != nil {
t.Fatalf("DeleteKeyset failed: %v", err)
}
// Should be gone
loaded, _ = store.LoadKeyset(k.ID)
if loaded != nil {
t.Error("Keyset should be deleted")
}
}