Skip to content

Commit cefe27a

Browse files
authored
feat: cosmos ADR-36 and EIP-191 signatures can be verified (#256)
* refactor: arbitrary byte array refs can be used for signature verification * feat(keys): EIP-191 signatures can be validated * feat(serde): add bech32 encoding/decoding * feat(keys): cosmos ADR-36 signatures can be validated * build: update Cargo.lock * test(keys): incorporate EIP-191 and Cosmos ADR-36 signatures into unit tests * fix(keys): add missing PartialEq impl for new signing key variants * feat(keys): all CryptoAlgorithms variants can be retrieved * fix(keys): remove incorrect signer trait name * test(keys): add test for internal signature verification * refactor(keys): remove potentially ambiguous key conversion methods This can now occur, because different verifying keys are using the same backing type. * docs(keys): add documentation for cosmos ADR-36 part * refactor(keys): signing a message has now a Result return type * chore: incorporate changes in zkvm elf
1 parent 16de9a4 commit cefe27a

22 files changed

+554
-158
lines changed

Cargo.lock

Lines changed: 83 additions & 79 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ serde = { version = "1.0.151", features = ["derive"] }
5656
serde_json = "1.0.79"
5757
serde_bytes = "0.11.15"
5858
base64 = "0.22.0"
59+
bech32 = "0.11.0"
5960
bincode = "1.3.3"
6061
hex = "0.4.3"
6162

@@ -105,6 +106,12 @@ ed25519-consensus = "2.1.0"
105106
k256 = { version = "0.13.4", features = ["ecdsa", "serde"] }
106107
p256 = { version = "0.13.2", features = ["ecdsa", "serde"] }
107108

109+
# signatures
110+
alloy-primitives = { version = "0.8.21", default-features = false, features = [
111+
"k256",
112+
] }
113+
ripemd = "0.1.3"
114+
108115
# celestia
109116
celestia-rpc = "=0.9.0"
110117
celestia-types = "=0.10.0"

crates/common/src/builder.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,8 @@ where
105105
self.service_id.as_bytes(),
106106
&key.to_bytes(),
107107
]);
108-
let signature = service_signing_key.sign(hash);
108+
let signature =
109+
service_signing_key.sign(hash).map_err(|_| TransactionError::SigningFailed)?;
109110

