1use std::time::Duration;
41
42use borsh::BorshSerialize;
43use serde::{Deserialize, Deserializer, Serialize, Serializer};
44use serde_with::{hex::Hex, serde_as};
45
46use crate::Near;
47use crate::error::Error;
48use crate::types::{AccountId, BlockReference, CryptoHash, PublicKey, Signature};
49
50pub const NEP413_TAG: u32 = (1 << 31) + 413;
55
56pub const DEFAULT_MAX_AGE: Duration = Duration::from_secs(5 * 60);
58
59#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct SignMessageParams {
66 pub message: String,
68
69 pub recipient: String,
71
72 pub nonce: [u8; 32],
75
76 pub callback_url: Option<String>,
78
79 pub state: Option<String>,
81}
82
83#[serde_as]
101#[derive(Debug, Clone, Serialize, Deserialize)]
102#[serde(rename_all = "camelCase")]
103pub struct AuthPayload {
104 pub signed_message: SignedMessage,
106
107 #[serde_as(as = "Hex")]
109 pub nonce: [u8; 32],
110
111 pub message: String,
113
114 pub recipient: String,
116
117 #[serde(skip_serializing_if = "Option::is_none")]
119 pub callback_url: Option<String>,
120}
121
122impl AuthPayload {
123 pub fn to_params(&self) -> SignMessageParams {
125 SignMessageParams {
126 message: self.message.clone(),
127 recipient: self.recipient.clone(),
128 nonce: self.nonce,
129 callback_url: self.callback_url.clone(),
130 state: self.signed_message.state.clone(),
131 }
132 }
133
134 pub fn from_signed(signed_message: SignedMessage, params: &SignMessageParams) -> Self {
168 Self {
169 signed_message,
170 nonce: params.nonce,
171 message: params.message.clone(),
172 recipient: params.recipient.clone(),
173 callback_url: params.callback_url.clone(),
174 }
175 }
176}
177
178#[derive(BorshSerialize)]
180struct Nep413Payload {
181 message: String,
182 nonce: [u8; 32],
183 recipient: String,
184 callback_url: Option<String>,
185}
186
187#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
189#[serde(rename_all = "camelCase")]
190pub struct SignedMessage {
191 pub account_id: AccountId,
193
194 pub public_key: PublicKey,
196
197 #[serde(
199 serialize_with = "serialize_signature_base64",
200 deserialize_with = "deserialize_signature_flexible"
201 )]
202 pub signature: Signature,
203
204 #[serde(skip_serializing_if = "Option::is_none")]
206 pub state: Option<String>,
207}
208
209#[derive(Debug, Clone)]
211pub struct VerifyOptions {
212 pub max_age: Duration,
216
217 pub require_full_access: bool,
221}
222
223impl Default for VerifyOptions {
224 fn default() -> Self {
225 Self {
226 max_age: DEFAULT_MAX_AGE,
227 require_full_access: true,
228 }
229 }
230}
231
232pub fn generate_nonce() -> [u8; 32] {
251 let mut nonce = [0u8; 32];
252
253 let timestamp = std::time::SystemTime::now()
255 .duration_since(std::time::UNIX_EPOCH)
256 .expect("Time went backwards")
257 .as_millis() as u64;
258 nonce[..8].copy_from_slice(×tamp.to_be_bytes());
259
260 rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut nonce[8..]);
262
263 nonce
264}
265
266pub fn extract_timestamp_from_nonce(nonce: &[u8; 32]) -> u64 {
268 u64::from_be_bytes(nonce[..8].try_into().unwrap())
269}
270
271pub fn serialize_message(params: &SignMessageParams) -> CryptoHash {
295 let tag_bytes = NEP413_TAG.to_le_bytes();
297
298 let payload = Nep413Payload {
300 message: params.message.clone(),
301 nonce: params.nonce,
302 recipient: params.recipient.clone(),
303 callback_url: params.callback_url.clone(),
304 };
305 let payload_bytes = borsh::to_vec(&payload).expect("Borsh serialization should not fail");
306
307 let mut combined = Vec::with_capacity(tag_bytes.len() + payload_bytes.len());
309 combined.extend_from_slice(&tag_bytes);
310 combined.extend_from_slice(&payload_bytes);
311
312 CryptoHash::hash(&combined)
314}
315
316pub fn verify_signature(
328 signed: &SignedMessage,
329 params: &SignMessageParams,
330 max_age: Duration,
331) -> bool {
332 if max_age != Duration::MAX {
334 let timestamp_ms = extract_timestamp_from_nonce(¶ms.nonce);
335 let now_ms = std::time::SystemTime::now()
336 .duration_since(std::time::UNIX_EPOCH)
337 .expect("Time went backwards")
338 .as_millis() as u64;
339
340 let age_ms = now_ms.saturating_sub(timestamp_ms);
341
342 if age_ms > max_age.as_millis() as u64 || timestamp_ms > now_ms {
344 return false;
345 }
346 }
347
348 let hash = serialize_message(params);
350
351 signed.signature.verify(hash.as_bytes(), &signed.public_key)
353}
354
355pub async fn verify(
385 signed: &SignedMessage,
386 params: &SignMessageParams,
387 near: &Near,
388 options: VerifyOptions,
389) -> Result<bool, Error> {
390 if !verify_signature(signed, params, options.max_age) {
392 return Ok(false);
393 }
394
395 if options.require_full_access {
397 let access_key_result = near
399 .rpc()
400 .view_access_key(
401 &signed.account_id,
402 &signed.public_key,
403 BlockReference::optimistic(),
404 )
405 .await;
406
407 match access_key_result {
408 Ok(access_key) => {
409 if !matches!(
411 access_key.permission,
412 crate::types::AccessKeyPermissionView::FullAccess
413 ) {
414 return Ok(false);
415 }
416 }
417 Err(_) => {
418 return Ok(false);
420 }
421 }
422 }
423
424 Ok(true)
425}
426
427fn serialize_signature_base64<S>(signature: &Signature, serializer: S) -> Result<S::Ok, S::Error>
433where
434 S: Serializer,
435{
436 use base64::prelude::*;
437 let base64_str = BASE64_STANDARD.encode(signature.as_bytes());
438 serializer.serialize_str(&base64_str)
439}
440
441fn deserialize_signature_flexible<'de, D>(deserializer: D) -> Result<Signature, D::Error>
446where
447 D: Deserializer<'de>,
448{
449 use base64::prelude::*;
450 use serde::de::Error;
451
452 let s: String = String::deserialize(deserializer)?;
453
454 if let Ok(bytes) = BASE64_STANDARD.decode(&s) {
456 if bytes.len() == 64 {
457 return Ok(Signature::ed25519_from_bytes(
458 bytes
459 .try_into()
460 .map_err(|_| D::Error::custom("Invalid signature length"))?,
461 ));
462 }
463 }
464
465 if let Some(data) = s.strip_prefix("ed25519:") {
467 let bytes = bs58::decode(data)
468 .into_vec()
469 .map_err(|e| D::Error::custom(format!("Invalid base58: {}", e)))?;
470 if bytes.len() == 64 {
471 return Ok(Signature::ed25519_from_bytes(
472 bytes
473 .try_into()
474 .map_err(|_| D::Error::custom("Invalid signature length"))?,
475 ));
476 }
477 }
478
479 if let Ok(bytes) = bs58::decode(&s).into_vec() {
481 if bytes.len() == 64 {
482 return Ok(Signature::ed25519_from_bytes(
483 bytes
484 .try_into()
485 .map_err(|_| D::Error::custom("Invalid signature length"))?,
486 ));
487 }
488 }
489
490 Err(D::Error::custom(
491 "Invalid signature format. Expected base64, ed25519:base58, or plain base58",
492 ))
493}
494
495#[cfg(test)]
500mod tests {
501 use super::*;
502
503 #[test]
504 fn test_generate_nonce() {
505 let nonce1 = generate_nonce();
506 let nonce2 = generate_nonce();
507
508 assert_eq!(nonce1.len(), 32);
509 assert_eq!(nonce2.len(), 32);
510
511 assert_ne!(nonce1, nonce2);
513
514 let ts1 = extract_timestamp_from_nonce(&nonce1);
516 let now = std::time::SystemTime::now()
517 .duration_since(std::time::UNIX_EPOCH)
518 .unwrap()
519 .as_millis() as u64;
520 assert!(now - ts1 < 1000);
521 }
522
523 #[test]
524 fn test_serialize_message() {
525 let params = SignMessageParams {
526 message: "Hello NEAR!".to_string(),
527 recipient: "example.near".to_string(),
528 nonce: [0u8; 32],
529 callback_url: None,
530 state: None,
531 };
532
533 let hash = serialize_message(¶ms);
534
535 assert_eq!(hash.as_bytes().len(), 32);
537
538 let hash2 = serialize_message(¶ms);
540 assert_eq!(hash, hash2);
541
542 let params2 = SignMessageParams {
544 message: "Hello NEAR!".to_string(),
545 recipient: "other.near".to_string(),
546 nonce: [0u8; 32],
547 callback_url: None,
548 state: None,
549 };
550 let hash3 = serialize_message(¶ms2);
551 assert_ne!(hash, hash3);
552 }
553
554 #[test]
555 fn test_signed_message_json_roundtrip() {
556 use crate::types::SecretKey;
557
558 let secret = SecretKey::generate_ed25519();
559 let params = SignMessageParams {
560 message: "Test".to_string(),
561 recipient: "app.near".to_string(),
562 nonce: generate_nonce(),
563 callback_url: None,
564 state: Some("csrf_token".to_string()),
565 };
566
567 let hash = serialize_message(¶ms);
568 let signature = secret.sign(hash.as_bytes());
569
570 let signed = SignedMessage {
571 account_id: "alice.near".parse().unwrap(),
572 public_key: secret.public_key(),
573 signature,
574 state: params.state.clone(),
575 };
576
577 let json = serde_json::to_string(&signed).unwrap();
579
580 let json_value: serde_json::Value = serde_json::from_str(&json).unwrap();
584 let sig_str = json_value["signature"].as_str().unwrap();
585 assert!(
587 !sig_str.contains(':'),
588 "Signature should be base64, not prefixed format: {}",
589 sig_str
590 );
591
592 let deserialized: SignedMessage = serde_json::from_str(&json).unwrap();
594 assert_eq!(signed.account_id, deserialized.account_id);
595 assert_eq!(signed.public_key, deserialized.public_key);
596 assert_eq!(
597 signed.signature.as_bytes(),
598 deserialized.signature.as_bytes()
599 );
600 assert_eq!(signed.state, deserialized.state);
601 }
602
603 #[test]
604 fn test_verify_signature_basic() {
605 use crate::types::SecretKey;
606
607 let secret = SecretKey::generate_ed25519();
608 let params = SignMessageParams {
609 message: "Test message".to_string(),
610 recipient: "myapp.com".to_string(),
611 nonce: generate_nonce(),
612 callback_url: None,
613 state: None,
614 };
615
616 let hash = serialize_message(¶ms);
617 let signature = secret.sign(hash.as_bytes());
618
619 let signed = SignedMessage {
620 account_id: "alice.near".parse().unwrap(),
621 public_key: secret.public_key(),
622 signature,
623 state: None,
624 };
625
626 assert!(verify_signature(&signed, ¶ms, DEFAULT_MAX_AGE));
628
629 let wrong_params = SignMessageParams {
631 message: "Wrong message".to_string(),
632 ..params.clone()
633 };
634 assert!(!verify_signature(&signed, &wrong_params, DEFAULT_MAX_AGE));
635
636 let other_secret = SecretKey::generate_ed25519();
638 let wrong_signed = SignedMessage {
639 public_key: other_secret.public_key(),
640 ..signed.clone()
641 };
642 assert!(!verify_signature(&wrong_signed, ¶ms, DEFAULT_MAX_AGE));
643 }
644
645 #[test]
646 fn test_verify_signature_expiration() {
647 use crate::types::SecretKey;
648
649 let secret = SecretKey::generate_ed25519();
650
651 let mut old_nonce = [0u8; 32];
653 let old_timestamp = std::time::SystemTime::now()
654 .duration_since(std::time::UNIX_EPOCH)
655 .unwrap()
656 .as_millis() as u64
657 - (10 * 60 * 1000); old_nonce[..8].copy_from_slice(&old_timestamp.to_be_bytes());
659
660 let params = SignMessageParams {
661 message: "Test".to_string(),
662 recipient: "app.com".to_string(),
663 nonce: old_nonce,
664 callback_url: None,
665 state: None,
666 };
667
668 let hash = serialize_message(¶ms);
669 let signature = secret.sign(hash.as_bytes());
670
671 let signed = SignedMessage {
672 account_id: "alice.near".parse().unwrap(),
673 public_key: secret.public_key(),
674 signature,
675 state: None,
676 };
677
678 assert!(!verify_signature(&signed, ¶ms, DEFAULT_MAX_AGE));
680
681 assert!(verify_signature(
683 &signed,
684 ¶ms,
685 Duration::from_secs(15 * 60)
686 ));
687
688 assert!(verify_signature(&signed, ¶ms, Duration::MAX));
690 }
691
692 #[test]
696 fn test_typescript_interoperability() {
697 use base64::prelude::*;
698
699 let nonce_base64 = "KNV0cOpvJ50D5vfF9pqWom8wo2sliQ4W+Wa7uZ3Uk6Y=";
705 let nonce_bytes = BASE64_STANDARD.decode(nonce_base64).unwrap();
706 let nonce: [u8; 32] = nonce_bytes.try_into().unwrap();
707
708 let params_no_callback = SignMessageParams {
710 message: "Hello NEAR!".to_string(),
711 recipient: "example.near".to_string(),
712 nonce,
713 callback_url: None,
714 state: None,
715 };
716
717 let expected_sig_no_callback = "NnJgPU1Ql7ccRTITIoOVsIfElmvH1RV7QAT4a9Vh6ShCOnjIzRwxqX54JzoQ/nK02p7VBMI2vJn48rpImIJwAw==";
718
719 let hash = serialize_message(¶ms_no_callback);
721
722 let public_key: crate::types::PublicKey =
724 "ed25519:2RM3EotCzEiVobm6aMjaup43k8cFffR4KHFtrqbZ79Qy"
725 .parse()
726 .unwrap();
727
728 let sig_bytes = BASE64_STANDARD.decode(expected_sig_no_callback).unwrap();
730 let signature = crate::types::Signature::ed25519_from_bytes(
731 sig_bytes.try_into().expect("signature should be 64 bytes"),
732 );
733
734 assert!(
736 signature.verify(hash.as_bytes(), &public_key),
737 "Signature verification failed - serialization mismatch with TypeScript"
738 );
739
740 let params_with_callback = SignMessageParams {
742 message: "Hello NEAR!".to_string(),
743 recipient: "example.near".to_string(),
744 nonce,
745 callback_url: Some("http://localhost:3000".to_string()),
746 state: None,
747 };
748
749 let expected_sig_with_callback = "zzZQ/GwAjrZVrTIFlvmmQbDQHllfzrr8urVWHaRt5cPfcXaCSZo35c5LDpPpTKivR6BxLyb3lcPM0FfCW5lcBQ==";
750
751 let hash = serialize_message(¶ms_with_callback);
752 let sig_bytes = BASE64_STANDARD.decode(expected_sig_with_callback).unwrap();
753 let signature = crate::types::Signature::ed25519_from_bytes(
754 sig_bytes.try_into().expect("signature should be 64 bytes"),
755 );
756
757 assert!(
758 signature.verify(hash.as_bytes(), &public_key),
759 "Signature verification with callback_url failed - serialization mismatch"
760 );
761 }
762
763 #[test]
765 fn test_deserialize_typescript_signed_message() {
766 let ts_json = r#"{
769 "accountId": "alice.testnet",
770 "publicKey": "ed25519:2RM3EotCzEiVobm6aMjaup43k8cFffR4KHFtrqbZ79Qy",
771 "signature": "NnJgPU1Ql7ccRTITIoOVsIfElmvH1RV7QAT4a9Vh6ShCOnjIzRwxqX54JzoQ/nK02p7VBMI2vJn48rpImIJwAw=="
772 }"#;
773
774 let signed: SignedMessage = serde_json::from_str(ts_json).unwrap();
776
777 assert_eq!(signed.account_id.as_str(), "alice.testnet");
778 assert_eq!(
779 signed.public_key.to_string(),
780 "ed25519:2RM3EotCzEiVobm6aMjaup43k8cFffR4KHFtrqbZ79Qy"
781 );
782 assert_eq!(signed.signature.as_bytes().len(), 64);
783 assert!(signed.state.is_none());
784 }
785
786 #[test]
788 fn test_deserialize_legacy_base58_signature() {
789 let legacy_json = r#"{
791 "accountId": "alice.testnet",
792 "publicKey": "ed25519:HeEp8gQPzs6rMPRN1hijJ7dXFmZLu3FPNKeLDpmLfFBT",
793 "signature": "ed25519:2DzVcjvceXbR6n9ot4C9xA8gVPrZRq8NqJj4b3DaLBmVk1TqXwK8yHcL6M6ezQD4HxXHhZQPbgjdNW7Tx8sjxSFe"
794 }"#;
795
796 let signed: SignedMessage = serde_json::from_str(legacy_json).unwrap();
798
799 assert_eq!(signed.account_id.as_str(), "alice.testnet");
800 assert_eq!(signed.signature.as_bytes().len(), 64);
801 }
802
803 #[test]
805 fn test_rust_to_typescript_roundtrip() {
806 use crate::types::SecretKey;
807
808 let secret = SecretKey::generate_ed25519();
809 let params = SignMessageParams {
810 message: "Cross-platform test".to_string(),
811 recipient: "myapp.com".to_string(),
812 nonce: generate_nonce(),
813 callback_url: None,
814 state: Some("session123".to_string()),
815 };
816
817 let hash = serialize_message(¶ms);
818 let signature = secret.sign(hash.as_bytes());
819
820 let signed = SignedMessage {
821 account_id: "alice.near".parse().unwrap(),
822 public_key: secret.public_key(),
823 signature,
824 state: params.state.clone(),
825 };
826
827 let json = serde_json::to_string(&signed).unwrap();
829
830 let json_value: serde_json::Value = serde_json::from_str(&json).unwrap();
832
833 assert!(json_value.get("accountId").is_some());
835 assert!(json_value.get("publicKey").is_some());
836 assert!(json_value.get("signature").is_some());
837
838 let sig_str = json_value["signature"].as_str().unwrap();
840 assert!(!sig_str.contains(':'));
841
842 let roundtrip: SignedMessage = serde_json::from_str(&json).unwrap();
844 assert_eq!(signed.account_id, roundtrip.account_id);
845 assert_eq!(signed.public_key, roundtrip.public_key);
846 assert_eq!(signed.signature.as_bytes(), roundtrip.signature.as_bytes());
847 assert_eq!(signed.state, roundtrip.state);
848
849 assert!(verify_signature(&roundtrip, ¶ms, Duration::MAX));
851 }
852
853 #[test]
855 fn test_deserialize_http_auth_payload() {
856 let nonce_hex = "28d57470ea6f279d03e6f7c5f69a96a26f30a36b25890e16f966bbb99dd493a6";
859
860 let http_payload = serde_json::json!({
862 "signedMessage": {
863 "accountId": "alice.testnet",
864 "publicKey": "ed25519:2RM3EotCzEiVobm6aMjaup43k8cFffR4KHFtrqbZ79Qy",
865 "signature": "NnJgPU1Ql7ccRTITIoOVsIfElmvH1RV7QAT4a9Vh6ShCOnjIzRwxqX54JzoQ/nK02p7VBMI2vJn48rpImIJwAw=="
866 },
867 "nonce": nonce_hex,
868 "message": "Hello NEAR!",
869 "recipient": "example.near"
870 });
871
872 let payload: AuthPayload = serde_json::from_value(http_payload).unwrap();
874
875 assert_eq!(payload.signed_message.account_id.as_str(), "alice.testnet");
877 assert_eq!(payload.message, "Hello NEAR!");
878 assert_eq!(payload.recipient, "example.near");
879 assert_eq!(payload.nonce.len(), 32);
880
881 let params = payload.to_params();
883 assert!(verify_signature(
884 &payload.signed_message,
885 ¶ms,
886 Duration::MAX
887 ));
888 }
889
890 #[test]
892 fn test_full_auth_flow_interop() {
893 let http_body = r#"{
896 "signedMessage": {
897 "accountId": "alice.testnet",
898 "publicKey": "ed25519:2RM3EotCzEiVobm6aMjaup43k8cFffR4KHFtrqbZ79Qy",
899 "signature": "NnJgPU1Ql7ccRTITIoOVsIfElmvH1RV7QAT4a9Vh6ShCOnjIzRwxqX54JzoQ/nK02p7VBMI2vJn48rpImIJwAw=="
900 },
901 "nonce": "28d57470ea6f279d03e6f7c5f69a96a26f30a36b25890e16f966bbb99dd493a6",
902 "message": "Hello NEAR!",
903 "recipient": "example.near"
904 }"#;
905
906 let payload: AuthPayload = serde_json::from_str(http_body).unwrap();
908
909 let params = payload.to_params();
911 let is_valid = verify_signature(&payload.signed_message, ¶ms, Duration::MAX);
912
913 assert!(is_valid, "Signature should be valid");
914 assert_eq!(payload.signed_message.account_id.as_str(), "alice.testnet");
915 }
916
917 #[test]
919 fn test_generate_auth_payload_from_rust() {
920 use crate::types::SecretKey;
921
922 let secret = SecretKey::generate_ed25519();
923 let params = SignMessageParams {
924 message: "Sign in to My App".to_string(),
925 recipient: "myapp.com".to_string(),
926 nonce: generate_nonce(),
927 callback_url: None,
928 state: None,
929 };
930
931 let hash = serialize_message(¶ms);
932 let signature = secret.sign(hash.as_bytes());
933
934 let signed = SignedMessage {
935 account_id: "alice.near".parse().unwrap(),
936 public_key: secret.public_key(),
937 signature,
938 state: None,
939 };
940
941 let payload = AuthPayload::from_signed(signed.clone(), ¶ms);
943
944 let json = serde_json::to_string(&payload).unwrap();
946
947 let json_value: serde_json::Value = serde_json::from_str(&json).unwrap();
949 let nonce_str = json_value["nonce"].as_str().unwrap();
950 assert!(
951 nonce_str.len() == 64, "Nonce should be hex encoded, got: {}",
953 nonce_str
954 );
955
956 let roundtrip: AuthPayload = serde_json::from_str(&json).unwrap();
958 assert_eq!(payload.nonce, roundtrip.nonce);
959 assert_eq!(payload.message, roundtrip.message);
960
961 let roundtrip_params = roundtrip.to_params();
963 assert!(verify_signature(
964 &roundtrip.signed_message,
965 &roundtrip_params,
966 Duration::MAX
967 ));
968 }
969}