mirror of
https://github.com/luxfi/hsm.git
synced 2026-10-30 16:22:43 +00:00
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.
61 lines
1.7 KiB
Go
61 lines
1.7 KiB
Go
// Copyright (C) 2020-2026, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package hsm
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
)
|
|
|
|
// airgappedSign is the common driver shared by every airgapped hardware
|
|
// wallet signer (Coldcard, Foundation Passport, Keystone Pro, NGRAVE
|
|
// Zero). It builds an AirgapChallenge using the per-device encoder,
|
|
// hands it to the configured AirgapTransport, and decodes the response.
|
|
//
|
|
// The function blocks for the full duration of the human ceremony. The
|
|
// caller's context is propagated to both DisplayChallenge and
|
|
// AwaitResponse so timeouts and cancellation work as expected.
|
|
func airgappedSign(
|
|
ctx context.Context,
|
|
transport AirgapTransport,
|
|
deviceID string,
|
|
encode func(message []byte, sessionID string) (AirgapChallenge, error),
|
|
decode func(response []byte) ([]byte, error),
|
|
message []byte,
|
|
) ([]byte, error) {
|
|
if transport == nil {
|
|
return nil, ErrAirgapTransportRequired
|
|
}
|
|
if len(message) == 0 {
|
|
return nil, errors.New("hsm/airgap: empty message")
|
|
}
|
|
sessionID, err := newSessionID()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
challenge, err := encode(message, sessionID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("hsm/airgap: encode challenge: %w", err)
|
|
}
|
|
challenge.DeviceID = deviceID
|
|
challenge.SessionID = sessionID
|
|
|
|
if err := transport.DisplayChallenge(ctx, challenge); err != nil {
|
|
return nil, fmt.Errorf("hsm/airgap: display: %w", err)
|
|
}
|
|
resp, err := transport.AwaitResponse(ctx, sessionID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("hsm/airgap: await: %w", err)
|
|
}
|
|
if len(resp) == 0 {
|
|
return nil, ErrAirgapResponseInvalid
|
|
}
|
|
sig, err := decode(resp)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("hsm/airgap: decode response: %w", err)
|
|
}
|
|
return sig, nil
|
|
}
|