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:
338
pkg/cashu/keyset/keyset.go
Normal file
338
pkg/cashu/keyset/keyset.go
Normal file
@@ -0,0 +1,338 @@
|
||||
// Package keyset manages Cashu mint keysets for blind signature tokens.
|
||||
// Keysets rotate periodically to limit key exposure and provide forward secrecy.
|
||||
package keyset
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
)
|
||||
|
||||
// DefaultActiveWindow is how long a keyset is valid for issuing new tokens.
|
||||
const DefaultActiveWindow = 7 * 24 * time.Hour // 1 week
|
||||
|
||||
// DefaultVerifyWindow is how long a keyset remains valid for verification.
|
||||
const DefaultVerifyWindow = 21 * 24 * time.Hour // 3 weeks
|
||||
|
||||
// Keyset represents a signing keyset with lifecycle management.
|
||||
type Keyset struct {
|
||||
ID string // 14-char hex ID (7 bytes)
|
||||
PrivateKey *secp256k1.PrivateKey // Signing key
|
||||
PublicKey *secp256k1.PublicKey // Verification key
|
||||
CreatedAt time.Time // When keyset was created
|
||||
ActiveAt time.Time // When keyset becomes active for signing
|
||||
ExpiresAt time.Time // When keyset can no longer sign (but can still verify)
|
||||
VerifyEnd time.Time // When keyset can no longer verify
|
||||
Active bool // Whether keyset is currently active for signing
|
||||
}
|
||||
|
||||
// New creates a new keyset with generated keys.
|
||||
func New() (*Keyset, error) {
|
||||
return NewWithTTL(DefaultActiveWindow, DefaultVerifyWindow)
|
||||
}
|
||||
|
||||
// NewWithTTL creates a new keyset with custom lifetimes.
|
||||
func NewWithTTL(activeTTL, verifyTTL time.Duration) (*Keyset, error) {
|
||||
// Generate random private key
|
||||
keyBytes := make([]byte, 32)
|
||||
if _, err := rand.Read(keyBytes); err != nil {
|
||||
return nil, fmt.Errorf("keyset: failed to generate key: %w", err)
|
||||
}
|
||||
|
||||
privKey := secp256k1.PrivKeyFromBytes(keyBytes)
|
||||
pubKey := privKey.PubKey()
|
||||
|
||||
now := time.Now()
|
||||
k := &Keyset{
|
||||
PrivateKey: privKey,
|
||||
PublicKey: pubKey,
|
||||
CreatedAt: now,
|
||||
ActiveAt: now,
|
||||
ExpiresAt: now.Add(activeTTL),
|
||||
VerifyEnd: now.Add(verifyTTL),
|
||||
Active: true,
|
||||
}
|
||||
|
||||
// Calculate ID from public key
|
||||
k.ID = k.calculateID()
|
||||
|
||||
return k, nil
|
||||
}
|
||||
|
||||
// NewFromPrivateKey creates a keyset from an existing private key.
|
||||
func NewFromPrivateKey(privKeyBytes []byte, createdAt time.Time, activeTTL, verifyTTL time.Duration) (*Keyset, error) {
|
||||
if len(privKeyBytes) != 32 {
|
||||
return nil, fmt.Errorf("keyset: private key must be 32 bytes")
|
||||
}
|
||||
|
||||
privKey := secp256k1.PrivKeyFromBytes(privKeyBytes)
|
||||
pubKey := privKey.PubKey()
|
||||
|
||||
k := &Keyset{
|
||||
PrivateKey: privKey,
|
||||
PublicKey: pubKey,
|
||||
CreatedAt: createdAt,
|
||||
ActiveAt: createdAt,
|
||||
ExpiresAt: createdAt.Add(activeTTL),
|
||||
VerifyEnd: createdAt.Add(verifyTTL),
|
||||
Active: true,
|
||||
}
|
||||
|
||||
k.ID = k.calculateID()
|
||||
|
||||
return k, nil
|
||||
}
|
||||
|
||||
// calculateID computes the keyset ID from the public key.
|
||||
// ID = hex(SHA256(compressed_pubkey)[0:7])
|
||||
func (k *Keyset) calculateID() string {
|
||||
compressed := k.PublicKey.SerializeCompressed()
|
||||
hash := sha256.Sum256(compressed)
|
||||
return hex.EncodeToString(hash[:7])
|
||||
}
|
||||
|
||||
// IsActiveForSigning returns true if keyset can be used to sign new tokens.
|
||||
func (k *Keyset) IsActiveForSigning() bool {
|
||||
now := time.Now()
|
||||
return k.Active && now.After(k.ActiveAt) && now.Before(k.ExpiresAt)
|
||||
}
|
||||
|
||||
// IsValidForVerification returns true if keyset can be used to verify tokens.
|
||||
func (k *Keyset) IsValidForVerification() bool {
|
||||
now := time.Now()
|
||||
return now.After(k.ActiveAt) && now.Before(k.VerifyEnd)
|
||||
}
|
||||
|
||||
// Deactivate marks the keyset as no longer active for signing.
|
||||
func (k *Keyset) Deactivate() {
|
||||
k.Active = false
|
||||
}
|
||||
|
||||
// SerializePrivateKey returns the private key as bytes for storage.
|
||||
func (k *Keyset) SerializePrivateKey() []byte {
|
||||
return k.PrivateKey.Serialize()
|
||||
}
|
||||
|
||||
// SerializePublicKey returns the compressed public key.
|
||||
func (k *Keyset) SerializePublicKey() []byte {
|
||||
return k.PublicKey.SerializeCompressed()
|
||||
}
|
||||
|
||||
// KeysetInfo is a public view of a keyset (without private key).
|
||||
type KeysetInfo struct {
|
||||
ID string `json:"id"`
|
||||
PublicKey string `json:"pubkey"`
|
||||
Active bool `json:"active"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
ExpiresAt int64 `json:"expires_at"`
|
||||
VerifyEnd int64 `json:"verify_end"`
|
||||
}
|
||||
|
||||
// Info returns public information about the keyset.
|
||||
func (k *Keyset) Info() KeysetInfo {
|
||||
return KeysetInfo{
|
||||
ID: k.ID,
|
||||
PublicKey: hex.EncodeToString(k.SerializePublicKey()),
|
||||
Active: k.IsActiveForSigning(),
|
||||
CreatedAt: k.CreatedAt.Unix(),
|
||||
ExpiresAt: k.ExpiresAt.Unix(),
|
||||
VerifyEnd: k.VerifyEnd.Unix(),
|
||||
}
|
||||
}
|
||||
|
||||
// Manager handles keyset lifecycle including rotation.
|
||||
type Manager struct {
|
||||
store Store
|
||||
activeTTL time.Duration
|
||||
verifyTTL time.Duration
|
||||
|
||||
mu sync.RWMutex
|
||||
current *Keyset // Current active keyset for signing
|
||||
verification []*Keyset // All keysets valid for verification (including current)
|
||||
}
|
||||
|
||||
// NewManager creates a keyset manager.
|
||||
func NewManager(store Store, activeTTL, verifyTTL time.Duration) *Manager {
|
||||
return &Manager{
|
||||
store: store,
|
||||
activeTTL: activeTTL,
|
||||
verifyTTL: verifyTTL,
|
||||
verification: make([]*Keyset, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// Init initializes the manager by loading existing keysets or creating a new one.
|
||||
func (m *Manager) Init() error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
// Load all valid keysets from store
|
||||
keysets, err := m.store.ListVerificationKeysets()
|
||||
if err != nil {
|
||||
return fmt.Errorf("manager: failed to load keysets: %w", err)
|
||||
}
|
||||
|
||||
// Find current active keyset
|
||||
var active *Keyset
|
||||
for _, k := range keysets {
|
||||
if k.IsActiveForSigning() {
|
||||
if active == nil || k.CreatedAt.After(active.CreatedAt) {
|
||||
active = k
|
||||
}
|
||||
}
|
||||
if k.IsValidForVerification() {
|
||||
m.verification = append(m.verification, k)
|
||||
}
|
||||
}
|
||||
|
||||
// If no active keyset, create one
|
||||
if active == nil {
|
||||
newKeyset, err := NewWithTTL(m.activeTTL, m.verifyTTL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("manager: failed to create initial keyset: %w", err)
|
||||
}
|
||||
if err := m.store.SaveKeyset(newKeyset); err != nil {
|
||||
return fmt.Errorf("manager: failed to save initial keyset: %w", err)
|
||||
}
|
||||
active = newKeyset
|
||||
m.verification = append(m.verification, newKeyset)
|
||||
}
|
||||
|
||||
m.current = active
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSigningKeyset returns the current active keyset for signing.
|
||||
func (m *Manager) GetSigningKeyset() *Keyset {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
return m.current
|
||||
}
|
||||
|
||||
// GetVerificationKeysets returns all keysets valid for verification.
|
||||
func (m *Manager) GetVerificationKeysets() []*Keyset {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
result := make([]*Keyset, 0, len(m.verification))
|
||||
for _, k := range m.verification {
|
||||
if k.IsValidForVerification() {
|
||||
result = append(result, k)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// FindByID returns the keyset with the given ID, if it's valid for verification.
|
||||
func (m *Manager) FindByID(id string) *Keyset {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
for _, k := range m.verification {
|
||||
if k.ID == id && k.IsValidForVerification() {
|
||||
return k
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RotateIfNeeded checks if rotation is needed and performs it.
|
||||
// Returns true if a new keyset was created.
|
||||
func (m *Manager) RotateIfNeeded() (bool, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
// Check if current keyset is still active
|
||||
if m.current != nil && m.current.IsActiveForSigning() {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Create new keyset
|
||||
newKeyset, err := NewWithTTL(m.activeTTL, m.verifyTTL)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("manager: failed to create new keyset: %w", err)
|
||||
}
|
||||
|
||||
// Deactivate old keyset
|
||||
if m.current != nil {
|
||||
m.current.Deactivate()
|
||||
}
|
||||
|
||||
// Save new keyset
|
||||
if err := m.store.SaveKeyset(newKeyset); err != nil {
|
||||
return false, fmt.Errorf("manager: failed to save new keyset: %w", err)
|
||||
}
|
||||
|
||||
// Update manager state
|
||||
m.current = newKeyset
|
||||
m.verification = append(m.verification, newKeyset)
|
||||
|
||||
// Prune expired verification keysets
|
||||
m.pruneExpired()
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// pruneExpired removes keysets that are no longer valid for verification.
|
||||
// Must be called with lock held.
|
||||
func (m *Manager) pruneExpired() {
|
||||
valid := make([]*Keyset, 0, len(m.verification))
|
||||
for _, k := range m.verification {
|
||||
if k.IsValidForVerification() {
|
||||
valid = append(valid, k)
|
||||
}
|
||||
}
|
||||
m.verification = valid
|
||||
}
|
||||
|
||||
// ListKeysetInfo returns public info for all verification keysets.
|
||||
func (m *Manager) ListKeysetInfo() []KeysetInfo {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
result := make([]KeysetInfo, 0, len(m.verification))
|
||||
for _, k := range m.verification {
|
||||
if k.IsValidForVerification() {
|
||||
result = append(result, k.Info())
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// StartRotationTicker starts a goroutine that rotates keysets periodically.
|
||||
// Returns a channel that receives true on each rotation.
|
||||
func (m *Manager) StartRotationTicker(interval time.Duration) (rotated <-chan bool, stop func()) {
|
||||
ticker := time.NewTicker(interval)
|
||||
ch := make(chan bool, 1)
|
||||
done := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
rotated, err := m.RotateIfNeeded()
|
||||
if err != nil {
|
||||
// Log error but continue
|
||||
continue
|
||||
}
|
||||
if rotated {
|
||||
select {
|
||||
case ch <- true:
|
||||
default:
|
||||
}
|
||||
}
|
||||
case <-done:
|
||||
ticker.Stop()
|
||||
close(ch)
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return ch, func() { close(done) }
|
||||
}
|
||||
278
pkg/cashu/keyset/keyset_test.go
Normal file
278
pkg/cashu/keyset/keyset_test.go
Normal file
@@ -0,0 +1,278 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
74
pkg/cashu/keyset/store.go
Normal file
74
pkg/cashu/keyset/store.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package keyset
|
||||
|
||||
// Store is the interface for persisting keysets.
|
||||
// Implement this interface for your database backend.
|
||||
type Store interface {
|
||||
// SaveKeyset persists a keyset.
|
||||
SaveKeyset(k *Keyset) error
|
||||
|
||||
// LoadKeyset loads a keyset by ID.
|
||||
LoadKeyset(id string) (*Keyset, error)
|
||||
|
||||
// ListActiveKeysets returns all keysets that can be used for signing.
|
||||
ListActiveKeysets() ([]*Keyset, error)
|
||||
|
||||
// ListVerificationKeysets returns all keysets that can be used for verification.
|
||||
ListVerificationKeysets() ([]*Keyset, error)
|
||||
|
||||
// DeleteKeyset removes a keyset from storage.
|
||||
DeleteKeyset(id string) error
|
||||
}
|
||||
|
||||
// MemoryStore is an in-memory implementation of Store for testing.
|
||||
type MemoryStore struct {
|
||||
keysets map[string]*Keyset
|
||||
}
|
||||
|
||||
// NewMemoryStore creates a new in-memory store.
|
||||
func NewMemoryStore() *MemoryStore {
|
||||
return &MemoryStore{
|
||||
keysets: make(map[string]*Keyset),
|
||||
}
|
||||
}
|
||||
|
||||
// SaveKeyset saves a keyset to memory.
|
||||
func (s *MemoryStore) SaveKeyset(k *Keyset) error {
|
||||
s.keysets[k.ID] = k
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadKeyset loads a keyset by ID.
|
||||
func (s *MemoryStore) LoadKeyset(id string) (*Keyset, error) {
|
||||
if k, ok := s.keysets[id]; ok {
|
||||
return k, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// ListActiveKeysets returns all active keysets.
|
||||
func (s *MemoryStore) ListActiveKeysets() ([]*Keyset, error) {
|
||||
result := make([]*Keyset, 0)
|
||||
for _, k := range s.keysets {
|
||||
if k.IsActiveForSigning() {
|
||||
result = append(result, k)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ListVerificationKeysets returns all keysets valid for verification.
|
||||
func (s *MemoryStore) ListVerificationKeysets() ([]*Keyset, error) {
|
||||
result := make([]*Keyset, 0)
|
||||
for _, k := range s.keysets {
|
||||
if k.IsValidForVerification() {
|
||||
result = append(result, k)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// DeleteKeyset removes a keyset.
|
||||
func (s *MemoryStore) DeleteKeyset(id string) error {
|
||||
delete(s.keysets, id)
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user