summon_api_sdk/
lib.rs

1use cardano_message_signing::{self as ms, Label};
2use cml_chain::address::Address;
3use cml_chain::certs::Credential;
4use cml_chain::crypto::hash;
5use cml_chain::{address::BaseAddress, certs::StakeCredential};
6use cml_crypto::chain_crypto::hash::Blake2b224;
7use cml_crypto::PublicKey;
8use cml_crypto::RawBytesEncoding;
9use cml_crypto::{Ed25519KeyHash, Ed25519Signature};
10use ms::utils::{FromBytes, ToBytes};
11use reqwest::header;
12use serde::{Deserialize, Serialize};
13use std::str;
14use wasm_bindgen::prelude::wasm_bindgen;
15use wasm_bindgen::JsError;
16
17/**
18 *   signature:  COSESign1 bytes
19 *   key: COSEKey hex bytes
20 */
21#[wasm_bindgen]
22#[derive(Serialize, Deserialize)]
23pub struct DataSignature {
24    signature: Vec<u8>, //COSESign1 bytes
25    key: Vec<u8>,       //COSEKey bytes
26}
27
28#[wasm_bindgen]
29impl DataSignature {
30    pub fn new(cose_signature: &[u8], cose_key: &[u8]) -> Self {
31        Self {
32            signature: cose_signature.to_vec(),
33            key: cose_key.to_vec(),
34        }
35    }
36    pub fn signature(&self) -> Vec<u8> {
37        self.signature.clone()
38    }
39    pub fn key(&self) -> Vec<u8> {
40        self.key.clone()
41    }
42}
43
44#[wasm_bindgen]
45#[derive(Serialize, Deserialize)]
46pub struct VerifyResponse {
47    error: String,
48    is_valid: bool,
49}
50
51#[wasm_bindgen]
52impl VerifyResponse {
53    pub fn new(error: &str, valid: bool) -> Self {
54        Self {
55            error: error.to_string(),
56            is_valid: valid,
57        }
58    }
59    pub fn error(&self) -> String {
60        self.error.clone()
61    }
62    pub fn is_valid(&self) -> bool {
63        self.is_valid.clone()
64    }
65}
66
67/**
68 *  **Verify data signed according to CIP-008**
69 *   returns ->
70 *   VerifyResponse {
71 *       error: String,
72 *       is_valid: bool,
73 *   }
74 */
75#[wasm_bindgen]
76pub fn verify_data_signature(
77    dt: DataSignature,
78    expected_message: &str,
79    expected_address_bech32: &str,
80) -> VerifyResponse {
81    let sign_res = verify_data_signature_cip008(dt, expected_message, expected_address_bech32);
82    match sign_res {
83        Ok(val) => VerifyResponse::new("", val),
84        Err(er) => VerifyResponse::new(&er.as_str(), false),
85    }
86}
87
88/**
89 *  **Verify data signed according to CIP-008**
90 *   returns ->
91 *    bool or error message
92 */
93pub fn verify_data_signature_cip008(
94    dt: DataSignature,
95    expected_message: &str,
96    expected_address_bech32: &str,
97) -> Result<bool, String> {
98    let cose_sign1 = ms::COSESign1::from_bytes(dt.signature).unwrap();
99
100    let payload_to_verify_opt = cose_sign1.payload();
101
102    if payload_to_verify_opt.is_none() {
103        return Ok(false);
104    }
105
106    let payload_to_verify = payload_to_verify_opt.unwrap();
107
108    let payload_text = str::from_utf8(&payload_to_verify).unwrap_or_else(|_| "DEFAULT");
109
110    if expected_message != payload_text {
111        return Ok(false);
112    }
113
114    let headers_to_verify = cose_sign1.headers();
115
116    let header_addr_val = headers_to_verify
117        .protected()
118        .deserialized_headers()
119        .header(&ms::Label::new_text("address".to_string()))
120        .unwrap()
121        .to_bytes();
122
123    let cbor_val_address = ms::cbor::CBORValue::from_bytes(header_addr_val)
124        .unwrap()
125        .as_bytes()
126        .unwrap();
127
128    let address_res = Address::from_raw_bytes(&cbor_val_address);
129
130    if address_res.is_err() {
131        return Err("failed parsing address from header".to_string());
132    }
133
134    let cose_key = ms::COSEKey::from_bytes(dt.key).unwrap();
135
136    let pk = PublicKey::from_raw_bytes(
137        &cose_key
138            .header(&ms::Label::new_int(&ms::utils::Int::new_negative(
139                ms::utils::BigNum::from_str("2").unwrap(),
140            )))
141            .unwrap()
142            .as_bytes()
143            .unwrap(),
144    )
145    .unwrap();
146
147    let is_address_valid =
148        verify_signing_address(expected_address_bech32, address_res.unwrap(), &pk);
149
150    if is_address_valid.is_err() || !is_address_valid.unwrap() {
151        return Ok(false);
152    }
153
154    let signed_data = cose_sign1.signed_data(None, None).unwrap().to_bytes();
155    let sig = Ed25519Signature::from_raw_bytes(&cose_sign1.signature()).unwrap();
156
157    if pk.verify(&signed_data, &sig) {
158        Ok(true)
159    } else {
160        Ok(false)
161    }
162}
163
164/**
165 *  **Verify signing address, needs to be full base address**
166 *   returns ->
167 *    Result<bool or error message>
168 */
169
170pub fn verify_signing_address(
171    expected_address_bech32: &str,
172    coseheader_address: Address,
173    cose_pubkey: &PublicKey,
174) -> Result<bool, JsError> {
175    let expected_address = Address::from_bech32(expected_address_bech32)?;
176    if coseheader_address.to_bech32(None)? != expected_address.to_bech32(None)? {
177        return Ok(false);
178    }
179
180    // check if BaseAddress
181    let base_address = BaseAddress::from_address(&coseheader_address);
182
183    if base_address.is_none() {
184        return Err(JsError::new("Signing address is not base address."));
185    }
186
187    //reconstruct address
188    let payment_key_hash = cose_pubkey.hash();
189    let stake_key = base_address.unwrap().stake;
190
191    let reconstructed_address = BaseAddress::new(
192        expected_address.network_id()?,
193        Credential::new_pub_key(payment_key_hash),
194        StakeCredential::from(stake_key),
195    );
196    if expected_address.to_bech32(None)? == reconstructed_address.to_address().to_bech32(None)? {
197        return Ok(true);
198    }
199    return Ok(false);
200}
201
202#[derive(Serialize, Deserialize)]
203#[wasm_bindgen]
204pub struct CustomClaim {
205    user_id: String,
206    address: String,
207}
208
209#[wasm_bindgen]
210impl CustomClaim {
211    pub fn new(user_id: &str, address: &str) -> Self {
212        Self {
213            user_id: user_id.to_string(),
214            address: address.to_string(),
215        }
216    }
217    pub fn user_id(&self) -> String {
218        self.user_id.clone()
219    }
220    pub fn address(&self) -> String {
221        self.address.clone()
222    }
223}
224
225/**
226 *  **Verify data signed according to CIP-30**
227 *   returns ->
228 *   VerifyResponse {
229 *       error: String,
230 *       is_valid: bool,
231 *   }
232 */
233#[wasm_bindgen]
234pub fn verify_data_signature_new(
235    dt: DataSignature,
236    expected_message: &str,
237    expected_address_bech32: &str,
238) -> VerifyResponse {
239    let sign_res = verify_data_signature_cip30(dt, expected_message, expected_address_bech32);
240    match sign_res {
241        Ok(val) => VerifyResponse::new("", val),
242        Err(er) => VerifyResponse::new(&er.as_str(), false),
243    }
244}
245
246/**
247 *  **Verify data signed according to CIP-30**
248 *   returns ->
249 *    bool or error message
250 */
251pub fn verify_data_signature_cip30(
252    dt: DataSignature,
253    expected_message: &str,
254    expected_address_bech32: &str,
255) -> Result<bool, String> {
256    let cose_sign1_res = ms::COSESign1::from_bytes(dt.signature);
257
258    if cose_sign1_res.is_err() {
259        return Err("Cose sign object could not be created from the signature".to_string());
260    }
261
262    let cose_sign1 = cose_sign1_res.unwrap();
263
264    let payload_to_verify_opt = cose_sign1.payload();
265
266    if payload_to_verify_opt.is_none() {
267        return Err("No payload".to_string());
268    }
269
270    let payload_to_verify = payload_to_verify_opt.unwrap();
271
272    let is_message_hashed = check_is_hashed_message(&cose_sign1).unwrap();
273
274    if is_message_hashed {
275        let expected_message_hash = Blake2b224::new(&expected_message.as_bytes()).to_string();
276        let payload_array: [u8; 28] = payload_to_verify
277            .as_slice()
278            .try_into()
279            .expect("Payload length is not 28 bytes");
280        let payload_text = Blake2b224::from(payload_array).to_string();
281        if expected_message_hash != payload_text {
282            println!("CIP30 Debug: Hashed message mismatch. Expected hash: {}, Got hash: {}", expected_message_hash, payload_text);
283            return Ok(false);
284        }
285    } else {
286        let payload_text = str::from_utf8(&payload_to_verify).unwrap_or_else(|_| "DEFAULT");
287        if expected_message != payload_text {
288            println!("CIP30 Debug: Unhashed message mismatch. Expected: '{}', Got: '{}'", expected_message, payload_text);
289            return Ok(false);
290        }
291       
292    }
293
294    let cose_key_res = ms::COSEKey::from_bytes(dt.key);
295    if cose_key_res.is_err() {
296        return Err(cose_key_res.err().unwrap().to_string());
297    }
298
299    let cose_key = cose_key_res.unwrap();
300
301    let pk = PublicKey::from_raw_bytes(
302        &cose_key
303            .header(&ms::Label::new_int(&ms::utils::Int::new_negative(
304                ms::utils::BigNum::from_str("2").unwrap(),
305            )))
306            .unwrap()
307            .as_bytes()
308            .unwrap(),
309    )
310    .unwrap();
311
312    let is_address_valid_result = verify_signing_address_new(expected_address_bech32, &pk);
313
314    match is_address_valid_result {
315        Ok(true) => { /* Address is valid, continue */ }
316        Ok(false) => {
317            let pk_hash = pk.hash().to_string();
318            println!("CIP30 Debug: Address validation returned false. Expected Address: {}, Public Key Hash: {}", expected_address_bech32, pk_hash);
319            return Ok(false);
320        }
321        Err(e) => {
322            let pk_hash = pk.hash().to_string();
323            // Note: We capture the error `e` here but might need to adjust how it's printed depending on its type.
324            // For now, using debug print.
325            println!("CIP30 Debug: Address validation failed with error. Expected Address: {}, Public Key Hash: {}, Error: {:?}", expected_address_bech32, pk_hash, e);
326            // Decide if an error during address validation should return Ok(false) or Err(...)
327            // Returning Ok(false) for consistency with the previous logic.
328            return Ok(false);
329        }
330    }
331
332    let signed_data = cose_sign1.signed_data(None, None).unwrap().to_bytes();
333    let sig = Ed25519Signature::from_raw_bytes(&cose_sign1.signature()).unwrap();
334
335    if pk.verify(&signed_data, &sig) {
336        Ok(true)
337    } else {
338        println!("CIP30 Debug: Final signature verification failed.");
339        Ok(false)
340    }
341}
342
343fn check_is_hashed_message(
344    cose_sign1: &cardano_message_signing::COSESign1,
345) -> Result<bool, String> {
346    let headers_to_verify = cose_sign1.headers();
347    let unprotected_headers = headers_to_verify.unprotected();
348    let is_hashed_opt = unprotected_headers.header(&Label::new_text("hashed".to_string()));
349    if is_hashed_opt.is_none() {
350        return Err("No hashed header".to_string());
351    }
352    let is_hashed_cbor = is_hashed_opt.unwrap();
353    let is_hashed_bool = if let Some(b) = is_hashed_cbor.as_special().and_then(|s| s.as_bool()) {
354        b
355    } else if let Some(s) = is_hashed_cbor.as_text() {
356        s == "true"
357    } else {
358        return Err("Invalid type for hashed header".to_string());
359    };
360    if is_hashed_bool {
361        return Ok(true);
362    }
363    return Ok(false);
364}
365
366fn extract_pubkey(credential: &Credential) -> Option<&Ed25519KeyHash> {
367    match credential {
368        Credential::PubKey { hash, .. } => Some(hash),
369        _ => None,
370    }
371}
372
373/**
374 *  **Verify signing address, needs to be full base address**
375 *   returns ->
376 *    Result ( bool or error message )
377 */
378pub fn verify_signing_address_new(
379    expected_address_bech32: &str,
380    cose_pubkey: &PublicKey,
381) -> Result<bool, JsError> {
382    let expected_credential = Address::from_bech32(expected_address_bech32)?
383        .staking_cred()
384        .expect("Address doesn't have staking credential")
385        .clone();
386
387    let expected_pubkeyhash = extract_pubkey(&expected_credential).unwrap();
388    let stake_key_hash = cose_pubkey.hash();
389    if expected_pubkeyhash.eq(&stake_key_hash) {
390        return Ok(true);
391    }
392    return Ok(false);
393}