kms_secp256k1_api/services/
casper_keys_service.rs

1use crate::config::Config;
2use crate::constants::{CASPER_SECP_LEN, CASPER_SECP_PREFIX};
3use crate::services::crypto_service::CryptoService;
4use crate::services::keys_service::{KeyEntry, KeysService, KeysServiceTrait, SigEntry};
5use casper_rust_wasm_sdk::types::hash::transaction_hash::TransactionHash;
6use casper_rust_wasm_sdk::types::public_key::PublicKey;
7use casper_rust_wasm_sdk::types::transaction::Transaction;
8use tracing::error;
9
10pub struct CasperKeysService {
11    keys_service: KeysService,
12}
13
14impl CasperKeysService {
15    /// Creates a new `CasperKeysService` instance.
16    ///
17    /// # Errors
18    ///
19    /// Returns an error if the `KeysService` initialization fails.
20    pub async fn new(config: Config, crypto_service: CryptoService) -> Result<Self, String> {
21        let keys_service = KeysService::new(config, crypto_service).await?;
22        Ok(Self { keys_service })
23    }
24}
25
26#[async_trait::async_trait]
27impl KeysServiceTrait for CasperKeysService {
28    /// Creates a new KMS key, derives its public key with an optional prefix `keys_serviced` on config,
29    /// registers an alias for it, and returns the formatted public key.
30    ///
31    /// # Arguments
32    ///
33    /// * `config` - Reference to the config struct used to decide prefixing.
34    ///
35    /// # Errors
36    ///
37    /// Returns an error if key creation, public key conversion, or alias creation fails.
38    async fn create_key(&mut self, _config: &Config) -> Result<KeyEntry, String> {
39        let (key_id, public_key_base64) = self
40            .keys_service
41            .kms_client_service
42            .create_key()
43            .await
44            .map_err(|e| {
45            let msg = format!("Failed to create_key in KmsClientService: {e}");
46            error!("{}", &msg);
47            msg
48        })?;
49
50        let public_key = self
51            .keys_service
52            .crypto_service
53            .public_key(&public_key_base64)
54            .map_err(|e| {
55                let msg = format!("public_key conversion failed: {e:?}");
56                error!("{}", &msg);
57                msg
58            })?; // generated key contains does not contain prefix
59
60        let address = Self::resolve_key(&public_key); // address == prefix + public_key
61
62        if public_key.is_empty() {
63            let msg = "No public key generated".to_string();
64            error!("{}", &msg);
65            return Err(msg);
66        }
67
68        // Create alias for the key
69        self.keys_service
70            .kms_client_service
71            .create_alias(&key_id, &address)
72            .await
73            .map_err(|e| {
74                let msg = format!("Error creating alias: {e:?}");
75                error!("{}", &msg);
76                msg
77            })?;
78
79        // info!("{}", &public_key_base64);
80
81        Ok(KeyEntry {
82            public_key: Some(public_key).into(),
83            address: address.into(),
84            public_key_base64: public_key_base64.into(),
85            key_id: key_id.into(),
86        })
87    }
88
89    /// Signs a transaction hash using the provided public key and configuration mode.
90    ///
91    /// Selects a prefix `keys_serviced` on the active mode (Casper or Ethereum),
92    /// and delegates signing to the internal `sign` method.
93    ///
94    /// # Arguments
95    ///
96    /// * `config` - Reference to the application configuration to determine signing mode.
97    /// * `transaction_hash` - The hash of the transaction to be signed.
98    /// * `public_key` - The public key corresponding to the private key for signing.
99    ///
100    /// # Errors
101    ///
102    /// Returns an error if the signing operation fails.
103    async fn sign_transaction_hash(
104        &mut self,
105        config: &Config,
106        transaction_hash: &str,
107        key: &str,
108    ) -> Result<SigEntry, String> {
109        if !config.is_casper_mode() {
110            return Err("Only Casper mode is supported".to_string());
111        }
112
113        if let Err(e) = TransactionHash::new(transaction_hash) {
114            error!(
115                "Error reading parameters: transaction_hash : {}",
116                transaction_hash
117            );
118            error!("Validation error: {:?}", e);
119            return Err(format!("Error reading transaction parameters: {e}"));
120        }
121
122        let public_key = Self::resolve_key(key);
123
124        if let Err(e) = PublicKey::new(&public_key) {
125            error!(
126                "Error reading parameters \npublic_key : {}\ntransaction_hash : {}",
127                public_key, transaction_hash
128            );
129            error!("Validation error: {:?}", e);
130            return Err(format!("Error reading transaction parameters: {e}"));
131        }
132
133        let signature = self
134            .keys_service
135            .sign(transaction_hash, &public_key, Some(CASPER_SECP_PREFIX))
136            .await?;
137
138        // Now verify the signature immediately
139        let verified = self
140            .verify(transaction_hash, &signature, &public_key)
141            .await?;
142        if !verified {
143            return Err("Signature verification failed after signing".to_string());
144        }
145
146        Ok(SigEntry {
147            address: public_key.clone().into(),
148            public_key: public_key.replacen(CASPER_SECP_PREFIX, "", 1).into(),
149            signature: signature.into(),
150        })
151    }
152
153    /// Signs a serialized Casper transaction using the provided public key.
154    ///
155    /// Parses the input transaction string, verifies the transaction hash and public key,
156    /// signs the hash, attaches the signature to the transaction, and returns the signed
157    /// transaction as a JSON string.
158    ///
159    /// # Arguments
160    ///
161    /// * `config` - Reference to the application configuration. Only Casper mode is supported.
162    /// * `transaction_str` - A JSON string representing the transaction to be signed.
163    /// * `public_key` - The public key corresponding to the signing key.
164    ///
165    /// # Errors
166    ///
167    /// Returns an error if:
168    /// - The application is not in Casper mode.
169    /// - The transaction JSON is invalid.
170    /// - The transaction hash or public key is invalid.
171    /// - The signing process fails.
172    /// - The final transaction serialization fails.
173    async fn sign_transaction(
174        &mut self,
175        config: &Config,
176        transaction_str: &str,
177        key: &str,
178    ) -> Result<String, String> {
179        if !config.is_casper_mode() {
180            return Err("Only Casper mode is supported".to_string());
181        }
182
183        let transaction: Transaction = Transaction::from_json_string(transaction_str)
184            .map_err(|e| format!("Failed to parse transaction: {e}"))?;
185
186        let transaction_hash_str = transaction.hash().to_string();
187
188        TransactionHash::new(&transaction_hash_str).map_err(|e| {
189            error!("Invalid transaction hash: {:?}", e);
190            "Invalid transaction hash".to_string()
191        })?;
192
193        let public_key = Self::resolve_key(key);
194
195        PublicKey::new(&public_key).map_err(|e| {
196            error!("Invalid public key: {:?}", e);
197            "Invalid public key".to_string()
198        })?;
199
200        let signature = self
201            .keys_service
202            .sign(&transaction_hash_str, &public_key, Some(CASPER_SECP_PREFIX))
203            .await
204            .map_err(|e| format!("Signing failed: {e}"))?;
205
206        // Verify the signature immediately
207        let verified = self
208            .verify(&transaction_hash_str, &signature, &public_key)
209            .await?;
210        if !verified {
211            return Err("Signature verification failed after signing".to_string());
212        }
213
214        let signed_transaction = transaction.add_signature(&public_key, &signature);
215
216        signed_transaction
217            .to_json_string()
218            .map_err(|e| format!("Failed to serialize signed transaction: {e}"))
219    }
220
221    async fn verify(
222        &mut self,
223        transaction_hash_hex: &str,
224        signature_hex: &str,
225        key: &str,
226    ) -> Result<bool, String> {
227        let key = Self::resolve_key(key);
228        self.keys_service
229            .verify(transaction_hash_hex, signature_hex, &key)
230    }
231
232    async fn verify_via_kms(
233        &mut self,
234        transaction_hash_hex: &str,
235        signature_hex: &str,
236        key: &str,
237    ) -> Result<bool, String> {
238        let key = Self::resolve_key(key);
239        self.keys_service
240            .verify_via_kms(transaction_hash_hex, signature_hex, &key)
241            .await
242    }
243
244    async fn delete_key(&mut self, key: &str) -> Result<bool, String> {
245        let key = Self::resolve_key(key);
246        self.keys_service.delete_key(&key).await
247    }
248
249    async fn list_keys(&mut self) -> Result<Vec<KeyEntry>, String> {
250        self.keys_service.list_keys().await
251    }
252}
253
254impl CasperKeysService {
255    /// Resolves a given key string to a Casper address format.
256    ///
257    /// This function checks whether the input `key` is a full Casper-compatible public key (by
258    /// comparing its length to the expected `CASPER_SECP_LEN`). If so, it returns the key as-is.
259    /// Otherwise, it assumes the input is a truncated or raw key, and prefixes it with
260    /// `CASPER_SECP_PREFIX` to form a valid Casper address format.
261    ///
262    /// # Parameters
263    /// - `key`: A string that is either a full-length Casper-compatible key or a truncated key.
264    ///
265    /// # Returns
266    /// - `Ok(String)`: The resolved Casper address string.
267    /// - `Err(String)`: This implementation does not return errors, but the signature allows for future error handling.
268    ///
269    /// # Behavior
270    /// - If `key.len() == CASPER_SECP_LEN`, the input is returned directly.
271    /// - Otherwise, the key is prefixed with `CASPER_SECP_PREFIX` and returned.
272    fn resolve_key(key: &str) -> String {
273        if key.len() == CASPER_SECP_LEN {
274            key.to_string()
275        } else {
276            format!("{CASPER_SECP_PREFIX}{key}")
277        }
278    }
279}
280
281#[cfg(test)]
282mod tests {
283    use base64::Engine;
284    use base64::engine::general_purpose::STANDARD;
285    use casper_rust_wasm_sdk::{
286        SDK, types::transaction_params::transaction_str_params::TransactionStrParams,
287    };
288    use regex::Regex;
289    use serde_json::json;
290
291    use super::*;
292    use crate::{
293        config::ConfigBuilder,
294        constants::{
295            CASPER_PUBLIC_KEY_PREFIXED, SIGNATURE, SIGNATURE_PREFIXED, SIGNATURE_RSV_LEN,
296            TRANSACTION_HASH, WASM_PATH,
297        },
298        services::{crypto_service::CryptoService, keys_service::KeyEntry},
299        wasm_loader::WasmLoader,
300    };
301
302    #[tokio::test]
303    async fn test_create_key() {
304        let config = ConfigBuilder::new()
305            .with_casper_mode()
306            .with_aws_mode(false)
307            .build();
308
309        let wasm_loader = WasmLoader::new(WASM_PATH)
310            .await
311            .expect("Failed to load WASM module");
312
313        let crypto_service =
314            CryptoService::new(&wasm_loader).expect("Failed to initialize CryptoService");
315
316        // Create the service via the real constructor, it will use MockKmsClientService internally
317        let mut service = CasperKeysService::new(config.clone(), crypto_service)
318            .await
319            .expect("Failed to create CasperKeysService");
320
321        // Call create_key on the service under test
322        let result = service.create_key(&config).await;
323
324        // Assert the key is returned with the casper prefix
325        assert!(result.is_ok(), "create_key failed: {result:?}");
326        let key = result.unwrap();
327        assert!(key.address.to_string().starts_with(CASPER_SECP_PREFIX));
328        assert!(!key.address.to_string().is_empty());
329    }
330
331    #[tokio::test]
332    async fn test_verify_signature() {
333        let config = ConfigBuilder::new()
334            .with_casper_mode()
335            .with_aws_mode(false)
336            .build();
337
338        let wasm_loader = WasmLoader::new(WASM_PATH)
339            .await
340            .expect("Failed to load WASM module");
341
342        let crypto_service =
343            CryptoService::new(&wasm_loader).expect("Failed to initialize CryptoService");
344
345        // Create CasperKeysService (uses mocked KMS + real CryptoService)
346        let mut service = CasperKeysService::new(config.clone(), crypto_service)
347            .await
348            .expect("Failed to create CasperKeysService");
349
350        //  Valid prefixed signature
351        let result = service
352            .verify(
353                TRANSACTION_HASH,
354                SIGNATURE_PREFIXED,
355                CASPER_PUBLIC_KEY_PREFIXED,
356            )
357            .await
358            .unwrap();
359        assert!(result, "Expected signature to verify correctly");
360
361        // Valid signature
362        let result = service
363            .verify(TRANSACTION_HASH, SIGNATURE, CASPER_PUBLIC_KEY_PREFIXED)
364            .await
365            .unwrap();
366        assert!(result, "Expected signature to verify correctly");
367
368        // Invalid signature
369        let result = service
370            .verify("bad_hash", "bad_signature", "bad_key")
371            .await
372            .unwrap();
373        assert!(!result, "Expected signature verification to fail");
374    }
375
376    #[tokio::test]
377    async fn test_verify_via_kms_signature() {
378        let config = ConfigBuilder::new()
379            .with_casper_mode()
380            .with_aws_mode(false)
381            .build();
382
383        let wasm_loader = WasmLoader::new(WASM_PATH)
384            .await
385            .expect("Failed to load WASM module");
386
387        let crypto_service =
388            CryptoService::new(&wasm_loader).expect("Failed to initialize CryptoService");
389
390        // Create CasperKeysService (uses mocked KMS + real CryptoService)
391        let mut service = CasperKeysService::new(config.clone(), crypto_service)
392            .await
393            .expect("Failed to create CasperKeysService");
394
395        // Valid prefixed signature
396        let result = service
397            .verify_via_kms(
398                TRANSACTION_HASH,
399                SIGNATURE_PREFIXED,
400                CASPER_PUBLIC_KEY_PREFIXED,
401            )
402            .await
403            .unwrap();
404        assert!(result, "Expected signature to verify correctly");
405
406        // Valid signature
407        let result = service
408            .verify_via_kms(TRANSACTION_HASH, SIGNATURE, CASPER_PUBLIC_KEY_PREFIXED)
409            .await
410            .unwrap();
411        assert!(result, "Expected signature to verify correctly");
412
413        // Invalid signature
414        let result = service
415            .verify_via_kms("bad_hash", "bad_signature", "bad_key")
416            .await
417            .unwrap();
418        assert!(!result, "Expected signature verification to fail");
419    }
420
421    #[tokio::test]
422    async fn test_delete_key() {
423        let config = ConfigBuilder::new()
424            .with_casper_mode()
425            .with_aws_mode(false)
426            .build();
427
428        let wasm_loader = WasmLoader::new(WASM_PATH)
429            .await
430            .expect("Failed to load WASM module");
431
432        let crypto_service =
433            CryptoService::new(&wasm_loader).expect("Failed to initialize CryptoService");
434
435        let mut service = CasperKeysService::new(config.clone(), crypto_service)
436            .await
437            .expect("Failed to create CasperKeysService");
438
439        let result = service
440            .delete_key("known_public_key")
441            .await
442            .expect("Failed to delete known public key");
443        assert!(result, "Expected key to be deleted successfully");
444
445        let result = service
446            .delete_key("unknown_key")
447            .await
448            .expect("Failed to delete unknown public key");
449        assert!(!result, "Expected key deletion to fail for unknown key");
450    }
451
452    #[tokio::test]
453    async fn test_list_keys() {
454        let config = ConfigBuilder::new()
455            .with_casper_mode()
456            .with_aws_mode(false)
457            .build();
458
459        let wasm_loader = WasmLoader::new(WASM_PATH)
460            .await
461            .expect("Failed to load WASM module");
462
463        let crypto_service =
464            CryptoService::new(&wasm_loader).expect("Failed to initialize CryptoService");
465
466        let mut service = CasperKeysService::new(config.clone(), crypto_service)
467            .await
468            .expect("Failed to create CasperKeysService");
469
470        let result = service.list_keys().await;
471
472        assert!(result.is_ok(), "Expected list_keys to succeed");
473
474        let keys = result.unwrap();
475        assert_eq!(
476            keys[0],
477            KeyEntry {
478                address: "address_1".to_string().into(),
479                public_key_base64: STANDARD.encode("public_key_1_base64").into(),
480                public_key: Some("public_key_1".to_string()).into(),
481                key_id: "key_id_1".to_string().into(),
482            }
483        );
484        assert_eq!(
485            keys[1],
486            KeyEntry {
487                address: "address_2".to_string().into(),
488                public_key_base64: STANDARD.encode("public_key_2_base64").into(),
489                public_key: Some("public_key_2".to_string()).into(),
490                key_id: "key_id_2".to_string().into(),
491            }
492        );
493    }
494
495    #[tokio::test]
496    async fn test_sign_transaction_hash() {
497        let config = ConfigBuilder::new()
498            .with_casper_mode()
499            .with_aws_mode(false)
500            .build();
501
502        let wasm_loader = WasmLoader::new(WASM_PATH)
503            .await
504            .expect("Failed to load WASM module");
505
506        let crypto_service =
507            CryptoService::new(&wasm_loader).expect("Failed to initialize CryptoService");
508
509        let mut service = CasperKeysService::new(config.clone(), crypto_service)
510            .await
511            .expect("Failed to create CasperKeysService");
512
513        let transaction_hash = TRANSACTION_HASH;
514
515        let public_key = CASPER_PUBLIC_KEY_PREFIXED;
516
517        let result = service
518            .sign_transaction_hash(&config, transaction_hash, public_key)
519            .await;
520
521        assert!(result.is_ok(), "Expected signing to succeed");
522
523        let signed = result.unwrap();
524
525        let expected_sig_hex = SIGNATURE;
526
527        let expected_prefixed = format!("{CASPER_SECP_PREFIX}{expected_sig_hex}");
528
529        assert_eq!(
530            signed.signature.to_string(),
531            expected_prefixed,
532            "Expected signature to match expected format"
533        );
534    }
535
536    #[tokio::test]
537    async fn test_sign_transaction() {
538        let config = ConfigBuilder::new()
539            .with_casper_mode()
540            .with_aws_mode(false)
541            .build();
542
543        let wasm_loader = WasmLoader::new(WASM_PATH)
544            .await
545            .expect("Failed to load WASM module");
546
547        let crypto_service =
548            CryptoService::new(&wasm_loader).expect("Failed to initialize CryptoService");
549
550        // Create the CasperKeysService with mocks + real crypto
551        let mut service = CasperKeysService::new(config.clone(), crypto_service)
552            .await
553            .expect("Failed to create CasperKeysService");
554
555        let public_key = CASPER_PUBLIC_KEY_PREFIXED;
556
557        // Build transaction params as you provided
558        let transaction_params = TransactionStrParams::default();
559        transaction_params.set_chain_name("casper-net-1");
560        transaction_params.set_initiator_addr(public_key);
561        transaction_params.set_payment_amount("100000000");
562
563        // Create SDK & transaction
564        let sdk = SDK::new(None, None, None);
565        let transaction = sdk
566            .make_transfer_transaction(None, public_key, "2500000000", transaction_params, None)
567            .expect("Failed to create transfer transaction");
568
569        let mut transaction_str = transaction.to_json_string().unwrap_or_default();
570
571        let re = Regex::new(r#""hash"\s*:\s*"[^"]+""#).unwrap();
572
573        // Replace transaction_hash by mock
574        transaction_str = re
575            .replace(&transaction_str, format!(r#""hash": "{TRANSACTION_HASH}""#))
576            .to_string();
577
578        // Call sign_transaction
579        let signed_transaction_json = service
580            .sign_transaction(&config, &transaction_str, public_key)
581            .await
582            .expect("Failed to sign transaction");
583
584        // Deserialize signed transaction to validate it contains signature
585        let signed_transaction = Transaction::from_json_string(&signed_transaction_json)
586            .expect("Failed to parse signed transaction");
587
588        // Extract approvals and verify
589        let approvals = signed_transaction.approvals();
590
591        for approval in approvals {
592            let signer = approval.signer().to_hex_string();
593            let signature = approval.signature().to_hex_string();
594
595            assert!(public_key.contains(&signer), "Unexpected signer: {signer}");
596
597            assert_eq!(
598                signature.len(),
599                SIGNATURE_RSV_LEN,
600                "Signature length incorrect for signer {}: {}",
601                signer,
602                signature.len()
603            );
604        }
605    }
606
607    #[tokio::test]
608    async fn test_sign_transaction_malformed_json() {
609        let config = ConfigBuilder::new()
610            .with_casper_mode()
611            .with_aws_mode(false)
612            .build();
613
614        let wasm_loader = WasmLoader::new(WASM_PATH)
615            .await
616            .expect("Failed to load WASM");
617
618        let crypto_service =
619            CryptoService::new(&wasm_loader).expect("Failed to create CryptoService");
620
621        let mut service = CasperKeysService::new(config.clone(), crypto_service)
622            .await
623            .expect("Failed to create service");
624
625        let bad_json = "{ this is not valid JSON }";
626
627        let result = service
628            .sign_transaction(&config, bad_json, CASPER_PUBLIC_KEY_PREFIXED)
629            .await;
630
631        assert!(result.is_err());
632        let err = result.unwrap_err();
633        assert!(
634            err.contains("Failed to parse json-args")
635                || err.contains("Failed to parse transaction"),
636            "Expected parsing error, got: {err}"
637        );
638    }
639
640    #[tokio::test]
641    async fn test_sign_transaction_missing_transaction_field() {
642        let config = ConfigBuilder::new()
643            .with_casper_mode()
644            .with_aws_mode(false)
645            .build();
646
647        let wasm_loader = WasmLoader::new(WASM_PATH)
648            .await
649            .expect("Failed to load WASM");
650
651        let crypto_service =
652            CryptoService::new(&wasm_loader).expect("Failed to create CryptoService");
653
654        let mut service = CasperKeysService::new(config.clone(), crypto_service)
655            .await
656            .expect("Failed to create service");
657
658        let minimal_transaction = json!({
659            "some": "value"
660        })
661        .to_string();
662
663        let result = service
664            .sign_transaction(&config, &minimal_transaction, CASPER_PUBLIC_KEY_PREFIXED)
665            .await;
666
667        assert!(
668            result.is_err(),
669            "Expected failure due to missing transaction fields"
670        );
671    }
672
673    #[tokio::test]
674    async fn test_sign_transaction_hash_with_invalid_input() {
675        let config = ConfigBuilder::new()
676            .with_casper_mode()
677            .with_aws_mode(false)
678            .build();
679
680        let wasm_loader = WasmLoader::new(WASM_PATH)
681            .await
682            .expect("Failed to load WASM module");
683
684        let crypto_service =
685            CryptoService::new(&wasm_loader).expect("Failed to initialize CryptoService");
686
687        let mut service = CasperKeysService::new(config.clone(), crypto_service)
688            .await
689            .expect("Failed to create CasperKeysService");
690
691        // Invalid public key and transaction hash
692        let result = service
693            .sign_transaction_hash(&config, "invalid_hash", "invalid_key")
694            .await;
695
696        assert!(
697            result.is_err(),
698            "Expected signing to fail due to invalid input"
699        );
700        let error = result.unwrap_err();
701        assert!(
702            error.contains("Error reading transaction parameters"),
703            "Unexpected error: {error}"
704        );
705    }
706
707    #[tokio::test]
708    async fn test_sign_transaction_malformed_approvals() {
709        let config = ConfigBuilder::new()
710            .with_casper_mode()
711            .with_aws_mode(false)
712            .build();
713
714        let wasm_loader = WasmLoader::new(WASM_PATH)
715            .await
716            .expect("Failed to load WASM");
717
718        let crypto_service =
719            CryptoService::new(&wasm_loader).expect("Failed to create CryptoService");
720
721        let mut service = CasperKeysService::new(config.clone(), crypto_service)
722            .await
723            .expect("Failed to create service");
724
725        // approvals should be an array, here it's a string (malformed)
726        let transaction = json!({
727            "hash": TRANSACTION_HASH,
728            "approvals": "not an array"
729        })
730        .to_string();
731
732        let result = service
733            .sign_transaction(&config, &transaction, CASPER_PUBLIC_KEY_PREFIXED)
734            .await;
735
736        assert!(
737            result.is_err(),
738            "Expected failure due to malformed approvals field"
739        );
740    }
741}