Skip to main content

aptos_sdk/account/
keyless.rs

1//! Keyless (OIDC-based) account support.
2//!
3//! Keyless accounts let users sign Aptos transactions with an OpenID Connect
4//! (OIDC) identity provider (Google, Apple, Microsoft, or a custom issuer)
5//! instead of managing a long-lived private key. The SDK derives a stable
6//! on-chain address from the JWT claims and a privacy-preserving pepper, then
7//! signs each transaction with a short-lived ephemeral Ed25519 key plus a
8//! zero-knowledge proof fetched from Aptos infrastructure.
9//!
10//! # Feature flag
11//!
12//! Enable the `keyless` feature (or the `full` meta-feature) in `Cargo.toml`:
13//!
14//! ```toml
15//! [dependencies]
16//! aptos-sdk = { version = "0.5", features = ["keyless"] }
17//! ```
18//!
19//! # Authentication flow
20//!
21//! 1. Generate an [`EphemeralKeyPair`] and read its [`EphemeralKeyPair::nonce`].
22//!    Persist the pair across the IdP redirect with [`EphemeralKeyPair::to_snapshot`]
23//!    / [`EphemeralKeyPair::from_snapshot`] (the TypeScript SDK uses `localStorage`).
24//! 2. Redirect the user through your IdP OAuth flow, passing the nonce in the
25//!    `nonce` parameter so it is embedded in the returned ID token (JWT).
26//! 3. Call [`KeylessAccount::from_jwt`] with the JWT, the ephemeral key, and
27//!    pepper / prover service clients (see below).
28//! 4. Use the resulting [`KeylessAccount`] anywhere an [`Account`]
29//!    is accepted — e.g. [`crate::transaction::builder::sign_transaction`] and
30//!    [`crate::Aptos::submit_transaction`].
31//!
32//! The Aptos [Keyless integration guide](https://aptos.dev/build/guides/aptos-keyless/integration-guide)
33//! walks through IdP configuration and the browser-side OAuth redirect.
34//!
35//! # Pepper and prover services
36//!
37//! Account derivation and signing require two Aptos-hosted HTTP services:
38//!
39//! - **Pepper service** — returns a per-user pepper used in address derivation.
40//!   Use [`HttpPepperService`] or implement [`PepperService`] for custom backends.
41//! - **Prover service** — returns a Groth16 zero-knowledge proof binding the JWT
42//!   to the ephemeral public key. Use [`HttpProverService`] or [`ProverService`].
43//!
44//! Default endpoints mirror the TypeScript SDK (devnet shown):
45//!
46//! | Network | Pepper URL | Prover URL |
47//! |---------|------------|------------|
48//! | devnet  | `https://api.devnet.aptoslabs.com/keyless/pepper/v0` | `https://api.devnet.aptoslabs.com/keyless/prover/v0` |
49//! | testnet | `https://api.testnet.aptoslabs.com/keyless/pepper/v0` | `https://api.testnet.aptoslabs.com/keyless/prover/v0` |
50//! | mainnet | `https://api.mainnet.aptoslabs.com/keyless/pepper/v0` | `https://api.mainnet.aptoslabs.com/keyless/prover/v0` |
51//!
52//! [`Network::pepper_url`] and [`Network::prover_url`] return these URLs.
53//!
54//! # Example
55//!
56//! ```rust,no_run
57//! # async fn example(jwt: &str) -> aptos_sdk::error::AptosResult<()> {
58//! use aptos_sdk::{
59//!     Aptos, AptosConfig,
60//!     account::{Account, EphemeralKeyPair, HttpPepperService, HttpProverService, KeylessAccount},
61//!     config::Network,
62//!     transaction::{EntryFunction, TransactionBuilder, builder::sign_transaction},
63//! };
64//! use url::Url;
65//!
66//! // 1. Generate an ephemeral key before starting the OAuth redirect.
67//! let ephemeral = EphemeralKeyPair::generate(3600);
68//! println!("Use this nonce in your IdP login URL: {}", ephemeral.nonce());
69//!
70//! // 2. After the user returns with a JWT, wire up Aptos keyless services.
71//! let pepper = HttpPepperService::new(
72//!     Url::parse(Network::Devnet.pepper_url().expect("devnet pepper URL")).unwrap(),
73//! );
74//! let prover = HttpProverService::new(
75//!     Url::parse(Network::Devnet.prover_url().expect("devnet prover URL")).unwrap(),
76//! );
77//!
78//! let account = KeylessAccount::from_jwt(jwt, ephemeral, &pepper, &prover).await?;
79//! println!("Keyless address: {}", account.address());
80//!
81//! // 3. Sign and submit a transaction like any other account type.
82//! let aptos = Aptos::new(AptosConfig::devnet())?;
83//! let recipient = aptos_sdk::types::AccountAddress::from_hex("0x1").unwrap();
84//! let payload = EntryFunction::apt_transfer(recipient, 1_000)?;
85//! let raw_txn = TransactionBuilder::new()
86//!     .sender(account.address())
87//!     .sequence_number(aptos.get_sequence_number(account.address()).await?)
88//!     .payload(payload.into())
89//!     .chain_id(aptos.chain_id())
90//!     .expiration_from_now(600)
91//!     .build()?;
92//! let signed = sign_transaction(&raw_txn, &account)?;
93//! aptos.submit_transaction(&signed).await?;
94//! # Ok(())
95//! # }
96//! ```
97//!
98//! See also the [`keyless_account`](https://docs.rs/aptos-sdk/latest/aptos_sdk/examples/keyless_account/index.html)
99//! example in this crate.
100
101use 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
113// Re-export JwkSet for use with from_jwt_with_jwks and refresh_proof_with_jwks
114pub use jsonwebtoken::jwk::JwkSet;
115
116/// Keyless signature payload for transaction authentication.
117#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
118pub struct KeylessSignature {
119    /// Ephemeral public key bytes.
120    pub ephemeral_public_key: Vec<u8>,
121    /// Signature produced by the ephemeral key.
122    pub ephemeral_signature: Vec<u8>,
123    /// Zero-knowledge proof bytes.
124    pub proof: Vec<u8>,
125}
126
127impl KeylessSignature {
128    /// Serializes the signature using BCS.
129    ///
130    /// # Errors
131    ///
132    /// Returns an error if BCS serialization fails.
133    pub fn to_bcs(&self) -> AptosResult<Vec<u8>> {
134        aptos_bcs::to_bytes(self).map_err(AptosError::bcs)
135    }
136}
137
138/// Short-lived key pair used for keyless signing.
139#[derive(Clone)]
140pub struct EphemeralKeyPair {
141    private_key: Ed25519PrivateKey,
142    public_key: Ed25519PublicKey,
143    expiry: SystemTime,
144    nonce: String,
145}
146
147impl EphemeralKeyPair {
148    /// Generates a new ephemeral key pair with the given expiry (in seconds).
149    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    /// Returns true if the key pair has expired.
166    pub fn is_expired(&self) -> bool {
167        SystemTime::now() >= self.expiry
168    }
169
170    /// Returns the nonce associated with this key pair.
171    pub fn nonce(&self) -> &str {
172        &self.nonce
173    }
174
175    /// Returns the public key.
176    pub fn public_key(&self) -> &Ed25519PublicKey {
177        &self.public_key
178    }
179
180    /// Serializes this key pair for storage across an OAuth redirect.
181    ///
182    /// Production apps should encrypt the returned snapshot at rest (the
183    /// TypeScript SDK stores an equivalent structure in `localStorage`).
184    ///
185    /// # Errors
186    ///
187    /// Returns an error if the expiry time cannot be represented as seconds
188    /// since the UNIX epoch.
189    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    /// Restores an ephemeral key pair previously saved via [`Self::to_snapshot`].
203    ///
204    /// # Errors
205    ///
206    /// Returns an error if the private key bytes are invalid.
207    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    /// Persists this key pair as JSON at `path` for reload after an OAuth redirect.
219    ///
220    /// # Errors
221    ///
222    /// Returns an error if snapshot serialization or the file write fails.
223    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    /// Loads a key pair previously written by [`Self::save_to_file`].
234    ///
235    /// # Errors
236    ///
237    /// Returns an error if the file cannot be read, JSON parsing fails, or the
238    /// snapshot cannot be restored.
239    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/// Serializable ephemeral key state for persisting across an OAuth redirect.
249///
250/// # Security
251///
252/// Contains a private key. Encrypt at rest in production applications.
253#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
254pub struct EphemeralKeyPairSnapshot {
255    /// Ed25519 private key bytes (`Ed25519PrivateKey::to_bytes` format).
256    private_key: Vec<u8>,
257    /// Expiry as seconds since the UNIX epoch.
258    expiry_unix_secs: u64,
259    /// Nonce embedded in the OIDC login URL and JWT.
260    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/// Supported OIDC providers.
274#[derive(Clone, Debug, PartialEq, Eq)]
275pub enum OidcProvider {
276    /// Google identity provider.
277    Google,
278    /// Apple identity provider.
279    Apple,
280    /// Microsoft identity provider.
281    Microsoft,
282    /// Custom OIDC provider.
283    Custom {
284        /// Issuer URL.
285        issuer: String,
286        /// JWKS URL.
287        jwks_url: String,
288    },
289}
290
291impl OidcProvider {
292    /// Returns the issuer URL.
293    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    /// Returns the JWKS URL.
303    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    /// Infers a provider from an issuer URL.
315    ///
316    /// # Security
317    ///
318    /// For unknown issuers, the JWKS URL is constructed as `{issuer}/.well-known/jwks.json`.
319    /// Non-HTTPS issuers are accepted at construction time but will produce an empty
320    /// JWKS URL, causing a clear error at JWKS fetch time. This prevents SSRF via
321    /// `http://`, `file://`, or other dangerous URL schemes without changing the
322    /// function signature. Callers controlling issuer input should additionally
323    /// validate the host (e.g., block private IP ranges) if SSRF is a concern.
324    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                // SECURITY: Only accept HTTPS issuers to prevent SSRF attacks.
331                // A malicious JWT could set `iss` to an internal URL (e.g.,
332                // http://169.254.169.254/) causing the SDK to make requests to
333                // attacker-chosen endpoints when fetching JWKS.
334                let jwks_url = if issuer.starts_with("https://") {
335                    format!("{issuer}/.well-known/jwks.json")
336                } else {
337                    // Non-HTTPS issuers get an invalid JWKS URL that will fail
338                    // at fetch time with a clear error rather than making requests
339                    // to potentially dangerous endpoints.
340                    String::new()
341                };
342                OidcProvider::Custom {
343                    issuer: issuer.to_string(),
344                    jwks_url,
345                }
346            }
347        }
348    }
349}
350
351/// Pepper bytes used in keyless address derivation.
352///
353/// # Security
354///
355/// The pepper is secret material used to derive keyless account addresses.
356/// It is automatically zeroized when dropped to prevent key material from
357/// lingering in memory.
358#[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    /// Creates a new pepper from raw bytes.
369    pub fn new(bytes: Vec<u8>) -> Self {
370        Self(bytes)
371    }
372
373    /// Returns the pepper as bytes.
374    pub fn as_bytes(&self) -> &[u8] {
375        &self.0
376    }
377
378    /// Creates a pepper from hex.
379    ///
380    /// # Errors
381    ///
382    /// Returns an error if the hex string is invalid or cannot be decoded.
383    pub fn from_hex(hex_str: &str) -> AptosResult<Self> {
384        Ok(Self(const_hex::decode(hex_str)?))
385    }
386
387    /// Returns the pepper as hex.
388    pub fn to_hex(&self) -> String {
389        const_hex::encode_prefixed(&self.0)
390    }
391}
392
393/// Zero-knowledge proof bytes.
394#[derive(Clone, Debug, PartialEq, Eq)]
395pub struct ZkProof(Vec<u8>);
396
397impl ZkProof {
398    /// Creates a new proof from raw bytes.
399    pub fn new(bytes: Vec<u8>) -> Self {
400        Self(bytes)
401    }
402
403    /// Returns the proof as bytes.
404    pub fn as_bytes(&self) -> &[u8] {
405        &self.0
406    }
407
408    /// Creates a proof from hex.
409    ///
410    /// # Errors
411    ///
412    /// Returns an error if the hex string is invalid or cannot be decoded.
413    pub fn from_hex(hex_str: &str) -> AptosResult<Self> {
414        Ok(Self(const_hex::decode(hex_str)?))
415    }
416
417    /// Returns the proof as hex.
418    pub fn to_hex(&self) -> String {
419        const_hex::encode_prefixed(&self.0)
420    }
421}
422
423/// Service for obtaining pepper values.
424pub trait PepperService: Send + Sync {
425    /// Fetches the pepper for a JWT.
426    fn get_pepper(
427        &self,
428        jwt: &str,
429    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = AptosResult<Pepper>> + Send + '_>>;
430}
431
432/// Service for generating zero-knowledge proofs.
433pub trait ProverService: Send + Sync {
434    /// Generates the proof for keyless authentication.
435    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/// HTTP pepper service client.
444#[derive(Clone, Debug)]
445pub struct HttpPepperService {
446    url: Url,
447    client: reqwest::Client,
448}
449
450impl HttpPepperService {
451    /// Creates a new HTTP pepper service client.
452    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            // SECURITY: Stream body with size limit to prevent OOM
486            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/// HTTP prover service client.
497#[derive(Clone, Debug)]
498pub struct HttpProverService {
499    url: Url,
500    client: reqwest::Client,
501}
502
503impl HttpProverService {
504    /// Creates a new HTTP prover service client.
505    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            // SECURITY: Stream body with size limit to prevent OOM
553            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
563/// Account authenticated via OIDC.
564pub 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    /// Creates a keyless account from an OIDC JWT token.
579    ///
580    /// This method verifies the JWT signature using the OIDC provider's JWKS endpoint
581    /// before extracting claims and creating the account.
582    ///
583    /// # Network Requests
584    ///
585    /// This method makes HTTP requests to:
586    /// - The OIDC provider's JWKS endpoint to fetch signing keys
587    /// - The pepper service to obtain the pepper
588    /// - The prover service to generate a ZK proof
589    ///
590    /// For more control over network calls and caching, use [`Self::from_jwt_with_jwks`]
591    /// with pre-fetched JWKS.
592    ///
593    /// # Errors
594    ///
595    /// This function will return an error if:
596    /// - The JWT signature verification fails
597    /// - The JWT cannot be decoded or is missing required claims (iss, aud, sub, nonce)
598    /// - The JWT nonce doesn't match the ephemeral key's nonce
599    /// - The JWT is expired
600    /// - The JWKS cannot be fetched from the provider (network timeout, DNS failure,
601    ///   connection errors, HTTP errors, or invalid JWKS response)
602    /// - The pepper service fails to return a pepper
603    /// - The prover service fails to generate a proof
604    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        // First, decode without verification to get the issuer for JWKS lookup
611        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        // Determine provider and fetch JWKS
618        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        // Now verify and decode the JWT properly
626        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    /// Creates a keyless account from a JWT with pre-fetched JWKS.
656    ///
657    /// This method is useful when you want to:
658    /// - Cache the JWKS to avoid repeated network requests
659    /// - Have more control over HTTP client configuration
660    /// - Implement custom caching strategies based on HTTP cache headers
661    ///
662    /// # Errors
663    ///
664    /// This function will return an error if:
665    /// - The JWT signature verification fails
666    /// - The JWT cannot be decoded or is missing required claims (iss, aud, sub, nonce)
667    /// - The JWT nonce doesn't match the ephemeral key's nonce
668    /// - The JWT is expired
669    /// - The pepper service fails to return a pepper
670    /// - The prover service fails to generate a proof
671    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        // Verify and decode the JWT using the provided JWKS
679        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    /// Returns the OIDC provider.
709    pub fn provider(&self) -> &OidcProvider {
710        &self.provider
711    }
712
713    /// Returns the issuer.
714    pub fn issuer(&self) -> &str {
715        &self.issuer
716    }
717
718    /// Returns the audience.
719    pub fn audience(&self) -> &str {
720        &self.audience
721    }
722
723    /// Returns the user identifier (sub claim).
724    pub fn user_id(&self) -> &str {
725        &self.user_id
726    }
727
728    /// Returns the proof.
729    pub fn proof(&self) -> &ZkProof {
730        &self.proof
731    }
732
733    /// Returns true if the account is still valid.
734    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    /// Refreshes the proof using a new JWT.
746    ///
747    /// This method verifies the JWT signature using the OIDC provider's JWKS endpoint.
748    ///
749    /// # Network Requests
750    ///
751    /// This method makes HTTP requests to fetch the JWKS from the OIDC provider.
752    /// For more control over network calls and caching, use [`Self::refresh_proof_with_jwks`].
753    ///
754    /// # Errors
755    ///
756    /// Returns an error if:
757    /// - The JWKS cannot be fetched (network timeout, DNS failure, connection errors)
758    /// - The JWT signature verification fails
759    /// - The JWT cannot be decoded
760    /// - The JWT nonce does not match the ephemeral key
761    /// - The JWT identity does not match the account
762    /// - The prover service fails to generate a new proof
763    pub async fn refresh_proof(
764        &mut self,
765        jwt: &str,
766        prover_service: &dyn ProverService,
767    ) -> AptosResult<()> {
768        // Fetch JWKS and verify JWT
769        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    /// Refreshes the proof using a new JWT with pre-fetched JWKS.
779    ///
780    /// This method is useful for caching the JWKS or using a custom HTTP client.
781    ///
782    /// # Errors
783    ///
784    /// Returns an error if:
785    /// - The JWT signature verification fails
786    /// - The JWT cannot be decoded
787    /// - The JWT nonce does not match the ephemeral key
788    /// - The JWT identity does not match the account
789    /// - The prover service fails to generate a new proof
790    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    /// Signs a message and returns the structured keyless signature.
818    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    /// Creates a keyless account from pre-verified JWT claims.
828    ///
829    /// This is useful for testing or when JWT verification is handled externally.
830    /// The caller is responsible for ensuring the JWT was properly verified.
831    ///
832    /// # Errors
833    ///
834    /// This function will return an error if:
835    /// - The nonce doesn't match the ephemeral key's nonce
836    /// - The pepper service fails to return a pepper
837    /// - The prover service fails to generate a proof
838    #[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
940/// Default timeout for JWKS fetch requests (10 seconds).
941const JWKS_FETCH_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
942
943/// Maximum JWKS response size: 1 MB (JWKS payloads are typically under 10 KB).
944const MAX_JWKS_RESPONSE_SIZE: usize = 1024 * 1024;
945
946/// Fetches the JWKS (JSON Web Key Set) from an OIDC provider.
947///
948/// # Errors
949///
950/// Returns an error if:
951/// - The JWKS cannot be fetched (network timeouts, DNS resolution failures,
952///   TLS/connection errors, or HTTP errors)
953/// - The JWKS endpoint returns a non-success status code
954/// - The response cannot be parsed as valid JWKS JSON
955async fn fetch_jwks(client: &reqwest::Client, jwks_url: &str) -> AptosResult<JwkSet> {
956    // SECURITY: Validate the JWKS URL scheme to prevent SSRF.
957    // The issuer comes from an untrusted JWT, so the derived JWKS URL could
958    // point to internal services (e.g., cloud metadata endpoints).
959    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    // Note: timeout is configured on the client, not per-request
969    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    // SECURITY: Stream body with size limit to prevent OOM from a
979    // compromised or malicious JWKS endpoint (including chunked encoding).
980    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
986/// Decodes and verifies a JWT using the provided JWKS.
987///
988/// This function:
989/// 1. Extracts the `kid` (key ID) from the JWT header
990/// 2. Finds the matching key in the JWKS
991/// 3. Verifies the signature and decodes the claims
992///
993/// # Errors
994///
995/// Returns an error if:
996/// - The JWT header cannot be decoded
997/// - No matching key is found in the JWKS
998/// - The signature verification fails
999/// - The claims cannot be decoded
1000fn decode_and_verify_jwt(jwt: &str, jwks: &JwkSet) -> AptosResult<JwtClaims> {
1001    // Decode header to get the key ID
1002    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    // Find the matching key in the JWKS
1011    let signing_key = jwks.find(kid).ok_or_else(|| {
1012        AptosError::InvalidJwt("no matching key found for provided key identifier".into())
1013    })?;
1014
1015    // Create decoding key from JWK
1016    let decoding_key = DecodingKey::from_jwk(signing_key)
1017        .map_err(|e| AptosError::InvalidJwt(format!("failed to create decoding key: {e}")))?;
1018
1019    // Determine the algorithm strictly from the JWK to prevent algorithm substitution attacks
1020    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        // RSA algorithms
1027        jsonwebtoken::jwk::KeyAlgorithm::RS256 => Algorithm::RS256,
1028        jsonwebtoken::jwk::KeyAlgorithm::RS384 => Algorithm::RS384,
1029        jsonwebtoken::jwk::KeyAlgorithm::RS512 => Algorithm::RS512,
1030        // RSA-PSS algorithms
1031        jsonwebtoken::jwk::KeyAlgorithm::PS256 => Algorithm::PS256,
1032        jsonwebtoken::jwk::KeyAlgorithm::PS384 => Algorithm::PS384,
1033        jsonwebtoken::jwk::KeyAlgorithm::PS512 => Algorithm::PS512,
1034        // ECDSA algorithms
1035        jsonwebtoken::jwk::KeyAlgorithm::ES256 => Algorithm::ES256,
1036        jsonwebtoken::jwk::KeyAlgorithm::ES384 => Algorithm::ES384,
1037        // EdDSA algorithm
1038        jsonwebtoken::jwk::KeyAlgorithm::EdDSA => Algorithm::EdDSA,
1039        _ => {
1040            return Err(AptosError::InvalidJwt(format!(
1041                "unsupported JWK algorithm: {jwk_alg:?}"
1042            )));
1043        }
1044    };
1045
1046    // Ensure the JWT header algorithm matches the JWK algorithm to prevent substitution
1047    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    // Configure validation - we'll validate exp ourselves with more detailed errors
1055    let mut validation = Validation::new(algorithm);
1056    validation.validate_exp = false;
1057    validation.validate_aud = false; // We'll check aud after decoding
1058    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
1066/// Decodes JWT claims without signature verification.
1067///
1068/// This is used only to extract the issuer (and other metadata) before we know
1069/// which JWKS endpoint to fetch. This is safe because:
1070/// 1. The extracted issuer is only used to determine which JWKS endpoint to fetch.
1071/// 2. The JWT is fully verified immediately afterwards using `decode_and_verify_jwt`.
1072/// 3. No security decisions are made based on these unverified claims.
1073fn decode_claims_unverified(jwt: &str) -> AptosResult<JwtClaims> {
1074    // Use dangerous decode only for initial issuer extraction to select the JWKS.
1075    // The JWT is not trusted at this point: no authorization decisions are made
1076    // based on these unverified claims, and the token is fully verified (including
1077    // signature and claims validation) in `decode_and_verify_jwt` right after the
1078    // appropriate JWKS has been fetched.
1079    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        // Create a test JWT for the services (they don't validate it)
1204        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        // Use from_verified_claims for unit testing since we can't mock JWKS
1227        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        // Use a different nonce to trigger mismatch
1280        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(), // This doesn't match ephemeral.nonce()
1285            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        // Create a JWT without a kid in the header
1375        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        // HS256 JWT without kid
1389        let jwt = encode(
1390            &Header::new(Algorithm::HS256),
1391            &claims,
1392            &EncodingKey::from_secret(b"secret"),
1393        )
1394        .unwrap();
1395
1396        // Empty JWKS
1397        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        // Create JWT with a kid in header (using HS256 for encoding)
1424        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        // Empty JWKS - no matching key
1430        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        // Completely invalid JWT
1446        let result = decode_and_verify_jwt("not-a-valid-jwt", &jwks);
1447        assert!(result.is_err());
1448
1449        // JWT with invalid base64
1450        let result = decode_and_verify_jwt("aaa.bbb.ccc", &jwks);
1451        assert!(result.is_err());
1452    }
1453}