Suggested by @mootz12 on #2569 (comment) — tracked as a follow-up so it can land as a single, generalized change.
Context
Both LedgerEntry (added in #2569) and SecureStoreEntry carry a cached/derived public key alongside the underlying signing primitive. *Entry::sign_tx_hash derives the SignatureHint from that cached key and then signs with the live device/keychain. If the cached key ever drifts from what the underlying signer actually produces (corrupted TOML, wrong device plugged in, alias points at a different hd-path than expected, schema migration glitch), the resulting transaction has a hint that disagrees with its signature — Horizon rejects it later with an opaque error rather than failing locally with a clear message.
The same drift class applies to the Soroban auth-entry path (*Entry::sign_payload), where signer::sign_soroban_authorization_entry embeds signer.get_public_key() (the cached value) next to the device-produced signature in the auth signature ScVal map.
Proposal
Add a post-sign ed25519 verification step in the Ledger / SecureStore signing paths that verifies the produced signature against the cached public key and errors with a clear message on mismatch. Should cover:
LedgerEntry::sign_tx_hash
LedgerEntry::sign_payload
SecureStoreEntry::sign_tx_hash
SecureStoreEntry::sign_payload
Gated on self.public_key.is_some() so the --sign-with-ledger cacheless flow (which fetches the pubkey live and has nothing to drift from) is unaffected. ed25519 verification is microseconds — cost is negligible.
Out of scope
SignerKind::Local and SignerKind::Lab, where the public key is derived from the same in-process primitive that signs and drift is not possible.
Suggested by @mootz12 on #2569 (comment) — tracked as a follow-up so it can land as a single, generalized change.
Context
Both
LedgerEntry(added in #2569) andSecureStoreEntrycarry a cached/derived public key alongside the underlying signing primitive.*Entry::sign_tx_hashderives theSignatureHintfrom that cached key and then signs with the live device/keychain. If the cached key ever drifts from what the underlying signer actually produces (corrupted TOML, wrong device plugged in, alias points at a different hd-path than expected, schema migration glitch), the resulting transaction has a hint that disagrees with its signature — Horizon rejects it later with an opaque error rather than failing locally with a clear message.The same drift class applies to the Soroban auth-entry path (
*Entry::sign_payload), wheresigner::sign_soroban_authorization_entryembedssigner.get_public_key()(the cached value) next to the device-produced signature in the auth signature ScVal map.Proposal
Add a post-sign ed25519 verification step in the Ledger / SecureStore signing paths that verifies the produced signature against the cached public key and errors with a clear message on mismatch. Should cover:
LedgerEntry::sign_tx_hashLedgerEntry::sign_payloadSecureStoreEntry::sign_tx_hashSecureStoreEntry::sign_payloadGated on
self.public_key.is_some()so the--sign-with-ledgercacheless flow (which fetches the pubkey live and has nothing to drift from) is unaffected. ed25519 verification is microseconds — cost is negligible.Out of scope
SignerKind::LocalandSignerKind::Lab, where the public key is derived from the same in-process primitive that signs and drift is not possible.