Files
zeekay 6f42074e06 Merge hanzo/mpc additions: ConsensusKV, HSM providers, policy engine, rename CLI to mpc-cli
- pkg/infra: Add abstract KV interface, ConsensusKV (dual-cert BFT with
  Ed25519 + ML-DSA-65), NatsKV, ConsulKVAdapter, HybridKV, SQLiteMeta
  with full test coverage
- pkg/hsm: Add Provider interface and implementations for AWS KMS,
  GCP Cloud KMS, Azure Key Vault, Zymbit HSM, Hanzo KMS, and file-based
  providers with registry and tests. Rename existing FileProvider to
  FilePasswordProvider to avoid collision with new Provider interface
- pkg/policy: Add transaction governance engine with signers, roles,
  spending limits, whitelist/blacklist, time windows, rate limiting,
  approval workflow, audit log, and FHE private policy evaluation
- contracts: Add ThresholdPolicy.sol for on-chain policy enforcement
- cmd: Rename lux-mpc-cli to mpc-cli
- Makefile: Update build targets for mpc-cli
2025-12-28 03:12:38 -08:00

254 lines
6.9 KiB
Go

//go:build zymbit
package hsm
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"github.com/luxfi/mpc/pkg/logger"
)
// ZymbitProvider implements Provider using the Zymbit Secure Compute Module REST API.
// Private keys never leave the hardware device; signing is performed on-device.
type ZymbitProvider struct {
endpoint string
slotID int
keyType string
client *http.Client
}
// NewZymbitProvider creates a Zymbit HSM provider using the REST API.
func NewZymbitProvider(cfg *ZymbitConfig) (*ZymbitProvider, error) {
if cfg == nil {
return nil, fmt.Errorf("hsm/zymbit: config is nil")
}
endpoint := cfg.APIEndpoint
if endpoint == "" {
return nil, fmt.Errorf("hsm/zymbit: api_endpoint is required")
}
keyType := cfg.KeyType
if keyType == "" {
keyType = "ed25519"
}
p := &ZymbitProvider{
endpoint: endpoint,
slotID: cfg.SlotID,
keyType: keyType,
client: &http.Client{
Timeout: 30 * time.Second,
},
}
logger.Info("hsm/zymbit: initialized",
"endpoint", endpoint,
"slotID", cfg.SlotID,
"keyType", keyType,
)
return p, nil
}
func (z *ZymbitProvider) Name() string { return "zymbit" }
// GetKey returns the public key for a key slot. The private key never leaves the device.
func (z *ZymbitProvider) GetKey(ctx context.Context, keyID string) ([]byte, error) {
url := fmt.Sprintf("%s/api/v1/keys/%s/public", z.endpoint, keyID)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("hsm/zymbit: create request: %w", err)
}
resp, err := z.client.Do(req)
if err != nil {
return nil, fmt.Errorf("hsm/zymbit: get key %q: %w", keyID, err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("hsm/zymbit: get key %q: status %d: %s", keyID, resp.StatusCode, body)
}
var result struct {
PublicKey []byte `json:"public_key"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("hsm/zymbit: decode response: %w", err)
}
return result.PublicKey, nil
}
// StoreKey generates a new key in the specified slot.
// The key material parameter is ignored; the device generates the key internally.
func (z *ZymbitProvider) StoreKey(ctx context.Context, keyID string, key []byte) error {
url := fmt.Sprintf("%s/api/v1/keys", z.endpoint)
body := map[string]interface{}{
"key_id": keyID,
"key_type": z.keyType,
"slot_id": z.slotID,
}
bodyJSON, err := json.Marshal(body)
if err != nil {
return fmt.Errorf("hsm/zymbit: marshal request: %w", err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(bodyJSON))
if err != nil {
return fmt.Errorf("hsm/zymbit: create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
resp, err := z.client.Do(req)
if err != nil {
return fmt.Errorf("hsm/zymbit: store key %q: %w", keyID, err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
respBody, _ := io.ReadAll(resp.Body)
return fmt.Errorf("hsm/zymbit: store key %q: status %d: %s", keyID, resp.StatusCode, respBody)
}
logger.Info("hsm/zymbit: generated key on device", "keyID", keyID, "slot", z.slotID)
return nil
}
// DeleteKey removes a key from the device.
func (z *ZymbitProvider) DeleteKey(ctx context.Context, keyID string) error {
url := fmt.Sprintf("%s/api/v1/keys/%s", z.endpoint, keyID)
req, err := http.NewRequestWithContext(ctx, http.MethodDelete, url, nil)
if err != nil {
return fmt.Errorf("hsm/zymbit: create request: %w", err)
}
resp, err := z.client.Do(req)
if err != nil {
return fmt.Errorf("hsm/zymbit: delete key %q: %w", keyID, err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("hsm/zymbit: delete key %q: status %d: %s", keyID, resp.StatusCode, body)
}
logger.Info("hsm/zymbit: deleted key", "keyID", keyID)
return nil
}
// ListKeys returns all key IDs from the device.
func (z *ZymbitProvider) ListKeys(ctx context.Context) ([]string, error) {
url := fmt.Sprintf("%s/api/v1/keys", z.endpoint)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("hsm/zymbit: create request: %w", err)
}
resp, err := z.client.Do(req)
if err != nil {
return nil, fmt.Errorf("hsm/zymbit: list keys: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("hsm/zymbit: list keys: status %d: %s", resp.StatusCode, body)
}
var result struct {
Keys []struct {
ID string `json:"key_id"`
} `json:"keys"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("hsm/zymbit: decode response: %w", err)
}
ids := make([]string, len(result.Keys))
for i, k := range result.Keys {
ids[i] = k.ID
}
return ids, nil
}
// Sign delegates signing to the Zymbit device. The private key never leaves hardware.
func (z *ZymbitProvider) Sign(ctx context.Context, keyID string, message []byte) ([]byte, error) {
url := fmt.Sprintf("%s/api/v1/keys/%s/sign", z.endpoint, keyID)
body := map[string]interface{}{
"message": message,
}
bodyJSON, err := json.Marshal(body)
if err != nil {
return nil, fmt.Errorf("hsm/zymbit: marshal request: %w", err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(bodyJSON))
if err != nil {
return nil, fmt.Errorf("hsm/zymbit: create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
resp, err := z.client.Do(req)
if err != nil {
return nil, fmt.Errorf("hsm/zymbit: sign with key %q: %w", keyID, err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
respBody, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("hsm/zymbit: sign with key %q: status %d: %s", keyID, resp.StatusCode, respBody)
}
var result struct {
Signature []byte `json:"signature"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("hsm/zymbit: decode sign response: %w", err)
}
return result.Signature, nil
}
// Healthy checks device connectivity via the API status endpoint.
func (z *ZymbitProvider) Healthy(ctx context.Context) error {
url := fmt.Sprintf("%s/api/v1/status", z.endpoint)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return fmt.Errorf("hsm/zymbit: create request: %w", err)
}
resp, err := z.client.Do(req)
if err != nil {
return fmt.Errorf("hsm/zymbit: health check: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("hsm/zymbit: health check: status %d: %s", resp.StatusCode, body)
}
return nil
}
// Close releases the HTTP client resources.
func (z *ZymbitProvider) Close() error {
z.client.CloseIdleConnections()
return nil
}