Skip to content

feat(runtime)!: ed25519 accounts sign the transaction text (0x03/0x04) and raw 0x00 never verifies; refuse small-order ed25519 keys, the sr25519 identity key and non-canonical ECDSA (spec 20) - #174

Merged
nol4lej merged 2 commits into
mainfrom
feat/ed25519-text-signatures
Oct 21, 2026

Conversation

@nol4lej

@nol4lej nol4lej commented Oct 21, 2026 •

Copy link
Copy Markdown
Member

Summary

Ed25519 accounts now sign a text that names the transaction instead of the raw signing payload, so Solana wallets (Phantom, MetaMask, Backpack, Solflare, Trust, Talisman) can sign Orbinum extrinsics and a signature obtained as a "login nonce" can no longer authorise one. OrbinumSignature is also hardened against keys and encodings the host primitives accept as forgeries or aliases, and split into a module per responsibility. Ships in runtime spec 20 ([Unreleased]).

Why

  • Solana wallets. Phantom refuses binary input to solana:signMessage and MetaMask rewrites it, so neither could produce a 0x00 signature. All six wallets tested sign UTF-8 text deterministically.
  • Blind signing. 0x00 verified ed25519 over raw bytes, and Backpack, Solflare, Trust and Talisman sign arbitrary bytes through signMessage. A site that had a user sign a payload's blake2_256 as a "nonce" could submit it as that user's transaction. The attacker picks the variant, so an account cannot opt out.
  • Forgeries the host accepts. ed25519_verify follows ZIP-215: for a small-order key, R = identity, s = 0 verifies any message. Account 0x0100…00 (EVM address 0x01 followed by zeros) is such a key. Confirmed on testnet (spec 19) with validate_transaction; it has no funds. The sr25519 identity key accepts R = 0, s = 0. ECDSA recovery accepts (r, n−s) and v + 27, so one signed extrinsic had up to four valid encodings.

Changes

OrbinumSignature, with explicit #[codec(index)]:

Tag Variant Verifies
0x00 Ed25519 never (kept in the metadata: polkadot-api builds no extrinsic for a signature enum without Ed25519, Sr25519 and Ecdsa)
0x01 Sr25519 as before; identity key refused
0x02 Ecdsa as before; only s ≤ n/2 and v < 4
0x03 Ed25519Text (new) ed25519 over "Orbinum transaction\nchain: 0x{genesis}\npayload: 0x{blake2_256(msg)}"
0x04 Ed25519WrappedText (new) the same text inside <Bytes>…</Bytes>, as a Substrate extension's signRaw signs it
  • msg is what Verify::verify receives (payload, or its blake2_256 above 256 bytes). It is always hashed again, since verify cannot tell which. The genesis comes from block_hash(0), which CheckGenesis already reads, so there is no extra read or PoV.
  • Both text variants refuse small-order keys (all 14 encodings).
  • orbinum_signature.rs → orbinum_signature/{mod, signer, ed25519, sr25519, ecdsa, text}.rs.
  • transaction_version is unchanged: no call encoding moved, and every existing index keeps its bytes.
  • RUNTIME_VERSIONS.md: spec 20 entries.

Client action required (before the upgrade)

Account Spec 20
sr25519 unchanged
ecdsa unchanged if it signs low-s, v ∈ {0,1} (sp_core, polkadot-js and noble already do)
ed25519 must sign the text: signRaw → tag 0x04, or signMessage → tag 0x03. Plain signPayload gives 0x00 → BadProof

Dependency security fixes

The daily cargo deny job started failing on new RustSec advisories, none caused by this change. Lockfile-only bumps, all within semver:

Advisory Crate Fix
RUSTSEC-2026-0379, RUSTSEC-2026-0380 (Cranelift miscompilation) wasmtime 36.0.18 → 36.0.19 (with cranelift 0.123.19)
RUSTSEC-2026-0382 rustls-webpki 0.103.15 → 0.103.16
RUSTSEC-2026-0383 webpki-root-certs 1.0.9 → 1.0.10
RUSTSEC-2026-0381 webpki-roots 0.25.4 ignored in deny.toml

webpki-roots 0.25.4 comes only through libp2p-websocket 0.44, pinned by sc-network's libp2p 0.54, and the fix exists only in 1.0.10, so it waits for the next polkadot-sdk bump. Those roots validate TLS on wss p2p dials; peers are still authenticated by Noise and peer id.

Testing

  • cargo test -p orbinum-runtime --lib: 85 passed. New suites runtime_tests::ed25519_text and runtime_tests::signature_hardening, using real Phantom and MetaMask signatures, full check() with short and long payloads, cross-variant refusal, small-order and identity forgeries (asserting first that the host accepts them), ECDSA aliases, and the metadata shape polkadot-api requires.
  • cargo clippy -p orbinum-runtime -p orbinum-node --all-targets: clean. rustfmt: clean. Release build (wasm): OK.
  • Dev node (spec 20), through validate_transaction and real inclusion:
    • polkadot-js: 0x03 and 0x04 accepted and included with ExtrinsicSuccess; tampered, blind, cross-framing and small-order signatures refused; raw 0x00 refused.
    • polkadot-api 3: sr25519, ed25519 0x04 and bare-text 0x03 transfers included, fee estimation for each, 0x00 → BadProof.
    • cargo deny check: advisories, bans, licenses and sources ok. Release build with wasmtime 36.0.19 and a --dev smoke run: blocks produced, no errors.

…) and raw 0x00 never verifies; refuse small-order ed25519 keys, the sr25519 identity key and non-canonical ECDSA (spec 20)
git commit -m "fix(deps): bump wasmtime 36.0.19, rustls-webpki 0.103.16 and webpki-root-certs 1.0.10 for RUSTSEC-2026-0379/0380/0382/0383; ignore RUSTSEC-2026-0381 (webpki-roots 0.25, pinned by libp2p-websocket)"
@nol4lej
nol4lej merged commit 87e0a00 into main Oct 21, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant