mirror of
https://github.com/luxfi/hsm.git
synced 2026-10-30 16:22:43 +00:00
The hardware-wallet signers committed in 6d2dc12 reference AirgapTransport,
AirgapFormat, AirgapChallenge and the airgap session helpers. Those types
plus the PKCS#11 / YubiHSM / Nitrokey signers were left untracked, so a
fresh checkout could not compile and the factory cases for coldcard,
foundation, keystone, and ngrave were not actually reachable from
NewSigner — the surface luxfi/mpc cmd/mpcd airgap_command.go is gated on.
This lands the missing files unchanged (real implementation, not stubs):
airgap.go AirgapTransport interface + MockTransport
airgap_random.go crypto/rand wrapper for session IDs
signer_nitrokey.go Nitrokey HSM 2 (PKCS#11 wrapper)
signer_pkcs11.go Universal PKCS#11 entrypoint
signer_pkcs11_cgo.go Real cgo build (//go:build pkcs11)
signer_pkcs11_stub.go Build-tag stub for non-cgo builds
signer_yubihsm.go YubiHSM 2 over yubihsm-connector
signer_yubihsm_helpers.go Object-ID parsing helpers
signer_yubihsm_test.go Coverage for YubiHSM helpers
After this commit hsm.NewSigner("coldcard"|"foundation"|"keystone"|
"ngrave", cfg) returns a real Signer; the file-mediated airgap ceremony
in luxfi/mpc cmd/mpcd remains the operator UX layer (Coldcard microSD is
the canonical product flow), but in-process signers are now wireable for
tests and kiosk transports.
131 lines
4.2 KiB
Go
131 lines
4.2 KiB
Go
// Copyright (C) 2020-2026, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package hsm
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
)
|
|
|
|
// Nitrokey HSM 2 is a USB-attached smart card HSM that exposes a
|
|
// PKCS#11 v2.40 interface via the OpenSC project's libopensc-pkcs11.so.
|
|
// It supports ECDSA on NIST P-256 and P-521, RSA up to 4096, and PSS.
|
|
//
|
|
// Architecture:
|
|
//
|
|
// mpcd ─ CGO ─> libopensc-pkcs11.so ─ PC/SC ─> pcscd ─ USB ─> Nitrokey HSM 2
|
|
//
|
|
// The signer is a thin preconfigured wrapper around PKCS11Signer. The
|
|
// only Nitrokey-specific value is the OpenSC library path — operators
|
|
// supply slot, label, and PIN exactly as for any other PKCS#11 token.
|
|
//
|
|
// Operator setup (Linux):
|
|
//
|
|
// 1. apt-get install opensc pcscd
|
|
// 2. systemctl enable --now pcscd
|
|
// 3. pkcs11-tool --module /usr/lib/opensc-pkcs11.so --list-slots
|
|
// 4. pkcs11-tool --module /usr/lib/opensc-pkcs11.so --keypairgen \
|
|
// --key-type EC:secp256r1 --label mpc-cosigner --login
|
|
//
|
|
// macOS: install OpenSC via `brew install opensc` (library lands at
|
|
// /opt/homebrew/lib/pkcs11/opensc-pkcs11.so on Apple Silicon, or
|
|
// /usr/local/lib/pkcs11/opensc-pkcs11.so on Intel). PC/SC is provided
|
|
// by the system pcscd.
|
|
|
|
// NitrokeyConfig configures a Nitrokey HSM 2 signer. It is identical
|
|
// to PKCS11Config minus the LibraryPath, which defaults to the OpenSC
|
|
// system path.
|
|
type NitrokeyConfig struct {
|
|
// LibraryPath overrides the OpenSC PKCS#11 module path. When empty
|
|
// the signer probes the standard locations.
|
|
LibraryPath string
|
|
|
|
// SlotID and TokenLabel select the Nitrokey among multiple PC/SC
|
|
// readers. TokenLabel is preferred.
|
|
SlotID uint
|
|
TokenLabel string
|
|
|
|
// Pin is the user PIN. The Nitrokey factory default is "648219"
|
|
// but operators MUST change it on first use. In production the PIN
|
|
// is supplied via KMS — never hard-coded.
|
|
Pin string
|
|
|
|
// KeyLabel is the CKA_LABEL of the key generated on the device.
|
|
KeyLabel string
|
|
|
|
// Mechanism defaults to CKM_ECDSA_SHA256.
|
|
Mechanism string
|
|
}
|
|
|
|
// nitrokeyDefaultLibraryPaths is the ordered probe list used when
|
|
// NitrokeyConfig.LibraryPath is empty.
|
|
var nitrokeyDefaultLibraryPaths = []string{
|
|
"/usr/lib/opensc-pkcs11.so",
|
|
"/usr/lib64/pkcs11/opensc-pkcs11.so",
|
|
"/usr/lib/x86_64-linux-gnu/pkcs11/opensc-pkcs11.so",
|
|
"/opt/homebrew/lib/pkcs11/opensc-pkcs11.so",
|
|
"/usr/local/lib/pkcs11/opensc-pkcs11.so",
|
|
}
|
|
|
|
// NewNitrokeySigner returns a Signer preconfigured for the Nitrokey
|
|
// HSM 2. The returned signer's Provider() reports "nitrokey" for clearer
|
|
// audit logs even though the wire protocol is PKCS#11.
|
|
func NewNitrokeySigner(cfg NitrokeyConfig) (Signer, error) {
|
|
libPath := cfg.LibraryPath
|
|
if libPath == "" {
|
|
var err error
|
|
libPath, err = probeNitrokeyLibrary()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
mech := cfg.Mechanism
|
|
if mech == "" {
|
|
mech = "ECDSA_SHA256"
|
|
}
|
|
inner, err := NewPKCS11Signer(PKCS11Config{
|
|
LibraryPath: libPath,
|
|
SlotID: cfg.SlotID,
|
|
TokenLabel: cfg.TokenLabel,
|
|
Pin: cfg.Pin,
|
|
KeyLabel: cfg.KeyLabel,
|
|
Mechanism: mech,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("hsm/nitrokey: %w", err)
|
|
}
|
|
return &nitrokeySigner{inner: inner}, nil
|
|
}
|
|
|
|
// nitrokeySigner re-labels a PKCS11Signer's Provider() output. All
|
|
// other behavior is delegated to the embedded PKCS#11 signer.
|
|
type nitrokeySigner struct {
|
|
inner *PKCS11Signer
|
|
}
|
|
|
|
func (s *nitrokeySigner) Provider() string { return "nitrokey" }
|
|
|
|
func (s *nitrokeySigner) Sign(ctx context.Context, keyID string, message []byte) ([]byte, error) {
|
|
return s.inner.Sign(ctx, keyID, message)
|
|
}
|
|
|
|
func (s *nitrokeySigner) Verify(ctx context.Context, keyID string, message, signature []byte) (bool, error) {
|
|
return s.inner.Verify(ctx, keyID, message, signature)
|
|
}
|
|
|
|
// Close releases the embedded PKCS#11 session.
|
|
func (s *nitrokeySigner) Close() error { return s.inner.Close() }
|
|
|
|
// probeNitrokeyLibrary returns the first existing OpenSC PKCS#11 module
|
|
// found in nitrokeyDefaultLibraryPaths.
|
|
func probeNitrokeyLibrary() (string, error) {
|
|
for _, p := range nitrokeyDefaultLibraryPaths {
|
|
if _, err := os.Stat(p); err == nil {
|
|
return p, nil
|
|
}
|
|
}
|
|
return "", fmt.Errorf("hsm/nitrokey: OpenSC PKCS#11 module not found in any of %v; install opensc and re-try (or set NitrokeyConfig.LibraryPath)", nitrokeyDefaultLibraryPaths)
|
|
}
|