diff --git a/cold-wallet-app/lib/screens/sign_transaction_screen.dart b/cold-wallet-app/lib/screens/sign_transaction_screen.dart index 8d969691..1373acfc 100644 --- a/cold-wallet-app/lib/screens/sign_transaction_screen.dart +++ b/cold-wallet-app/lib/screens/sign_transaction_screen.dart @@ -58,11 +58,20 @@ class _SignTransactionScreenState extends ConsumerState { }); try { + final parsed = _parsed; + if (parsed == null) { + setState(() { + _signing = false; + _error = 'Cannot sign an undecoded payload.'; + }); + return; + } // Returns signature ++ publicKey; the hot wallet splits it and rebuilds the // extrinsic via submitExtrinsicWithExternalSignature. final signed = signMessageWithPubkey( keypair: keypair, message: QuantusSigningPayload.signablePayload(widget.request.payload), + specVersion: parsed.extensions.specVersion, ); setState(() { _signing = false; diff --git a/mobile-app/lib/v2/screens/send/keystone_signature_scan_screen.dart b/mobile-app/lib/v2/screens/send/keystone_signature_scan_screen.dart index 15eeface..882beeac 100644 --- a/mobile-app/lib/v2/screens/send/keystone_signature_scan_screen.dart +++ b/mobile-app/lib/v2/screens/send/keystone_signature_scan_screen.dart @@ -77,7 +77,11 @@ class _KeystoneSignatureScanScreenState extends ConsumerState> _simulateSignature() async { final keypair = await widget.session.account.getKeypair(); - final signed = signMessageWithPubkey(keypair: keypair, message: widget.unsignedData.encodedPayloadToSign); + final signed = signMessageWithPubkey( + keypair: keypair, + message: widget.unsignedData.encodedPayloadToSign, + specVersion: widget.unsignedData.payloadToSign.specVersion, + ); return encodeUr(data: signed); } diff --git a/quantus_sdk/lib/src/extensions/keypair_extensions.dart b/quantus_sdk/lib/src/extensions/keypair_extensions.dart index 2b071eb5..235ad75d 100644 --- a/quantus_sdk/lib/src/extensions/keypair_extensions.dart +++ b/quantus_sdk/lib/src/extensions/keypair_extensions.dart @@ -7,8 +7,9 @@ extension KeypairExtensions on crypto.Keypair { String get ss58Address => crypto.toAccountId(obj: this); Uint8List get addressBytes => Address.decode(ss58Address).pubkey; - /// Hedged (randomized) ML-DSA signature. Fresh entropy is generated per call. - Uint8List sign(List message) => crypto.signMessage(keypair: this, message: message, entropy: _hedgeEntropy()); + /// Hedged ML-DSA signature. [specVersion] 148+ uses QUANTUS_EXTRINSIC; earlier specs use none. + Uint8List sign(List message, {required int specVersion}) => + crypto.signMessage(keypair: this, message: message, entropy: _hedgeEntropy(), specVersion: specVersion); } crypto.U8Array32 _hedgeEntropy() { diff --git a/quantus_sdk/lib/src/rust/api/crypto.dart b/quantus_sdk/lib/src/rust/api/crypto.dart index f1a42db2..48d3482d 100644 --- a/quantus_sdk/lib/src/rust/api/crypto.dart +++ b/quantus_sdk/lib/src/rust/api/crypto.dart @@ -38,14 +38,43 @@ String firstHashToAddress({required String firstHashHex}) => Keypair generateKeypairFromSeed({required List seed}) => RustLib.instance.api.crateApiCryptoGenerateKeypairFromSeed(seed: seed); -Uint8List signMessage({required Keypair keypair, required List message, U8Array32? entropy}) => - RustLib.instance.api.crateApiCryptoSignMessage(keypair: keypair, message: message, entropy: entropy); - -Uint8List signMessageWithPubkey({required Keypair keypair, required List message, U8Array32? entropy}) => - RustLib.instance.api.crateApiCryptoSignMessageWithPubkey(keypair: keypair, message: message, entropy: entropy); - -bool verifyMessage({required Keypair keypair, required List message, required List signature}) => - RustLib.instance.api.crateApiCryptoVerifyMessage(keypair: keypair, message: message, signature: signature); +/// Signs `message`. Spec 148+ uses [`signing_context::EXTRINSIC`]; earlier specs use none. +Uint8List signMessage({ + required Keypair keypair, + required List message, + U8Array32? entropy, + required int specVersion, +}) => RustLib.instance.api.crateApiCryptoSignMessage( + keypair: keypair, + message: message, + entropy: entropy, + specVersion: specVersion, +); + +Uint8List signMessageWithPubkey({ + required Keypair keypair, + required List message, + U8Array32? entropy, + required int specVersion, +}) => RustLib.instance.api.crateApiCryptoSignMessageWithPubkey( + keypair: keypair, + message: message, + entropy: entropy, + specVersion: specVersion, +); + +/// Verifies under the same context [`sign_message`] would use for `spec_version`. +bool verifyMessage({ + required Keypair keypair, + required List message, + required List signature, + required int specVersion, +}) => RustLib.instance.api.crateApiCryptoVerifyMessage( + keypair: keypair, + message: message, + signature: signature, + specVersion: specVersion, +); Keypair crystalAlice() => RustLib.instance.api.crateApiCryptoCrystalAlice(); diff --git a/quantus_sdk/lib/src/rust/frb_generated.dart b/quantus_sdk/lib/src/rust/frb_generated.dart index 06cdd153..05c6a004 100644 --- a/quantus_sdk/lib/src/rust/frb_generated.dart +++ b/quantus_sdk/lib/src/rust/frb_generated.dart @@ -144,12 +144,18 @@ abstract class RustLibApi extends BaseApi { void crateApiCryptoSetDefaultSs58Prefix({required int prefix}); - Uint8List crateApiCryptoSignMessage({required Keypair keypair, required List message, U8Array32? entropy}); + Uint8List crateApiCryptoSignMessage({ + required Keypair keypair, + required List message, + U8Array32? entropy, + required int specVersion, + }); Uint8List crateApiCryptoSignMessageWithPubkey({ required Keypair keypair, required List message, U8Array32? entropy, + required int specVersion, }); BigInt crateApiCryptoSignatureBytes(); @@ -162,6 +168,7 @@ abstract class RustLibApi extends BaseApi { required Keypair keypair, required List message, required List signature, + required int specVersion, }); int crateApiWormholeWormholeComputeOutputAmount({required int inputAmount, required int feeBps}); @@ -772,7 +779,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { const TaskConstMeta(debugName: 'set_default_ss58_prefix', argNames: ['prefix']); @override - Uint8List crateApiCryptoSignMessage({required Keypair keypair, required List message, U8Array32? entropy}) { + Uint8List crateApiCryptoSignMessage({ + required Keypair keypair, + required List message, + U8Array32? entropy, + required int specVersion, + }) { return handler.executeSync( SyncTask( callFfi: () { @@ -780,24 +792,26 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_box_autoadd_keypair(keypair, serializer); sse_encode_list_prim_u_8_loose(message, serializer); sse_encode_opt_u_8_array_32(entropy, serializer); + sse_encode_u_32(specVersion, serializer); return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 29)!; }, codec: SseCodec(decodeSuccessData: sse_decode_list_prim_u_8_strict, decodeErrorData: null), constMeta: kCrateApiCryptoSignMessageConstMeta, - argValues: [keypair, message, entropy], + argValues: [keypair, message, entropy, specVersion], apiImpl: this, ), ); } TaskConstMeta get kCrateApiCryptoSignMessageConstMeta => - const TaskConstMeta(debugName: 'sign_message', argNames: ['keypair', 'message', 'entropy']); + const TaskConstMeta(debugName: 'sign_message', argNames: ['keypair', 'message', 'entropy', 'specVersion']); @override Uint8List crateApiCryptoSignMessageWithPubkey({ required Keypair keypair, required List message, U8Array32? entropy, + required int specVersion, }) { return handler.executeSync( SyncTask( @@ -806,18 +820,21 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_box_autoadd_keypair(keypair, serializer); sse_encode_list_prim_u_8_loose(message, serializer); sse_encode_opt_u_8_array_32(entropy, serializer); + sse_encode_u_32(specVersion, serializer); return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 30)!; }, codec: SseCodec(decodeSuccessData: sse_decode_list_prim_u_8_strict, decodeErrorData: null), constMeta: kCrateApiCryptoSignMessageWithPubkeyConstMeta, - argValues: [keypair, message, entropy], + argValues: [keypair, message, entropy, specVersion], apiImpl: this, ), ); } - TaskConstMeta get kCrateApiCryptoSignMessageWithPubkeyConstMeta => - const TaskConstMeta(debugName: 'sign_message_with_pubkey', argNames: ['keypair', 'message', 'entropy']); + TaskConstMeta get kCrateApiCryptoSignMessageWithPubkeyConstMeta => const TaskConstMeta( + debugName: 'sign_message_with_pubkey', + argNames: ['keypair', 'message', 'entropy', 'specVersion'], + ); @override BigInt crateApiCryptoSignatureBytes() { @@ -883,6 +900,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { required Keypair keypair, required List message, required List signature, + required int specVersion, }) { return handler.executeSync( SyncTask( @@ -891,18 +909,19 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_box_autoadd_keypair(keypair, serializer); sse_encode_list_prim_u_8_loose(message, serializer); sse_encode_list_prim_u_8_loose(signature, serializer); + sse_encode_u_32(specVersion, serializer); return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 34)!; }, codec: SseCodec(decodeSuccessData: sse_decode_bool, decodeErrorData: null), constMeta: kCrateApiCryptoVerifyMessageConstMeta, - argValues: [keypair, message, signature], + argValues: [keypair, message, signature, specVersion], apiImpl: this, ), ); } TaskConstMeta get kCrateApiCryptoVerifyMessageConstMeta => - const TaskConstMeta(debugName: 'verify_message', argNames: ['keypair', 'message', 'signature']); + const TaskConstMeta(debugName: 'verify_message', argNames: ['keypair', 'message', 'signature', 'specVersion']); @override int crateApiWormholeWormholeComputeOutputAmount({required int inputAmount, required int feeBps}) { diff --git a/quantus_sdk/lib/src/services/substrate_service.dart b/quantus_sdk/lib/src/services/substrate_service.dart index 8fa43f78..d862dc9f 100644 --- a/quantus_sdk/lib/src/services/substrate_service.dart +++ b/quantus_sdk/lib/src/services/substrate_service.dart @@ -329,7 +329,10 @@ class SubstrateService { extrinsic = _encodeSignedExtrinsic( signer: Uint8List.fromList(senderWallet.addressBytes), method: encodedCall, - signature: _combineSignatureAndPubkey(senderWallet.sign(payload), senderWallet.publicKey), + signature: _combineSignatureAndPubkey( + senderWallet.sign(payload, specVersion: ctx.runtimeVersion.specVersion), + senderWallet.publicKey, + ), blockNumber: ctx.blockNumber, nonce: ctx.nonce, ); diff --git a/quantus_sdk/rust/Cargo.lock b/quantus_sdk/rust/Cargo.lock index cd1b9c75..7f893213 100644 --- a/quantus_sdk/rust/Cargo.lock +++ b/quantus_sdk/rust/Cargo.lock @@ -2834,18 +2834,18 @@ checksum = "5872607e25ea4ee5fb37e64bf1462168e1a36a4e719cdc8a105533c708253918" [[package]] name = "qp-rusty-crystals-dilithium" -version = "4.1.0" +version = "4.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2facf5349ce149731e6d21dc338f847c78e4a3a65e92e086e2cee244e7cf0457" +checksum = "789877c169226a35d2ea686bbd9d506becc693f7bb0ee91acc03f74491e80c0f" dependencies = [ "zeroize", ] [[package]] name = "qp-rusty-crystals-hdwallet" -version = "4.1.0" +version = "4.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40119eb0c9a085722330c15ce17b71ca3aaecf5fa5a44891a47c1c9ec2c3109c" +checksum = "51ec6c3db4055c217a503c45d0c101cf3c10d4fc1e562f0588aa55dda4f60a4d" dependencies = [ "bip39", "getrandom 0.2.17", @@ -2863,9 +2863,9 @@ dependencies = [ [[package]] name = "qp-wormhole-aggregator" -version = "4.2.0" +version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d36c1e817cba09384f092f6f1cda8016cc39637c878c1b80788d8c7d5bd75b40" +checksum = "6604a9d9248e3270d330155fc27a4a84b382849b2aecb1f92a110511314fe07b" dependencies = [ "anyhow", "hex", @@ -2881,9 +2881,9 @@ dependencies = [ [[package]] name = "qp-wormhole-circuit" -version = "4.2.0" +version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418648e0835bb47c7e6fa908bce246e9e9fce042f857b15e652b2591ff869e76" +checksum = "55167daf965a3616b74184171148b94c64b3a48771d6433a1a43377a6e46c6e8" dependencies = [ "anyhow", "hex", @@ -2895,9 +2895,9 @@ dependencies = [ [[package]] name = "qp-wormhole-circuit-builder" -version = "4.2.0" +version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0716238af4fa0c0efdf06f30ccbff90534c0d9e700c3d90aa9d3be79fb10d2fe" +checksum = "f4fe35e0d8ad019e37e447e092da348fe4c52ae691d84dfd919fc9b58e7effe5" dependencies = [ "anyhow", "clap", @@ -2910,18 +2910,18 @@ dependencies = [ [[package]] name = "qp-wormhole-inputs" -version = "4.2.0" +version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9385fee220d58083a3c833ff9ee87c2346f77c7e869f35bbe12ecd8dad3d076f" +checksum = "9c07863a2211a17b46319289c5ef22e5670a3fb8653386bb366c418193254793" dependencies = [ "anyhow", ] [[package]] name = "qp-wormhole-prover" -version = "4.2.0" +version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4c90421378b57eeb08f3e60f426aa861b2ad741942dcd795c43b5665171026a" +checksum = "7203e454113812e1ca85aee7687dc5f270f7c297d113a647a3a00b6d43c5a4f6" dependencies = [ "anyhow", "qp-plonky2", @@ -2931,9 +2931,9 @@ dependencies = [ [[package]] name = "qp-zk-circuits-common" -version = "4.2.0" +version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78477b533829319a41b946bf8bdc1bc778f9ed1806f156eb584aa3a6ff764b3b" +checksum = "dec45510701f160fdb730bdb622d6cdd0a62547e607c9b1ecb19558202797f81" dependencies = [ "anyhow", "qp-plonky2", @@ -3966,7 +3966,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.3", "once_cell", "rustix 1.1.4", "windows-sys 0.61.2", diff --git a/quantus_sdk/rust/Cargo.toml b/quantus_sdk/rust/Cargo.toml index 0446228d..7518aa82 100644 --- a/quantus_sdk/rust/Cargo.toml +++ b/quantus_sdk/rust/Cargo.toml @@ -9,8 +9,8 @@ crate-type = ["cdylib", "staticlib", "rlib"] [dependencies] # NOTE: Quantus chain dependencies. qp-poseidon-core = "3.1.0" -qp-rusty-crystals-dilithium = { version = "4.1.0", default-features = false } -qp-rusty-crystals-hdwallet = { version = "4.1.0" } +qp-rusty-crystals-dilithium = { version = "4.1.1", default-features = false } +qp-rusty-crystals-hdwallet = { version = "4.1.1" } flutter_rust_bridge = "=2.12.0" hex = "0.4.3" @@ -23,12 +23,12 @@ quantus_ur = { git = "https://github.com/Quantus-Network/quantus_ur.git", tag = anyhow = "1.0" blake3 = "1.8" qp-plonky2 = { version = "1.5.5", default-features = false, features = ["rand", "std"] } -qp-wormhole-circuit = { version = "4.2.0", default-features = false, features = ["std"] } -qp-wormhole-prover = { version = "4.2.0", default-features = false, features = ["std"] } -qp-wormhole-aggregator = { version = "4.2.0", default-features = false, features = ["std"] } -qp-wormhole-inputs = { version = "4.2.0", default-features = false, features = ["std"] } -qp-zk-circuits-common = { version = "4.2.0", default-features = false, features = ["std"] } -qp-wormhole-circuit-builder = { version = "4.2.0" } +qp-wormhole-circuit = { version = "4.3.0", default-features = false, features = ["std"] } +qp-wormhole-prover = { version = "4.3.0", default-features = false, features = ["std"] } +qp-wormhole-aggregator = { version = "4.3.0", default-features = false, features = ["std"] } +qp-wormhole-inputs = { version = "4.3.0", default-features = false, features = ["std"] } +qp-zk-circuits-common = { version = "4.3.0", default-features = false, features = ["std"] } +qp-wormhole-circuit-builder = { version = "4.3.0" } [dev-dependencies] tempfile = "3" diff --git a/quantus_sdk/rust/src/api/crypto.rs b/quantus_sdk/rust/src/api/crypto.rs index 80c2edba..1dd8491b 100644 --- a/quantus_sdk/rust/src/api/crypto.rs +++ b/quantus_sdk/rust/src/api/crypto.rs @@ -1,3 +1,4 @@ +use crate::signing_context; use qp_poseidon_core::{hash_bytes, hash_to_bytes, serialization::bytes_to_digest}; use qp_rusty_crystals_dilithium::ml_dsa_87; pub use qp_rusty_crystals_hdwallet::HDLatticeError; @@ -133,13 +134,23 @@ pub fn generate_keypair_from_seed(seed: Vec) -> Keypair { Keypair::from_ml_dsa(ml_dsa_keypair) } +/// Signs `message`. Spec 148+ uses [`signing_context::EXTRINSIC`]; earlier specs use none. #[flutter_rust_bridge::frb(sync)] -pub fn sign_message(keypair: &Keypair, message: &[u8], entropy: Option<[u8; 32]>) -> Vec { +pub fn sign_message( + keypair: &Keypair, + message: &[u8], + entropy: Option<[u8; 32]>, + spec_version: u32, +) -> Vec { let ml_dsa_keypair = keypair.to_ml_dsa(); let mut entropy = entropy; let hedge = entropy.as_mut().map(SensitiveBytes32::new); let signature = ml_dsa_keypair - .sign(message, None, hedge.as_ref()) + .sign( + message, + signing_context::context_for_spec(spec_version), + hedge.as_ref(), + ) .expect("Signing failed"); signature.to_vec() } @@ -149,18 +160,29 @@ pub fn sign_message_with_pubkey( keypair: &Keypair, message: &[u8], entropy: Option<[u8; 32]>, + spec_version: u32, ) -> Vec { - let signature = sign_message(keypair, message, entropy); + let signature = sign_message(keypair, message, entropy, spec_version); let mut result = Vec::with_capacity(signature.len() + keypair.public_key.len()); result.extend_from_slice(&signature); result.extend_from_slice(&keypair.public_key); result } +/// Verifies under the same context [`sign_message`] would use for `spec_version`. #[flutter_rust_bridge::frb(sync)] -pub fn verify_message(keypair: &Keypair, message: &[u8], signature: &[u8]) -> bool { +pub fn verify_message( + keypair: &Keypair, + message: &[u8], + signature: &[u8], + spec_version: u32, +) -> bool { let ml_dsa_keypair = keypair.to_ml_dsa(); - ml_dsa_keypair.verify(&message, &signature, None) + ml_dsa_keypair.verify( + &message, + &signature, + signing_context::context_for_spec(spec_version), + ) } #[flutter_rust_bridge::frb(sync)] @@ -203,32 +225,85 @@ pub fn init_app() { mod tests { use super::*; + const SPEC_WITH_CONTEXT: u32 = signing_context::EXTRINSIC_MIN_SPEC; + const SPEC_WITHOUT_CONTEXT: u32 = signing_context::EXTRINSIC_MIN_SPEC - 1; + #[test] fn test_sign_and_verify() { - // Test with a simple message let message = b"Hello, World!"; let keypair = crystal_alice(); - - // Sign the message - let signature = sign_message(&keypair, message, None); - - // Verify the signature - let is_valid = verify_message(&keypair, message, &signature); + let signature = sign_message(&keypair, message, None, SPEC_WITH_CONTEXT); + let is_valid = verify_message(&keypair, message, &signature, SPEC_WITH_CONTEXT); assert!(is_valid, "Signature verification failed"); } #[test] - fn test_sign_and_verify_with_different_keypair() { - // Test with a simple message + fn test_context_for_spec() { + assert_eq!(signing_context::context_for_spec(147), None); + assert_eq!( + signing_context::context_for_spec(148), + Some(signing_context::EXTRINSIC) + ); + assert_eq!( + signing_context::context_for_spec(149), + Some(signing_context::EXTRINSIC) + ); + } + + #[test] + fn test_signature_is_bound_to_the_extrinsic_context() { let message = b"Hello, World!"; let keypair = crystal_alice(); + let signature = sign_message(&keypair, message, None, SPEC_WITH_CONTEXT); + let public = ml_dsa_87::PublicKey::from_bytes(&keypair.public_key).unwrap(); + + assert_eq!(signing_context::EXTRINSIC, b"QUANTUS_EXTRINSIC"); + assert!(public.verify(message, &signature, Some(signing_context::EXTRINSIC))); + assert!(!public.verify(message, &signature, None)); + assert!(verify_message( + &keypair, + message, + &signature, + SPEC_WITH_CONTEXT + )); + assert!(!verify_message( + &keypair, + message, + &signature, + SPEC_WITHOUT_CONTEXT + )); + } - // Sign the message - let signature = sign_message(&keypair, message, None); + #[test] + fn test_pre_148_signs_with_empty_context() { + let message = b"Hello, World!"; + let keypair = crystal_alice(); + let signature = sign_message(&keypair, message, None, SPEC_WITHOUT_CONTEXT); + let public = ml_dsa_87::PublicKey::from_bytes(&keypair.public_key).unwrap(); + + assert!(public.verify(message, &signature, None)); + assert!(!public.verify(message, &signature, Some(signing_context::EXTRINSIC))); + assert!(verify_message( + &keypair, + message, + &signature, + SPEC_WITHOUT_CONTEXT + )); + assert!(!verify_message( + &keypair, + message, + &signature, + SPEC_WITH_CONTEXT + )); + } - // Try to verify with a different keypair + #[test] + fn test_sign_and_verify_with_different_keypair() { + let message = b"Hello, World!"; + let keypair = crystal_alice(); + let signature = sign_message(&keypair, message, None, SPEC_WITH_CONTEXT); let different_keypair = crystal_bob(); - let is_valid = verify_message(&different_keypair, message, &signature); + let is_valid = verify_message(&different_keypair, message, &signature, SPEC_WITH_CONTEXT); assert!( !is_valid, "Signature should not be valid with different keypair" @@ -237,29 +312,19 @@ mod tests { #[test] fn test_sign_and_verify_with_empty_message() { - // Test with an empty message let message = b""; let keypair = crystal_alice(); - - // Sign the message - let signature = sign_message(&keypair, message, None); - - // Verify the signature - let is_valid = verify_message(&keypair, message, &signature); + let signature = sign_message(&keypair, message, None, SPEC_WITH_CONTEXT); + let is_valid = verify_message(&keypair, message, &signature, SPEC_WITH_CONTEXT); assert!(is_valid, "Signature verification failed for empty message"); } #[test] fn test_sign_and_verify_with_long_message() { - // Test with a longer message let message = b"This is a longer message that should also work correctly with our signing and verification process."; let keypair = crystal_alice(); - - // Sign the message - let signature = sign_message(&keypair, message, None); - - // Verify the signature - let is_valid = verify_message(&keypair, message, &signature); + let signature = sign_message(&keypair, message, None, SPEC_WITH_CONTEXT); + let is_valid = verify_message(&keypair, message, &signature, SPEC_WITH_CONTEXT); assert!(is_valid, "Signature verification failed for long message"); } } diff --git a/quantus_sdk/rust/src/api/wormhole.rs b/quantus_sdk/rust/src/api/wormhole.rs index b1a4c08c..ebfe8d04 100644 --- a/quantus_sdk/rust/src/api/wormhole.rs +++ b/quantus_sdk/rust/src/api/wormhole.rs @@ -10,7 +10,7 @@ use qp_zk_circuits_common::{ }; use std::path::{Path, PathBuf}; -pub const ZK_CIRCUITS_VERSION: &str = "4.2.0"; +pub const ZK_CIRCUITS_VERSION: &str = "4.3.0"; fn versioned_bins_dir(base: &Path) -> PathBuf { base.join(format!("v{}", ZK_CIRCUITS_VERSION)) @@ -390,7 +390,6 @@ pub fn generate_proof( state_root: vec_to_digest(&input.state_root, "state_root")?, extrinsics_root: vec_to_digest(&input.extrinsics_root, "extrinsics_root")?, digest: digest_padded, - input_amount: input.input_amount, zk_tree_root: vec_to_32(&input.zk_tree_root, "zk_tree_root")?, zk_merkle_siblings, zk_merkle_positions: input.positions.clone(), @@ -406,6 +405,7 @@ pub fn generate_proof( exit_account_2: vec_to_digest(&input.exit_account_2, "exit_account_2")?, block_hash: vec_to_digest(&input.block_hash, "block_hash")?, block_number: input.block_number, + input_amount: input.input_amount, }; let circuit_inputs = CircuitInputs { public, private }; diff --git a/quantus_sdk/rust/src/frb_generated.rs b/quantus_sdk/rust/src/frb_generated.rs index 32073ca7..e6799d16 100644 --- a/quantus_sdk/rust/src/frb_generated.rs +++ b/quantus_sdk/rust/src/frb_generated.rs @@ -946,12 +946,14 @@ fn wire__crate__api__crypto__sign_message_impl( let api_keypair = ::sse_decode(&mut deserializer); let api_message = >::sse_decode(&mut deserializer); let api_entropy = >::sse_decode(&mut deserializer); + let api_spec_version = ::sse_decode(&mut deserializer); deserializer.end(); transform_result_sse::<_, ()>((move || { let output_ok = Result::<_, ()>::Ok(crate::api::crypto::sign_message( &api_keypair, &api_message, api_entropy, + api_spec_version, ))?; Ok(output_ok) })()) @@ -982,12 +984,14 @@ fn wire__crate__api__crypto__sign_message_with_pubkey_impl( let api_keypair = ::sse_decode(&mut deserializer); let api_message = >::sse_decode(&mut deserializer); let api_entropy = >::sse_decode(&mut deserializer); + let api_spec_version = ::sse_decode(&mut deserializer); deserializer.end(); transform_result_sse::<_, ()>((move || { let output_ok = Result::<_, ()>::Ok(crate::api::crypto::sign_message_with_pubkey( &api_keypair, &api_message, api_entropy, + api_spec_version, ))?; Ok(output_ok) })()) @@ -1107,12 +1111,14 @@ fn wire__crate__api__crypto__verify_message_impl( let api_keypair = ::sse_decode(&mut deserializer); let api_message = >::sse_decode(&mut deserializer); let api_signature = >::sse_decode(&mut deserializer); + let api_spec_version = ::sse_decode(&mut deserializer); deserializer.end(); transform_result_sse::<_, ()>((move || { let output_ok = Result::<_, ()>::Ok(crate::api::crypto::verify_message( &api_keypair, &api_message, &api_signature, + api_spec_version, ))?; Ok(output_ok) })()) diff --git a/quantus_sdk/rust/src/lib.rs b/quantus_sdk/rust/src/lib.rs index cbb071f8..fd84100b 100644 --- a/quantus_sdk/rust/src/lib.rs +++ b/quantus_sdk/rust/src/lib.rs @@ -1,2 +1,3 @@ pub mod api; mod frb_generated; +pub mod signing_context; diff --git a/quantus_sdk/rust/src/signing_context.rs b/quantus_sdk/rust/src/signing_context.rs new file mode 100644 index 00000000..39dfd8ae --- /dev/null +++ b/quantus_sdk/rust/src/signing_context.rs @@ -0,0 +1,20 @@ +//! FIPS 204 ML-DSA context strings for domain separation. +//! +//! `sign` / `verify` hash the context into the signature, so a signature made +//! under one string will not verify under another, even over the same message +//! and key. These must match the chain's +//! `primitives/dilithium-crypto/src/signing_context.rs`. +//! +//! Each string is at most 255 bytes, as required by FIPS 204. + +/// On-chain extrinsic signatures. +pub const EXTRINSIC: &[u8] = b"QUANTUS_EXTRINSIC"; + +/// First spec that verifies extrinsics under [`EXTRINSIC`]. Earlier specs use the empty context. +pub const EXTRINSIC_MIN_SPEC: u32 = 148; + +const _: () = assert!(EXTRINSIC.len() <= 255); + +pub fn context_for_spec(spec_version: u32) -> Option<&'static [u8]> { + (spec_version >= EXTRINSIC_MIN_SPEC).then_some(EXTRINSIC) +} diff --git a/quantus_sdk/test/generate_keys_test.dart b/quantus_sdk/test/generate_keys_test.dart index cc270eff..3a7d1dec 100644 --- a/quantus_sdk/test/generate_keys_test.dart +++ b/quantus_sdk/test/generate_keys_test.dart @@ -40,17 +40,32 @@ void main() { // Test signing and verification final message = [1, 2, 3, 4, 5]; - final signature = keypair.sign(message); + final signature = keypair.sign(message, specVersion: 148); // Verify signature expect(signature, isNotEmpty); // Verify the signature is valid - final isValid = verifyMessage(keypair: keypair, message: message, signature: signature); + final isValid = verifyMessage(keypair: keypair, message: message, signature: signature, specVersion: 148); expect(isValid, isTrue); }); + test('spec 148 binds QUANTUS_EXTRINSIC; earlier specs use the empty context', () { + const testMnemonic = + 'situate more drip void arrest just action prepare engine undo honey delay sponsor come achieve symptom crumble solution glass garden fury valid garbage old'; + final keypair = generateKeypair(mnemonicStr: testMnemonic); + final message = [1, 2, 3, 4, 5]; + + final withContext = keypair.sign(message, specVersion: 148); + final withoutContext = keypair.sign(message, specVersion: 147); + + expect(verifyMessage(keypair: keypair, message: message, signature: withContext, specVersion: 148), isTrue); + expect(verifyMessage(keypair: keypair, message: message, signature: withContext, specVersion: 147), isFalse); + expect(verifyMessage(keypair: keypair, message: message, signature: withoutContext, specVersion: 147), isTrue); + expect(verifyMessage(keypair: keypair, message: message, signature: withoutContext, specVersion: 148), isFalse); + }); + test('should generate different keypairs for different mnemonics', () { // These are valid BIP39 mnemonics - DO NOT use for real wallets const mnemonic1 = @@ -122,8 +137,9 @@ void main() { const hexPayload = '0200007416854906f03a9dff66e3270a736c44e15970ac03a638471523a03069f276ca0700e876481755010000007400000002000000826beefbe2be72645ff376f18de745ac196dc77637436090de4174180706118e5a77ae1c95817ee664cf733fafa7baa8e6244b396a54e57a5bc414b24c52800600'; final payload = hex.decode(hexPayload); - final signature = keypair.sign(payload); - final isValid = verifyMessage(keypair: keypair, message: payload, signature: signature); + const specVersion = 116; + final signature = keypair.sign(payload, specVersion: specVersion); + final isValid = verifyMessage(keypair: keypair, message: payload, signature: signature, specVersion: specVersion); expect(isValid, true); print('signature: ${hex.encode(signature)}'); final hashedSignature = const Blake2bHasher(32).hash(signature); @@ -142,7 +158,8 @@ void main() { final signable = QuantusSigningPayload.signablePayload(payload); // Cold wallet (Keystone) signs the signable payload and emits signature ++ pubkey. - final signed = signMessageWithPubkey(keypair: keypair, message: signable); + const specVersion = 116; + final signed = signMessageWithPubkey(keypair: keypair, message: signable, specVersion: specVersion); // The signature QR is a multi-part animated UR (the "5 / 81 frames" case). final parts = encodeUr(data: signed); @@ -158,7 +175,10 @@ void main() { final publicKey = bytes.sublist(sigSize); expect(publicKey, equals(keypair.publicKey)); - expect(verifyMessage(keypair: keypair, message: signable, signature: signature), isTrue); + expect( + verifyMessage(keypair: keypair, message: signable, signature: signature, specVersion: specVersion), + isTrue, + ); }); }); }