110111
let operation = Operation::CreateAccount {
111112
id: self.id.clone(),

crates/common/src/test_transaction_builder.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ impl TestTransactionBuilder {
187187
// Simulate some external service signing account creation credentials
188188
let vk = signing_key.verifying_key();
189189
let hash = Digest::hash_items(&[id.as_bytes(), service_id.as_bytes(), &vk.to_bytes()]);
190-
let signature = service_signing_key.sign(hash);
190+
let signature = service_signing_key.sign(hash).unwrap();
191191

192192
let op = Operation::CreateAccount {
193193
id: id.to_string(),
@@ -341,7 +341,7 @@ impl TestTransactionBuilder {
341341
) -> UncommittedTransaction {
342342
let value_signature_bundle = SignatureBundle {
343343
verifying_key: value_signing_key.verifying_key(),
344-
signature: value_signing_key.sign(&value),
344+
signature: value_signing_key.sign(&value).unwrap(),
345345
};
346346
self.add_pre_signed_data(id, value, value_signature_bundle, signing_key)
347347
}
@@ -354,7 +354,7 @@ impl TestTransactionBuilder {
354354
) -> UncommittedTransaction {
355355
let value_signature_bundle = SignatureBundle {
356356
verifying_key: value_signing_key.verifying_key(),
357-
signature: value_signing_key.sign(&value),
357+
signature: value_signing_key.sign(&value).unwrap(),
358358
};
359359
self.add_pre_signed_data_verified_with_root(id, value, value_signature_bundle)
360360
}
@@ -386,7 +386,7 @@ impl TestTransactionBuilder {
386386
) -> UncommittedTransaction {
387387
let bundle = SignatureBundle {
388388
verifying_key: signing_key.verifying_key(),
389-
signature: signing_key.sign(&value),
389+
signature: signing_key.sign(&value).unwrap(),
390390
};
391391
self.add_data(id, value, bundle, signing_key)
392392
}
@@ -403,7 +403,7 @@ impl TestTransactionBuilder {
403403
let account_signing_key = account_signing_keys.first().unwrap();
404404
let bundle = SignatureBundle {
405405
verifying_key: account_signing_key.verifying_key(),
406-
signature: account_signing_key.sign(&value),
406+
signature: account_signing_key.sign(&value).unwrap(),
407407
};
408408

409409
self.add_data_verified_with_root(id, value, bundle)
@@ -486,7 +486,7 @@ impl TestTransactionBuilder {
486486
let account_signing_key = account_signing_keys.first().unwrap();
487487
let bundle = SignatureBundle {
488488
verifying_key: account_signing_key.verifying_key(),
489-
signature: account_signing_key.sign(&value),
489+
signature: account_signing_key.sign(&value).unwrap(),
490490
};
491491

492492
self.set_pre_signed_data(id, value, bundle, account_signing_key)
@@ -501,7 +501,7 @@ impl TestTransactionBuilder {
501501
) -> UncommittedTransaction {
502502
let value_signature_bundle = SignatureBundle {
503503
verifying_key: value_signing_key.verifying_key(),
504-
signature: value_signing_key.sign(&value),
504+
signature: value_signing_key.sign(&value).unwrap(),
505505
};
506506
self.set_pre_signed_data(id, value, value_signature_bundle, signing_key)
507507
}
@@ -514,7 +514,7 @@ impl TestTransactionBuilder {
514514
) -> UncommittedTransaction {
515515
let value_signature_bundle = SignatureBundle {
516516
verifying_key: value_signing_key.verifying_key(),
517-
signature: value_signing_key.sign(&value),
517+
signature: value_signing_key.sign(&value).unwrap(),
518518
};
519519
self.set_pre_signed_data_verified_with_root(id, value, value_signature_bundle)
520520
}

crates/common/src/transaction.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ impl UnsignedTransaction {
2424
/// Signs the transaction with the given [`SigningKey`] and gives out a full [`Transaction`].
2525
pub fn sign(self, sk: &SigningKey) -> Result<Transaction, TransactionError> {
2626
let bytes = self.signing_payload()?;
27-
let signature = sk.sign(&bytes);
27+
let signature = sk.sign(&bytes).map_err(|_| TransactionError::SigningFailed)?;
2828

2929
Ok(Transaction {
3030
id: self.id,

crates/da/src/lib.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,11 @@ pub struct FinalizedEpoch {
3737
}
3838

3939
impl FinalizedEpoch {
40-
pub fn insert_signature(&mut self, key: &SigningKey) {
40+
pub fn insert_signature(&mut self, key: &SigningKey) -> Result<()> {
4141
let plaintext = self.encode_to_bytes().unwrap();
42-
let signature = key.sign(&plaintext);
42+
let signature = key.sign(&plaintext)?;
4343
self.signature = Some(signature.to_bytes().to_hex());
44+
Ok(())
4445
}
4546

4647
pub fn verify_signature(&self, vk: VerifyingKey) -> Result<()> {

crates/keys/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ repository.workspace = true
1010
# serde
1111
prism-serde.workspace = true
1212
serde.workspace = true
13+
serde_json.workspace = true
1314

1415
# OAS spec
1516
utoipa.workspace = true
@@ -19,6 +20,10 @@ ed25519-consensus.workspace = true
1920
k256.workspace = true
2021
p256.workspace = true
2122

23+
# signatures
24+
alloy-primitives.workspace = true
25+
ripemd.workspace = true
26+
2227
# misc
2328
anyhow.workspace = true
2429
sha2.workspace = true

crates/keys/src/algorithm.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,23 @@ pub enum CryptoAlgorithm {
1111
Secp256k1,
1212
/// ECDSA signatures using the NIST P-256 curve, also known as prime256v1
1313
Secp256r1,
14+
/// Signatures according to ethereum's EIP-191
15+
Eip191,
16+
/// Signatures according to Cosmos' ADR-36
17+
CosmosAdr36,
18+
}
19+
20+
impl CryptoAlgorithm {
21+
/// Returns a vector containing all variants of `CryptoAlgorithm`.
22+
pub fn all() -> Vec<Self> {
23+
vec![
24+
Self::Ed25519,
25+
Self::Secp256k1,
26+
Self::Secp256r1,
27+
Self::Eip191,
28+
Self::CosmosAdr36,
29+
]
30+
}
1431
}
1532

1633
impl std::str::FromStr for CryptoAlgorithm {
@@ -21,6 +38,8 @@ impl std::str::FromStr for CryptoAlgorithm {
2138
"ed25519" => Ok(CryptoAlgorithm::Ed25519),
2239
"secp256k1" => Ok(CryptoAlgorithm::Secp256k1),
2340
"secp256r1" => Ok(CryptoAlgorithm::Secp256r1),
41+
"eip191" => Ok(CryptoAlgorithm::Eip191),
42+
"cosmos_adr36" => Ok(CryptoAlgorithm::CosmosAdr36),
2443
_ => Err(()),
2544
}
2645
}

crates/keys/src/cosmos.rs

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
use anyhow::Result;
2+
use k256::ecdsa::VerifyingKey as Secp256k1VerifyingKey;
3+
use prism_serde::{bech32::ToBech32, raw_or_b64};
4+
use ripemd::Ripemd160;
5+
use serde::{Deserialize, Serialize};
6+
use sha2::{Digest, Sha256};
7+
8+
#[derive(Serialize, Deserialize)]
9+
struct CosmosSignDoc {
10+
account_number: String,
11+
chain_id: String,
12+
fee: CosmosFee,
13+
memo: String,
14+
msgs: Vec<CosmosMessage>,
15+
sequence: String,
16+
}
17+
18+
#[derive(Serialize, Deserialize)]
19+
struct CosmosFee {
20+
amount: Vec<String>,
21+
gas: String,
22+
}
23+
24+
#[derive(Serialize, Deserialize)]
25+
struct CosmosMessage {
26+
#[serde(rename = "type")]
27+
msg_type: String,
28+
value: CosmosMessageValue,
29+
}
30+
31+
#[derive(Serialize, Deserialize)]
32+
struct CosmosMessageValue {
33+
#[serde(with = "raw_or_b64")]
34+
data: Vec<u8>,
35+
signer: String,
36+
}
37+
38+
impl CosmosSignDoc {
39+
fn new(signer: String, data: Vec<u8>) -> CosmosSignDoc {
40+
CosmosSignDoc {
41+
chain_id: "".to_string(),
42+
account_number: "0".to_string(),
43+
sequence: "0".to_string(),
44+
fee: CosmosFee {
45+
gas: "0".to_string(),
46+
amount: vec![],
47+
},
48+
msgs: vec![CosmosMessage {
49+
msg_type: "sign/MsgSignData".to_string(),
50+
value: CosmosMessageValue { signer, data },
51+
}],
52+
memo: "".to_string(),
53+
}
54+
}
55+
}
56+
57+
/// Hashes a message according to the Cosmos ADR-36 specification.
58+
///
59+
/// This function creates a standardized Cosmos sign doc from the provided message,
60+
/// serializes it according to ADR-36 requirements, and returns its SHA256 hash.
61+
///
62+
/// # Arguments
63+
/// * `message` - The message to be hashed, which can be any type that can be referenced as a byte slice
64+
/// * `verifying_key` - The Secp256k1 verifying key associated with the signer
65+
///
66+
/// # Returns
67+
/// * `Result<Vec<u8>>` - The SHA256 hash of the serialized sign doc or an error
68+
pub fn cosmos_adr36_hash_message(
69+
message: impl AsRef<[u8]>,
70+
verifying_key: &Secp256k1VerifyingKey,
71+
) -> Result<Vec<u8>> {
72+
// TODO: Support arbitrary address prefixes
73+
// At the moment we expect users to use "cosmoshub-4" as chainId when
74+
// signing prism data via `signArbitrary(..)`, resulting in "cosmos" as address prefix
75+
const ADDRESS_PREFIX: &str = "cosmos";
76+
77+
let signer = signer_from_key(ADDRESS_PREFIX, verifying_key)?;
78+
let serialized_sign_doc = create_serialized_adr36_sign_doc(message.as_ref().to_vec(), signer)?;
79+
let hashed_sign_doc = Sha256::digest(&serialized_sign_doc).to_vec();
80+
Ok(hashed_sign_doc)
81+
}
82+
83+
/// Creates a serialized Cosmos ADR-36 sign document.
84+
///
85+
/// This function constructs a CosmosSignDoc with the provided data and signer,
86+
/// serializes it to JSON, and escapes certain HTML special characters to comply
87+
/// with ADR-36 requirements.
88+
///
89+
/// # Arguments
90+
/// * `data` - The binary data to be included in the sign document
91+
/// * `signer` - The bech32-encoded address of the signer
92+
///
93+
/// # Returns
94+
/// * `Result<Vec<u8>>` - The serialized sign document as bytes or an error
95+
fn create_serialized_adr36_sign_doc(data: Vec<u8>, signer: String) -> Result<Vec<u8>> {
96+
let adr36_sign_doc = CosmosSignDoc::new(signer, data);
97+
98+
let sign_doc_str = serde_json::to_string(&adr36_sign_doc)?
99+
.replace("<", "\\u003c")
100+
.replace(">", "\\u003e")
101+
.replace("&", "\\u0026");
102+
Ok(sign_doc_str.into_bytes())
103+
}
104+
105+
/// Derives a Cosmos bech32-encoded address from a Secp256k1 verifying key.
106+
///
107+
/// This follows the Cosmos address derivation process:
108+
/// 1. Takes the SEC1-encoded public key bytes
109+
/// 2. Computes SHA256 hash of those bytes
110+
/// 3. Computes RIPEMD160 hash of the SHA256 result
111+
/// 4. Encodes the resulting 20-byte hash with bech32 using the provided prefix
112+
///
113+
/// # Arguments
114+
/// * `address_prefix` - The bech32 human-readable part (e.g., "cosmos")
115+
/// * `verifying_key` - The Secp256k1 verifying key to derive the address from
116+
///
117+
/// # Returns
118+
/// * `Result<String>` - The bech32-encoded address or an error
119+
fn signer_from_key(address_prefix: &str, verifying_key: &Secp256k1VerifyingKey) -> Result<String> {
120+
let verifying_key_bytes = verifying_key.to_sec1_bytes();
121+
let hashed_key_bytes = Sha256::digest(verifying_key_bytes);
122+
let cosmos_address = Ripemd160::digest(hashed_key_bytes);
123+
124+
let signer = cosmos_address.to_bech32(address_prefix)?;
125+
Ok(signer)
126+
}

crates/keys/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
mod algorithm;
2+
mod cosmos;
23
mod payload;
34
mod signatures;
45
mod signing_keys;

0 commit comments

Comments
 (0)