Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
resolver = "3"
members = [
"admin-utils",
"adr36",
"auth-call",
"bitmap",
"borsh-utils",
Expand Down Expand Up @@ -45,6 +46,7 @@ rust-version = "1.86.0"

[workspace.dependencies]
defuse-admin-utils.path = "admin-utils"
defuse-adr36.path = "adr36"
defuse-auth-call.path = "auth-call"
defuse-bitmap.path = "bitmap"
defuse-borsh-utils.path = "borsh-utils"
Expand Down
17 changes: 17 additions & 0 deletions adr36/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "defuse-adr36"
edition.workspace = true
version.workspace = true
rust-version.workspace = true
repository.workspace = true

[dependencies]
defuse-crypto = { workspace = true, features = ["serde"] }
near-sdk.workspace = true

[dev-dependencies]
near-sdk = { workspace = true, features = ["unit-testing"] }
hex-literal.workspace = true

[lints]
workspace = true
153 changes: 153 additions & 0 deletions adr36/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
use defuse_crypto::serde::AsCurve;
use defuse_crypto::{Curve, Payload, Secp256k1, SignedPayload};
use near_sdk::base64::engine::{Engine, general_purpose::STANDARD};
use near_sdk::serde_json::json;
use near_sdk::{CryptoHash, env, near};

/// [ADR-36 Standard reference](https://github.com/cosmos/cosmos-sdk/blob/main/docs/architecture/adr-036-arbitrary-signature.md)
/// [Usage docs](https://docs.keplr.app/api/guide/sign-arbitrary#adr-36-signing-with-signamino)
#[near(serializers = [json])]
#[serde(rename_all = "snake_case")]
#[derive(Debug, Clone)]
pub struct Adr36Payload {
pub message: String,
/// The Bech32 address of the account that will sign the message.
/// Example: `cosmos1skjwj5whet0lpe65qaq4rpq03hjxlwd5c9m9s6`, where:
/// * `cosmos`: network prefix
/// * `1`: separator
/// * remainder: data + checksum
pub signer: String,
}

impl Adr36Payload {
#[inline]
pub const fn new(message: String, signer: String) -> Self {
Self { message, signer }
}

/// [Implementation reference](https://github.com/chainapsis/keplr-wallet/blob/59b2e18122dc2ec3b12d3005fec709e4bcc885f8/packages/cosmos/src/adr-36/amino.ts#L88)
#[inline]
pub fn prehash(&self) -> Vec<u8> {
let json = json!({
"account_number": "0",
"chain_id": "",
"fee": {
"amount": [],
"gas": "0",
},
"memo": "",
"msgs": [
{
"type": "sign/MsgSignData",
"value": {
"data": STANDARD.encode(self.message.as_bytes()),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to docs:

It's applications developers decision how Data should be treated, by treated we mean the serialization and deserialization process and the Object Data should represent.

Since we already represent self.message as String, does it make sense to avoid base64 encoding here and embed message as-is?

Copy link
Author

@kuksag kuksag Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. I'm gonna say I've overlooked that, because I was referencing implementations of the most used wallets on Cosmos SDK, and they wrap a message into base64:

To comply with the standard and to have something working in practise, we can verify the signature against both b64(message) (as of right now) and plaindata.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, ok, if that's a convention across ecosystem - I'm ok with leaving it in current state, i.e. always encode as base64

"signer": self.signer,
},
},
],
"sequence": "0",
});
json.to_string().into_bytes()
}
}

impl Payload for Adr36Payload {
#[inline]
fn hash(&self) -> CryptoHash {
env::sha256_array(self.prehash())
}
}

#[near(serializers = [json])]
#[derive(Debug, Clone)]
pub struct SignedAdr36Payload {
pub payload: Adr36Payload,

#[serde_as(as = "AsCurve<Secp256k1>")]
pub signature: <Secp256k1 as Curve>::Signature,
}

impl Payload for SignedAdr36Payload {
#[inline]
fn hash(&self) -> CryptoHash {
self.payload.hash()
}
}

impl SignedPayload for SignedAdr36Payload {
type PublicKey = <Secp256k1 as Curve>::PublicKey;

#[inline]
fn verify(&self) -> Option<Self::PublicKey> {
Secp256k1::verify(&self.signature, &self.payload.hash(), &())
}
}

#[cfg(test)]
mod tests {
use crate::{Adr36Payload, SignedAdr36Payload};
use defuse_crypto::{Payload, SignedPayload};
use hex_literal::hex;
use near_sdk::CryptoHash;

const fn fix_v_in_signature(mut sig: [u8; 65]) -> [u8; 65] {
if *sig.last().unwrap() >= 27 {
// Ethereum only uses uncompressed keys, with corresponding value v=27/28
// https://bitcoin.stackexchange.com/a/38909/58790
*sig.last_mut().unwrap() -= 27;
}
sig
}

/// golang cosmos-sdk [repro](https://gist.github.com/kuksag/eeb8ef3a77e6751d53db006b206925ab)
const REFERENCE_MESSAGE: &str = "Hello, ADR-036!";
const REFERENCE_SECP256K1_SIGNER: &str = "cosmos1mnyn7x24xj6vraxeeq56dfkxa009tvhgknhm04";
const REFERENCE_SHA256_HASH_MESSAGE_HEX: CryptoHash =
hex!("5ac8daed449a016684fd64bade7510b75ccd7c6eefa31b60a10eb577b37575e3");
const REFERENCE_SIGNATURE: [u8; 65] = hex!(
"043485aac9cd7de64da9548b72635c061e07b20063488c0d3affc3c843b33c0458f799ac6592b6260c5ce326be4996d95ee8cfbea4ae76b820f2a7a01ad3a5cc1b"
);
const WRONG_REFERENCE_SIGNATURE: [u8; 65] = hex!(
"c6ada709bab5a03bdbeb7e53e54ff77afbfec9e4f7b3d1b588e24f09d6f5dc305fa7cdb36c78d9f0c31859879eb930d28c890bcf9e27944e7e8808b1a53c09661c"
);
const REFERENCE_PUBKEY: [u8; 64] = hex!(
"4646ae5047316b4230d0086c8acec687f00b1cd9d1dc634f6cb358ac0a9a8ffffe77b4dd0a4bfb95851f3b7355c781dd60f8418fc8a65d14907aff47c903a559"
);

#[test]
fn test_expected_sha256_hash() {
let payload = Adr36Payload::new(
REFERENCE_MESSAGE.to_string(),
REFERENCE_SECP256K1_SIGNER.to_string(),
);
assert_eq!(payload.hash(), REFERENCE_SHA256_HASH_MESSAGE_HEX);
}

#[test]
fn test_reference_signature_verification_works() {
let payload = Adr36Payload::new(
REFERENCE_MESSAGE.to_string(),
REFERENCE_SECP256K1_SIGNER.to_string(),
);
let signature = fix_v_in_signature(REFERENCE_SIGNATURE);

assert_eq!(
SignedAdr36Payload { payload, signature }.verify(),
Some(REFERENCE_PUBKEY)
);
}

#[test]
fn test_reference_signature_verification_fails() {
let payload = Adr36Payload::new(
REFERENCE_MESSAGE.to_string(),
REFERENCE_SECP256K1_SIGNER.to_string(),
);
let signature = fix_v_in_signature(WRONG_REFERENCE_SIGNATURE);

assert_ne!(
SignedAdr36Payload { payload, signature }.verify(),
Some(REFERENCE_PUBKEY)
);
}
}
1 change: 1 addition & 0 deletions core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ repository.workspace = true

[dependencies]
defuse-auth-call.workspace = true
defuse-adr36.workspace = true
defuse-bitmap.workspace = true
defuse-crypto = { workspace = true, features = ["serde"] }
defuse-deadline.workspace = true
Expand Down
26 changes: 26 additions & 0 deletions core/src/payload/adr36.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
use crate::payload::{DefusePayload, ExtractDefusePayload};
use defuse_adr36::{Adr36Payload, SignedAdr36Payload};
use near_sdk::{serde::de::DeserializeOwned, serde_json};

impl<T> ExtractDefusePayload<T> for SignedAdr36Payload
where
T: DeserializeOwned,
{
type Error = serde_json::Error;

#[inline]
fn extract_defuse_payload(self) -> Result<DefusePayload<T>, Self::Error> {
self.payload.extract_defuse_payload()
}
}

impl<T> ExtractDefusePayload<T> for Adr36Payload
where
T: DeserializeOwned,
{
type Error = serde_json::Error;

fn extract_defuse_payload(self) -> Result<DefusePayload<T>, Self::Error> {
serde_json::from_str(&self.message)
}
}
1 change: 1 addition & 0 deletions core/src/payload/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod adr36;
pub mod erc191;
pub mod multi;
pub mod nep413;
Expand Down
18 changes: 13 additions & 5 deletions core/src/payload/multi.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
use super::{
DefusePayload, ExtractDefusePayload, raw::SignedRawEd25519Payload,
webauthn::SignedWebAuthnPayload,
};
use defuse_adr36::SignedAdr36Payload;
use defuse_crypto::{Payload, PublicKey, SignedPayload};
use defuse_erc191::SignedErc191Payload;
use defuse_nep413::SignedNep413Payload;
Expand All @@ -7,11 +12,6 @@ use defuse_ton_connect::SignedTonConnectPayload;
use derive_more::derive::From;
use near_sdk::{CryptoHash, near, serde::de::DeserializeOwned, serde_json};

use super::{
DefusePayload, ExtractDefusePayload, raw::SignedRawEd25519Payload,
webauthn::SignedWebAuthnPayload,
};

#[near(serializers = [json])]
#[serde(tag = "standard", rename_all = "snake_case")]
#[derive(Debug, Clone, From)]
Expand Down Expand Up @@ -51,6 +51,11 @@ pub enum MultiPayload {
/// SEP-53: The standard for signing data off-chain for Stellar accounts.
/// See [SEP-53](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0053.md)
Sep53(SignedSep53Payload),

/// ADR-36: The standard for signing data off-chain for accounts on Cosmos SDK.
/// Standard reference: https://github.com/cosmos/cosmos-sdk/blob/main/docs/architecture/adr-036-arbitrary-signature.md
/// Keplr-Wallet docs (implementation reference): https://docs.keplr.app/api/guide/sign-arbitrary#adr-36-signing-with-signamino
Adr36(SignedAdr36Payload),
}

impl Payload for MultiPayload {
Expand All @@ -68,6 +73,7 @@ impl Payload for MultiPayload {
Self::WebAuthn(payload) => payload.hash(),
Self::TonConnect(payload) => payload.hash(),
Self::Sep53(payload) => payload.hash(),
Self::Adr36(payload) => payload.hash(),
}
}
}
Expand All @@ -85,6 +91,7 @@ impl SignedPayload for MultiPayload {
Self::WebAuthn(payload) => payload.verify(),
Self::TonConnect(payload) => payload.verify().map(PublicKey::Ed25519),
Self::Sep53(payload) => payload.verify().map(PublicKey::Ed25519),
Self::Adr36(payload) => payload.verify().map(PublicKey::Secp256k1),
}
}
}
Expand All @@ -105,6 +112,7 @@ where
Self::WebAuthn(payload) => payload.extract_defuse_payload(),
Self::TonConnect(payload) => payload.extract_defuse_payload(),
Self::Sep53(payload) => payload.extract_defuse_payload(),
Self::Adr36(payload) => payload.extract_defuse_payload(),
}
}
}
Expand Down
Loading