From 58b13fea894b03fcb57c8b44c7f7fa3d315a3cf6 Mon Sep 17 00:00:00 2001 From: nol4lej Date: Wed, 21 Oct 2026 12:35:58 -0300 Subject: [PATCH 1/2] 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) --- template/runtime/RUNTIME_VERSIONS.md | 42 +- template/runtime/src/orbinum_signature.rs | 178 ------- .../runtime/src/orbinum_signature/ecdsa.rs | 64 +++ .../runtime/src/orbinum_signature/ed25519.rs | 27 ++ template/runtime/src/orbinum_signature/mod.rs | 82 ++++ .../runtime/src/orbinum_signature/signer.rs | 67 +++ .../runtime/src/orbinum_signature/sr25519.rs | 11 + .../runtime/src/orbinum_signature/text.rs | 77 +++ .../runtime/src/runtime_tests/ed25519_text.rs | 446 ++++++++++++++++++ template/runtime/src/runtime_tests/mod.rs | 2 + .../src/runtime_tests/signature_hardening.rs | 209 ++++++++ 11 files changed, 1026 insertions(+), 179 deletions(-) delete mode 100644 template/runtime/src/orbinum_signature.rs create mode 100644 template/runtime/src/orbinum_signature/ecdsa.rs create mode 100644 template/runtime/src/orbinum_signature/ed25519.rs create mode 100644 template/runtime/src/orbinum_signature/mod.rs create mode 100644 template/runtime/src/orbinum_signature/signer.rs create mode 100644 template/runtime/src/orbinum_signature/sr25519.rs create mode 100644 template/runtime/src/orbinum_signature/text.rs create mode 100644 template/runtime/src/runtime_tests/ed25519_text.rs create mode 100644 template/runtime/src/runtime_tests/signature_hardening.rs diff --git a/template/runtime/RUNTIME_VERSIONS.md b/template/runtime/RUNTIME_VERSIONS.md index 319502d4..6764e5eb 100644 --- a/template/runtime/RUNTIME_VERSIONS.md +++ b/template/runtime/RUNTIME_VERSIONS.md @@ -26,7 +26,9 @@ EVM precompiles: the Balances precompile only runs as a direct call and meters by weight, and every Orbinum precompile address holds code, so Solidity high-level calls reach them. EVM transaction validation and the modexp precompile follow upstream Frontier (polkadot-evm/frontier, changes after -#1790). `transaction_version` is unchanged. Ships with +#1790). Ed25519 keys sign only text that names an Orbinum transaction: +raw `Ed25519` (`0x00`) never verifies, and Solana wallets and Substrate extensions +sign through the new `Ed25519Text` (`0x03`) and `Ed25519WrappedText` (`0x04`). `transaction_version` is unchanged. Ships with `pallet-evm-precompile-balances` 0.2.0. - **Balances precompile (`0x…0802`)** refuses static calls, delegated calls @@ -62,6 +64,44 @@ precompile follow upstream Frontier (polkadot-evm/frontier, changes after - **Digest check.** `pallet_ethereum::on_finalize` panics on a block with more than one Frontier pre-runtime log, so the block is invalid. Before, it treated that block as having no log (Frontier #1809). +- **`OrbinumSignature::Ed25519Text` (`0x03`).** Ed25519 over the UTF-8 text + `"Orbinum transaction\nchain: 0x{genesis}\npayload: 0x{blake2_256(msg)}"` + (lowercase hex), where `msg` is what `Verify::verify` receives: the signing + payload, or its blake2_256 above 256 bytes. Phantom refuses binary input to + `solana:signMessage` and MetaMask rewrites it, so Solana accounts sign + extrinsics through this variant; the account is the raw ed25519 key. +- **`OrbinumSignature::Ed25519WrappedText` (`0x04`).** The same text inside + `…`: what a Substrate extension's `signRaw` signs for it + (polkadot-js `u8aWrapBytes`), so ed25519 accounts in polkadot-js, Talisman or + SubWallet keep signing. +- **Raw `Ed25519` (`0x00`) refused.** The variant stays at index 0 but never + verifies: polkadot-api builds no extrinsic for a signature enum without + `Ed25519`, `Sr25519` and `Ecdsa`, so removing it would break every client, + sr25519 accounts included. Solana wallets sign arbitrary bytes through `signMessage` (Backpack, + Solflare, Trust and Talisman sign a 32-byte binary "nonce"), and the account + cannot opt out of a variant, so a site that had a user sign a payload's + blake2_256 as a login nonce could submit it as that user's `0x00` + transaction. Every ed25519 signature must now cover text that says "Orbinum + transaction". **Clients:** an ed25519 account signs with `signRaw` of the + text and tags the result `0x04` (extension) or `0x03` (`signMessage`); plain + `signPayload` gives `0x00` and is refused (`BadProof`). Every index keeps + its encoding (`#[codec(index)]`), and no call encoding changes, so + `transaction_version` is unchanged. +- **Signature hardening.** Every `OrbinumSignature` variant refuses what the + host primitive would accept as a forgery or an alias: + - **Ed25519Text / Ed25519WrappedText:** small-order keys (all encodings, either sign + bit). Under ZIP-215, `R = identity, s = 0` verifies any message for them. + The account `0x0100…00` (EVM address `0x01` followed by zeros) is such a key, + so before this anyone could sign as it. + - **Sr25519:** the identity key (`0x00…00`), under which `R = 0, s = 0` + verifies any message. `CheckNonZeroSender` already kept it out of + extrinsics. + - **Ecdsa:** only `s ≤ n/2` and a raw recovery id (`0`–`3`). The host also + recovers the same key from `(r, n − s)` with the other id and from `v + 27`, + so one signed extrinsic had up to four valid encodings and hashes. `sp_core`, + polkadot-js and noble already sign this way. + - The module is split by responsibility under `src/orbinum_signature/` + (`signer`, `ed25519`, `sr25519`, `ecdsa`, `text`). ### spec 19 — tx 7 — 2026-10-13 (`v0.5.0`) diff --git a/template/runtime/src/orbinum_signature.rs b/template/runtime/src/orbinum_signature.rs deleted file mode 100644 index 69724dd1..00000000 --- a/template/runtime/src/orbinum_signature.rs +++ /dev/null @@ -1,178 +0,0 @@ -//! OrbinumSignature — unified signature type for Orbinum. -//! -//! Extends the standard [`sp_runtime::MultiSignature`] so that the ECDSA variant -//! derives `AccountId32` using Ethereum-compatible address encoding: -//! -//! ```text -//! AccountId32 = [keccak256(uncompressed_pubkey[1..])[12..] | 0x00 × 12] -//! ``` -//! -//! This is identical to the mapping produced by `EeSuffixAddressMapping`, meaning -//! the same secp256k1 keypair produces the **same** `AccountId32` from both an -//! EVM transaction and a native Substrate extrinsic. -//! -//! Sr25519 and Ed25519 variants behave identically to `MultiSignature`. -//! -//! ## SCALE discriminants -//! -//! The variant ordering mirrors `sp_runtime::MultiSignature` so that SCALE-encoded -//! bytes remain wire-compatible with existing tooling: -//! -//! | Discriminant | Variant | -//! |:---:|----------| -//! | 0x00 | Ed25519 | -//! | 0x01 | Sr25519 | -//! | 0x02 | Ecdsa | - -use scale_codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen}; -use scale_info::TypeInfo; -use sp_core::{ecdsa, ed25519, sr25519, H160}; -use sp_runtime::traits::{IdentifyAccount, Lazy, Verify}; - -use crate::evm_account::evm_bytes_to_account_id_bytes; - -/// The account ID type used by this runtime. -pub type AccountId = sp_runtime::AccountId32; - -/// Derive an Ethereum `H160` address from the 64-byte uncompressed public-key -/// body (the 64 bytes *after* the `0x04` prefix byte). -/// -/// This is the standard Ethereum address derivation: -/// `keccak256(uncompressed_pubkey[1..])[12..]` -fn pub64_to_eth_address(pub64: &[u8; 64]) -> H160 { - let hash = sp_io::hashing::keccak_256(pub64); - H160::from_slice(&hash[12..]) -} - -// ─── OrbinumSigner ──────────────────────────────────────────────────────────── - -/// The signer type that corresponds to [`OrbinumSignature`]. -/// -/// For ECDSA keys the `AccountId` is derived as `[eth_address | 0x00 × 12]` -/// rather than `blake2_256(compressed_pubkey)` used by `MultiSigner`. -/// -/// Variant order matches `sp_runtime::MultiSigner` for SCALE compatibility. -#[derive( - Clone, - Eq, - PartialEq, - Debug, - Encode, - Decode, - DecodeWithMemTracking, - TypeInfo, - MaxEncodedLen -)] -pub enum OrbinumSigner { - Ed25519(ed25519::Public), - Sr25519(sr25519::Public), - /// ECDSA key — `AccountId` derived as `[eth_address | 0x00 × 12]`. - Ecdsa(ecdsa::Public), -} - -impl From for OrbinumSigner { - fn from(k: ed25519::Public) -> Self { - Self::Ed25519(k) - } -} - -impl From for OrbinumSigner { - fn from(k: sr25519::Public) -> Self { - Self::Sr25519(k) - } -} - -impl From for OrbinumSigner { - fn from(k: ecdsa::Public) -> Self { - Self::Ecdsa(k) - } -} - -impl IdentifyAccount for OrbinumSigner { - type AccountId = AccountId; - - fn into_account(self) -> AccountId { - match self { - OrbinumSigner::Ed25519(k) => sp_runtime::MultiSigner::Ed25519(k).into_account(), - OrbinumSigner::Sr25519(k) => sp_runtime::MultiSigner::Sr25519(k).into_account(), - OrbinumSigner::Ecdsa(k) => { - // Decompress the 33-byte compressed key to 65-byte uncompressed form, - // then derive the Ethereum address with keccak256. - use libsecp256k1::{PublicKey, PublicKeyFormat}; - let compressed: &[u8; 33] = k.as_ref(); - let pk = match PublicKey::parse_slice(compressed, Some(PublicKeyFormat::Compressed)) - { - Ok(pk) => pk, - // Invalid key — fall back to standard MultiSigner derivation. - Err(_) => return sp_runtime::MultiSigner::Ecdsa(k).into_account(), - }; - let uncompressed = pk.serialize(); // [u8; 65], 0x04 prefix - let pub64: [u8; 64] = uncompressed[1..65] - .try_into() - .expect("uncompressed key without prefix is exactly 64 bytes; qed"); - let h160 = pub64_to_eth_address(&pub64); - AccountId::from(evm_bytes_to_account_id_bytes(*h160.as_fixed_bytes())) - } - } - } -} - -// ─── OrbinumSignature ───────────────────────────────────────────────────────── - -/// Unified signature type for Orbinum. -/// -/// - `Ed25519` / `Sr25519`: identical to [`sp_runtime::MultiSignature`]. -/// - `Ecdsa`: verifies using `blake2_256` of the payload; the signer `AccountId` -/// is derived as `[keccak256(uncompressed_pubkey[1..])[12..] | 0x00 × 12]`, -/// matching `EeSuffixAddressMapping`. -/// -/// Variant order mirrors `sp_runtime::MultiSignature` for SCALE wire compatibility -/// (Ed25519 = 0x00, Sr25519 = 0x01, Ecdsa = 0x02). -#[derive( - Clone, - Eq, - PartialEq, - Debug, - Encode, - Decode, - DecodeWithMemTracking, - TypeInfo, - MaxEncodedLen -)] -pub enum OrbinumSignature { - Ed25519(ed25519::Signature), - Sr25519(sr25519::Signature), - /// ECDSA: payload hashed with blake2_256; AccountId via keccak256 / EVM-suffix. - Ecdsa(ecdsa::Signature), -} - -impl Verify for OrbinumSignature { - type Signer = OrbinumSigner; - - fn verify>(&self, mut msg: L, signer: &AccountId) -> bool { - match self { - OrbinumSignature::Ed25519(sig) => { - sp_runtime::MultiSignature::Ed25519(*sig).verify(msg, signer) - } - OrbinumSignature::Sr25519(sig) => { - sp_runtime::MultiSignature::Sr25519(*sig).verify(msg, signer) - } - OrbinumSignature::Ecdsa(sig) => { - // 1. Hash the payload with blake2_256 (standard Substrate signing). - let hash = sp_io::hashing::blake2_256(msg.get()); - // 2. Recover the 64-byte uncompressed public key (without 0x04 prefix). - let sig_bytes: &[u8; 65] = sig.as_ref(); - let pub64 = match sp_io::crypto::secp256k1_ecdsa_recover(sig_bytes, &hash) { - Ok(k) => k, - Err(_) => return false, - }; - // 3. Derive the Ethereum address: keccak256(pub64)[12..]. - let h160 = pub64_to_eth_address(&pub64); - // 4. Build AccountId32 = [H160 | 0x00 × 12] and compare. - let expected = - AccountId::from(evm_bytes_to_account_id_bytes(*h160.as_fixed_bytes())); - expected == *signer - } - } - } -} diff --git a/template/runtime/src/orbinum_signature/ecdsa.rs b/template/runtime/src/orbinum_signature/ecdsa.rs new file mode 100644 index 00000000..501ae151 --- /dev/null +++ b/template/runtime/src/orbinum_signature/ecdsa.rs @@ -0,0 +1,64 @@ +//! ECDSA (secp256k1) verification and the EVM-compatible account. + +use crate::{evm_account::evm_bytes_to_account_id_bytes, AccountId}; +use hex_literal::hex; +use sp_core::ecdsa::Signature; + +/// secp256k1 group order `n` halved, big-endian. +const HALF_ORDER: [u8; 32] = + hex!("7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0"); + +/// `[keccak256(pubkey)[12..] ‖ 0x00 × 12]` for the 64-byte uncompressed key without its +/// `0x04` prefix: the account `EeSuffixAddressMapping` gives the key's EVM address. +pub(super) fn account_from_pubkey(pub64: &[u8; 64]) -> AccountId { + let hash = sp_io::hashing::keccak_256(pub64); + let mut address = [0u8; 20]; + address.copy_from_slice(&hash[12..]); + AccountId::from(evm_bytes_to_account_id_bytes(address)) +} + +/// The host also recovers the same key from `(r, n − s)` with the other recovery id, and +/// from `v + 27`; requiring a low `s` and the raw id (0–3) leaves one encoding. +fn is_canonical(sig: &[u8; 65]) -> bool { + sig[32..64] <= HALF_ORDER[..] && sig[64] < 4 +} + +pub(super) fn verify(sig: &Signature, msg: &[u8], signer: &AccountId) -> bool { + let sig: &[u8; 65] = sig.as_ref(); + if !is_canonical(sig) { + return false; + } + sp_io::crypto::secp256k1_ecdsa_recover(sig, &sp_io::hashing::blake2_256(msg)) + .is_ok_and(|pub64| account_from_pubkey(&pub64) == *signer) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn sig_with(s: [u8; 32], v: u8) -> [u8; 65] { + let mut sig = [1u8; 65]; + sig[32..64].copy_from_slice(&s); + sig[64] = v; + sig + } + + #[test] + fn half_order_is_the_largest_canonical_s() { + let mut above = HALF_ORDER; + above[31] += 1; + assert!(is_canonical(&sig_with(HALF_ORDER, 0))); + assert!(!is_canonical(&sig_with(above, 0))); + assert!(!is_canonical(&sig_with([0xff; 32], 0))); + } + + #[test] + fn only_raw_recovery_ids_are_canonical() { + for v in 0..4 { + assert!(is_canonical(&sig_with([0; 32], v))); + } + for v in [4, 26, 27, 28, 29, 30, 255] { + assert!(!is_canonical(&sig_with([0; 32], v))); + } + } +} diff --git a/template/runtime/src/orbinum_signature/ed25519.rs b/template/runtime/src/orbinum_signature/ed25519.rs new file mode 100644 index 00000000..8d1fb15d --- /dev/null +++ b/template/runtime/src/orbinum_signature/ed25519.rs @@ -0,0 +1,27 @@ +//! Ed25519 verification, shared by `Ed25519Text` and `Ed25519WrappedText`. + +use hex_literal::hex; +use sp_core::ed25519::{Public, Signature}; + +/// Every encoding (canonical or not) of the small-order points of edwards25519, compared +/// with the sign bit cleared (libsodium's `has_small_order` list). `ed25519_verify` +/// follows ZIP-215, under which such a key accepts a forged signature for any message. +const SMALL_ORDER_KEYS: [[u8; 32]; 7] = [ + hex!("0000000000000000000000000000000000000000000000000000000000000000"), + hex!("0100000000000000000000000000000000000000000000000000000000000000"), + hex!("26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc05"), + hex!("c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac037a"), + hex!("ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f"), + hex!("edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f"), + hex!("eeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f"), +]; + +fn has_small_order(key: &[u8; 32]) -> bool { + SMALL_ORDER_KEYS + .iter() + .any(|k| k[..31] == key[..31] && k[31] == key[31] & 0x7f) +} + +pub(super) fn verify(sig: &Signature, msg: &[u8], key: &[u8; 32]) -> bool { + !has_small_order(key) && sp_io::crypto::ed25519_verify(sig, msg, &Public::from_raw(*key)) +} diff --git a/template/runtime/src/orbinum_signature/mod.rs b/template/runtime/src/orbinum_signature/mod.rs new file mode 100644 index 00000000..53e66058 --- /dev/null +++ b/template/runtime/src/orbinum_signature/mod.rs @@ -0,0 +1,82 @@ +//! `OrbinumSignature`: the runtime's extrinsic signature. +//! +//! | Discriminant | Variant | Signed bytes | Account | +//! |:---:|----------------------|------------------------------------|---------------------------------------| +//! | 0x00 | `Ed25519` | never verifies | — | +//! | 0x01 | `Sr25519` | `msg` | raw key | +//! | 0x02 | `Ecdsa` | `blake2_256(msg)` | `[keccak256(pubkey)[12..] ‖ 0x00×12]` | +//! | 0x03 | `Ed25519Text` | [`text_message`] of `msg` | raw key | +//! | 0x04 | `Ed25519WrappedText` | [`wrapped_text_message`] of `msg` | raw key | +//! +//! `msg` is what `Verify::verify` receives: the signing payload, or its blake2_256 when +//! longer than 256 bytes. 0x01–0x02 keep `sp_runtime::MultiSignature`'s wire format. The +//! ECDSA account matches `EeSuffixAddressMapping`, so one secp256k1 key is the same +//! account on the EVM and Substrate paths. +//! +//! Ed25519 keys sign only text that names an Orbinum transaction. A raw ed25519 signature +//! over `msg` (`Ed25519`) is never accepted: Solana wallets sign arbitrary bytes through `signMessage`, +//! so a signature they give for a "login nonce" could be a payload's blake2_256. +//! `Ed25519Text` covers wallets that sign text as given (Phantom, MetaMask, Backpack…), +//! `Ed25519WrappedText` Substrate extensions' `signRaw`. `Ed25519` stays in the type only +//! because clients read it from the metadata: polkadot-api refuses to build any extrinsic +//! for a signature enum without `Ed25519`, `Sr25519` and `Ecdsa`. +//! +//! Each variant accepts a single encoding per signature and refuses keys that verify +//! forged signatures: small-order ed25519 keys, the sr25519 identity key, and ECDSA +//! signatures with a high `s` or an aliased recovery id. + +mod ecdsa; +mod ed25519; +mod signer; +mod sr25519; +mod text; + +pub use signer::OrbinumSigner; +#[cfg(test)] +pub use text::{text_message, wrapped_text_message, TEXT_MESSAGE_LEN, WRAPPED_TEXT_MESSAGE_LEN}; + +use crate::AccountId; +use scale_codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen}; +use scale_info::TypeInfo; +use sp_runtime::traits::{Lazy, Verify}; + +/// Indices are wire format: never reuse one. +#[derive( + Clone, + Eq, + PartialEq, + Debug, + Encode, + Decode, + DecodeWithMemTracking, + TypeInfo, + MaxEncodedLen +)] +pub enum OrbinumSignature { + /// Refused: see the module doc. + #[codec(index = 0)] + Ed25519(sp_core::ed25519::Signature), + #[codec(index = 1)] + Sr25519(sp_core::sr25519::Signature), + #[codec(index = 2)] + Ecdsa(sp_core::ecdsa::Signature), + #[codec(index = 3)] + Ed25519Text(sp_core::ed25519::Signature), + #[codec(index = 4)] + Ed25519WrappedText(sp_core::ed25519::Signature), +} + +impl Verify for OrbinumSignature { + type Signer = OrbinumSigner; + + fn verify>(&self, mut msg: L, signer: &AccountId) -> bool { + let key: &[u8; 32] = signer.as_ref(); + match self { + Self::Ed25519(_) => false, + Self::Sr25519(sig) => sr25519::verify(sig, msg.get(), key), + Self::Ecdsa(sig) => ecdsa::verify(sig, msg.get(), signer), + Self::Ed25519Text(sig) => text::verify(sig, msg.get(), key), + Self::Ed25519WrappedText(sig) => text::verify_wrapped(sig, msg.get(), key), + } + } +} diff --git a/template/runtime/src/orbinum_signature/signer.rs b/template/runtime/src/orbinum_signature/signer.rs new file mode 100644 index 00000000..13de5528 --- /dev/null +++ b/template/runtime/src/orbinum_signature/signer.rs @@ -0,0 +1,67 @@ +//! `OrbinumSigner`: the public key behind an `OrbinumSignature`, and its account. + +use crate::AccountId; +use scale_codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen}; +use scale_info::TypeInfo; +use sp_core::{ecdsa, ed25519, sr25519}; +use sp_runtime::{traits::IdentifyAccount, MultiSigner}; + +/// Variant order matches `sp_runtime::MultiSigner`. +#[derive( + Clone, + Eq, + PartialEq, + Debug, + Encode, + Decode, + DecodeWithMemTracking, + TypeInfo, + MaxEncodedLen +)] +pub enum OrbinumSigner { + Ed25519(ed25519::Public), + Sr25519(sr25519::Public), + /// Account `[eth_address ‖ 0x00 × 12]`, not `MultiSigner`'s `blake2_256(key)`. + Ecdsa(ecdsa::Public), +} + +impl From for OrbinumSigner { + fn from(k: ed25519::Public) -> Self { + Self::Ed25519(k) + } +} + +impl From for OrbinumSigner { + fn from(k: sr25519::Public) -> Self { + Self::Sr25519(k) + } +} + +impl From for OrbinumSigner { + fn from(k: ecdsa::Public) -> Self { + Self::Ecdsa(k) + } +} + +impl IdentifyAccount for OrbinumSigner { + type AccountId = AccountId; + + fn into_account(self) -> AccountId { + match self { + Self::Ed25519(k) => MultiSigner::Ed25519(k).into_account(), + Self::Sr25519(k) => MultiSigner::Sr25519(k).into_account(), + Self::Ecdsa(k) => { + use libsecp256k1::{PublicKey, PublicKeyFormat}; + match PublicKey::parse_slice(k.as_ref(), Some(PublicKeyFormat::Compressed)) { + Ok(pk) => { + let mut pub64 = [0u8; 64]; + pub64.copy_from_slice(&pk.serialize()[1..]); + super::ecdsa::account_from_pubkey(&pub64) + } + // No ECDSA signature recovers an invalid key, so this account never verifies. + Err(_) => MultiSigner::Ecdsa(k).into_account(), + } + } + } + } +} diff --git a/template/runtime/src/orbinum_signature/sr25519.rs b/template/runtime/src/orbinum_signature/sr25519.rs new file mode 100644 index 00000000..55f77b06 --- /dev/null +++ b/template/runtime/src/orbinum_signature/sr25519.rs @@ -0,0 +1,11 @@ +//! Sr25519 verification. + +use sp_core::sr25519::{Public, Signature}; + +/// The Ristretto identity, the group's only small-order element: under it `R = 0, s = 0` +/// verifies any message. +const IDENTITY: [u8; 32] = [0; 32]; + +pub(super) fn verify(sig: &Signature, msg: &[u8], key: &[u8; 32]) -> bool { + *key != IDENTITY && sp_io::crypto::sr25519_verify(sig, msg, &Public::from_raw(*key)) +} diff --git a/template/runtime/src/orbinum_signature/text.rs b/template/runtime/src/orbinum_signature/text.rs new file mode 100644 index 00000000..985680af --- /dev/null +++ b/template/runtime/src/orbinum_signature/text.rs @@ -0,0 +1,77 @@ +//! `Ed25519Text` and `Ed25519WrappedText`: ed25519 over a UTF-8 rendering of the payload. + +use sp_core::ed25519::Signature; + +const HEAD: &[u8] = b"Orbinum transaction\nchain: 0x"; +const PAYLOAD: &[u8] = b"\npayload: 0x"; +const OPEN: &[u8] = b""; +const CLOSE: &[u8] = b""; + +/// Length of every [`text_message`]: both hashes render as 64 hex characters. +pub const TEXT_MESSAGE_LEN: usize = HEAD.len() + 64 + PAYLOAD.len() + 64; + +/// Length of every [`wrapped_text_message`]. +pub const WRAPPED_TEXT_MESSAGE_LEN: usize = OPEN.len() + TEXT_MESSAGE_LEN + CLOSE.len(); + +/// The text an `Ed25519Text` signature covers, with lowercase hex: +/// +/// ```text +/// Orbinum transaction +/// chain: 0x{genesis} +/// payload: 0x{blake2_256(msg)} +/// ``` +/// +/// `msg` may already be a blake2_256 (payloads over 256 bytes); `verify` cannot tell, so +/// it is always hashed again. +pub fn text_message(genesis: &[u8; 32], msg: &[u8]) -> [u8; TEXT_MESSAGE_LEN] { + let mut out = [0u8; TEXT_MESSAGE_LEN]; + concat_into( + &mut out, + [ + HEAD, + &hex32(genesis), + PAYLOAD, + &hex32(&sp_io::hashing::blake2_256(msg)), + ], + ); + out +} + +/// [`text_message`] inside `…`: what a Substrate extension's `signRaw` +/// signs for that text (polkadot-js `u8aWrapBytes`). +pub fn wrapped_text_message(genesis: &[u8; 32], msg: &[u8]) -> [u8; WRAPPED_TEXT_MESSAGE_LEN] { + let mut out = [0u8; WRAPPED_TEXT_MESSAGE_LEN]; + concat_into(&mut out, [OPEN, &text_message(genesis, msg), CLOSE]); + out +} + +fn concat_into(out: &mut [u8], parts: [&[u8]; N]) { + let mut at = 0; + for part in parts { + out[at..at + part.len()].copy_from_slice(part); + at += part.len(); + } +} + +fn hex32(bytes: &[u8; 32]) -> [u8; 64] { + const DIGITS: &[u8; 16] = b"0123456789abcdef"; + let mut out = [0u8; 64]; + for (i, b) in bytes.iter().enumerate() { + out[2 * i] = DIGITS[(b >> 4) as usize]; + out[2 * i + 1] = DIGITS[(b & 0x0f) as usize]; + } + out +} + +/// Same source as `CheckGenesis`, which already read it to build the payload. +fn genesis() -> [u8; 32] { + frame_system::Pallet::::block_hash(0).to_fixed_bytes() +} + +pub(super) fn verify(sig: &Signature, msg: &[u8], key: &[u8; 32]) -> bool { + super::ed25519::verify(sig, &text_message(&genesis(), msg), key) +} + +pub(super) fn verify_wrapped(sig: &Signature, msg: &[u8], key: &[u8; 32]) -> bool { + super::ed25519::verify(sig, &wrapped_text_message(&genesis(), msg), key) +} diff --git a/template/runtime/src/runtime_tests/ed25519_text.rs b/template/runtime/src/runtime_tests/ed25519_text.rs new file mode 100644 index 00000000..a64de674 --- /dev/null +++ b/template/runtime/src/runtime_tests/ed25519_text.rs @@ -0,0 +1,446 @@ +//! `OrbinumSignature::Ed25519Text`: ed25519 over the text rendering of the payload. + +use super::with_ext; +use crate::{ + orbinum_signature::{ + text_message, wrapped_text_message, OrbinumSignature, TEXT_MESSAGE_LEN, + WRAPPED_TEXT_MESSAGE_LEN, + }, + AccountId, Runtime, RuntimeCall, SignedExtra, SignedPayload, UncheckedExtrinsic, +}; +use hex_literal::hex; +use scale_codec::{Decode, Encode, MaxEncodedLen}; +use sp_core::{ed25519, Pair, H256}; +use sp_runtime::{ + traits::{Checkable, Verify}, + transaction_validity::{InvalidTransaction, TransactionValidityError}, + MultiSignature, +}; + +/// Public testnet genesis; the wallet vectors below were signed against it. +const TESTNET_GENESIS: [u8; 32] = + hex!("58e2de05e550e64bb73bb2aeb706431dae70dcd1b589d477033c6e3aef3680ae"); + +/// `system.remark("orbinum solana probe")` signing payload (spec 19, tx 7, nonce 0, immortal). +const SHORT_PAYLOAD: [u8; 100] = hex!( + "0000506f7262696e756d20736f6c616e612070726f626500000000130000000700000058e2de05e550e64b + b73bb2aeb706431dae70dcd1b589d477033c6e3aef3680ae58e2de05e550e64bb73bb2aeb706431dae70dc + d1b589d477033c6e3aef3680ae00" +); + +/// blake2_256 of the 381-byte `system.remark("x" × 300)` payload: what `verify` receives. +const LONG_PAYLOAD_HASH: [u8; 32] = + hex!("eaf7d84f0f7506f11bfc84c0eecbdfc8de11c799069005f3128c1130dbd43abd"); + +/// Signatures produced by the browser wallets through `solana:signMessage` over +/// `textMessage` (`scripts/solana/lib/payload.mjs`), on 2026-10-19. +struct WalletVector { + key: [u8; 32], + short: [u8; 64], + long: [u8; 64], +} + +const PHANTOM: WalletVector = WalletVector { + key: hex!("64c602b56868980acc7d978f3e6564079b9d1323b3578660986d9561cbf39ff5"), + short: hex!( + "ec3a49751ef729e15d8cd227a49f696799c0b763d6601d0fd19cc3246010f2ac + 07405822f2b7cefb999f5d3d15e1bd8dd18c7c7da980d07852d8f8d4fb0f0b04" + ), + long: hex!( + "f951cf2f7d5b32cbd8d966fca779a1f94e884af0745cbc0f7197a0f33405b5d9 + 7ef6e405df389646c908c46e148720bf7b8ddb82ccb49dab2905c6921d21d609" + ), +}; + +const METAMASK: WalletVector = WalletVector { + key: hex!("8a5c9983fd2b7cce2eaedb7ac261f9072de8f08879f6efb0d563d68167badb63"), + short: hex!( + "830d09e131b63721bcde6bdc6fa4e4a96df2a9c9bc6d53064a64b76fe33637cf + 572d4c927a2980447655d848f8ef3da6243b5418b590109164d537fbcd0c0d07" + ), + long: hex!( + "a3d4b9f4d8729fb25892b903d2f0e2294062f022fc5b38f5a190d8903a8b1016 + fd35192abd652cb1385b650995fff2439f64899a27025216c39adf5bb4c4710d" + ), +}; + +/// Independent of the runtime's own encoder. +fn to_hex(bytes: &[u8]) -> String { + bytes.iter().map(|b| format!("{b:02x}")).collect() +} + +fn set_genesis(genesis: [u8; 32]) { + frame_system::BlockHash::::insert(0u32, H256(genesis)); +} + +fn text_sig(sig: [u8; 64]) -> OrbinumSignature { + OrbinumSignature::Ed25519Text(ed25519::Signature::from_raw(sig)) +} + +fn keypair() -> (ed25519::Pair, AccountId) { + let pair = ed25519::Pair::from_seed(&[7u8; 32]); + let account = AccountId::from(pair.public().0); + (pair, account) +} + +fn signed_extra(nonce: u32) -> SignedExtra { + ( + frame_system::CheckNonZeroSender::new(), + frame_system::CheckSpecVersion::new(), + frame_system::CheckTxVersion::new(), + frame_system::CheckGenesis::new(), + frame_system::CheckEra::from(sp_runtime::generic::Era::Immortal), + frame_system::CheckNonce::from(nonce), + frame_system::CheckWeight::new(), + pallet_transaction_payment::ChargeTransactionPayment::from(0u128), + frame_metadata_hash_extension::CheckMetadataHash::new(false), + ) +} + +/// The signing payload before the >256-byte hashing that `SignedPayload::encode` applies. +fn raw_payload(call: &RuntimeCall, nonce: u32) -> Vec { + SignedPayload::new(call.clone(), signed_extra(nonce)) + .unwrap() + .deconstruct() + .encode() +} + +fn remark(len: usize) -> RuntimeCall { + RuntimeCall::System(frame_system::Call::remark { + remark: vec![b'x'; len], + }) +} + +/// Signs `call` the way a text-only wallet does: over `text_message` of the bytes +/// `verify` will receive. +fn sign_text(pair: &ed25519::Pair, call: &RuntimeCall, nonce: u32) -> OrbinumSignature { + let payload = SignedPayload::new(call.clone(), signed_extra(nonce)).unwrap(); + let genesis = frame_system::Pallet::::block_hash(0u32); + let text = payload.using_encoded(|msg| text_message(genesis.as_fixed_bytes(), msg)); + OrbinumSignature::Ed25519Text(pair.sign(&text)) +} + +/// Signs `call` the way a Substrate extension's `signRaw` does over the same text. +fn sign_wrapped_text(pair: &ed25519::Pair, call: &RuntimeCall, nonce: u32) -> OrbinumSignature { + let payload = SignedPayload::new(call.clone(), signed_extra(nonce)).unwrap(); + let genesis = frame_system::Pallet::::block_hash(0u32); + let text = payload.using_encoded(|msg| wrapped_text_message(genesis.as_fixed_bytes(), msg)); + OrbinumSignature::Ed25519WrappedText(pair.sign(&text)) +} + +fn check( + call: RuntimeCall, + account: &AccountId, + signature: OrbinumSignature, + nonce: u32, +) -> Result<(), TransactionValidityError> { + let xt = UncheckedExtrinsic::new_signed(call, account.clone(), signature, signed_extra(nonce)); + // Round-trip the wire format, as the pool does. + let xt = UncheckedExtrinsic::decode(&mut &xt.encode()[..]).unwrap(); + xt.check(&frame_system::ChainContext::::default()) + .map(|_| ()) +} + +// ─── Message format ─────────────────────────────────────────────────────────── + +#[test] +fn text_message_matches_the_documented_format() { + let genesis = [0xabu8; 32]; + let msg = b"payload bytes"; + let expected = format!( + "Orbinum transaction\nchain: 0x{}\npayload: 0x{}", + to_hex(&genesis), + to_hex(&sp_io::hashing::blake2_256(msg)), + ); + assert_eq!(&text_message(&genesis, msg)[..], expected.as_bytes()); + assert_eq!(TEXT_MESSAGE_LEN, 169); +} + +#[test] +fn text_message_hashes_a_32_byte_message_again() { + let genesis = [1u8; 32]; + let text = text_message(&genesis, &LONG_PAYLOAD_HASH); + let tail = format!( + "payload: 0x{}", + to_hex(&sp_io::hashing::blake2_256(&LONG_PAYLOAD_HASH)) + ); + assert!(text.ends_with(tail.as_bytes())); +} + +// ─── Real wallet signatures ─────────────────────────────────────────────────── + +#[test] +fn wallet_signatures_verify() { + with_ext(|| { + set_genesis(TESTNET_GENESIS); + for (name, w) in [("phantom", PHANTOM), ("metamask", METAMASK)] { + let account = AccountId::from(w.key); + assert!( + text_sig(w.short).verify(&SHORT_PAYLOAD[..], &account), + "{name}: short payload" + ); + assert!( + text_sig(w.long).verify(&LONG_PAYLOAD_HASH[..], &account), + "{name}: hashed long payload" + ); + } + }); +} + +#[test] +fn wallet_signature_rejected_on_another_chain() { + with_ext(|| { + set_genesis([0x11; 32]); + let account = AccountId::from(PHANTOM.key); + assert!(!text_sig(PHANTOM.short).verify(&SHORT_PAYLOAD[..], &account)); + }); +} + +#[test] +fn wallet_signature_rejected_for_altered_payload() { + with_ext(|| { + set_genesis(TESTNET_GENESIS); + let account = AccountId::from(PHANTOM.key); + let mut altered = SHORT_PAYLOAD; + altered[3] ^= 0x01; + assert!(!text_sig(PHANTOM.short).verify(&altered[..], &account)); + }); +} + +#[test] +fn wallet_signature_rejected_for_another_account() { + with_ext(|| { + set_genesis(TESTNET_GENESIS); + let other = AccountId::from(METAMASK.key); + assert!(!text_sig(PHANTOM.short).verify(&SHORT_PAYLOAD[..], &other)); + }); +} + +#[test] +fn wallet_signature_rejected_for_swapped_payload() { + with_ext(|| { + set_genesis(TESTNET_GENESIS); + let account = AccountId::from(PHANTOM.key); + assert!(!text_sig(PHANTOM.short).verify(&LONG_PAYLOAD_HASH[..], &account)); + assert!(!text_sig(PHANTOM.long).verify(&SHORT_PAYLOAD[..], &account)); + }); +} + +// ─── Separation between the ed25519 framings ────────────────────────────────── + +#[test] +fn a_raw_ed25519_signature_verifies_under_no_variant() { + with_ext(|| { + set_genesis(TESTNET_GENESIS); + let (pair, account) = keypair(); + let sig = pair.sign(&SHORT_PAYLOAD); + assert!(!OrbinumSignature::Ed25519Text(sig).verify(&SHORT_PAYLOAD[..], &account)); + assert!(!OrbinumSignature::Ed25519WrappedText(sig).verify(&SHORT_PAYLOAD[..], &account)); + }); +} + +#[test] +fn text_and_wrapped_text_signatures_do_not_cross() { + with_ext(|| { + set_genesis(TESTNET_GENESIS); + let account = AccountId::from(PHANTOM.key); + let sig = ed25519::Signature::from_raw(PHANTOM.short); + assert!(OrbinumSignature::Ed25519Text(sig).verify(&SHORT_PAYLOAD[..], &account)); + assert!(!OrbinumSignature::Ed25519WrappedText(sig).verify(&SHORT_PAYLOAD[..], &account)); + + let (pair, account) = keypair(); + let wrapped = pair.sign(&wrapped_text_message(&TESTNET_GENESIS, &SHORT_PAYLOAD)); + assert!(OrbinumSignature::Ed25519WrappedText(wrapped).verify(&SHORT_PAYLOAD[..], &account)); + assert!(!OrbinumSignature::Ed25519Text(wrapped).verify(&SHORT_PAYLOAD[..], &account)); + }); +} + +/// The bytes polkadot-js `signRaw` signs for the text: `u8aWrapBytes`. +#[test] +fn wrapped_text_message_is_the_text_inside_bytes_tags() { + let text = text_message(&TESTNET_GENESIS, &SHORT_PAYLOAD); + let expected = [b"".as_slice(), &text, b""].concat(); + assert_eq!( + &wrapped_text_message(&TESTNET_GENESIS, &SHORT_PAYLOAD)[..], + &expected[..] + ); + assert_eq!(WRAPPED_TEXT_MESSAGE_LEN, 184); +} + +// ─── Full extrinsic check ───────────────────────────────────────────────────── + +#[test] +fn signed_extrinsic_with_short_payload_passes_check() { + with_ext(|| { + set_genesis(TESTNET_GENESIS); + let (pair, account) = keypair(); + let call = remark(20); + assert!(raw_payload(&call, 0).len() <= 256); + let sig = sign_text(&pair, &call, 0); + assert_eq!(check(call, &account, sig, 0), Ok(())); + }); +} + +#[test] +fn signed_extrinsic_with_hashed_long_payload_passes_check() { + with_ext(|| { + set_genesis(TESTNET_GENESIS); + let (pair, account) = keypair(); + let call = remark(300); + assert!(raw_payload(&call, 0).len() > 256); + let sig = sign_text(&pair, &call, 0); + assert_eq!(check(call, &account, sig, 0), Ok(())); + }); +} + +#[test] +fn wrapped_text_extrinsics_pass_check_short_and_long() { + with_ext(|| { + set_genesis(TESTNET_GENESIS); + let (pair, account) = keypair(); + for len in [20, 300] { + let call = remark(len); + let sig = sign_wrapped_text(&pair, &call, 0); + assert_eq!( + check(call, &account, sig, 0), + Ok(()), + "remark of {len} bytes" + ); + } + }); +} + +#[test] +fn signed_extrinsic_with_unhashed_long_payload_fails_check() { + with_ext(|| { + set_genesis(TESTNET_GENESIS); + let (pair, account) = keypair(); + let call = remark(300); + let text = text_message(&TESTNET_GENESIS, &raw_payload(&call, 0)); + let sig = OrbinumSignature::Ed25519Text(pair.sign(&text)); + assert_eq!( + check(call, &account, sig, 0), + Err(InvalidTransaction::BadProof.into()) + ); + }); +} + +#[test] +fn signed_extrinsic_with_changed_extension_fails_check() { + with_ext(|| { + set_genesis(TESTNET_GENESIS); + let (pair, account) = keypair(); + let sig = sign_text(&pair, &remark(20), 0); + assert_eq!( + check(remark(20), &account, sig, 1), + Err(InvalidTransaction::BadProof.into()) + ); + }); +} + +#[test] +fn signed_extrinsic_with_changed_call_fails_check() { + with_ext(|| { + set_genesis(TESTNET_GENESIS); + let (pair, account) = keypair(); + let sig = sign_text(&pair, &remark(20), 0); + assert_eq!( + check(remark(21), &account, sig, 0), + Err(InvalidTransaction::BadProof.into()) + ); + }); +} + +#[test] +fn signed_extrinsic_signed_for_another_chain_fails_check() { + with_ext(|| { + set_genesis([0x22; 32]); + let (pair, account) = keypair(); + let sig = sign_text(&pair, &remark(20), 0); + set_genesis(TESTNET_GENESIS); + assert_eq!( + check(remark(20), &account, sig, 0), + Err(InvalidTransaction::BadProof.into()) + ); + }); +} + +// ─── Wire format ────────────────────────────────────────────────────────────── + +#[test] +fn discriminants_and_lengths_are_stable() { + use sp_core::{ecdsa, sr25519}; + + let (pair, _) = keypair(); + let sig = pair.sign(b"wire"); + + // 0x00 decodes as `Ed25519`, kept for the metadata; it never verifies. + let raw = OrbinumSignature::Ed25519(sig).encode(); + assert_eq!(raw, MultiSignature::Ed25519(sig).encode()); + + let sr = sr25519::Signature::from_raw([1u8; 64]); + assert_eq!( + OrbinumSignature::Sr25519(sr).encode(), + MultiSignature::Sr25519(sr).encode() + ); + let ec = ecdsa::Signature::from_raw([2u8; 65]); + assert_eq!( + OrbinumSignature::Ecdsa(ec).encode(), + MultiSignature::Ecdsa(ec).encode() + ); + + for (tag, variant) in [ + (0x03u8, OrbinumSignature::Ed25519Text(sig)), + (0x04u8, OrbinumSignature::Ed25519WrappedText(sig)), + ] { + let encoded = variant.encode(); + assert_eq!(encoded[0], tag); + assert_eq!(&encoded[1..], &sig.0[..]); + assert_eq!( + OrbinumSignature::decode(&mut &encoded[..]).unwrap(), + variant + ); + } + + let unknown = [&[0x05u8][..], &sig.0[..]].concat(); + assert!(OrbinumSignature::decode(&mut &unknown[..]).is_err()); + + assert_eq!(OrbinumSignature::max_encoded_len(), 66); +} + +/// polkadot-api builds no extrinsic for a signature enum lacking `Ed25519`, `Sr25519` or +/// `Ecdsa`, and tags them with the fixed indices 0, 1 and 2. +#[test] +fn signature_metadata_keeps_the_shape_polkadot_api_requires() { + use scale_info::{TypeDef, TypeInfo}; + + let TypeDef::Variant(def) = OrbinumSignature::type_info().type_def else { + panic!("OrbinumSignature is not an enum in the metadata"); + }; + let index_of = |name: &str| { + def.variants + .iter() + .find(|v| v.name == name) + .map(|v| v.index) + }; + assert_eq!(index_of("Ed25519"), Some(0)); + assert_eq!(index_of("Sr25519"), Some(1)); + assert_eq!(index_of("Ecdsa"), Some(2)); + assert_eq!(index_of("Ed25519Text"), Some(3)); + assert_eq!(index_of("Ed25519WrappedText"), Some(4)); +} + +#[test] +fn a_valid_raw_ed25519_signature_never_verifies() { + with_ext(|| { + set_genesis(TESTNET_GENESIS); + let (pair, account) = keypair(); + let sig = pair.sign(&SHORT_PAYLOAD); + assert!(sp_io::crypto::ed25519_verify( + &sig, + &SHORT_PAYLOAD, + &pair.public() + )); + assert!(!OrbinumSignature::Ed25519(sig).verify(&SHORT_PAYLOAD[..], &account)); + }); +} diff --git a/template/runtime/src/runtime_tests/mod.rs b/template/runtime/src/runtime_tests/mod.rs index 45b2bc8e..3654a36d 100644 --- a/template/runtime/src/runtime_tests/mod.rs +++ b/template/runtime/src/runtime_tests/mod.rs @@ -2,9 +2,11 @@ mod accounts; mod block_author; +mod ed25519_text; mod host_functions; mod metadata_hash; mod precompile_code; +mod signature_hardening; mod weights; use sp_io::TestExternalities; diff --git a/template/runtime/src/runtime_tests/signature_hardening.rs b/template/runtime/src/runtime_tests/signature_hardening.rs new file mode 100644 index 00000000..54c85464 --- /dev/null +++ b/template/runtime/src/runtime_tests/signature_hardening.rs @@ -0,0 +1,209 @@ +//! Keys and encodings `OrbinumSignature` refuses although the host primitive accepts them. + +use super::with_ext; +use crate::{ + orbinum_signature::{text_message, wrapped_text_message, OrbinumSignature, OrbinumSigner}, + AccountId, Runtime, +}; +use hex_literal::hex; +use sp_core::{ecdsa, ed25519, sr25519, Pair, H256}; +use sp_runtime::traits::{IdentifyAccount, Verify}; + +const MSG: &[u8] = b"any message"; +const GENESIS: [u8; 32] = [0x5a; 32]; + +/// secp256k1 group order, big-endian. +const SECP256K1_ORDER: [u8; 32] = + hex!("fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141"); + +/// `n − s` for a big-endian scalar. +fn order_minus(s: &[u8]) -> [u8; 32] { + let mut out = [0u8; 32]; + let mut borrow = 0i16; + for i in (0..32).rev() { + let d = SECP256K1_ORDER[i] as i16 - s[i] as i16 - borrow; + borrow = (d < 0) as i16; + out[i] = (d + 256 * borrow) as u8; + } + out +} + +// ─── Ed25519 / Ed25519Text ──────────────────────────────────────────────────── + +/// Every small-order edwards25519 encoding, sign bit cleared. +const SMALL_ORDER: [[u8; 32]; 7] = [ + hex!("0000000000000000000000000000000000000000000000000000000000000000"), + hex!("0100000000000000000000000000000000000000000000000000000000000000"), + hex!("26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc05"), + hex!("c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac037a"), + hex!("ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f"), + hex!("edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f"), + hex!("eeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f"), +]; + +/// R = identity, s = 0: under ZIP-215, valid for a small-order key and any message. +const ED25519_FORGERY: [u8; 64] = { + let mut sig = [0u8; 64]; + sig[0] = 0x01; + sig +}; + +#[test] +fn ed25519_small_order_keys_are_refused() { + with_ext(|| { + frame_system::BlockHash::::insert(0u32, H256(GENESIS)); + let forged = ed25519::Signature::from_raw(ED25519_FORGERY); + let text = text_message(&GENESIS, MSG); + let wrapped = wrapped_text_message(&GENESIS, MSG); + for key in SMALL_ORDER { + let public = ed25519::Public::from_raw(key); + // The host accepts the forgery, so each refusal below comes from the key check. + assert!(sp_io::crypto::ed25519_verify(&forged, &text, &public)); + assert!(sp_io::crypto::ed25519_verify(&forged, &wrapped, &public)); + + let mut negated = key; + negated[31] |= 0x80; + for k in [key, negated] { + let account = AccountId::from(k); + assert!( + !OrbinumSignature::Ed25519Text(forged).verify(MSG, &account), + "Ed25519Text accepted {k:02x?}" + ); + assert!( + !OrbinumSignature::Ed25519WrappedText(forged).verify(MSG, &account), + "Ed25519WrappedText accepted {k:02x?}" + ); + } + } + }); +} + +/// The EVM address `0x01 00…00` maps to the identity point's encoding. +#[test] +fn ed25519_forgery_cannot_spend_from_the_evm_identity_account() { + with_ext(|| { + let account = crate::evm_h160_to_account_id(sp_core::H160::from(hex!( + "0100000000000000000000000000000000000000" + ))); + assert_eq!( + >::as_ref(&account), + &SMALL_ORDER[1] + ); + frame_system::BlockHash::::insert(0u32, H256(GENESIS)); + let forged = ed25519::Signature::from_raw(ED25519_FORGERY); + assert!(!OrbinumSignature::Ed25519Text(forged).verify(MSG, &account)); + assert!(!OrbinumSignature::Ed25519WrappedText(forged).verify(MSG, &account)); + }); +} + +#[test] +fn ed25519_ordinary_key_still_verifies() { + with_ext(|| { + frame_system::BlockHash::::insert(0u32, H256(GENESIS)); + let pair = ed25519::Pair::from_seed(&[3u8; 32]); + let account = OrbinumSigner::from(pair.public()).into_account(); + let text = pair.sign(&text_message(&GENESIS, MSG)); + let wrapped = pair.sign(&wrapped_text_message(&GENESIS, MSG)); + assert!(OrbinumSignature::Ed25519Text(text).verify(MSG, &account)); + assert!(OrbinumSignature::Ed25519WrappedText(wrapped).verify(MSG, &account)); + assert!(!OrbinumSignature::Ed25519Text(text).verify(&b"other"[..], &account)); + }); +} + +/// A Solana wallet signs any bytes through `signMessage`, e.g. a payload's blake2_256 +/// shown as a login nonce. That signature verifies as raw ed25519 but under no variant. +#[test] +fn a_blind_signature_over_the_payload_hash_authorizes_nothing() { + with_ext(|| { + frame_system::BlockHash::::insert(0u32, H256(GENESIS)); + let pair = ed25519::Pair::from_seed(&[4u8; 32]); + let account = OrbinumSigner::from(pair.public()).into_account(); + let payload_hash = sp_io::hashing::blake2_256(b"a payload longer than 256 bytes"); + let blind = pair.sign(&payload_hash); + assert!(sp_io::crypto::ed25519_verify( + &blind, + &payload_hash, + &pair.public() + )); + assert!(!OrbinumSignature::Ed25519Text(blind).verify(&payload_hash[..], &account)); + assert!(!OrbinumSignature::Ed25519WrappedText(blind).verify(&payload_hash[..], &account)); + assert!(!OrbinumSignature::Ed25519(blind).verify(&payload_hash[..], &account)); + }); +} + +// ─── Sr25519 ────────────────────────────────────────────────────────────────── + +#[test] +fn sr25519_identity_key_is_refused() { + with_ext(|| { + // R = 0, s = 0, with schnorrkel's marker bit. + let mut raw = [0u8; 64]; + raw[63] = 0x80; + let forged = sr25519::Signature::from_raw(raw); + let identity = [0u8; 32]; + assert!(sp_io::crypto::sr25519_verify( + &forged, + MSG, + &sr25519::Public::from_raw(identity) + )); + assert!(!OrbinumSignature::Sr25519(forged).verify(MSG, &AccountId::from(identity))); + }); +} + +#[test] +fn sr25519_ordinary_key_still_verifies() { + let pair = sr25519::Pair::from_string("//Alice", None).unwrap(); + let account = OrbinumSigner::from(pair.public()).into_account(); + assert!(OrbinumSignature::Sr25519(pair.sign(MSG)).verify(MSG, &account)); +} + +// ─── Ecdsa ──────────────────────────────────────────────────────────────────── + +fn ecdsa_alice() -> (ecdsa::Signature, AccountId) { + let pair = ecdsa::Pair::from_string("//Alice", None).unwrap(); + ( + pair.sign(MSG), + OrbinumSigner::from(pair.public()).into_account(), + ) +} + +fn recovers_same_key(a: &[u8; 65], b: &[u8; 65]) -> bool { + let hash = sp_io::hashing::blake2_256(MSG); + match ( + sp_io::crypto::secp256k1_ecdsa_recover(a, &hash), + sp_io::crypto::secp256k1_ecdsa_recover(b, &hash), + ) { + (Ok(ka), Ok(kb)) => ka == kb, + _ => false, + } +} + +#[test] +fn ecdsa_canonical_signature_verifies() { + let (sig, account) = ecdsa_alice(); + assert!( + sig.0[32..64] <= order_minus(&sig.0[32..64])[..], + "sp_core signs with a low s" + ); + assert!(sig.0[64] < 2); + assert!(OrbinumSignature::Ecdsa(sig).verify(MSG, &account)); +} + +#[test] +fn ecdsa_high_s_alias_is_refused() { + let (sig, account) = ecdsa_alice(); + let mut alias = sig.0; + alias[32..64].copy_from_slice(&order_minus(&sig.0[32..64])); + alias[64] ^= 1; + assert!(recovers_same_key(&sig.0, &alias)); + assert!(!OrbinumSignature::Ecdsa(ecdsa::Signature::from_raw(alias)).verify(MSG, &account)); +} + +#[test] +fn ecdsa_offset_recovery_id_is_refused() { + let (sig, account) = ecdsa_alice(); + let mut alias = sig.0; + alias[64] += 27; + assert!(recovers_same_key(&sig.0, &alias)); + assert!(!OrbinumSignature::Ecdsa(ecdsa::Signature::from_raw(alias)).verify(MSG, &account)); +} From cc40742dfe3d1fa8d21d4323099dc8f24cc7b321 Mon Sep 17 00:00:00 2001 From: nol4lej Date: Wed, 21 Oct 2026 13:17:52 -0300 Subject: [PATCH 2/2] git add Cargo.lock deny.toml 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)" --- Cargo.lock | 156 ++++++++++++++++++++++++++--------------------------- deny.toml | 5 ++ 2 files changed, 83 insertions(+), 78 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d9f053e4..29512042 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3315,36 +3315,36 @@ dependencies = [ [[package]] name = "cranelift-assembler-x64" -version = "0.123.18" +version = "0.123.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e92993516bcc43fd4e511b31d2e0c0c24f6b7ac18bb39e32cac2bfc05ea378e" +checksum = "b3741b9ad77e89c710586b2fcdd619baf28482d92b70d88cecb2a081dd912ae6" dependencies = [ "cranelift-assembler-x64-meta", ] [[package]] name = "cranelift-assembler-x64-meta" -version = "0.123.18" +version = "0.123.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97eeeb61b08ec484f274119faf3a8a1b80dd300b0208c0247d16d05982fc89fe" +checksum = "8243e149085051b23faebd352ef7031aeebc5f45e63aaefd63ee7f810c22c7df" dependencies = [ "cranelift-srcgen", ] [[package]] name = "cranelift-bforest" -version = "0.123.18" +version = "0.123.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "782eb44158681a105d157cb0ce63e3dc83f78ef5674266c12234f49da3f5c6e8" +checksum = "c538af3aee13eb6aa8a85d2ef85b4d0aa41623dae8b19f59bbacfd5d6c114920" dependencies = [ "cranelift-entity", ] [[package]] name = "cranelift-bitset" -version = "0.123.18" +version = "0.123.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edd59af193204c4f025fb2c2d9ee0aeb1f60929edf000b0dfa3d45543a62f264" +checksum = "2292ea3964d7d751031da1a1261d64feb09e89bd4fbebf3c78f43f9c34c20a11" dependencies = [ "serde", "serde_derive", @@ -3352,9 +3352,9 @@ dependencies = [ [[package]] name = "cranelift-codegen" -version = "0.123.18" +version = "0.123.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c29bd985057d790db25f6bc60b369b311aa2d75c2f496a7e9993585bfc7d60" +checksum = "8967a699a4982d02c32246a00f2aa4cee5bccfaaa042608d5e151cb99cba210e" dependencies = [ "bumpalo", "cranelift-assembler-x64", @@ -3379,9 +3379,9 @@ dependencies = [ [[package]] name = "cranelift-codegen-meta" -version = "0.123.18" +version = "0.123.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9ef8a8495f19212f92db8425f768ffc67000c5cb417a0664475a8d09596700f" +checksum = "e258eeeb6903b4e91e314a3667ab2a1e2924738ef2df08bdb50355e38decc749" dependencies = [ "cranelift-assembler-x64-meta", "cranelift-codegen-shared", @@ -3392,24 +3392,24 @@ dependencies = [ [[package]] name = "cranelift-codegen-shared" -version = "0.123.18" +version = "0.123.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3aa5099097666baf45bd68ef036327c8f29d8785fffaaa7db3b3b6b7e94bcc6" +checksum = "e1d36ff0de30460c01dad152ca2097bcabef2f16c659b9ffbca2f67e9bbdb321" [[package]] name = "cranelift-control" -version = "0.123.18" +version = "0.123.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26f67f4f4e92c2934f40c2ff4574400bd27326932aad937a438b7fb459f60a63" +checksum = "acc880d91877efb6199031bda872726ca3287dc1e18ccf3f61c07bfdab62930b" dependencies = [ "arbitrary", ] [[package]] name = "cranelift-entity" -version = "0.123.18" +version = "0.123.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8393d09f0abaa504896069e79f4eb6f15eb1853e5645732b082dac1ed292aaa8" +checksum = "5f7af321c4b6e5be4729e4a5b1ed0fc78fc31695c1ecd3c9b45b91908bf3d0a3" dependencies = [ "cranelift-bitset", "serde", @@ -3418,9 +3418,9 @@ dependencies = [ [[package]] name = "cranelift-frontend" -version = "0.123.18" +version = "0.123.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "580cfaecbd11476dd82f6a393123dcbd9dfe583467df8d0c8441c641444d4985" +checksum = "3463f1b26faac3491008cede8e9cdf7ceb83139143bd92ba86c14b0882aaf1bb" dependencies = [ "cranelift-codegen", "log", @@ -3430,15 +3430,15 @@ dependencies = [ [[package]] name = "cranelift-isle" -version = "0.123.18" +version = "0.123.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c044843adb7d94b1facebaad846c0a762f79a77953417e3f0182093bcbbdc38" +checksum = "90b76554b329634c4241c5670a96058affca99ac3b33daa14fb221bd68c0038e" [[package]] name = "cranelift-native" -version = "0.123.18" +version = "0.123.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d4c1707214e53f2040e4d13430069ea22623287b2750ed6d62e40eba88d91a" +checksum = "36e7c67bcfd30a8160187a6d52b2ec0cbe45698a9d61f12d1292bd24701f7ed2" dependencies = [ "cranelift-codegen", "libc", @@ -3447,9 +3447,9 @@ dependencies = [ [[package]] name = "cranelift-srcgen" -version = "0.123.18" +version = "0.123.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d355a35edc49f628db7b793f3527eed31b560e77c3e8ed0e5258aad8df8a5249" +checksum = "1ccca83c45ddfe0412b1b2e4fe509d28d1bdbc014e306574a848519741bebc36" [[package]] name = "crc" @@ -5226,7 +5226,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -7659,7 +7659,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -7668,7 +7668,7 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "82cb6a9f675da968c63b6208c641b9dca58fc0133ae53375736b1767b0cab8bd" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -15611,9 +15611,9 @@ dependencies = [ [[package]] name = "pulley-interpreter" -version = "36.0.18" +version = "36.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "334ae8982dcf354e5f28e79fc6f935601e028b729cc174d3937eeeaa859057b8" +checksum = "c956c7140270b72e3bf03381a51f2eb79c70fd6881fc17cd027fa7cd3f56ae5a" dependencies = [ "cranelift-bitset", "log", @@ -15623,9 +15623,9 @@ dependencies = [ [[package]] name = "pulley-macros" -version = "36.0.18" +version = "36.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cd207bca4bc5e46cb4fcea87196dcbd78befc628c64c67d11dc99fce5cd5369" +checksum = "74db420d7ce2bb03eed5b81c1f778bda5771b3fd818e2b496dfc3a2783ceb60e" dependencies = [ "proc-macro2", "quote", @@ -15724,7 +15724,7 @@ dependencies = [ "once_cell", "socket2 0.6.5", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -16658,7 +16658,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -16671,7 +16671,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -16685,7 +16685,7 @@ dependencies = [ "once_cell", "ring 0.17.14", "rustls-pki-types", - "rustls-webpki 0.103.15", + "rustls-webpki 0.103.16", "subtle 2.6.1", "zeroize", ] @@ -16726,11 +16726,11 @@ dependencies = [ "rustls", "rustls-native-certs", "rustls-platform-verifier-android", - "rustls-webpki 0.103.15", + "rustls-webpki 0.103.16", "security-framework", "security-framework-sys", "webpki-root-certs 0.26.11", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -16747,11 +16747,11 @@ dependencies = [ "rustls", "rustls-native-certs", "rustls-platform-verifier-android", - "rustls-webpki 0.103.15", + "rustls-webpki 0.103.16", "security-framework", "security-framework-sys", - "webpki-root-certs 1.0.9", - "windows-sys 0.59.0", + "webpki-root-certs 1.0.10", + "windows-sys 0.52.0", ] [[package]] @@ -16772,9 +16772,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.15" +version = "0.103.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +checksum = "4443b1b6d39d7c663139942cc4558262562d2c84123bb6282ef177009ca8b281" dependencies = [ "aws-lc-rs", "ring 0.17.14", @@ -20969,7 +20969,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix 1.1.4", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -20988,7 +20988,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ "rustix 1.1.4", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -22230,9 +22230,9 @@ dependencies = [ [[package]] name = "wasmtime" -version = "36.0.18" +version = "36.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70095b9fe63cc2a87ed206729e11544610decc184c62898c5d201ff1d666d82e" +checksum = "60e5f244a2c9d40afa9e9a607ee7f048c0648bb1c22cc28b130c5fc71e23d71d" dependencies = [ "addr2line", "anyhow", @@ -22278,9 +22278,9 @@ dependencies = [ [[package]] name = "wasmtime-environ" -version = "36.0.18" +version = "36.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a67ea8904b6e369e2218780bd1f9186f1980cb616445eb2c1f3c4e45c8397865" +checksum = "3e246cd5107aa406cfa4acf6942e864bd189ae8136bab853365147da131c9ca3" dependencies = [ "anyhow", "cpp_demangle", @@ -22303,18 +22303,18 @@ dependencies = [ [[package]] name = "wasmtime-internal-asm-macros" -version = "36.0.18" +version = "36.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0488a52ae93e654baecb08ce04d15d478756eb2168c2bdab7915710109161ce" +checksum = "5380a8106fea8bfedb7208bb11ced9e7488ee44394ca889ede73acd3bdfd9d0b" dependencies = [ "cfg-if", ] [[package]] name = "wasmtime-internal-cache" -version = "36.0.18" +version = "36.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e941dc94f323850facf75e49a6cd13d71f116a99c6a49fc34717f6e732616b2d" +checksum = "4108e1964b3a02b4b5604ea1a2918a0cbcab512e5de0ccf4c062fc6c06b2eb8e" dependencies = [ "anyhow", "base64", @@ -22332,9 +22332,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-cranelift" -version = "36.0.18" +version = "36.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63ea7ee133de9109a4a1fc63dae3afc9ea32e92b8dada824fa4595dbf175f80" +checksum = "7339fdd64a3fde574505adea23244e065d5b1de0e72c8dd87e150f5d4ab63a63" dependencies = [ "anyhow", "cfg-if", @@ -22359,9 +22359,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-fiber" -version = "36.0.18" +version = "36.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69662a0a4dbe9784d1e8add9e3c5d2db0ca348085c624c27a70992dbe2eff4a3" +checksum = "6f3afc72d3c72f71d4ebc72e83f479ba8ca24409c41d43e5e0228f0bf82b873e" dependencies = [ "anyhow", "cc", @@ -22375,9 +22375,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-jit-debug" -version = "36.0.18" +version = "36.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00cb07d23903c0be359d6b920d94774ea322759c3f4d6721b61afa03602e4804" +checksum = "21d33adf2d1eae36df9b6b033a1d549c7553ea607f4f9a81f75d51627725c354" dependencies = [ "cc", "object 0.37.3", @@ -22387,9 +22387,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-jit-icache-coherence" -version = "36.0.18" +version = "36.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2b47c37a6d026e2755fe2cf7ec5c1f4b0fba62e448ea910cdf6a07a4982c6a" +checksum = "5bde2fcf14cc76084242c6fe792e49778b5a9008c7df28ca4fe21c35c69d5f55" dependencies = [ "anyhow", "cfg-if", @@ -22399,24 +22399,24 @@ dependencies = [ [[package]] name = "wasmtime-internal-math" -version = "36.0.18" +version = "36.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b269f0ba44102c57fd5f94f1bf4395ccd4fc2b84698fc78f627a6f2a47131a62" +checksum = "4bd982249e933e730859f84c17254c7e95f38c3b0cdf81695bfc6a270e237852" dependencies = [ "libm", ] [[package]] name = "wasmtime-internal-slab" -version = "36.0.18" +version = "36.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e35ac9d503d503703cd24d532e53e2e93c86109b451df40a9801500045aee25" +checksum = "8280b359384cc412bddf0023193eeaffadcb0d7fbbf860f2b7e0580e8806a344" [[package]] name = "wasmtime-internal-unwinder" -version = "36.0.18" +version = "36.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83ce1d0930da44ed375701e6b4f187b46249f7efe2b1d8c1c77229a4adc82de9" +checksum = "bb43477c41769fec0d031d32cb3236195acfb1e6aa4efdf463e5fd001c23f6cc" dependencies = [ "anyhow", "cfg-if", @@ -22427,9 +22427,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-versioned-export-macros" -version = "36.0.18" +version = "36.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b6633be262400bd7abab4ff767a16e8b6a41ff117be54bf76dabecea9f96225" +checksum = "b33afac17819f2f767ea280b4b87a6c95458ef1ae43da410cfc5b26813ecb78f" dependencies = [ "proc-macro2", "quote", @@ -22438,9 +22438,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-winch" -version = "36.0.18" +version = "36.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "509d1e1cf8e3b3306f291407c92bca8381d9294f8c54ad74b692538f1f5b23c7" +checksum = "1757c8b0626385c93994c7a3c7a23ee5b469061465b601c1fdce71ff61b11c82" dependencies = [ "anyhow", "cranelift-codegen", @@ -22493,14 +22493,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e" dependencies = [ - "webpki-root-certs 1.0.9", + "webpki-root-certs 1.0.10", ] [[package]] name = "webpki-root-certs" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" +checksum = "58ec80e73fd8baf57bf0d9615554c75348bda990d6d5bbeab964dd068319fecb" dependencies = [ "rustls-pki-types", ] @@ -22670,7 +22670,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -22681,9 +22681,9 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "winch-codegen" -version = "36.0.18" +version = "36.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e470d7e674c41ad32dddc02d8d8a94983e0a4c516028bda60747e6dd862a567" +checksum = "69940a29fc6b01b0e6b141912b700c17eaf956d58d63bc0c6e733435caf020e4" dependencies = [ "anyhow", "cranelift-assembler-x64", diff --git a/deny.toml b/deny.toml index 83343235..0085fe60 100644 --- a/deny.toml +++ b/deny.toml @@ -18,6 +18,11 @@ ignore = [ # tracing-subscriber 0.2.25 — pinned by ark-relations (ZK crates), no upgrade path "RUSTSEC-2025-0055", + # webpki-roots 0.25.4 (distrusted emSign roots) — pinned by libp2p-websocket 0.44 + # through sc-network's libp2p 0.54; fixed only in 1.0.10. Those roots validate the + # TLS of `wss` p2p dials, and peers are still authenticated by Noise and peer id. + "RUSTSEC-2026-0381", + # Unmaintained — all transitive, no direct upgrade path "RUSTSEC-2025-0161", # libsecp256k1 0.7.2 (from frontier fp-evm/pallet-evm) "RUSTSEC-2026-0119", # hickory-proto 0.24.x (from libp2p-dns → sc-network)