mpcd: author missing buildHSMSignerConfig helper (refs #227)

Commit af4139d ("deployment-mode flag + audit dispatcher") referenced
buildHSMSignerConfig at the HSM signer wiring site but the helper file
was never staged — same pattern as the missing airgap files repaired
under #208. The current main.go works around the gap by passing nil to
hsm.NewSigner, which loses the operator-supplied --hsm-signer-key-id and
prevents per-provider config promotion.

cmd/mpcd/hsm_signer_config.go:
  - validates provider name against the luxfi/hsm v1.1.3 factory list
    (aws/gcp/azure, zymbit, yubihsm, nitrokey, pkcs11, kmip, coldcard,
    foundation, keystone, ngrave, gridplus/lattice, ledger, trezor,
    tr31, mldsa, local) and the documented aliases.
  - promotes --hsm-signer-key-id into the canonical config key for each
    cloud KMS provider (aws.key_id, gcp.crypto_key, azure.key_name).
  - leaves vendor-specific surface (slot/pin/library, vault URL,
    transport) to the factory's MPC_HSM_<P>_* env-var fallback so the
    CLI flag surface stays minimal.

cmd/mpcd/main.go: route --hsm-signer through the helper and surface
config errors before reaching hsm.NewSigner.

cmd/mpcd/hsm_signer_config_test.go: covers cloud-KMS key-id promotion,
hardware-wallet empty-map case, alias acceptance, case-insensitive
provider names, and rejection of empty + unknown providers. Helper
+ tests pass under `go test -race -count=1` in isolation.

go.sum: refresh github.com/luxfi/edwards25519 v0.1.0 hashes to match
proxy.golang.org. Pre-existing checksum drift blocked all `go build`
attempts on this branch and on main.

Note: cmd/mpcd as a whole still does not compile on this branch — the
same af4139d commit also dropped pkg/api/handlers_approval.go without
adding the Server.approval field, and pkg/policy/fhe_threshold_decryptor.go
references symbols that never landed. Both are out of scope for #227 and
will be repaired under follow-up tickets in the same #208-style cleanup.
This commit is contained in:
2026-04-28 01:14:50 -07:00
parent 0a8fa2e0cc
commit 39fcdca875
4 changed files with 284 additions and 8 deletions
+130
View File
@@ -0,0 +1,130 @@
// Copyright (C) 2020-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package main
import (
"fmt"
"strings"
"github.com/urfave/cli/v3"
)
// buildHSMSignerConfig assembles the per-provider configuration map that
// hsm.NewSigner consumes. The luxfi/hsm factory reads each provider's
// config keys from the supplied map first, then falls back to its
// documented MPC_HSM_<PROVIDER>_* environment variables. The job of this
// helper is therefore narrow:
//
// 1. Reject unknown provider names early — a typo at startup is much
// cheaper to surface here than as a "no signer wired" failure when
// the first co-signing intent arrives.
// 2. Promote the operator-facing CLI flags (--hsm-signer-key-id) into
// the canonical config keys for the providers that consume a single
// opaque key reference (cloud KMS).
// 3. Leave the remaining provider-specific knobs (slot/pin/library for
// PKCS#11, work-dir for airgap wallets, vault URL for Azure) to the
// factory's environment-variable fallback. We do NOT mint new CLI
// flags for every provider's tuning surface — there is one and only
// one place those values live, the documented MPC_HSM_<P>_* names.
//
// The returned map is always non-nil so the factory cannot observe an
// ambiguous "provider configured but no overrides" state.
func buildHSMSignerConfig(c *cli.Command, provider string) (map[string]string, error) {
provider = strings.TrimSpace(strings.ToLower(provider))
if provider == "" {
return nil, fmt.Errorf("hsm signer: provider not specified")
}
if !isSupportedHSMSignerProvider(provider) {
return nil, fmt.Errorf("hsm signer: unsupported provider %q (supported: %s)",
provider, strings.Join(supportedHSMSignerProviders(), ", "))
}
cfg := map[string]string{}
keyID := strings.TrimSpace(c.String("hsm-signer-key-id"))
switch provider {
case "aws":
// AWS KMS signer reads region from config or AWS_REGION; the key
// ARN is identified at sign-time via the operator-supplied
// hsm-signer-key-id. We pass it through under the canonical key
// name AWSKMSSigner consults.
if keyID != "" {
cfg["key_id"] = keyID
}
case "gcp":
// GCP KMS signer derives project/location/keyring/key from env
// (GCP_PROJECT_ID, GCP_KMS_LOCATION, GCP_KMS_KEYRING, GCP_KMS_KEY).
// hsm-signer-key-id, when supplied, becomes the crypto key name
// override.
if keyID != "" {
cfg["crypto_key"] = keyID
}
case "azure":
// Azure Key Vault signer reads vault_url from env (AZURE_VAULT_URL).
// hsm-signer-key-id maps to the key name within the vault.
if keyID != "" {
cfg["key_name"] = keyID
}
case "zymbit", "yubihsm", "yubico", "yubi", "nitrokey",
"coldcard", "foundation", "keystone", "ngrave",
"gridplus", "lattice", "ledger", "trezor",
"pkcs11", "kmip", "tr31",
"mldsa", "pq", "post-quantum",
"local":
// Each of these providers reads its full configuration surface
// from the documented MPC_HSM_<PROVIDER>_* environment variables.
// The factory handles env fallback per key, so we deliberately
// do not duplicate that surface as CLI flags. Operators wire
// vendor-specific knobs (slots, pins, library paths, vault URLs,
// transport implementations) through env vars provisioned out of
// band, typically via KMS-synced secrets.
default:
// isSupportedHSMSignerProvider guards this path; this is dead
// code retained as a defensive switch fall-through so the
// compiler proves exhaustiveness when the provider list grows.
return nil, fmt.Errorf("hsm signer: provider %q passed validation but has no config branch", provider)
}
return cfg, nil
}
// supportedHSMSignerProviders lists the canonical signer provider names
// accepted by luxfi/hsm.NewSigner. Kept in lockstep with that factory's
// switch — adding an entry here without a matching factory case is a
// configuration lie.
func supportedHSMSignerProviders() []string {
return []string{
"aws", "gcp", "azure",
"zymbit",
"yubihsm", "nitrokey",
"pkcs11", "kmip",
"coldcard", "foundation", "keystone", "ngrave",
"gridplus", "ledger", "trezor",
"tr31", "mldsa",
"local",
}
}
// isSupportedHSMSignerProvider returns true when the given provider name
// (already lower-cased + trimmed) is one luxfi/hsm.NewSigner will accept.
// Aliases honored by the factory (yubico/yubi for yubihsm, lattice for
// gridplus, pq/post-quantum for mldsa) are accepted here so the surface
// stays consistent between the helper and the factory.
func isSupportedHSMSignerProvider(provider string) bool {
switch provider {
case "aws", "gcp", "azure",
"zymbit",
"yubihsm", "yubico", "yubi",
"nitrokey",
"pkcs11", "kmip",
"coldcard", "foundation", "keystone", "ngrave",
"gridplus", "lattice",
"ledger", "trezor",
"tr31",
"mldsa", "pq", "post-quantum",
"local":
return true
}
return false
}
+137
View File
@@ -0,0 +1,137 @@
// Copyright (C) 2020-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package main
import (
"context"
"testing"
"github.com/urfave/cli/v3"
)
// newCmdWithFlags builds an *cli.Command with the same hsm-signer-key-id
// flag the real start subcommand registers, then runs it once with the
// supplied argv so that c.String("hsm-signer-key-id") returns the value
// the test wired in. We do this through the Action callback because
// urfave/cli/v3 does not expose a way to hand-construct a populated
// *cli.Command outside of an invocation.
func newCmdWithFlags(t *testing.T, keyID string) *cli.Command {
t.Helper()
var captured *cli.Command
cmd := &cli.Command{
Name: "test",
Flags: []cli.Flag{
&cli.StringFlag{Name: "hsm-signer-key-id"},
},
Action: func(_ context.Context, c *cli.Command) error {
captured = c
return nil
},
}
args := []string{"test"}
if keyID != "" {
args = append(args, "--hsm-signer-key-id", keyID)
}
if err := cmd.Run(context.Background(), args); err != nil {
t.Fatalf("cli run: %v", err)
}
if captured == nil {
t.Fatalf("cli action did not fire")
}
return captured
}
func TestBuildHSMSignerConfig_AWSPromotesKeyID(t *testing.T) {
c := newCmdWithFlags(t, "arn:aws:kms:us-east-1:111122223333:key/abcd")
cfg, err := buildHSMSignerConfig(c, "aws")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got := cfg["key_id"]; got != "arn:aws:kms:us-east-1:111122223333:key/abcd" {
t.Fatalf("aws key_id = %q", got)
}
}
func TestBuildHSMSignerConfig_GCPPromotesKeyID(t *testing.T) {
c := newCmdWithFlags(t, "projects/p/locations/l/keyRings/r/cryptoKeys/k")
cfg, err := buildHSMSignerConfig(c, "gcp")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got := cfg["crypto_key"]; got != "projects/p/locations/l/keyRings/r/cryptoKeys/k" {
t.Fatalf("gcp crypto_key = %q", got)
}
}
func TestBuildHSMSignerConfig_AzurePromotesKeyID(t *testing.T) {
c := newCmdWithFlags(t, "intent-cosigner")
cfg, err := buildHSMSignerConfig(c, "azure")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got := cfg["key_name"]; got != "intent-cosigner" {
t.Fatalf("azure key_name = %q", got)
}
}
func TestBuildHSMSignerConfig_HardwareWalletReturnsEmptyMap(t *testing.T) {
c := newCmdWithFlags(t, "")
for _, p := range []string{
"coldcard", "foundation", "keystone", "ngrave",
"ledger", "trezor", "gridplus", "lattice",
"yubihsm", "nitrokey", "zymbit",
"pkcs11", "kmip", "tr31",
"mldsa", "local",
} {
cfg, err := buildHSMSignerConfig(c, p)
if err != nil {
t.Fatalf("provider %s: %v", p, err)
}
if cfg == nil {
t.Fatalf("provider %s: nil cfg", p)
}
if len(cfg) != 0 {
t.Fatalf("provider %s: expected empty cfg, got %v", p, cfg)
}
}
}
func TestBuildHSMSignerConfig_EmptyProviderRejected(t *testing.T) {
c := newCmdWithFlags(t, "")
if _, err := buildHSMSignerConfig(c, ""); err == nil {
t.Fatalf("expected error for empty provider")
}
}
func TestBuildHSMSignerConfig_UnknownProviderRejected(t *testing.T) {
c := newCmdWithFlags(t, "")
if _, err := buildHSMSignerConfig(c, "luna-network-hsm"); err == nil {
t.Fatalf("expected error for unknown provider")
}
}
func TestBuildHSMSignerConfig_ProviderCaseInsensitive(t *testing.T) {
c := newCmdWithFlags(t, "key-1")
cfg, err := buildHSMSignerConfig(c, " AWS ")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got := cfg["key_id"]; got != "key-1" {
t.Fatalf("aws key_id = %q", got)
}
}
func TestSupportedHSMSignerProviders_NonEmpty(t *testing.T) {
if len(supportedHSMSignerProviders()) == 0 {
t.Fatalf("supportedHSMSignerProviders returned empty list")
}
}
func TestIsSupportedHSMSignerProvider_AcceptsAliases(t *testing.T) {
for _, alias := range []string{"yubico", "yubi", "lattice", "pq", "post-quantum"} {
if !isSupportedHSMSignerProvider(alias) {
t.Fatalf("alias %q rejected", alias)
}
}
}
+15 -6
View File
@@ -725,15 +725,24 @@ func runNodeConsensus(ctx context.Context, c *cli.Command) error {
apiServer := mpcapi.NewServer(database, mpcBackend, jwtSecret)
apiServer.StartScheduler(ctx)
// Wire HSM signer for intent co-signing
// Wire HSM signer for intent co-signing.
// Provider-specific config is folded into a single map; each
// underlying provider reads only the keys it understands
// and falls back to MPC_HSM_<PROVIDER>_* env vars for the
// rest of its tuning surface.
signerType := c.String("hsm-signer")
if signerType != "" {
signer, signerErr := hsm.NewSigner(signerType, nil)
if signerErr != nil {
logger.Error("Failed to create HSM signer", signerErr, "provider", signerType)
signerConfig, cfgErr := buildHSMSignerConfig(c, signerType)
if cfgErr != nil {
logger.Error("Failed to build HSM signer config", cfgErr, "provider", signerType)
} else {
apiServer.SetHSM(signer)
logger.Info("HSM signer configured for co-signing", "provider", signer.Provider())
signer, signerErr := hsm.NewSigner(signerType, signerConfig)
if signerErr != nil {
logger.Error("Failed to create HSM signer", signerErr, "provider", signerType)
} else {
apiServer.SetHSM(signer)
logger.Info("HSM signer configured for co-signing", "provider", signer.Provider())
}
}
}
+2 -2
View File
@@ -370,8 +370,8 @@ github.com/luxfi/crypto v1.17.45 h1:uGK0y4+aLipE/M0YIQ5hcsWv0ZG0E4cPv03a94K/eLE=
github.com/luxfi/crypto v1.17.45/go.mod h1:GnAkhQ7HNs3X0Tzx5nOONS3kl0yRmWHbDcRO5ffILsg=
github.com/luxfi/database v1.17.44 h1:hfiTls7sqbweW+o4iaZqB8P997paC+vpgWmhN6v5MJ0=
github.com/luxfi/database v1.17.44/go.mod h1:6Ey5y3I0WNLHbxIlIdFqUuKfBg+b0fAgTA8FgRgQ8zg=
github.com/luxfi/edwards25519 v0.1.0 h1:zuSrBtgV4NUiJ8dzzJiLU4o+A0AHGnztRLUvZ24mAYk=
github.com/luxfi/edwards25519 v0.1.0/go.mod h1:tFuuyXvI+xqUoHz9pwC+kqsALCGHSO1SymL1NjCOmFM=
github.com/luxfi/edwards25519 v0.1.0 h1:YPoT831TZMslvNyy/KuTtpHi4BGZWgpwak06RCaYLzo=
github.com/luxfi/edwards25519 v0.1.0/go.mod h1:MBoV+bPEz1tSMADPMSUaYdZ+agY3syG8B6z7RmJBH0A=
github.com/luxfi/fhe v1.7.7 h1:RP26i3CxvNy2y/3LKiJPgd9uR5MrlG/MvQnv5rkQFhk=
github.com/luxfi/fhe v1.7.7/go.mod h1:FHfGNgKL5ZLcnKVKFVNLDJOBbQyQm5DGpVrMnY8ASJo=
github.com/luxfi/geth v1.16.79 h1:MtP8ZUuSVZDjmZDa1kTJCl0PpG8+wGbQft652GO6a3A=