Files
mpc/pkg/approval/ecdsa_helpers.go
zeekay 73c66f43ae approval(ledger-device): real provider via ledgerctl shell-out
Replaces the NOTIMPL stub with a working ApprovalProvider that delegates
to the host-side ledgerctl tool for USB / APDU access. No new C
dependencies, no firmware required — the same Ledger app shipped in
luxfi/ledger handles every chain in the Lux/Hanzo/Zoo/Pars family via
its existing CLA=0xE0 path.

Pubkeys are parsed as compressed SEC1 secp256k1 (33 bytes) and pinned
at enrollment time. Approvals sign the canonical-intent digest; the
device-returned signature is normalized (DER, 64-byte r||s, or 65-byte
recoverable) and verified locally against the pinned pubkey before
returning — a tampering ledgerctl wrapper would be caught here.

10 tests, all green:
  - default config, normalization, error rejection
  - cross-provider isolation
  - tampered-digest / unenrolled-approver rejection
  - end-to-end ceremony via a fake ledgerctl wrapper that re-invokes
    the test binary (TestMain sentinel pattern, no external deps)
2025-12-28 16:27:17 -08:00

51 lines
1.4 KiB
Go

package approval
import (
"encoding/asn1"
"errors"
"math/big"
)
// ecdsaSig is the ASN.1 SEQUENCE { r INTEGER, s INTEGER } that ecdsa.VerifyASN1
// expects.
type ecdsaSig struct {
R, S *big.Int
}
// ecdsaRawToASN1 converts a raw P-256 r||s signature (64 bytes) to ASN.1 DER.
// Azure Key Vault and some FIDO2 authenticators return raw signatures; AWS
// KMS / Google Cloud KMS / OpenSSL return DER. The approval package
// normalizes to DER so all ECDSA verification uses ecdsa.VerifyASN1.
func ecdsaRawToASN1(raw []byte) ([]byte, error) {
if len(raw) != 64 {
return nil, errors.New("ecdsa: raw signature must be 64 bytes (r||s for P-256)")
}
sig := ecdsaSig{
R: new(big.Int).SetBytes(raw[:32]),
S: new(big.Int).SetBytes(raw[32:]),
}
return asn1.Marshal(sig)
}
// ecdsaASN1ToRaw converts an ASN.1 DER signature to raw r||s. Used when the
// destination expects raw form (some on-chain verifiers).
func ecdsaASN1ToRaw(der []byte) ([]byte, error) {
var sig ecdsaSig
if _, err := asn1.Unmarshal(der, &sig); err != nil {
return nil, err
}
out := make([]byte, 64)
rBytes := sig.R.Bytes()
sBytes := sig.S.Bytes()
copy(out[32-len(rBytes):32], rBytes)
copy(out[64-len(sBytes):], sBytes)
return out, nil
}
// marshalECDSASig wraps asn1.Marshal so callers do not have to import
// encoding/asn1 themselves. The output is the same SEQUENCE { r, s }
// that ecdsa.VerifyASN1 expects.
func marshalECDSASig(sig ecdsaSig) ([]byte, error) {
return asn1.Marshal(sig)
}