1use crate::{Result, X402Error};
4use ethereum_types::{Address, H256, U256};
5use k256::ecdsa::{RecoveryId, Signature as K256Signature};
6use secp256k1::{Message, Secp256k1, SecretKey};
7use serde_json::json;
8use std::str::FromStr;
9
10pub const EIP712_DOMAIN: &str = r#"{"name":"USD Coin","version":"2","chainId":8453,"verifyingContract":"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"}"#;
12
13pub mod jwt {
15 use super::*;
16 use jsonwebtoken::{Algorithm, Header};
17
18 #[derive(Debug, serde::Serialize)]
20 struct Claims {
21 iss: String,
22 sub: String,
23 aud: String,
24 iat: u64,
25 exp: u64,
26 uri: String,
27 }
28
29 #[derive(Debug, Clone)]
31 pub struct JwtOptions {
32 pub key_id: String,
33 pub key_secret: String,
34 pub request_method: String,
35 pub request_host: String,
36 pub request_path: String,
37 }
38
39 impl JwtOptions {
40 pub fn new(
42 key_id: impl Into<String>,
43 key_secret: impl Into<String>,
44 request_method: impl Into<String>,
45 request_host: impl Into<String>,
46 request_path: impl Into<String>,
47 ) -> Self {
48 Self {
49 key_id: key_id.into(),
50 key_secret: key_secret.into(),
51 request_method: request_method.into(),
52 request_host: request_host.into(),
53 request_path: request_path.into(),
54 }
55 }
56 }
57
58 pub fn generate_jwt(options: JwtOptions) -> Result<String> {
60 let request_host = options.request_host.trim_start_matches("https://");
62
63 let now = chrono::Utc::now().timestamp() as u64;
64 let exp = now + 300; let claims = Claims {
67 iss: options.key_id.clone(),
68 sub: options.key_id,
69 aud: request_host.to_string(),
70 iat: now,
71 exp,
72 uri: options.request_path,
73 };
74
75 let header = Header::new(Algorithm::HS256);
76 let key = jsonwebtoken::EncodingKey::from_secret(options.key_secret.as_bytes());
77 let token = jsonwebtoken::encode(&header, &claims, &key)
78 .map_err(|e| X402Error::config(format!("JWT encoding failed: {}", e)))?;
79
80 Ok(token)
81 }
82
83 pub fn create_auth_header(
85 api_key_id: &str,
86 api_key_secret: &str,
87 request_host: &str,
88 request_path: &str,
89 ) -> Result<String> {
90 let options = JwtOptions::new(
91 api_key_id,
92 api_key_secret,
93 "POST", request_host,
95 request_path,
96 );
97
98 let token = generate_jwt(options)?;
99 Ok(format!("Bearer {}", token))
100 }
101
102 pub fn create_auth_header_with_method(
104 api_key_id: &str,
105 api_key_secret: &str,
106 request_method: &str,
107 request_host: &str,
108 request_path: &str,
109 ) -> Result<String> {
110 let options = JwtOptions::new(
111 api_key_id,
112 api_key_secret,
113 request_method,
114 request_host,
115 request_path,
116 );
117
118 let token = generate_jwt(options)?;
119 Ok(format!("Bearer {}", token))
120 }
121}
122
123pub mod eip712 {
125 use super::*;
126
127 #[derive(Debug, Clone)]
129 pub struct Domain {
130 pub name: String,
131 pub version: String,
132 pub chain_id: u64,
133 pub verifying_contract: Address,
134 }
135
136 #[derive(Debug, Clone)]
138 pub struct TypedData {
139 pub domain: Domain,
140 pub primary_type: String,
141 pub types: serde_json::Value,
142 pub message: serde_json::Value,
143 }
144
145 pub fn create_transfer_with_authorization_hash(
147 domain: &Domain,
148 from: Address,
149 to: Address,
150 value: U256,
151 valid_after: U256,
152 valid_before: U256,
153 nonce: H256,
154 ) -> Result<H256> {
155 let types = json!({
156 "EIP712Domain": [
157 {"name": "name", "type": "string"},
158 {"name": "version", "type": "string"},
159 {"name": "chainId", "type": "uint256"},
160 {"name": "verifyingContract", "type": "address"}
161 ],
162 "TransferWithAuthorization": [
163 {"name": "from", "type": "address"},
164 {"name": "to", "type": "address"},
165 {"name": "value", "type": "uint256"},
166 {"name": "validAfter", "type": "uint256"},
167 {"name": "validBefore", "type": "uint256"},
168 {"name": "nonce", "type": "bytes32"}
169 ]
170 });
171
172 let message = json!({
173 "from": format!("{:?}", from),
174 "to": format!("{:?}", to),
175 "value": format!("0x{:x}", value),
176 "validAfter": format!("0x{:x}", valid_after),
177 "validBefore": format!("0x{:x}", valid_before),
178 "nonce": format!("{:?}", nonce)
179 });
180
181 let typed_data = TypedData {
182 domain: domain.clone(),
183 primary_type: "TransferWithAuthorization".to_string(),
184 types,
185 message,
186 };
187
188 hash_typed_data(&typed_data)
189 }
190
191 pub fn hash_typed_data(typed_data: &TypedData) -> Result<H256> {
193 let domain_separator = hash_domain(&typed_data.domain)?;
196 let struct_hash = hash_struct(
197 &typed_data.primary_type,
198 &typed_data.types,
199 &typed_data.message,
200 )?;
201
202 let mut data = Vec::new();
204 data.extend_from_slice(&[0x19, 0x01]); data.extend_from_slice(domain_separator.as_bytes());
206 data.extend_from_slice(struct_hash.as_bytes());
207
208 Ok(H256::from_slice(&keccak256(&data)))
209 }
210
211 fn hash_domain(domain: &Domain) -> Result<H256> {
213 let domain_type_hash = keccak256(
214 b"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)",
215 );
216
217 let name_hash = keccak256(domain.name.as_bytes());
218 let version_hash = keccak256(domain.version.as_bytes());
219 let chain_id_hash = keccak256(&domain.chain_id.to_be_bytes());
220 let verifying_contract_hash = keccak256(domain.verifying_contract.as_bytes());
221
222 let mut data = Vec::new();
223 data.extend_from_slice(&domain_type_hash);
224 data.extend_from_slice(&name_hash);
225 data.extend_from_slice(&version_hash);
226 data.extend_from_slice(&chain_id_hash);
227 data.extend_from_slice(&verifying_contract_hash);
228
229 Ok(H256::from_slice(&keccak256(&data)))
230 }
231
232 fn hash_struct(
234 primary_type: &str,
235 _types: &serde_json::Value,
236 message: &serde_json::Value,
237 ) -> Result<H256> {
238 let type_hash = keccak256(
242 format!("{}(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)", primary_type)
243 .as_bytes()
244 );
245
246 let encoded_message = encode_message_fields(message)?;
248 let message_hash = keccak256(&encoded_message);
249
250 let mut data = Vec::new();
252 data.extend_from_slice(&type_hash);
253 data.extend_from_slice(&message_hash);
254
255 Ok(H256::from_slice(&keccak256(&data)))
256 }
257
258 fn encode_message_fields(message: &serde_json::Value) -> Result<Vec<u8>> {
260 let mut encoded = Vec::new();
262
263 if let Some(from) = message.get("from") {
265 if let Some(addr_str) = from.as_str() {
266 let addr = Address::from_str(addr_str)
267 .map_err(|_| X402Error::invalid_authorization("Invalid from address"))?;
268 let mut padded = [0u8; 32];
269 padded[12..32].copy_from_slice(addr.as_bytes());
270 encoded.extend_from_slice(&padded);
271 }
272 }
273
274 if let Some(to) = message.get("to") {
276 if let Some(addr_str) = to.as_str() {
277 let addr = Address::from_str(addr_str)
278 .map_err(|_| X402Error::invalid_authorization("Invalid to address"))?;
279 let mut padded = [0u8; 32];
280 padded[12..32].copy_from_slice(addr.as_bytes());
281 encoded.extend_from_slice(&padded);
282 }
283 }
284
285 if let Some(value) = message.get("value") {
287 if let Some(value_str) = value.as_str() {
288 let value_hex = value_str.trim_start_matches("0x");
289 let value_bytes = hex::decode(value_hex)
290 .map_err(|_| X402Error::invalid_authorization("Invalid value format"))?;
291 let mut padded = [0u8; 32];
292 let start = 32 - value_bytes.len();
293 padded[start..].copy_from_slice(&value_bytes);
294 encoded.extend_from_slice(&padded);
295 }
296 }
297
298 if let Some(valid_after) = message.get("validAfter") {
300 if let Some(valid_after_str) = valid_after.as_str() {
301 let valid_after_hex = valid_after_str.trim_start_matches("0x");
302 let valid_after_bytes = hex::decode(valid_after_hex)
303 .map_err(|_| X402Error::invalid_authorization("Invalid validAfter format"))?;
304 let mut padded = [0u8; 32];
305 let start = 32 - valid_after_bytes.len();
306 padded[start..].copy_from_slice(&valid_after_bytes);
307 encoded.extend_from_slice(&padded);
308 }
309 }
310
311 if let Some(valid_before) = message.get("validBefore") {
313 if let Some(valid_before_str) = valid_before.as_str() {
314 let valid_before_hex = valid_before_str.trim_start_matches("0x");
315 let valid_before_bytes = hex::decode(valid_before_hex)
316 .map_err(|_| X402Error::invalid_authorization("Invalid validBefore format"))?;
317 let mut padded = [0u8; 32];
318 let start = 32 - valid_before_bytes.len();
319 padded[start..].copy_from_slice(&valid_before_bytes);
320 encoded.extend_from_slice(&padded);
321 }
322 }
323
324 if let Some(nonce) = message.get("nonce") {
326 if let Some(nonce_str) = nonce.as_str() {
327 let nonce_hex = nonce_str.trim_start_matches("0x");
328 let nonce_bytes = hex::decode(nonce_hex)
329 .map_err(|_| X402Error::invalid_authorization("Invalid nonce format"))?;
330 if nonce_bytes.len() != 32 {
331 return Err(X402Error::invalid_authorization("Nonce must be 32 bytes"));
332 }
333 encoded.extend_from_slice(&nonce_bytes);
334 }
335 }
336
337 Ok(encoded)
338 }
339
340 fn keccak256(data: &[u8]) -> [u8; 32] {
342 use sha3::{Digest, Keccak256};
343 Keccak256::digest(data).into()
344 }
345
346 pub fn sha3_256(data: &[u8]) -> [u8; 32] {
352 use sha3::{Digest, Sha3_256};
353 Sha3_256::digest(data).into()
354 }
355}
356
357pub mod signature {
359 use super::*;
360 use k256::ecdsa::VerifyingKey;
361
362 pub fn verify_eip712_signature(
364 signature: &str,
365 message_hash: H256,
366 expected_address: Address,
367 ) -> Result<bool> {
368 let sig_bytes = hex::decode(signature.trim_start_matches("0x"))
369 .map_err(|_| X402Error::invalid_signature("Invalid hex signature"))?;
370
371 if sig_bytes.len() != 65 {
372 return Err(X402Error::invalid_signature("Signature must be 65 bytes"));
373 }
374
375 let r = H256::from_slice(&sig_bytes[0..32]);
376 let s = H256::from_slice(&sig_bytes[32..64]);
377 let v = sig_bytes[64];
378
379 let recovery_id = RecoveryId::try_from(v)
380 .map_err(|_| X402Error::invalid_signature("Invalid recovery ID"))?;
381
382 let mut sig_bytes = [0u8; 64];
384 sig_bytes[0..32].copy_from_slice(r.as_bytes());
385 sig_bytes[32..64].copy_from_slice(s.as_bytes());
386
387 let k256_sig = K256Signature::try_from(&sig_bytes[..])
388 .map_err(|_| X402Error::invalid_signature("Invalid signature format"))?;
389
390 let verifying_key =
392 VerifyingKey::recover_from_prehash(message_hash.as_bytes(), &k256_sig, recovery_id)
393 .map_err(|_| X402Error::invalid_signature("Failed to recover public key"))?;
394
395 let recovered_address = ethereum_address_from_pubkey(&verifying_key)?;
397
398 Ok(recovered_address == expected_address)
399 }
400
401 pub fn sign_message_hash(message_hash: H256, private_key: &str) -> Result<String> {
403 let private_key_bytes = hex::decode(private_key.trim_start_matches("0x"))
404 .map_err(|_| X402Error::invalid_signature("Invalid hex private key"))?;
405
406 let secret_key = SecretKey::from_slice(&private_key_bytes)
407 .map_err(|_| X402Error::invalid_signature("Invalid private key"))?;
408
409 let secp = Secp256k1::new();
410 let message = Message::from_digest_slice(message_hash.as_bytes())
411 .map_err(|_| X402Error::invalid_signature("Invalid message hash"))?;
412
413 let signature = secp.sign_ecdsa(&message, &secret_key);
414 let serialized = signature.serialize_compact();
415
416 let recovery_id = compute_recovery_id(&signature, &message, &secret_key)?;
419
420 let _k256_sig = K256Signature::try_from(&serialized[..])
422 .map_err(|_| X402Error::invalid_signature("Failed to convert signature"))?;
423
424 let mut sig_bytes = [0u8; 65];
426 sig_bytes[0..32].copy_from_slice(&serialized[0..32]);
427 sig_bytes[32..64].copy_from_slice(&serialized[32..64]);
428 sig_bytes[64] = recovery_id;
429
430 Ok(format!("0x{}", hex::encode(sig_bytes)))
431 }
432
433 fn ethereum_address_from_pubkey(pubkey: &k256::ecdsa::VerifyingKey) -> Result<Address> {
435 let pubkey_bytes = pubkey.to_sec1_bytes();
436 if pubkey_bytes.len() != 65 {
437 return Err(X402Error::invalid_signature("Invalid public key length"));
438 }
439
440 let pubkey_hash = keccak256(&pubkey_bytes[1..]);
442
443 let mut address_bytes = [0u8; 20];
445 address_bytes.copy_from_slice(&pubkey_hash[12..]);
446
447 Ok(Address::from(address_bytes))
448 }
449
450 fn compute_recovery_id(
452 signature: &secp256k1::ecdsa::Signature,
453 message: &Message,
454 private_key: &SecretKey,
455 ) -> Result<u8> {
456 let secp = Secp256k1::new();
457
458 let public_key = private_key.public_key(&secp);
460
461 for recovery_id in 0..2 {
463 let recovery_id_enum = secp256k1::ecdsa::RecoveryId::from_i32(recovery_id as i32);
465 if let Ok(recovery_id_enum) = recovery_id_enum {
466 if let Ok(recoverable_sig) = secp256k1::ecdsa::RecoverableSignature::from_compact(
468 &signature.serialize_compact(),
469 recovery_id_enum,
470 ) {
471 if let Ok(recovered_key) = secp.recover_ecdsa(message, &recoverable_sig) {
473 if recovered_key == public_key {
475 return Ok(recovery_id);
476 }
477 }
478 }
479 }
480 }
481
482 Err(X402Error::invalid_signature(
483 "Could not determine recovery ID",
484 ))
485 }
486
487 fn keccak256(data: &[u8]) -> [u8; 32] {
489 use sha3::{Digest, Keccak256};
490 Keccak256::digest(data).into()
491 }
492
493 pub fn generate_nonce() -> H256 {
495 use rand::RngCore;
496 let mut bytes = [0u8; 32];
497 rand::thread_rng().fill_bytes(&mut bytes);
498 H256::from_slice(&bytes)
499 }
500
501 pub fn verify_payment_payload(
503 payload: &crate::types::ExactEvmPayload,
504 expected_from: &str,
505 network: &str,
506 ) -> Result<bool> {
507 let from_addr = Address::from_str(expected_from)
508 .map_err(|_| X402Error::invalid_signature("Invalid from address"))?;
509
510 let auth = &payload.authorization;
512
513 let network_config = crate::types::NetworkConfig::from_name(network)
515 .ok_or_else(|| X402Error::invalid_signature("Unsupported network"))?;
516
517 let message_hash = eip712::create_transfer_with_authorization_hash(
518 &eip712::Domain {
519 name: "USD Coin".to_string(),
520 version: "2".to_string(),
521 chain_id: network_config.chain_id,
522 verifying_contract: Address::from_str(&network_config.usdc_contract)
523 .map_err(|_| X402Error::invalid_signature("Invalid verifying contract"))?,
524 },
525 Address::from_str(&auth.from)
526 .map_err(|_| X402Error::invalid_signature("Invalid from address"))?,
527 Address::from_str(&auth.to)
528 .map_err(|_| X402Error::invalid_signature("Invalid to address"))?,
529 U256::from_str_radix(&auth.value, 10)
530 .map_err(|_| X402Error::invalid_signature("Invalid value"))?,
531 U256::from_str_radix(&auth.valid_after, 10)
532 .map_err(|_| X402Error::invalid_signature("Invalid valid_after"))?,
533 U256::from_str_radix(&auth.valid_before, 10)
534 .map_err(|_| X402Error::invalid_signature("Invalid valid_before"))?,
535 H256::from_str(&auth.nonce)
536 .map_err(|_| X402Error::invalid_signature("Invalid nonce"))?,
537 )?;
538
539 verify_eip712_signature(&payload.signature, message_hash, from_addr)
540 }
541}
542
543#[cfg(test)]
544mod tests {
545 use super::*;
546 use ethereum_types::Address;
547
548 #[test]
549 fn test_jwt_creation() {
550 let token = jwt::create_auth_header(
551 "test_key",
552 "test_secret",
553 "api.cdp.coinbase.com",
554 "/platform/v2/x402/verify",
555 );
556 assert!(token.is_ok());
557 assert!(token.unwrap().starts_with("Bearer "));
558 }
559
560 #[test]
561 fn test_domain_creation() {
562 let domain = eip712::Domain {
563 name: "USD Coin".to_string(),
564 version: "2".to_string(),
565 chain_id: 8453,
566 verifying_contract: Address::from_str("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913")
567 .unwrap(),
568 };
569
570 assert_eq!(domain.name, "USD Coin");
571 assert_eq!(domain.version, "2");
572 assert_eq!(domain.chain_id, 8453);
573 }
574
575 #[test]
576 fn test_nonce_generation() {
577 let nonce1 = signature::generate_nonce();
578 let nonce2 = signature::generate_nonce();
579
580 assert_ne!(nonce1, nonce2);
582
583 assert_eq!(nonce1.as_bytes().len(), 32);
585 assert_eq!(nonce2.as_bytes().len(), 32);
586 }
587
588 #[test]
589 fn test_payment_payload_verification() {
590 let auth = crate::types::ExactEvmPayloadAuthorization::new(
592 "0x857b06519E91e3A54538791bDbb0E22373e36b66",
593 "0x209693Bc6afc0C5328bA36FaF03C514EF312287C",
594 "1000000000000000000", "1745323800", "1745323985", "0xf3746613c2d920b5fdabc0856f2aeb2d4f88ee6037b8cc5d04a71a4462f13480", );
599
600 let payload = crate::types::ExactEvmPayload {
601 signature: "0x2d6a7588d6acca505cbf0d9a4a227e0c52c6c34008c8e8986a1283259764173608a2ce6496642e377d6da8dbbf5836e9bd15092f9ecab05ded3d6293af148b571c".to_string(),
602 authorization: auth,
603 };
604
605 let result = signature::verify_payment_payload(
607 &payload,
608 "0x857b06519E91e3A54538791bDbb0E22373e36b66",
609 "base-sepolia",
610 );
611 match result {
612 Ok(_) => println!("Verification succeeded"),
613 Err(e) => println!("Verification failed with error: {}", e),
614 }
615 let _ = result;
618 }
619
620 #[test]
621 fn test_invalid_payment_payload_validation() {
622 let auth = crate::types::ExactEvmPayloadAuthorization::new(
624 "0x857b06519E91e3A54538791bDbb0E22373e36b66",
625 "0x209693Bc6afc0C5328bA36FaF03C514EF312287C",
626 "1000000",
627 "1745323800",
628 "1745323985",
629 "0xf3746613c2d920b5fdabc0856f2aeb2d4f88ee6037b8cc5d04a71a4462f13480",
630 );
631
632 let payload = crate::types::ExactEvmPayload {
633 signature: "0x2d6a7588d6acca505cbf0d9a4a227e0c52c6c34008c8e8986a1283259764173608a2ce6496642e377d6da8dbbf5836e9bd15092f9ecab05ded3d6293af148b571c".to_string(),
634 authorization: auth,
635 };
636
637 let valid_payment_payload = crate::types::PaymentPayload {
639 x402_version: 1,
640 scheme: "exact".to_string(),
641 network: "base-sepolia".to_string(),
642 payload: payload.clone(),
643 };
644
645 let result = signature::verify_payment_payload(
647 &valid_payment_payload.payload,
648 "0x857b06519E91e3A54538791bDbb0E22373e36b66",
649 "base-sepolia",
650 );
651
652 match result {
654 Ok(_) => println!("Verification succeeded"),
655 Err(e) => println!("Verification failed with error: {}", e),
656 }
657
658 }
661}