3c52aa1bb2
---ci--- project: oy phase: 4 milestone: v0.5 status: complete requirements: covered: [REQ-036] partial: [] ---/ci---
218 lines
7.6 KiB
Go
218 lines
7.6 KiB
Go
package keeper
|
|
|
|
// keyring_mem.go holds the in-memory test-only memKeyring impl of the
|
|
// CustodyKeyring interface (D-058, P4-02-01). data-engineer territory (P4
|
|
// phase-specific — removed after P4 per PERSONAS.md).
|
|
//
|
|
// D-054: simtest-grade — NO real MPC, NO real HSM, NO real hardware. The
|
|
// memKeyring signs with a throwaway ed25519 key per assetID (generated
|
|
// in-process; the seed is not persisted). Real MPC/HSM backing is deferred
|
|
// (operational, Year 3+). This impl exists so the x/hub custody handlers
|
|
// can be exercised end-to-end in simtest without a custody vendor.
|
|
//
|
|
// Rotation: Rotate(assetID) swaps the keymap entry for assetID with a fresh
|
|
// ed25519 keypair and bumps the version (monotonic uint64). A subsequent
|
|
// Status reports the new active key version; a subsequent Sign uses the new
|
|
// key (D-058: no cross-block caching — the handler consults Status/Sign per
|
|
// operation, so rotation is observed immediately). The previous key is
|
|
// retained as a Rotated entry so Derive can still return the historical
|
|
// pubkey for verification of prior signatures.
|
|
//
|
|
// Revocation: Revoke(assetID) marks the active key Revoked (terminal).
|
|
// Subsequent Sign/Derive against the assetID return ErrKeyringRevoked/
|
|
// ErrKeyringInactive. The key material is wiped (defensive — simtest grade).
|
|
//
|
|
// Thread safety: the simtest is single-threaded per-block (SDK store
|
|
// semantics); the memKeyring uses a mutex so concurrent test paths are
|
|
// safe (mirrors x/partner/types/types.go Keeper stub pattern).
|
|
//
|
|
// Lexicon note (REQ-012): "memKeyring", "Sign", "Derive", "rotation",
|
|
// "revocation", "ed25519" are all lexicon-clean. No banned terms.
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ed25519"
|
|
"crypto/rand"
|
|
"fmt"
|
|
"sync"
|
|
|
|
"github.com/oy/openyield/x/hub/types"
|
|
)
|
|
|
|
// memKeyring is the in-memory test-only CustodyKeyring impl (D-058).
|
|
// NOT for production use — real MPC/HSM backing is deferred (Year 3+).
|
|
type memKeyring struct {
|
|
mu sync.Mutex
|
|
keys map[string]*keyEntry // assetID → active key entry
|
|
}
|
|
|
|
// keyEntry is the per-assetID key record. The active key is the one used
|
|
// for Sign/Derive; the rotated keys are retained for historical Derive
|
|
// (verification of prior signatures).
|
|
type keyEntry struct {
|
|
priv ed25519.PrivateKey
|
|
pub ed25519.PublicKey
|
|
status types.KeyringStatus
|
|
version uint64
|
|
rotated []*keyEntry // historical (Rotated) entries, newest-first
|
|
}
|
|
|
|
// NewMemKeyring returns a fresh empty in-memory CustodyKeyring (D-058).
|
|
// Keys are generated lazily on the first Register/Sign/Derive for an
|
|
// assetID (or explicitly via Register).
|
|
func NewMemKeyring() types.CustodyKeyring {
|
|
return &memKeyring{keys: make(map[string]*keyEntry)}
|
|
}
|
|
|
|
// Register ensures an active key exists for assetID. If one already exists
|
|
// and is Active, this is a no-op (returns the existing version). If the
|
|
// assetID is unknown, a fresh ed25519 keypair is generated (version 1).
|
|
// Register is a convenience for test setup; the handler does not require
|
|
// explicit registration (Sign/Derive auto-register on first use).
|
|
func (m *memKeyring) Register(ctx context.Context, assetID string) (types.PubKey, uint64, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if e, ok := m.keys[assetID]; ok && e.status == types.KeyringActive {
|
|
return types.PubKey(e.pub), e.version, nil
|
|
}
|
|
e, err := newActiveEntry(1)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
m.keys[assetID] = e
|
|
return types.PubKey(e.pub), e.version, nil
|
|
}
|
|
|
|
// Sign produces an ed25519 signature over payload with the active key for
|
|
// assetID. Auto-registers on first use (lazy key generation). Returns
|
|
// ErrKeyringInactive if the key is Rotated or Revoked.
|
|
func (m *memKeyring) Sign(ctx context.Context, assetID string, payload []byte) ([]byte, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
e, ok := m.keys[assetID]
|
|
if !ok {
|
|
// Lazy auto-register on first Sign.
|
|
ne, err := newActiveEntry(1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
m.keys[assetID] = ne
|
|
e = ne
|
|
}
|
|
if e.status != types.KeyringActive {
|
|
return nil, types.ErrKeyringInactive
|
|
}
|
|
return ed25519.Sign(e.priv, payload), nil
|
|
}
|
|
|
|
// Derive returns the active public key for assetID. Auto-registers on first
|
|
// use. Returns ErrKeyringRevoked if the key is Revoked; returns the
|
|
// historical pubkey if the key is Rotated (for verification of prior
|
|
// signatures).
|
|
func (m *memKeyring) Derive(ctx context.Context, assetID string) (types.PubKey, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
e, ok := m.keys[assetID]
|
|
if !ok {
|
|
// Lazy auto-register on first Derive.
|
|
ne, err := newActiveEntry(1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
m.keys[assetID] = ne
|
|
e = ne
|
|
}
|
|
if e.status == types.KeyringRevoked {
|
|
return nil, types.ErrKeyringRevoked
|
|
}
|
|
// Active or Rotated: return the pubkey (Rotated returns the historical
|
|
// pubkey of that entry — the entry's own pubkey, not the new active).
|
|
return types.PubKey(e.pub), nil
|
|
}
|
|
|
|
// Status reports the active key's status + version for assetID. Returns
|
|
// ErrKeyringUnknownAsset if the assetID is not registered (Status does NOT
|
|
// auto-register — the handler consults Status before Sign to enforce
|
|
// rotation safety; auto-register on Status would mask a missing-asset bug).
|
|
func (m *memKeyring) Status(ctx context.Context, assetID string) (types.KeyringStatus, uint64, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
e, ok := m.keys[assetID]
|
|
if !ok {
|
|
return "", 0, types.ErrKeyringUnknownAsset
|
|
}
|
|
return e.status, e.version, nil
|
|
}
|
|
|
|
// Rotate swaps the active key for assetID with a fresh ed25519 keypair and
|
|
// bumps the version (monotonic). The previous key is retained as a Rotated
|
|
// entry (newest-first in e.rotated). A subsequent Sign uses the new key;
|
|
// Derive against the Rotated entry returns the historical pubkey. Returns
|
|
// the new version. This is the test-only rotation helper (D-058); the
|
|
// simtest exercises rotation via this method.
|
|
func (m *memKeyring) Rotate(assetID string) (uint64, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
e, ok := m.keys[assetID]
|
|
if !ok {
|
|
// Auto-register on Rotate (convenience for test setup).
|
|
ne, err := newActiveEntry(1)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
m.keys[assetID] = ne
|
|
return ne.version, nil
|
|
}
|
|
if e.status == types.KeyringRevoked {
|
|
return 0, types.ErrKeyringRevoked
|
|
}
|
|
// Promote current active to Rotated, generate a new active.
|
|
newVersion := e.version + 1
|
|
ne, err := newActiveEntry(newVersion)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
old := e
|
|
old.status = types.KeyringRotated
|
|
ne.rotated = append([]*keyEntry{old}, e.rotated...)
|
|
m.keys[assetID] = ne
|
|
return newVersion, nil
|
|
}
|
|
|
|
// Revoke marks the active key for assetID as Revoked (terminal). Subsequent
|
|
// Sign/Derive against the assetID return ErrKeyringInactive/ErrKeyringRevoked.
|
|
// The key material is wiped (defensive — simtest grade). Returns
|
|
// ErrKeyringUnknownAsset if the assetID is not registered.
|
|
func (m *memKeyring) Revoke(assetID string) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
e, ok := m.keys[assetID]
|
|
if !ok {
|
|
return types.ErrKeyringUnknownAsset
|
|
}
|
|
e.status = types.KeyringRevoked
|
|
// Defensive: wipe the private key material (simtest grade — a real
|
|
// impl would zeroize the HSM key slot).
|
|
wipe := make(ed25519.PrivateKey, ed25519.PrivateKeySize)
|
|
e.priv = wipe
|
|
return nil
|
|
}
|
|
|
|
// newActiveEntry generates a fresh ed25519 keypair with the given version
|
|
// and status=Active.
|
|
func newActiveEntry(version uint64) (*keyEntry, error) {
|
|
pub, priv, err := ed25519.GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("memKeyring: generate ed25519 key: %w", err)
|
|
}
|
|
return &keyEntry{
|
|
priv: priv,
|
|
pub: pub,
|
|
status: types.KeyringActive,
|
|
version: version,
|
|
}, nil
|
|
}
|
|
|
|
// Compile-time assertion: memKeyring implements types.CustodyKeyring.
|
|
var _ types.CustodyKeyring = (*memKeyring)(nil)
|