1use crate::account::account::{Account, AuthenticationKey};
102use crate::crypto::{Ed25519PrivateKey, Ed25519PublicKey, KEYLESS_SCHEME};
103use crate::error::{AptosError, AptosResult};
104use crate::types::AccountAddress;
105use jsonwebtoken::{Algorithm, DecodingKey, Validation, decode, decode_header};
106use rand::RngCore;
107use serde::{Deserialize, Serialize};
108use sha3::{Digest, Sha3_256};
109use std::fmt;
110use std::time::{Duration, SystemTime, UNIX_EPOCH};
111use url::Url;
112
113pub use jsonwebtoken::jwk::JwkSet;
115
116#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
118pub struct KeylessSignature {
119 pub ephemeral_public_key: Vec<u8>,
121 pub ephemeral_signature: Vec<u8>,
123 pub proof: Vec<u8>,
125}
126
127impl KeylessSignature {
128 pub fn to_bcs(&self) -> AptosResult<Vec<u8>> {
134 aptos_bcs::to_bytes(self).map_err(AptosError::bcs)
135 }
136}
137
138#[derive(Clone)]
140pub struct EphemeralKeyPair {
141 private_key: Ed25519PrivateKey,
142 public_key: Ed25519PublicKey,
143 expiry: SystemTime,
144 nonce: String,
145}
146
147impl EphemeralKeyPair {
148 pub fn generate(expiry_secs: u64) -> Self {
150 let private_key = Ed25519PrivateKey::generate();
151 let public_key = private_key.public_key();
152 let nonce = {
153 let mut bytes = [0u8; 16];
154 rand::rngs::OsRng.fill_bytes(&mut bytes);
155 const_hex::encode(bytes)
156 };
157 Self {
158 private_key,
159 public_key,
160 expiry: SystemTime::now() + Duration::from_secs(expiry_secs),
161 nonce,
162 }
163 }
164
165 pub fn is_expired(&self) -> bool {
167 SystemTime::now() >= self.expiry
168 }
169
170 pub fn nonce(&self) -> &str {
172 &self.nonce
173 }
174
175 pub fn public_key(&self) -> &Ed25519PublicKey {
177 &self.public_key
178 }
179
180 pub fn to_snapshot(&self) -> AptosResult<EphemeralKeyPairSnapshot> {
190 let expiry_unix_secs = self
191 .expiry
192 .duration_since(UNIX_EPOCH)
193 .map_err(|_| AptosError::InvalidJwt("ephemeral key expiry before UNIX epoch".into()))?
194 .as_secs();
195 Ok(EphemeralKeyPairSnapshot {
196 private_key: self.private_key.to_bytes().to_vec(),
197 expiry_unix_secs,
198 nonce: self.nonce.clone(),
199 })
200 }
201
202 pub fn from_snapshot(snapshot: &EphemeralKeyPairSnapshot) -> AptosResult<Self> {
208 let private_key = Ed25519PrivateKey::from_bytes(&snapshot.private_key)?;
209 let public_key = private_key.public_key();
210 Ok(Self {
211 private_key,
212 public_key,
213 expiry: UNIX_EPOCH + Duration::from_secs(snapshot.expiry_unix_secs),
214 nonce: snapshot.nonce.clone(),
215 })
216 }
217
218 pub fn save_to_file(&self, path: &std::path::Path) -> AptosResult<()> {
224 let snapshot = self.to_snapshot()?;
225 let json = serde_json::to_string_pretty(&snapshot)
226 .map_err(|e| AptosError::InvalidJwt(format!("failed to encode snapshot: {e}")))?;
227 std::fs::write(path, json).map_err(|e| {
228 AptosError::Internal(format!("failed to write ephemeral key file: {e}"))
229 })?;
230 Ok(())
231 }
232
233 pub fn load_from_file(path: &std::path::Path) -> AptosResult<Self> {
240 let contents = std::fs::read_to_string(path)
241 .map_err(|e| AptosError::Internal(format!("failed to read ephemeral key file: {e}")))?;
242 let snapshot: EphemeralKeyPairSnapshot = serde_json::from_str(&contents)
243 .map_err(|e| AptosError::InvalidJwt(format!("failed to decode snapshot: {e}")))?;
244 Self::from_snapshot(&snapshot)
245 }
246}
247
248#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
254pub struct EphemeralKeyPairSnapshot {
255 private_key: Vec<u8>,
257 expiry_unix_secs: u64,
259 nonce: String,
261}
262
263impl fmt::Debug for EphemeralKeyPair {
264 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
265 f.debug_struct("EphemeralKeyPair")
266 .field("public_key", &self.public_key)
267 .field("expiry", &self.expiry)
268 .field("nonce", &self.nonce)
269 .finish_non_exhaustive()
270 }
271}
272
273#[derive(Clone, Debug, PartialEq, Eq)]
275pub enum OidcProvider {
276 Google,
278 Apple,
280 Microsoft,
282 Custom {
284 issuer: String,
286 jwks_url: String,
288 },
289}
290
291impl OidcProvider {
292 pub fn issuer(&self) -> &str {
294 match self {
295 OidcProvider::Google => "https://accounts.google.com",
296 OidcProvider::Apple => "https://appleid.apple.com",
297 OidcProvider::Microsoft => "https://login.microsoftonline.com/common/v2.0",
298 OidcProvider::Custom { issuer, .. } => issuer,
299 }
300 }
301
302 pub fn jwks_url(&self) -> &str {
304 match self {
305 OidcProvider::Google => "https://www.googleapis.com/oauth2/v3/certs",
306 OidcProvider::Apple => "https://appleid.apple.com/auth/keys",
307 OidcProvider::Microsoft => {
308 "https://login.microsoftonline.com/common/discovery/v2.0/keys"
309 }
310 OidcProvider::Custom { jwks_url, .. } => jwks_url,
311 }
312 }
313
314 pub fn from_issuer(issuer: &str) -> Self {
325 match issuer {
326 "https://accounts.google.com" => OidcProvider::Google,
327 "https://appleid.apple.com" => OidcProvider::Apple,
328 "https://login.microsoftonline.com/common/v2.0" => OidcProvider::Microsoft,
329 _ => {
330 let jwks_url = if issuer.starts_with("https://") {
335 format!("{issuer}/.well-known/jwks.json")
336 } else {
337 String::new()
341 };
342 OidcProvider::Custom {
343 issuer: issuer.to_string(),
344 jwks_url,
345 }
346 }
347 }
348 }
349}
350
351#[derive(Clone, PartialEq, Eq, zeroize::Zeroize, zeroize::ZeroizeOnDrop)]
359pub struct Pepper(Vec<u8>);
360
361impl std::fmt::Debug for Pepper {
362 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
363 write!(f, "Pepper(REDACTED)")
364 }
365}
366
367impl Pepper {
368 pub fn new(bytes: Vec<u8>) -> Self {
370 Self(bytes)
371 }
372
373 pub fn as_bytes(&self) -> &[u8] {
375 &self.0
376 }
377
378 pub fn from_hex(hex_str: &str) -> AptosResult<Self> {
384 Ok(Self(const_hex::decode(hex_str)?))
385 }
386
387 pub fn to_hex(&self) -> String {
389 const_hex::encode_prefixed(&self.0)
390 }
391}
392
393#[derive(Clone, Debug, PartialEq, Eq)]
395pub struct ZkProof(Vec<u8>);
396
397impl ZkProof {
398 pub fn new(bytes: Vec<u8>) -> Self {
400 Self(bytes)
401 }
402
403 pub fn as_bytes(&self) -> &[u8] {
405 &self.0
406 }
407
408 pub fn from_hex(hex_str: &str) -> AptosResult<Self> {
414 Ok(Self(const_hex::decode(hex_str)?))
415 }
416
417 pub fn to_hex(&self) -> String {
419 const_hex::encode_prefixed(&self.0)
420 }
421}
422
423pub trait PepperService: Send + Sync {
425 fn get_pepper(
427 &self,
428 jwt: &str,
429 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = AptosResult<Pepper>> + Send + '_>>;
430}
431
432pub trait ProverService: Send + Sync {
434 fn generate_proof<'a>(
436 &'a self,
437 jwt: &'a str,
438 ephemeral_key: &'a EphemeralKeyPair,
439 pepper: &'a Pepper,
440 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = AptosResult<ZkProof>> + Send + 'a>>;
441}
442
443#[derive(Clone, Debug)]
445pub struct HttpPepperService {
446 url: Url,
447 client: reqwest::Client,
448}
449
450impl HttpPepperService {
451 pub fn new(url: Url) -> Self {
453 Self {
454 url,
455 client: reqwest::Client::new(),
456 }
457 }
458}
459
460#[derive(Serialize)]
461struct PepperRequest<'a> {
462 jwt: &'a str,
463}
464
465#[derive(Deserialize)]
466struct PepperResponse {
467 pepper: String,
468}
469
470impl PepperService for HttpPepperService {
471 fn get_pepper(
472 &self,
473 jwt: &str,
474 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = AptosResult<Pepper>> + Send + '_>> {
475 let jwt = jwt.to_owned();
476 Box::pin(async move {
477 let response = self
478 .client
479 .post(self.url.clone())
480 .json(&PepperRequest { jwt: &jwt })
481 .send()
482 .await?
483 .error_for_status()?;
484
485 let bytes =
487 crate::config::read_response_bounded(response, MAX_JWKS_RESPONSE_SIZE).await?;
488 let payload: PepperResponse = serde_json::from_slice(&bytes).map_err(|e| {
489 AptosError::InvalidJwt(format!("failed to parse pepper response: {e}"))
490 })?;
491 Pepper::from_hex(&payload.pepper)
492 })
493 }
494}
495
496#[derive(Clone, Debug)]
498pub struct HttpProverService {
499 url: Url,
500 client: reqwest::Client,
501}
502
503impl HttpProverService {
504 pub fn new(url: Url) -> Self {
506 Self {
507 url,
508 client: reqwest::Client::new(),
509 }
510 }
511}
512
513#[derive(Serialize)]
514struct ProverRequest<'a> {
515 jwt: &'a str,
516 ephemeral_public_key: String,
517 nonce: &'a str,
518 pepper: String,
519}
520
521#[derive(Deserialize)]
522struct ProverResponse {
523 proof: String,
524}
525
526impl ProverService for HttpProverService {
527 fn generate_proof<'a>(
528 &'a self,
529 jwt: &'a str,
530 ephemeral_key: &'a EphemeralKeyPair,
531 pepper: &'a Pepper,
532 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = AptosResult<ZkProof>> + Send + 'a>>
533 {
534 Box::pin(async move {
535 let request = ProverRequest {
536 jwt,
537 ephemeral_public_key: const_hex::encode_prefixed(
538 ephemeral_key.public_key.to_bytes(),
539 ),
540 nonce: ephemeral_key.nonce(),
541 pepper: pepper.to_hex(),
542 };
543
544 let response = self
545 .client
546 .post(self.url.clone())
547 .json(&request)
548 .send()
549 .await?
550 .error_for_status()?;
551
552 let bytes =
554 crate::config::read_response_bounded(response, MAX_JWKS_RESPONSE_SIZE).await?;
555 let payload: ProverResponse = serde_json::from_slice(&bytes).map_err(|e| {
556 AptosError::InvalidJwt(format!("failed to parse prover response: {e}"))
557 })?;
558 ZkProof::from_hex(&payload.proof)
559 })
560 }
561}
562
563pub struct KeylessAccount {
565 ephemeral_key: EphemeralKeyPair,
566 provider: OidcProvider,
567 issuer: String,
568 audience: String,
569 user_id: String,
570 pepper: Pepper,
571 proof: ZkProof,
572 address: AccountAddress,
573 auth_key: AuthenticationKey,
574 jwt_expiration: Option<SystemTime>,
575}
576
577impl KeylessAccount {
578 pub async fn from_jwt(
605 jwt: &str,
606 ephemeral_key: EphemeralKeyPair,
607 pepper_service: &dyn PepperService,
608 prover_service: &dyn ProverService,
609 ) -> AptosResult<Self> {
610 let unverified_claims = decode_claims_unverified(jwt)?;
612 let issuer = unverified_claims
613 .iss
614 .as_ref()
615 .ok_or_else(|| AptosError::InvalidJwt("missing iss claim".into()))?;
616
617 let provider = OidcProvider::from_issuer(issuer);
619 let client = reqwest::Client::builder()
620 .timeout(JWKS_FETCH_TIMEOUT)
621 .build()
622 .map_err(|e| AptosError::InvalidJwt(format!("failed to create HTTP client: {e}")))?;
623 let jwks = fetch_jwks(&client, provider.jwks_url()).await?;
624
625 let claims = decode_and_verify_jwt(jwt, &jwks)?;
627 let (issuer, audience, user_id, exp, nonce) = extract_claims(&claims)?;
628
629 if nonce != ephemeral_key.nonce() {
630 return Err(AptosError::InvalidJwt("JWT nonce mismatch".into()));
631 }
632
633 let pepper = pepper_service.get_pepper(jwt).await?;
634 let proof = prover_service
635 .generate_proof(jwt, &ephemeral_key, &pepper)
636 .await?;
637
638 let address = derive_keyless_address(&issuer, &audience, &user_id, &pepper);
639 let auth_key = AuthenticationKey::new(address.to_bytes());
640
641 Ok(Self {
642 provider: OidcProvider::from_issuer(&issuer),
643 issuer,
644 audience,
645 user_id,
646 pepper,
647 proof,
648 address,
649 auth_key,
650 jwt_expiration: exp,
651 ephemeral_key,
652 })
653 }
654
655 pub async fn from_jwt_with_jwks(
672 jwt: &str,
673 jwks: &JwkSet,
674 ephemeral_key: EphemeralKeyPair,
675 pepper_service: &dyn PepperService,
676 prover_service: &dyn ProverService,
677 ) -> AptosResult<Self> {
678 let claims = decode_and_verify_jwt(jwt, jwks)?;
680 let (issuer, audience, user_id, exp, nonce) = extract_claims(&claims)?;
681
682 if nonce != ephemeral_key.nonce() {
683 return Err(AptosError::InvalidJwt("JWT nonce mismatch".into()));
684 }
685
686 let pepper = pepper_service.get_pepper(jwt).await?;
687 let proof = prover_service
688 .generate_proof(jwt, &ephemeral_key, &pepper)
689 .await?;
690
691 let address = derive_keyless_address(&issuer, &audience, &user_id, &pepper);
692 let auth_key = AuthenticationKey::new(address.to_bytes());
693
694 Ok(Self {
695 provider: OidcProvider::from_issuer(&issuer),
696 issuer,
697 audience,
698 user_id,
699 pepper,
700 proof,
701 address,
702 auth_key,
703 jwt_expiration: exp,
704 ephemeral_key,
705 })
706 }
707
708 pub fn provider(&self) -> &OidcProvider {
710 &self.provider
711 }
712
713 pub fn issuer(&self) -> &str {
715 &self.issuer
716 }
717
718 pub fn audience(&self) -> &str {
720 &self.audience
721 }
722
723 pub fn user_id(&self) -> &str {
725 &self.user_id
726 }
727
728 pub fn proof(&self) -> &ZkProof {
730 &self.proof
731 }
732
733 pub fn is_valid(&self) -> bool {
735 if self.ephemeral_key.is_expired() {
736 return false;
737 }
738
739 match self.jwt_expiration {
740 Some(exp) => SystemTime::now() < exp,
741 None => true,
742 }
743 }
744
745 pub async fn refresh_proof(
764 &mut self,
765 jwt: &str,
766 prover_service: &dyn ProverService,
767 ) -> AptosResult<()> {
768 let client = reqwest::Client::builder()
770 .timeout(JWKS_FETCH_TIMEOUT)
771 .build()
772 .map_err(|e| AptosError::InvalidJwt(format!("failed to create HTTP client: {e}")))?;
773 let jwks = fetch_jwks(&client, self.provider.jwks_url()).await?;
774 self.refresh_proof_with_jwks(jwt, &jwks, prover_service)
775 .await
776 }
777
778 pub async fn refresh_proof_with_jwks(
791 &mut self,
792 jwt: &str,
793 jwks: &JwkSet,
794 prover_service: &dyn ProverService,
795 ) -> AptosResult<()> {
796 let claims = decode_and_verify_jwt(jwt, jwks)?;
797 let (issuer, audience, user_id, exp, nonce) = extract_claims(&claims)?;
798
799 if nonce != self.ephemeral_key.nonce() {
800 return Err(AptosError::InvalidJwt("JWT nonce mismatch".into()));
801 }
802
803 if issuer != self.issuer || audience != self.audience || user_id != self.user_id {
804 return Err(AptosError::InvalidJwt(
805 "JWT identity does not match account".into(),
806 ));
807 }
808
809 let proof = prover_service
810 .generate_proof(jwt, &self.ephemeral_key, &self.pepper)
811 .await?;
812 self.proof = proof;
813 self.jwt_expiration = exp;
814 Ok(())
815 }
816
817 pub fn sign_keyless(&self, message: &[u8]) -> KeylessSignature {
819 let signature = self.ephemeral_key.private_key.sign(message).to_bytes();
820 KeylessSignature {
821 ephemeral_public_key: self.ephemeral_key.public_key.to_bytes().to_vec(),
822 ephemeral_signature: signature.to_vec(),
823 proof: self.proof.as_bytes().to_vec(),
824 }
825 }
826
827 #[doc(hidden)]
839 #[allow(clippy::too_many_arguments)]
840 pub async fn from_verified_claims(
841 issuer: String,
842 audience: String,
843 user_id: String,
844 nonce: String,
845 exp: Option<SystemTime>,
846 ephemeral_key: EphemeralKeyPair,
847 pepper_service: &dyn PepperService,
848 prover_service: &dyn ProverService,
849 jwt_for_services: &str,
850 ) -> AptosResult<Self> {
851 if nonce != ephemeral_key.nonce() {
852 return Err(AptosError::InvalidJwt("nonce mismatch".into()));
853 }
854
855 let pepper = pepper_service.get_pepper(jwt_for_services).await?;
856 let proof = prover_service
857 .generate_proof(jwt_for_services, &ephemeral_key, &pepper)
858 .await?;
859
860 let address = derive_keyless_address(&issuer, &audience, &user_id, &pepper);
861 let auth_key = AuthenticationKey::new(address.to_bytes());
862
863 Ok(Self {
864 provider: OidcProvider::from_issuer(&issuer),
865 issuer,
866 audience,
867 user_id,
868 pepper,
869 proof,
870 address,
871 auth_key,
872 jwt_expiration: exp,
873 ephemeral_key,
874 })
875 }
876}
877
878impl Account for KeylessAccount {
879 fn address(&self) -> AccountAddress {
880 self.address
881 }
882
883 fn authentication_key(&self) -> AuthenticationKey {
884 self.auth_key
885 }
886
887 fn sign(&self, message: &[u8]) -> crate::error::AptosResult<Vec<u8>> {
888 let signature = self.sign_keyless(message);
889 signature
890 .to_bcs()
891 .map_err(|e| crate::error::AptosError::Bcs(e.to_string()))
892 }
893
894 fn public_key_bytes(&self) -> Vec<u8> {
895 self.ephemeral_key.public_key.to_bytes().to_vec()
896 }
897
898 fn signature_scheme(&self) -> u8 {
899 KEYLESS_SCHEME
900 }
901}
902
903impl fmt::Debug for KeylessAccount {
904 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
905 f.debug_struct("KeylessAccount")
906 .field("address", &self.address)
907 .field("provider", &self.provider)
908 .field("issuer", &self.issuer)
909 .field("audience", &self.audience)
910 .field("user_id", &self.user_id)
911 .finish_non_exhaustive()
912 }
913}
914
915#[derive(Debug, Deserialize)]
916struct JwtClaims {
917 iss: Option<String>,
918 aud: Option<AudClaim>,
919 sub: Option<String>,
920 exp: Option<u64>,
921 nonce: Option<String>,
922}
923
924#[derive(Debug, Deserialize)]
925#[serde(untagged)]
926enum AudClaim {
927 Single(String),
928 Multiple(Vec<String>),
929}
930
931impl AudClaim {
932 fn first(&self) -> Option<&str> {
933 match self {
934 AudClaim::Single(value) => Some(value.as_str()),
935 AudClaim::Multiple(values) => values.first().map(std::string::String::as_str),
936 }
937 }
938}
939
940const JWKS_FETCH_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
942
943const MAX_JWKS_RESPONSE_SIZE: usize = 1024 * 1024;
945
946async fn fetch_jwks(client: &reqwest::Client, jwks_url: &str) -> AptosResult<JwkSet> {
956 let parsed_url = Url::parse(jwks_url)
960 .map_err(|e| AptosError::InvalidJwt(format!("invalid JWKS URL: {e}")))?;
961 if parsed_url.scheme() != "https" {
962 return Err(AptosError::InvalidJwt(format!(
963 "JWKS URL must use HTTPS scheme, got: {}",
964 parsed_url.scheme()
965 )));
966 }
967
968 let response = client.get(jwks_url).send().await?;
970
971 if !response.status().is_success() {
972 return Err(AptosError::InvalidJwt(format!(
973 "JWKS endpoint returned status: {}",
974 response.status()
975 )));
976 }
977
978 let bytes = crate::config::read_response_bounded(response, MAX_JWKS_RESPONSE_SIZE).await?;
981 let jwks: JwkSet = serde_json::from_slice(&bytes)
982 .map_err(|e| AptosError::InvalidJwt(format!("failed to parse JWKS: {e}")))?;
983 Ok(jwks)
984}
985
986fn decode_and_verify_jwt(jwt: &str, jwks: &JwkSet) -> AptosResult<JwtClaims> {
1001 let header = decode_header(jwt)
1003 .map_err(|e| AptosError::InvalidJwt(format!("failed to decode JWT header: {e}")))?;
1004
1005 let kid = header
1006 .kid
1007 .as_ref()
1008 .ok_or_else(|| AptosError::InvalidJwt("JWT header missing 'kid' field".into()))?;
1009
1010 let signing_key = jwks.find(kid).ok_or_else(|| {
1012 AptosError::InvalidJwt("no matching key found for provided key identifier".into())
1013 })?;
1014
1015 let decoding_key = DecodingKey::from_jwk(signing_key)
1017 .map_err(|e| AptosError::InvalidJwt(format!("failed to create decoding key: {e}")))?;
1018
1019 let jwk_alg = signing_key
1021 .common
1022 .key_algorithm
1023 .ok_or_else(|| AptosError::InvalidJwt("JWK missing 'alg' (key_algorithm) field".into()))?;
1024
1025 let algorithm = match jwk_alg {
1026 jsonwebtoken::jwk::KeyAlgorithm::RS256 => Algorithm::RS256,
1028 jsonwebtoken::jwk::KeyAlgorithm::RS384 => Algorithm::RS384,
1029 jsonwebtoken::jwk::KeyAlgorithm::RS512 => Algorithm::RS512,
1030 jsonwebtoken::jwk::KeyAlgorithm::PS256 => Algorithm::PS256,
1032 jsonwebtoken::jwk::KeyAlgorithm::PS384 => Algorithm::PS384,
1033 jsonwebtoken::jwk::KeyAlgorithm::PS512 => Algorithm::PS512,
1034 jsonwebtoken::jwk::KeyAlgorithm::ES256 => Algorithm::ES256,
1036 jsonwebtoken::jwk::KeyAlgorithm::ES384 => Algorithm::ES384,
1037 jsonwebtoken::jwk::KeyAlgorithm::EdDSA => Algorithm::EdDSA,
1039 _ => {
1040 return Err(AptosError::InvalidJwt(format!(
1041 "unsupported JWK algorithm: {jwk_alg:?}"
1042 )));
1043 }
1044 };
1045
1046 if header.alg != algorithm {
1048 return Err(AptosError::InvalidJwt(format!(
1049 "JWT header algorithm ({:?}) does not match JWK algorithm ({:?})",
1050 header.alg, algorithm
1051 )));
1052 }
1053
1054 let mut validation = Validation::new(algorithm);
1056 validation.validate_exp = false;
1057 validation.validate_aud = false; validation.set_required_spec_claims::<String>(&[]);
1059
1060 let data = decode::<JwtClaims>(jwt, &decoding_key, &validation)
1061 .map_err(|e| AptosError::InvalidJwt(format!("JWT verification failed: {e}")))?;
1062
1063 Ok(data.claims)
1064}
1065
1066fn decode_claims_unverified(jwt: &str) -> AptosResult<JwtClaims> {
1074 let data = jsonwebtoken::dangerous::insecure_decode::<JwtClaims>(jwt)
1080 .map_err(|e| AptosError::InvalidJwt(format!("failed to decode JWT claims: {e}")))?;
1081 Ok(data.claims)
1082}
1083
1084fn extract_claims(
1085 claims: &JwtClaims,
1086) -> AptosResult<(String, String, String, Option<SystemTime>, String)> {
1087 let issuer = claims
1088 .iss
1089 .clone()
1090 .ok_or_else(|| AptosError::InvalidJwt("missing iss claim".into()))?;
1091 let audience = claims
1092 .aud
1093 .as_ref()
1094 .and_then(|aud| aud.first())
1095 .map(std::string::ToString::to_string)
1096 .ok_or_else(|| AptosError::InvalidJwt("missing aud claim".into()))?;
1097 let user_id = claims
1098 .sub
1099 .clone()
1100 .ok_or_else(|| AptosError::InvalidJwt("missing sub claim".into()))?;
1101 let nonce = claims
1102 .nonce
1103 .clone()
1104 .ok_or_else(|| AptosError::InvalidJwt("missing nonce claim".into()))?;
1105
1106 let exp_time = claims.exp.map(|exp| UNIX_EPOCH + Duration::from_secs(exp));
1107 if let Some(exp) = exp_time
1108 && SystemTime::now() >= exp
1109 {
1110 let exp_secs = claims.exp.unwrap_or(0);
1111 return Err(AptosError::InvalidJwt(format!(
1112 "JWT is expired (exp: {exp_secs} seconds since UNIX_EPOCH)"
1113 )));
1114 }
1115
1116 Ok((issuer, audience, user_id, exp_time, nonce))
1117}
1118
1119fn derive_keyless_address(
1120 issuer: &str,
1121 audience: &str,
1122 user_id: &str,
1123 pepper: &Pepper,
1124) -> AccountAddress {
1125 let issuer_hash = sha3_256_bytes(issuer.as_bytes());
1126 let audience_hash = sha3_256_bytes(audience.as_bytes());
1127 let user_hash = sha3_256_bytes(user_id.as_bytes());
1128
1129 let mut hasher = Sha3_256::new();
1130 hasher.update(issuer_hash);
1131 hasher.update(audience_hash);
1132 hasher.update(user_hash);
1133 hasher.update(pepper.as_bytes());
1134 hasher.update([KEYLESS_SCHEME]);
1135 let result = hasher.finalize();
1136
1137 let mut address = [0u8; 32];
1138 address.copy_from_slice(&result);
1139 AccountAddress::new(address)
1140}
1141
1142fn sha3_256_bytes(data: &[u8]) -> [u8; 32] {
1143 let mut hasher = Sha3_256::new();
1144 hasher.update(data);
1145 let result = hasher.finalize();
1146 let mut output = [0u8; 32];
1147 output.copy_from_slice(&result);
1148 output
1149}
1150
1151#[cfg(test)]
1152mod tests {
1153 use super::*;
1154 use jsonwebtoken::{Algorithm, EncodingKey, Header, encode};
1155
1156 struct StaticPepperService {
1157 pepper: Pepper,
1158 }
1159
1160 impl PepperService for StaticPepperService {
1161 fn get_pepper(
1162 &self,
1163 _jwt: &str,
1164 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = AptosResult<Pepper>> + Send + '_>>
1165 {
1166 Box::pin(async move { Ok(self.pepper.clone()) })
1167 }
1168 }
1169
1170 struct StaticProverService {
1171 proof: ZkProof,
1172 }
1173
1174 impl ProverService for StaticProverService {
1175 fn generate_proof<'a>(
1176 &'a self,
1177 _jwt: &'a str,
1178 _ephemeral_key: &'a EphemeralKeyPair,
1179 _pepper: &'a Pepper,
1180 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = AptosResult<ZkProof>> + Send + 'a>>
1181 {
1182 Box::pin(async move { Ok(self.proof.clone()) })
1183 }
1184 }
1185
1186 #[derive(Serialize, Deserialize)]
1187 struct TestClaims {
1188 iss: String,
1189 aud: String,
1190 sub: String,
1191 exp: u64,
1192 nonce: String,
1193 }
1194
1195 #[tokio::test]
1196 async fn test_keyless_account_creation() {
1197 let ephemeral = EphemeralKeyPair::generate(3600);
1198 let now = SystemTime::now()
1199 .duration_since(UNIX_EPOCH)
1200 .expect("time went backwards")
1201 .as_secs();
1202
1203 let claims = TestClaims {
1205 iss: "https://accounts.google.com".to_string(),
1206 aud: "client-id".to_string(),
1207 sub: "user-123".to_string(),
1208 exp: now + 3600,
1209 nonce: ephemeral.nonce().to_string(),
1210 };
1211
1212 let jwt = encode(
1213 &Header::new(Algorithm::HS256),
1214 &claims,
1215 &EncodingKey::from_secret(b"secret"),
1216 )
1217 .unwrap();
1218
1219 let pepper_service = StaticPepperService {
1220 pepper: Pepper::new(vec![1, 2, 3, 4]),
1221 };
1222 let prover_service = StaticProverService {
1223 proof: ZkProof::new(vec![9, 9, 9]),
1224 };
1225
1226 let exp_time = UNIX_EPOCH + std::time::Duration::from_secs(now + 3600);
1228 let account = KeylessAccount::from_verified_claims(
1229 "https://accounts.google.com".to_string(),
1230 "client-id".to_string(),
1231 "user-123".to_string(),
1232 ephemeral.nonce().to_string(),
1233 Some(exp_time),
1234 ephemeral,
1235 &pepper_service,
1236 &prover_service,
1237 &jwt,
1238 )
1239 .await
1240 .unwrap();
1241
1242 assert_eq!(account.issuer(), "https://accounts.google.com");
1243 assert_eq!(account.audience(), "client-id");
1244 assert_eq!(account.user_id(), "user-123");
1245 assert!(account.is_valid());
1246 assert!(!account.address().is_zero());
1247 }
1248
1249 #[tokio::test]
1250 async fn test_keyless_account_nonce_mismatch() {
1251 let ephemeral = EphemeralKeyPair::generate(3600);
1252 let now = SystemTime::now()
1253 .duration_since(UNIX_EPOCH)
1254 .expect("time went backwards")
1255 .as_secs();
1256
1257 let claims = TestClaims {
1258 iss: "https://accounts.google.com".to_string(),
1259 aud: "client-id".to_string(),
1260 sub: "user-123".to_string(),
1261 exp: now + 3600,
1262 nonce: ephemeral.nonce().to_string(),
1263 };
1264
1265 let jwt = encode(
1266 &Header::new(Algorithm::HS256),
1267 &claims,
1268 &EncodingKey::from_secret(b"secret"),
1269 )
1270 .unwrap();
1271
1272 let pepper_service = StaticPepperService {
1273 pepper: Pepper::new(vec![1, 2, 3, 4]),
1274 };
1275 let prover_service = StaticProverService {
1276 proof: ZkProof::new(vec![9, 9, 9]),
1277 };
1278
1279 let result = KeylessAccount::from_verified_claims(
1281 "https://accounts.google.com".to_string(),
1282 "client-id".to_string(),
1283 "user-123".to_string(),
1284 "wrong-nonce".to_string(), None,
1286 ephemeral,
1287 &pepper_service,
1288 &prover_service,
1289 &jwt,
1290 )
1291 .await;
1292
1293 assert!(result.is_err());
1294 assert!(matches!(result, Err(AptosError::InvalidJwt(_))));
1295 }
1296
1297 #[test]
1298 fn test_decode_claims_unverified() {
1299 let now = SystemTime::now()
1300 .duration_since(UNIX_EPOCH)
1301 .expect("time went backwards")
1302 .as_secs();
1303
1304 let claims = TestClaims {
1305 iss: "https://accounts.google.com".to_string(),
1306 aud: "test-aud".to_string(),
1307 sub: "test-sub".to_string(),
1308 exp: now + 3600,
1309 nonce: "test-nonce".to_string(),
1310 };
1311
1312 let jwt = encode(
1313 &Header::new(Algorithm::HS256),
1314 &claims,
1315 &EncodingKey::from_secret(b"secret"),
1316 )
1317 .unwrap();
1318
1319 let decoded = decode_claims_unverified(&jwt).unwrap();
1320 assert_eq!(decoded.iss.unwrap(), "https://accounts.google.com");
1321 assert_eq!(decoded.sub.unwrap(), "test-sub");
1322 assert_eq!(decoded.nonce.unwrap(), "test-nonce");
1323 }
1324
1325 #[test]
1326 fn test_ephemeral_key_pair_snapshot_roundtrip() {
1327 let ephemeral = EphemeralKeyPair::generate(3600);
1328 let snapshot = ephemeral.to_snapshot().unwrap();
1329 let restored = EphemeralKeyPair::from_snapshot(&snapshot).unwrap();
1330
1331 assert_eq!(ephemeral.nonce(), restored.nonce());
1332 assert_eq!(
1333 ephemeral.public_key().to_bytes(),
1334 restored.public_key().to_bytes()
1335 );
1336 assert_eq!(ephemeral.is_expired(), restored.is_expired());
1337 }
1338
1339 #[test]
1340 fn test_ephemeral_key_pair_file_roundtrip() {
1341 let ephemeral = EphemeralKeyPair::generate(3600);
1342 let path =
1343 std::env::temp_dir().join(format!("aptos-sdk-ephemeral-{}.json", ephemeral.nonce()));
1344
1345 ephemeral.save_to_file(&path).unwrap();
1346 let restored = EphemeralKeyPair::load_from_file(&path).unwrap();
1347 let _ = std::fs::remove_file(&path);
1348
1349 assert_eq!(ephemeral.nonce(), restored.nonce());
1350 assert_eq!(
1351 ephemeral.public_key().to_bytes(),
1352 restored.public_key().to_bytes()
1353 );
1354 }
1355
1356 #[test]
1357 fn test_oidc_provider_detection() {
1358 assert!(matches!(
1359 OidcProvider::from_issuer("https://accounts.google.com"),
1360 OidcProvider::Google
1361 ));
1362 assert!(matches!(
1363 OidcProvider::from_issuer("https://appleid.apple.com"),
1364 OidcProvider::Apple
1365 ));
1366 assert!(matches!(
1367 OidcProvider::from_issuer("https://unknown.example.com"),
1368 OidcProvider::Custom { .. }
1369 ));
1370 }
1371
1372 #[test]
1373 fn test_decode_and_verify_jwt_missing_kid() {
1374 let now = SystemTime::now()
1376 .duration_since(UNIX_EPOCH)
1377 .expect("time went backwards")
1378 .as_secs();
1379
1380 let claims = TestClaims {
1381 iss: "https://accounts.google.com".to_string(),
1382 aud: "test-aud".to_string(),
1383 sub: "test-sub".to_string(),
1384 exp: now + 3600,
1385 nonce: "test-nonce".to_string(),
1386 };
1387
1388 let jwt = encode(
1390 &Header::new(Algorithm::HS256),
1391 &claims,
1392 &EncodingKey::from_secret(b"secret"),
1393 )
1394 .unwrap();
1395
1396 let jwks = JwkSet { keys: vec![] };
1398
1399 let result = decode_and_verify_jwt(&jwt, &jwks);
1400 assert!(result.is_err());
1401 let err = result.unwrap_err();
1402 assert!(
1403 matches!(&err, AptosError::InvalidJwt(msg) if msg.contains("kid")),
1404 "Expected error about missing kid, got: {err:?}"
1405 );
1406 }
1407
1408 #[test]
1409 fn test_decode_and_verify_jwt_no_matching_key() {
1410 let now = SystemTime::now()
1411 .duration_since(UNIX_EPOCH)
1412 .expect("time went backwards")
1413 .as_secs();
1414
1415 let claims = TestClaims {
1416 iss: "https://accounts.google.com".to_string(),
1417 aud: "test-aud".to_string(),
1418 sub: "test-sub".to_string(),
1419 exp: now + 3600,
1420 nonce: "test-nonce".to_string(),
1421 };
1422
1423 let mut header = Header::new(Algorithm::HS256);
1425 header.kid = Some("test-kid-123".to_string());
1426
1427 let jwt = encode(&header, &claims, &EncodingKey::from_secret(b"secret")).unwrap();
1428
1429 let jwks = JwkSet { keys: vec![] };
1431
1432 let result = decode_and_verify_jwt(&jwt, &jwks);
1433 assert!(result.is_err());
1434 let err = result.unwrap_err();
1435 assert!(
1436 matches!(&err, AptosError::InvalidJwt(msg) if msg.contains("no matching key")),
1437 "Expected error about no matching key, got: {err:?}"
1438 );
1439 }
1440
1441 #[test]
1442 fn test_decode_and_verify_jwt_invalid_jwt_format() {
1443 let jwks = JwkSet { keys: vec![] };
1444
1445 let result = decode_and_verify_jwt("not-a-valid-jwt", &jwks);
1447 assert!(result.is_err());
1448
1449 let result = decode_and_verify_jwt("aaa.bbb.ccc", &jwks);
1451 assert!(result.is_err());
1452 }
1453}