Files
hsm/signer_ngrave.go
zeekay 6d2dc12ff8 hardware wallets + universal factory: Coldcard, Foundation, Keystone, NGRAVE, GridPlus, Ledger, Trezor
Add airgapped hardware wallet signers (Coldcard PSBT, Foundation BBQr,
Keystone UR, NGRAVE UR) wired through #106's AirgapTransport protocol.
Add online wallet signers shelling out to vendor CLIs (Ledger via
ledgerctl, Trezor via trezorctl) — no CGO/HID dependency. GridPlus
Lattice1 ships as a documented NotImpl with the full Connect-API
protocol spec; the Sign path returns a clear error rather than fake
output.

Extend factory.go to register the new providers plus the universal
PKCS#11 (Thales/Utimaco/Entrust/CloudHSM/SoftHSM2) and Nitrokey
HSM 2 paths from #106. Configuration is pulled from a shared config
map or matching MPC_HSM_<PROVIDER>_* environment variables.

Tests: 19 new signer + factory tests using MockAirgapTransport. All
pass under `go test ./...`. Real hardware tests are gated by build
tags (pkcs11, nitrokey, airgap_coldcard, …) and are not exercised in
CI.
2025-12-27 16:00:00 -08:00

114 lines
3.8 KiB
Go

// Copyright (C) 2020-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package hsm
import (
"bytes"
"context"
"errors"
"fmt"
"strings"
)
// NGRAVE Zero is a multi-coin airgapped hardware wallet certified to
// Common Criteria EAL7+ — the highest commercially available smart-card
// certification, used by passport eIDs and military-grade systems.
//
// The Zero exchanges signing requests via animated QR codes on its
// 4-inch capacitive screen using NGRAVE's variant of the Blockchain
// Commons UR (Uniform Resources) format. The wire envelope is the same
// as Keystone (FormatUR) but NGRAVE publishes its own ur-types prefixed
// "ngr-..." for proprietary extensions.
//
// Architecture:
//
// mpcd ─[NGRAVE UR request]─> AirgapTransport
// │
// │ animated QR (Liquid Air pairing)
// ▼
// NGRAVE Zero (offline)
// │
// │ on-device review + fingerprint
// ▼
// AirgapTransport ─[NGRAVE UR signed]─> mpcd
//
// In production NGRAVE supports both standard ur-types (eth-sign-request,
// btc-sign-request) for cross-vendor compatibility and ngr-* types for
// features unique to the Zero (e.g., Liquid Air pairing handshakes).
// NGRAVEConfig.URType is required and validated against the request.
// NGRAVEConfig configures an NGRAVE Zero signer.
type NGRAVEConfig struct {
// DeviceID is the operator-facing identifier of the target device.
DeviceID string
// URType identifies the request format. Standard types ("eth-sign-request",
// "btc-sign-request", "sol-sign-request") and NGRAVE-prefixed types
// ("ngr-eth-sign-request") are both accepted.
URType URType
// Transport mediates the airgapped QR ceremony.
Transport AirgapTransport
}
// NGRAVESigner produces signatures via an NGRAVE Zero.
type NGRAVESigner struct {
cfg NGRAVEConfig
}
// NewNGRAVESigner constructs an NGRAVE Zero signer.
func NewNGRAVESigner(cfg NGRAVEConfig) (*NGRAVESigner, error) {
if cfg.DeviceID == "" {
return nil, errors.New("hsm/ngrave: DeviceID required")
}
if cfg.URType == "" {
return nil, errors.New("hsm/ngrave: URType required")
}
return &NGRAVESigner{cfg: cfg}, nil
}
// Provider returns "ngrave".
func (s *NGRAVESigner) Provider() string { return "ngrave" }
// Sign blocks for the full UR round-trip with the NGRAVE Zero. The
// message MUST be a UR-encoded request matching cfg.URType.
func (s *NGRAVESigner) Sign(ctx context.Context, _ string, message []byte) ([]byte, error) {
return airgappedSign(
ctx,
s.cfg.Transport,
s.cfg.DeviceID,
s.encodeChallenge,
s.decodeResponse,
message,
)
}
// Verify is unsupported. NGRAVE signatures verify with the chain's
// native algorithm (secp256k1, ed25519, …) and the chain client.
func (s *NGRAVESigner) Verify(_ context.Context, _ string, _, _ []byte) (bool, error) {
return false, errors.New("hsm/ngrave: Verify not supported (chain-specific)")
}
func (s *NGRAVESigner) encodeChallenge(message []byte, _ string) (AirgapChallenge, error) {
if err := validateURPrefix(message, s.cfg.URType); err != nil {
return AirgapChallenge{}, fmt.Errorf("hsm/ngrave: %w", err)
}
return AirgapChallenge{
Format: FormatUR,
Payload: message,
}, nil
}
func (s *NGRAVESigner) decodeResponse(response []byte) ([]byte, error) {
if !bytes.HasPrefix(response, []byte("ur:")) {
return nil, fmt.Errorf("hsm/ngrave: %w (response missing 'ur:' prefix)", ErrAirgapResponseInvalid)
}
// NGRAVE may return either standard or ngr-prefixed signature types;
// either is acceptable.
if !strings.HasPrefix(string(response), "ur:") {
return nil, ErrAirgapResponseInvalid
}
return response, nil
}