Skip to main content

cairn_cli/
crypto.rs

1/// Cryptographic utilities for EIP-4361 signing and Ed25519 DID operations.
2/// 
3/// This module provides the signing primitives needed for:
4/// - EIP-4361 challenge signing (wallet authentication)
5/// - Ed25519 DID signing (PoI commitment signatures)
6/// - JWKS verification (Proof of Transport validation)
7use crate::errors::CairnError;
8
9/// Sign an EIP-4361 message with an Ethereum private key.
10/// Returns the hex-encoded signature.
11pub fn sign_eip4361_message(message: &str, private_key_hex: &str) -> Result<String, CairnError> {
12    use ethers::signers::{LocalWallet, Signer};
13    use ethers::core::types::Signature;
14
15    let wallet: LocalWallet = private_key_hex
16        .parse()
17        .map_err(|e: ethers::signers::WalletError| CairnError::SignatureError(e.to_string()))?;
18
19    // ethers uses blocking sign_message under the hood for LocalWallet
20    let rt = tokio::runtime::Handle::current();
21    let sig: Signature = rt
22        .block_on(wallet.sign_message(message))
23        .map_err(|e| CairnError::SignatureError(e.to_string()))?;
24
25    Ok(format!("0x{}", sig))
26}
27
28/// Sign a payload with an Ed25519 key (for DID-based PoI signatures).
29pub fn ed25519_sign(payload: &[u8], secret_key_bytes: &[u8]) -> Result<Vec<u8>, CairnError> {
30    use ed25519_dalek::{SigningKey, Signer};
31
32    if secret_key_bytes.len() != 32 {
33        return Err(CairnError::SignatureError(
34            "Ed25519 secret key must be 32 bytes".to_string(),
35        ));
36    }
37
38    let mut key_bytes = [0u8; 32];
39    key_bytes.copy_from_slice(secret_key_bytes);
40    let signing_key = SigningKey::from_bytes(&key_bytes);
41    let signature = signing_key.sign(payload);
42
43    Ok(signature.to_bytes().to_vec())
44}
45
46/// Read a key file and return the raw bytes.
47pub fn read_key_file(path: &str) -> Result<Vec<u8>, CairnError> {
48    let content = std::fs::read_to_string(path)
49        .map_err(|e| CairnError::InvalidInput(format!("Cannot read key file '{}': {}", path, e)))?;
50
51    let trimmed = content.trim();
52
53    // Try Solana JSON keypair array format: [114,147,178,...] (64 bytes, first 32 = secret)
54    if trimmed.starts_with('[') {
55        if let Ok(bytes) = serde_json::from_str::<Vec<u8>>(trimmed) {
56            return Ok(bytes);
57        }
58    }
59
60    // Try hex decoding
61    if let Ok(bytes) = hex::decode(trimmed) {
62        return Ok(bytes);
63    }
64
65    // Try base64
66    if let Ok(bytes) = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, trimmed) {
67        return Ok(bytes);
68    }
69
70    // Return raw bytes
71    Ok(trimmed.as_bytes().to_vec())
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    #[test]
79    fn debug_solana_signature_mismatch() {
80        let wallet_file: Vec<u8> = vec![114,147,178,86,109,101,92,89,64,113,139,40,164,247,79,32,2,170,19,0,34,189,59,224,230,196,225,47,14,188,224,220,160,91,118,254,241,12,235,238,190,24,75,113,77,47,51,79,21,242,145,102,154,93,23,141,237,38,200,128,108,195,168,227];
81        
82        // Emulating what happens in `src/cli/auth.rs` lines 114-118
83        let secret = &wallet_file[..32]; 
84        
85        let wallet_address = bs58::encode(&wallet_file[32..]).into_string();
86        let expected_challenge = format!(
87            "Welcome to Backpac Agent Access.\n\n\
88             Please sign this message to verify ownership of this wallet and establish a session.\n\n\
89             Wallet: {}\n\
90             DID: {}\n\
91             Chain: {}\n\
92             Nonce: {}",
93            wallet_address.to_lowercase(),
94            "did:key:z6MkhaXgBZDvotDkL5257faiztiCEsJUTtcCjdReE7m1",
95            "solana:devnet",
96            "bb82335399edce14da9652840e11ea873f1f982664399c3c9000df07bced96d0"
97        );
98        
99        let sig_bytes = ed25519_sign(expected_challenge.as_bytes(), secret).unwrap();
100        let sig_b58 = bs58::encode(sig_bytes).into_string();
101        println!("Rust Signature (Base58): {}", sig_b58);
102        assert_eq!(sig_b58, "5S2LiVAuHoMdLc12qAiyXVfYHBuU2qRFxbBgQV1hgwRz2KgSVeWzkLhDJVUiaepkKWPNYkiMJARiCDSxnPvsEDmR");
103    }
104}