tempo_primitives/transaction/
tt_signature.rs

1use super::tempo_transaction::{
2    MAX_WEBAUTHN_SIGNATURE_LENGTH, P256_SIGNATURE_LENGTH, SECP256K1_SIGNATURE_LENGTH, SignatureType,
3};
4use alloy_primitives::{Address, B256, Bytes, Signature, keccak256};
5use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
6use p256::{
7    EncodedPoint,
8    ecdsa::{Signature as P256Signature, VerifyingKey, signature::hazmat::PrehashVerifier},
9};
10use sha2::{Digest, Sha256};
11use std::sync::OnceLock;
12
13/// Signature type identifiers
14/// Note: Secp256k1 has no identifier - detected by length (65 bytes)
15pub const SIGNATURE_TYPE_P256: u8 = 0x01;
16pub const SIGNATURE_TYPE_WEBAUTHN: u8 = 0x02;
17pub const SIGNATURE_TYPE_KEYCHAIN: u8 = 0x03;
18
19// Minimum authenticatorData is 37 bytes (32 rpIdHash + 1 flags + 4 signCount)
20const MIN_AUTH_DATA_LEN: usize = 37;
21
22/// P256 signature with pre-hash flag
23#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
24#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
25#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
26#[cfg_attr(feature = "reth-codec", derive(reth_codecs::Compact))]
27#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
28#[cfg_attr(test, reth_codecs::add_arbitrary_tests(compact))]
29pub struct P256SignatureWithPreHash {
30    pub r: B256,
31    pub s: B256,
32    pub pub_key_x: B256,
33    pub pub_key_y: B256,
34    pub pre_hash: bool,
35}
36
37/// WebAuthn signature with authenticator data
38#[derive(Clone, Debug, PartialEq, Eq, Hash)]
39#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
40#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
41#[cfg_attr(feature = "reth-codec", derive(reth_codecs::Compact))]
42#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
43#[cfg_attr(test, reth_codecs::add_arbitrary_tests(compact))]
44pub struct WebAuthnSignature {
45    pub r: B256,
46    pub s: B256,
47    pub pub_key_x: B256,
48    pub pub_key_y: B256,
49    /// authenticatorData || clientDataJSON (variable length)
50    pub webauthn_data: Bytes,
51}
52
53/// Primitive signature types that can be used standalone or within a Keychain signature.
54/// This enum contains only the base signature types: Secp256k1, P256, and WebAuthn.
55/// It does NOT support Keychain signatures to prevent recursion.
56///
57/// Note: This enum uses custom RLP encoding via `to_bytes()` and does NOT derive Compact.
58/// The Compact encoding is handled at the parent struct level (e.g., KeyAuthorization).
59#[derive(Clone, Debug, PartialEq, Eq, Hash)]
60#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
61#[cfg_attr(feature = "serde", serde(tag = "type", rename_all = "camelCase"))]
62#[cfg_attr(
63    all(test, feature = "reth-codec"),
64    reth_codecs::add_arbitrary_tests(compact, rlp)
65)]
66#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
67pub enum PrimitiveSignature {
68    /// Standard secp256k1 ECDSA signature (65 bytes: r, s, v)
69    Secp256k1(Signature),
70
71    /// P256 signature with embedded public key (129 bytes)
72    P256(P256SignatureWithPreHash),
73
74    /// WebAuthn signature with variable-length authenticator data
75    WebAuthn(WebAuthnSignature),
76}
77
78impl PrimitiveSignature {
79    /// Parse signature from bytes with backward compatibility
80    ///
81    /// For backward compatibility with existing secp256k1 signatures:
82    /// - If length is 65 bytes: treat as secp256k1 signature (no type identifier)
83    /// - Otherwise: first byte is the signature type identifier
84    pub fn from_bytes(data: &[u8]) -> Result<Self, &'static str> {
85        if data.is_empty() {
86            return Err("Signature data is empty");
87        }
88
89        // Backward compatibility: exactly 65 bytes means secp256k1 without type identifier
90        if data.len() == SECP256K1_SIGNATURE_LENGTH {
91            let sig = Signature::try_from(data)
92                .map_err(|_| "Failed to parse secp256k1 signature: invalid signature values")?;
93            return Ok(Self::Secp256k1(sig));
94        }
95
96        // For all other lengths, first byte is the type identifier
97        if data.len() < 2 {
98            return Err("Signature data too short: expected type identifier + signature data");
99        }
100
101        let type_id = data[0];
102        let sig_data = &data[1..];
103
104        match type_id {
105            SIGNATURE_TYPE_P256 => {
106                if sig_data.len() != P256_SIGNATURE_LENGTH {
107                    return Err("Invalid P256 signature length");
108                }
109                Ok(Self::P256(P256SignatureWithPreHash {
110                    r: B256::from_slice(&sig_data[0..32]),
111                    s: B256::from_slice(&sig_data[32..64]),
112                    pub_key_x: B256::from_slice(&sig_data[64..96]),
113                    pub_key_y: B256::from_slice(&sig_data[96..128]),
114                    pre_hash: sig_data[128] != 0,
115                }))
116            }
117            SIGNATURE_TYPE_WEBAUTHN => {
118                let len = sig_data.len();
119                if !(128..=MAX_WEBAUTHN_SIGNATURE_LENGTH).contains(&len) {
120                    return Err("Invalid WebAuthn signature length");
121                }
122                Ok(Self::WebAuthn(WebAuthnSignature {
123                    r: B256::from_slice(&sig_data[len - 128..len - 96]),
124                    s: B256::from_slice(&sig_data[len - 96..len - 64]),
125                    pub_key_x: B256::from_slice(&sig_data[len - 64..len - 32]),
126                    pub_key_y: B256::from_slice(&sig_data[len - 32..]),
127                    webauthn_data: Bytes::copy_from_slice(&sig_data[..len - 128]),
128                }))
129            }
130
131            _ => Err("Unknown signature type identifier"),
132        }
133    }
134
135    /// Encode signature to bytes
136    ///
137    /// For backward compatibility:
138    /// - Secp256k1: encoded WITHOUT type identifier (65 bytes)
139    /// - P256/WebAuthn: encoded WITH type identifier prefix
140    pub fn to_bytes(&self) -> Bytes {
141        match self {
142            Self::Secp256k1(sig) => {
143                // Backward compatibility: no type identifier for secp256k1
144                // Ensure exactly 65 bytes by using a fixed-size buffer
145                let sig_bytes = sig.as_bytes();
146                assert_eq!(
147                    sig_bytes.len(),
148                    SECP256K1_SIGNATURE_LENGTH,
149                    "Secp256k1 signature must be exactly 65 bytes"
150                );
151                Bytes::copy_from_slice(&sig_bytes)
152            }
153            Self::P256(p256_sig) => {
154                let mut bytes = Vec::with_capacity(1 + 129);
155                bytes.push(SIGNATURE_TYPE_P256);
156                bytes.extend_from_slice(p256_sig.r.as_slice());
157                bytes.extend_from_slice(p256_sig.s.as_slice());
158                bytes.extend_from_slice(p256_sig.pub_key_x.as_slice());
159                bytes.extend_from_slice(p256_sig.pub_key_y.as_slice());
160                bytes.push(if p256_sig.pre_hash { 1 } else { 0 });
161                Bytes::from(bytes)
162            }
163            Self::WebAuthn(webauthn_sig) => {
164                let mut bytes = Vec::with_capacity(1 + webauthn_sig.webauthn_data.len() + 128);
165                bytes.push(SIGNATURE_TYPE_WEBAUTHN);
166                bytes.extend_from_slice(&webauthn_sig.webauthn_data);
167                bytes.extend_from_slice(webauthn_sig.r.as_slice());
168                bytes.extend_from_slice(webauthn_sig.s.as_slice());
169                bytes.extend_from_slice(webauthn_sig.pub_key_x.as_slice());
170                bytes.extend_from_slice(webauthn_sig.pub_key_y.as_slice());
171                Bytes::from(bytes)
172            }
173        }
174    }
175
176    /// Get the length of the encoded signature in bytes
177    ///
178    /// For backward compatibility:
179    /// - Secp256k1: 65 bytes (no type identifier)
180    /// - P256/WebAuthn: includes 1-byte type identifier prefix
181    pub fn encoded_length(&self) -> usize {
182        match self {
183            Self::Secp256k1(_) => SECP256K1_SIGNATURE_LENGTH,
184            Self::P256(_) => 1 + P256_SIGNATURE_LENGTH,
185            Self::WebAuthn(webauthn_sig) => 1 + webauthn_sig.webauthn_data.len() + 128,
186        }
187    }
188
189    /// Get signature type
190    pub fn signature_type(&self) -> SignatureType {
191        match self {
192            Self::Secp256k1(_) => SignatureType::Secp256k1,
193            Self::P256(_) => SignatureType::P256,
194            Self::WebAuthn(_) => SignatureType::WebAuthn,
195        }
196    }
197
198    /// Get the in-memory size of the signature
199    pub fn size(&self) -> usize {
200        match self {
201            Self::Secp256k1(_) => SECP256K1_SIGNATURE_LENGTH,
202            Self::P256(_) => 1 + P256_SIGNATURE_LENGTH,
203            Self::WebAuthn(webauthn_sig) => 1 + webauthn_sig.webauthn_data.len() + 128,
204        }
205    }
206
207    /// Recover the signer address from the signature
208    ///
209    /// This function verifies the signature and extracts the address based on signature type:
210    /// - secp256k1: Uses standard ecrecover (signature verification + address recovery)
211    /// - P256: Verifies P256 signature then derives address from public key
212    /// - WebAuthn: Parses WebAuthn data, verifies P256 signature, derives address
213    pub fn recover_signer(
214        &self,
215        sig_hash: &B256,
216    ) -> Result<Address, alloy_consensus::crypto::RecoveryError> {
217        match self {
218            Self::Secp256k1(sig) => {
219                // Standard secp256k1 recovery using alloy's built-in methods
220                // This simultaneously verifies the signature AND recovers the address
221                Ok(sig.recover_address_from_prehash(sig_hash)?)
222            }
223            Self::P256(p256_sig) => {
224                // Prepare message hash for verification
225                let message_hash = if p256_sig.pre_hash {
226                    // Some P256 implementations (like Web Crypto) require pre-hashing
227                    B256::from_slice(&Sha256::digest(sig_hash.as_slice()))
228                } else {
229                    *sig_hash
230                };
231
232                // Verify P256 signature cryptographically
233                verify_p256_signature_internal(
234                    p256_sig.r.as_slice(),
235                    p256_sig.s.as_slice(),
236                    p256_sig.pub_key_x.as_slice(),
237                    p256_sig.pub_key_y.as_slice(),
238                    &message_hash,
239                )
240                .map_err(|_| alloy_consensus::crypto::RecoveryError::new())?;
241
242                // Derive and return address
243                Ok(derive_p256_address(
244                    &p256_sig.pub_key_x,
245                    &p256_sig.pub_key_y,
246                ))
247            }
248            Self::WebAuthn(webauthn_sig) => {
249                // Parse and verify WebAuthn data, compute challenge hash
250                let message_hash =
251                    verify_webauthn_data_internal(&webauthn_sig.webauthn_data, sig_hash)
252                        .map_err(|_| alloy_consensus::crypto::RecoveryError::new())?;
253
254                // Verify P256 signature over the computed message hash
255                verify_p256_signature_internal(
256                    webauthn_sig.r.as_slice(),
257                    webauthn_sig.s.as_slice(),
258                    webauthn_sig.pub_key_x.as_slice(),
259                    webauthn_sig.pub_key_y.as_slice(),
260                    &message_hash,
261                )
262                .map_err(|_| alloy_consensus::crypto::RecoveryError::new())?;
263
264                // Derive and return address
265                Ok(derive_p256_address(
266                    &webauthn_sig.pub_key_x,
267                    &webauthn_sig.pub_key_y,
268                ))
269            }
270        }
271    }
272}
273
274impl Default for PrimitiveSignature {
275    fn default() -> Self {
276        Self::Secp256k1(Signature::test_signature())
277    }
278}
279
280impl alloy_rlp::Encodable for PrimitiveSignature {
281    fn encode(&self, out: &mut dyn alloy_rlp::BufMut) {
282        let bytes = self.to_bytes();
283        alloy_rlp::Encodable::encode(&bytes, out);
284    }
285
286    fn length(&self) -> usize {
287        self.to_bytes().length()
288    }
289}
290
291impl alloy_rlp::Decodable for PrimitiveSignature {
292    fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
293        let bytes: Bytes = alloy_rlp::Decodable::decode(buf)?;
294        Self::from_bytes(&bytes).map_err(alloy_rlp::Error::Custom)
295    }
296}
297
298#[cfg(feature = "reth-codec")]
299impl reth_codecs::Compact for PrimitiveSignature {
300    fn to_compact<B>(&self, buf: &mut B) -> usize
301    where
302        B: alloy_rlp::BufMut + AsMut<[u8]>,
303    {
304        let bytes = self.to_bytes();
305        // Delegate to Bytes::to_compact which handles variable-length encoding
306        bytes.to_compact(buf)
307    }
308
309    fn from_compact(buf: &[u8], len: usize) -> (Self, &[u8]) {
310        // Delegate to Bytes::from_compact which handles variable-length decoding
311        let (bytes, rest) = Bytes::from_compact(buf, len);
312        let signature = Self::from_bytes(&bytes)
313            .expect("Failed to decode PrimitiveSignature from compact encoding");
314        (signature, rest)
315    }
316}
317
318/// Keychain signature wrapping another signature with a user address
319/// This allows an access key to sign on behalf of a root account
320///
321/// Format: 0x03 || user_address (20 bytes) || inner_signature
322///
323/// The user_address is the root account this transaction is being executed for.
324/// The inner signature proves an authorized access key signed the transaction.
325/// The handler validates that user_address has authorized the access key in the KeyChain precompile.
326#[derive(Clone, Debug)]
327#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
328#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
329#[cfg_attr(test, reth_codecs::add_arbitrary_tests(compact))]
330pub struct KeychainSignature {
331    /// Root account address that this transaction is being executed for
332    pub user_address: Address,
333    /// The actual signature from the access key (can be Secp256k1, P256, or WebAuthn, but NOT another Keychain)
334    pub signature: PrimitiveSignature,
335    /// Cached access key ID recovered from the inner signature.
336    /// This is an implementation detail - use `key_id()` to access.
337    /// Uses OnceLock for thread-safe interior mutability.
338    /// Note: Excluded from PartialEq, Eq, Hash, and Compact as it's a cache.
339    #[cfg_attr(
340        feature = "serde",
341        serde(
342            serialize_with = "serialize_once_lock",
343            rename = "keyId",
344            skip_deserializing,
345        )
346    )]
347    cached_key_id: OnceLock<Address>,
348}
349
350impl KeychainSignature {
351    /// Create a new KeychainSignature
352    pub fn new(user_address: Address, signature: PrimitiveSignature) -> Self {
353        Self {
354            user_address,
355            signature,
356            cached_key_id: OnceLock::new(),
357        }
358    }
359
360    /// Get the access key ID for Keychain signatures.
361    ///
362    /// For Keychain signatures, this returns the access key address that signed the transaction.
363    /// The key_id is recovered from the inner signature on first access and cached for
364    /// subsequent calls. Returns None for non-Keychain signatures.
365    ///
366    /// This follows the pattern used in alloy for lazy hash computation.
367    pub fn key_id(
368        &self,
369        sig_hash: &B256,
370    ) -> Result<Address, alloy_consensus::crypto::RecoveryError> {
371        // Check if already cached
372        if let Some(cached) = self.cached_key_id.get() {
373            return Ok(*cached);
374        }
375
376        // Not cached - recover and cache
377        let key_id = self.signature.recover_signer(sig_hash)?;
378        let _ = self.cached_key_id.set(key_id);
379        Ok(key_id)
380    }
381}
382
383// Manual implementations of PartialEq, Eq, and Hash that exclude cached_key_id
384// since it's just a cache and doesn't affect the logical equality of signatures
385impl PartialEq for KeychainSignature {
386    fn eq(&self, other: &Self) -> bool {
387        self.user_address == other.user_address && self.signature == other.signature
388    }
389}
390
391impl Eq for KeychainSignature {}
392
393impl core::hash::Hash for KeychainSignature {
394    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
395        self.user_address.hash(state);
396        self.signature.hash(state);
397    }
398}
399
400// Manual Compact implementation that excludes cached_key_id (cache field)
401#[cfg(feature = "reth-codec")]
402impl reth_codecs::Compact for KeychainSignature {
403    fn to_compact<B>(&self, buf: &mut B) -> usize
404    where
405        B: alloy_rlp::BufMut + AsMut<[u8]>,
406    {
407        // Only encode user_address and signature, skip cached_key_id
408        let mut written = 0;
409        written += self.user_address.to_compact(buf);
410        written += self.signature.to_compact(buf);
411        written
412    }
413
414    fn from_compact(buf: &[u8], len: usize) -> (Self, &[u8]) {
415        // Decode user_address and signature, initialize cached_key_id as empty
416        let (user_address, rest) = Address::from_compact(buf, len);
417        let remaining_len = len - (buf.len() - rest.len());
418        let (signature, rest) = PrimitiveSignature::from_compact(rest, remaining_len);
419
420        (
421            Self {
422                user_address,
423                signature,
424                cached_key_id: OnceLock::new(),
425            },
426            rest,
427        )
428    }
429}
430
431// Manual Arbitrary implementation that excludes cached_key_id (cache field)
432#[cfg(any(test, feature = "arbitrary"))]
433impl<'a> arbitrary::Arbitrary<'a> for KeychainSignature {
434    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
435        Ok(Self {
436            user_address: u.arbitrary()?,
437            signature: u.arbitrary()?,
438            cached_key_id: OnceLock::new(), // Always start with empty cache
439        })
440    }
441}
442
443/// AA transaction signature supporting multiple signature schemes
444///
445/// Note: Uses custom Compact implementation that delegates to `to_bytes()` / `from_bytes()`.
446#[derive(Clone, Debug, PartialEq, Eq, Hash)]
447#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
448#[cfg_attr(feature = "serde", serde(untagged, rename_all = "camelCase"))]
449#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
450#[cfg_attr(test, reth_codecs::add_arbitrary_tests(compact, rlp))]
451pub enum TempoSignature {
452    /// Primitive signature types: Secp256k1, P256, or WebAuthn
453    Primitive(PrimitiveSignature),
454
455    /// Keychain signature - wraps another signature with a key identifier
456    /// Format: key_id (20 bytes) + inner signature
457    /// IMP: The inner signature MUST NOT be another Keychain (validated at runtime)
458    /// Note: Recursion is prevented by KeychainSignature's custom Arbitrary impl
459    Keychain(KeychainSignature),
460}
461
462impl TempoSignature {
463    /// Parse signature from bytes with backward compatibility
464    ///
465    /// For backward compatibility with existing secp256k1 signatures:
466    /// - If length is 65 bytes: treat as secp256k1 signature (no type identifier)
467    /// - Otherwise: first byte is the signature type identifier
468    pub fn from_bytes(data: &[u8]) -> Result<Self, &'static str> {
469        if data.is_empty() {
470            return Err("Signature data is empty");
471        }
472
473        // Check if this is a Keychain signature (type identifier 0x03)
474        // We need to handle this specially before delegating to PrimitiveSignature
475        if data.len() > 1
476            && data.len() != SECP256K1_SIGNATURE_LENGTH
477            && data[0] == SIGNATURE_TYPE_KEYCHAIN
478        {
479            let sig_data = &data[1..];
480
481            // Keychain format: user_address (20 bytes) || inner_signature
482            if sig_data.len() < 20 {
483                return Err("Invalid Keychain signature: too short for user_address");
484            }
485
486            let user_address = Address::from_slice(&sig_data[0..20]);
487            let inner_sig_bytes = &sig_data[20..];
488
489            // Parse inner signature using PrimitiveSignature (which doesn't support Keychain)
490            // This automatically prevents recursive keychain signatures at compile time
491            let inner_signature = PrimitiveSignature::from_bytes(inner_sig_bytes)?;
492
493            return Ok(Self::Keychain(KeychainSignature {
494                user_address,
495                signature: inner_signature,
496                cached_key_id: OnceLock::new(),
497            }));
498        }
499
500        // For all non-Keychain signatures, delegate to PrimitiveSignature
501        let primitive = PrimitiveSignature::from_bytes(data)?;
502        Ok(Self::Primitive(primitive))
503    }
504
505    /// Encode signature to bytes
506    ///
507    /// For backward compatibility:
508    /// - Secp256k1: encoded WITHOUT type identifier (65 bytes)
509    /// - P256/WebAuthn: encoded WITH type identifier prefix
510    pub fn to_bytes(&self) -> Bytes {
511        match self {
512            Self::Primitive(primitive_sig) => primitive_sig.to_bytes(),
513            Self::Keychain(keychain_sig) => {
514                // Format: 0x03 | user_address (20 bytes) | inner_signature
515                let inner_bytes = keychain_sig.signature.to_bytes();
516                let mut bytes = Vec::with_capacity(1 + 20 + inner_bytes.len());
517                bytes.push(SIGNATURE_TYPE_KEYCHAIN);
518                bytes.extend_from_slice(keychain_sig.user_address.as_slice());
519                bytes.extend_from_slice(&inner_bytes);
520                Bytes::from(bytes)
521            }
522        }
523    }
524
525    /// Get the length of the encoded signature in bytes
526    ///
527    /// For backward compatibility:
528    /// - Secp256k1: 65 bytes (no type identifier)
529    /// - P256/WebAuthn: includes 1-byte type identifier prefix
530    pub fn encoded_length(&self) -> usize {
531        match self {
532            Self::Primitive(primitive_sig) => primitive_sig.encoded_length(),
533            Self::Keychain(keychain_sig) => 1 + 20 + keychain_sig.signature.encoded_length(),
534        }
535    }
536
537    /// Get signature type
538    pub fn signature_type(&self) -> SignatureType {
539        match self {
540            Self::Primitive(primitive_sig) => primitive_sig.signature_type(),
541            Self::Keychain(keychain_sig) => keychain_sig.signature.signature_type(),
542        }
543    }
544
545    /// Get the in-memory size of the signature
546    pub fn size(&self) -> usize {
547        match self {
548            Self::Primitive(primitive_sig) => primitive_sig.size(),
549            Self::Keychain(keychain_sig) => 1 + 20 + keychain_sig.signature.size(),
550        }
551    }
552
553    /// Recover the signer address from the signature
554    ///
555    /// This function verifies the signature and extracts the address based on signature type:
556    /// - secp256k1: Uses standard ecrecover (signature verification + address recovery)
557    /// - P256: Verifies P256 signature then derives address from public key
558    /// - WebAuthn: Parses WebAuthn data, verifies P256 signature, derives address
559    /// - Keychain: Validates inner signature and returns user_address
560    ///
561    /// For Keychain signatures, this performs full validation of the inner signature.
562    /// The access key address is cached in the KeychainSignature for later use.
563    pub fn recover_signer(
564        &self,
565        sig_hash: &B256,
566    ) -> Result<Address, alloy_consensus::crypto::RecoveryError> {
567        match self {
568            Self::Primitive(primitive_sig) => primitive_sig.recover_signer(sig_hash),
569            Self::Keychain(keychain_sig) => {
570                // Ensure validity of the keychain signature and cache the key id
571                keychain_sig.key_id(sig_hash)?;
572
573                // Return the user_address - the root account this transaction is for
574                Ok(keychain_sig.user_address)
575            }
576        }
577    }
578
579    /// Check if this is a Keychain signature
580    pub fn is_keychain(&self) -> bool {
581        matches!(self, Self::Keychain(_))
582    }
583
584    /// Get the Keychain signature if this is a Keychain signature
585    pub fn as_keychain(&self) -> Option<&KeychainSignature> {
586        match self {
587            Self::Keychain(keychain_sig) => Some(keychain_sig),
588            _ => None,
589        }
590    }
591}
592
593impl Default for TempoSignature {
594    fn default() -> Self {
595        Self::Primitive(PrimitiveSignature::default())
596    }
597}
598
599impl alloy_rlp::Encodable for TempoSignature {
600    fn encode(&self, out: &mut dyn alloy_rlp::BufMut) {
601        let bytes = self.to_bytes();
602        alloy_rlp::Encodable::encode(&bytes, out);
603    }
604
605    fn length(&self) -> usize {
606        self.to_bytes().length()
607    }
608}
609
610impl alloy_rlp::Decodable for TempoSignature {
611    fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
612        let bytes: Bytes = alloy_rlp::Decodable::decode(buf)?;
613        Self::from_bytes(&bytes).map_err(alloy_rlp::Error::Custom)
614    }
615}
616
617#[cfg(feature = "reth-codec")]
618impl reth_codecs::Compact for TempoSignature {
619    fn to_compact<B>(&self, buf: &mut B) -> usize
620    where
621        B: alloy_rlp::BufMut + AsMut<[u8]>,
622    {
623        let bytes = self.to_bytes();
624        // Delegate to Bytes::to_compact which handles variable-length encoding
625        bytes.to_compact(buf)
626    }
627
628    fn from_compact(buf: &[u8], len: usize) -> (Self, &[u8]) {
629        // Delegate to Bytes::from_compact which handles variable-length decoding
630        let (bytes, rest) = Bytes::from_compact(buf, len);
631        let signature = Self::from_bytes(&bytes)
632            .expect("Failed to decode TempoSignature from compact encoding");
633        (signature, rest)
634    }
635}
636
637impl From<Signature> for TempoSignature {
638    fn from(signature: Signature) -> Self {
639        Self::Primitive(PrimitiveSignature::Secp256k1(signature))
640    }
641}
642
643// ============================================================================
644// Helper Functions for Signature Verification
645// ============================================================================
646
647/// Derives a P256 address from public key coordinates
648pub fn derive_p256_address(pub_key_x: &B256, pub_key_y: &B256) -> Address {
649    let hash = keccak256([pub_key_x.as_slice(), pub_key_y.as_slice()].concat());
650
651    // Take last 20 bytes as address
652    Address::from_slice(&hash[12..])
653}
654
655/// Verifies a P256 signature using the provided components
656///
657/// This performs actual cryptographic verification of the P256 signature
658/// according to the spec. Called during `recover_signer()` to ensure only
659/// valid signatures enter the mempool.
660fn verify_p256_signature_internal(
661    r: &[u8],
662    s: &[u8],
663    pub_key_x: &[u8],
664    pub_key_y: &[u8],
665    message_hash: &B256,
666) -> Result<(), &'static str> {
667    // Parse public key from affine coordinates
668    let encoded_point = EncodedPoint::from_affine_coordinates(
669        pub_key_x.into(),
670        pub_key_y.into(),
671        false, // Not compressed
672    );
673
674    let verifying_key =
675        VerifyingKey::from_encoded_point(&encoded_point).map_err(|_| "Invalid P256 public key")?;
676
677    let signature = P256Signature::from_slice(&[r, s].concat())
678        .map_err(|_| "Invalid P256 signature encoding")?;
679
680    // Verify signature
681    verifying_key
682        .verify_prehash(message_hash.as_slice(), &signature)
683        .map_err(|_| "P256 signature verification failed")
684}
685
686/// Parses and validates WebAuthn data, returning the message hash for P256 verification
687/// ref: <https://www.w3.org/TR/webauthn-2/#sctn-authenticator-data>
688///
689/// According to the spec, this:
690/// 1. Parses authenticatorData and clientDataJSON
691/// 2. Validates authenticatorData (min 37 bytes, UP flag set)
692/// 3. Validates clientDataJSON (type="webauthn.get", challenge matches tx_hash)
693/// 4. Computes message hash = sha256(authenticatorData || sha256(clientDataJSON))
694fn verify_webauthn_data_internal(
695    webauthn_data: &[u8],
696    tx_hash: &B256,
697) -> Result<B256, &'static str> {
698    // Ensure that we have clientDataJSON after authenticatorData
699    if webauthn_data.len() < MIN_AUTH_DATA_LEN + 32 {
700        return Err("WebAuthn data too short");
701    }
702
703    // Check flags (byte 32): UP (bit 0), AT (bit 6), ED (bit 7)
704    let flags = webauthn_data[32];
705    let (up_flag, at_flag, ed_flag) = (flags & 0x01, flags & 0x40, flags & 0x80);
706
707    // UP flag MUST be set
708    if up_flag == 0 {
709        return Err("User Presence (UP) flag not set in authenticatorData");
710    }
711
712    // AT flag must NOT be set for assertion signatures (`webauthn.get`)
713    if at_flag != 0 {
714        return Err("AT flag must not be set for assertion signatures");
715    }
716
717    // Determine authenticatorData length
718    let auth_data_len = if ed_flag == 0 {
719        // If ED flag is not set, exactly 37 bytes (no extensions)
720        MIN_AUTH_DATA_LEN
721    } else {
722        // ED flag must NOT be set, as Tempo AA doesn't support extensions
723        // NOTE: If we ever want to support extensions, we will have to parse CBOR data
724        return Err("ED flag must not be set, as Tempo doesn't support extensions");
725    };
726
727    let authenticator_data = &webauthn_data[..auth_data_len];
728    let client_data_json = &webauthn_data[auth_data_len..];
729
730    // Validate clientDataJSON
731    let json_str =
732        core::str::from_utf8(client_data_json).map_err(|_| "clientDataJSON is not valid UTF-8")?;
733
734    // Basic JSON structure validation
735    if !json_str.starts_with('{') || !json_str.ends_with('}') {
736        return Err("clientDataJSON is not valid JSON");
737    }
738
739    // Check for required type field
740    if !json_str.contains("\"type\":\"webauthn.get\"") {
741        return Err("clientDataJSON missing required type field");
742    }
743
744    // Verify challenge matches tx_hash (Base64URL encoded)
745    let challenge_b64url = URL_SAFE_NO_PAD.encode(tx_hash.as_slice());
746    let challenge_property = format!("\"challenge\":\"{challenge_b64url}\"");
747    if !json_str.contains(&challenge_property) {
748        return Err("clientDataJSON challenge does not match transaction hash");
749    }
750
751    // Compute message hash according to spec:
752    // messageHash = sha256(authenticatorData || sha256(clientDataJSON))
753    let client_data_hash = Sha256::digest(client_data_json);
754
755    let mut final_hasher = Sha256::new();
756    final_hasher.update(authenticator_data);
757    final_hasher.update(client_data_hash);
758    let message_hash = final_hasher.finalize();
759
760    Ok(B256::from_slice(&message_hash))
761}
762
763#[cfg(feature = "serde")]
764/// Helper function to serialize a [`OnceLock`] as an [`Option`] if it's initialized.
765fn serialize_once_lock<S>(value: &OnceLock<Address>, serializer: S) -> Result<S::Ok, S::Error>
766where
767    S: serde::Serializer,
768{
769    serde::Serialize::serialize(&value.get(), serializer)
770}
771
772#[cfg(test)]
773mod tests {
774    use super::*;
775    use alloy_primitives::hex;
776
777    #[test]
778    fn test_p256_signature_verification_invalid_pubkey() {
779        // Invalid public key should fail
780        let r = [0u8; 32];
781        let s = [0u8; 32];
782        let pub_key_x = [0u8; 32]; // Invalid: point not on curve
783        let pub_key_y = [0u8; 32];
784        let message_hash = B256::ZERO;
785
786        let result = verify_p256_signature_internal(&r, &s, &pub_key_x, &pub_key_y, &message_hash);
787        assert!(result.is_err());
788    }
789
790    #[test]
791    fn test_p256_signature_verification_invalid_signature() {
792        use p256::{ecdsa::SigningKey, elliptic_curve::rand_core::OsRng};
793
794        // Generate a valid key pair
795        let signing_key = SigningKey::random(&mut OsRng);
796        let verifying_key = signing_key.verifying_key();
797
798        // Extract public key coordinates
799        let encoded_point = verifying_key.to_encoded_point(false);
800        let pub_key_x = encoded_point.x().unwrap();
801        let pub_key_y = encoded_point.y().unwrap();
802
803        // Use invalid signature (all zeros)
804        let r = [0u8; 32];
805        let s = [0u8; 32];
806        let message_hash = B256::ZERO;
807
808        let result = verify_p256_signature_internal(
809            &r,
810            &s,
811            pub_key_x.as_slice(),
812            pub_key_y.as_slice(),
813            &message_hash,
814        );
815        assert!(
816            result.is_err(),
817            "Invalid signature should fail verification"
818        );
819    }
820
821    #[test]
822    fn test_p256_signature_verification_valid() {
823        use p256::{
824            ecdsa::{SigningKey, signature::hazmat::PrehashSigner},
825            elliptic_curve::rand_core::OsRng,
826        };
827        use sha2::{Digest, Sha256};
828
829        // Generate a valid key pair
830        let signing_key = SigningKey::random(&mut OsRng);
831        let verifying_key = signing_key.verifying_key();
832
833        // Create a message and sign it
834        let message = b"test message";
835        let message_hash = B256::from_slice(&Sha256::digest(message));
836
837        // Sign the message
838        let signature: p256::ecdsa::Signature =
839            signing_key.sign_prehash(message_hash.as_slice()).unwrap();
840        let sig_bytes = signature.to_bytes();
841        let r = &sig_bytes[0..32];
842        let s = &sig_bytes[32..64];
843
844        // Extract public key coordinates
845        let encoded_point = verifying_key.to_encoded_point(false);
846        let pub_key_x = encoded_point.x().unwrap();
847        let pub_key_y = encoded_point.y().unwrap();
848
849        // Verify the signature
850        let result = verify_p256_signature_internal(
851            r,
852            s,
853            pub_key_x.as_slice(),
854            pub_key_y.as_slice(),
855            &message_hash,
856        );
857        assert!(
858            result.is_ok(),
859            "Valid P256 signature should verify successfully"
860        );
861    }
862
863    #[test]
864    fn test_webauthn_data_verification_too_short() {
865        // WebAuthn data must be at least 37 bytes (authenticatorData minimum)
866        let short_data = vec![0u8; 36];
867        let tx_hash = B256::ZERO;
868
869        let result = verify_webauthn_data_internal(&short_data, &tx_hash);
870        assert!(result.is_err());
871        assert_eq!(result.unwrap_err(), "WebAuthn data too short");
872    }
873
874    #[test]
875    fn test_webauthn_data_verification_missing_up_flag() {
876        // Create authenticatorData without UP flag set
877        let mut auth_data = vec![0u8; 37];
878        auth_data[32] = 0x00; // flags byte with UP flag not set
879
880        // Add minimal clientDataJSON
881        let client_data = b"{\"type\":\"webauthn.get\",\"challenge\":\"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\"}";
882        let mut webauthn_data = auth_data;
883        webauthn_data.extend_from_slice(client_data);
884
885        let tx_hash = B256::ZERO;
886        let result = verify_webauthn_data_internal(&webauthn_data, &tx_hash);
887
888        assert!(result.is_err());
889        assert_eq!(
890            result.unwrap_err(),
891            "User Presence (UP) flag not set in authenticatorData"
892        );
893    }
894
895    #[test]
896    fn test_webauthn_data_verification_invalid_type() {
897        // Create valid authenticatorData with UP flag
898        let mut auth_data = vec![0u8; 37];
899        auth_data[32] = 0x01; // flags byte with UP flag set
900
901        // Add clientDataJSON with wrong type
902        let client_data = b"{\"type\":\"webauthn.create\",\"challenge\":\"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\"}";
903        let mut webauthn_data = auth_data;
904        webauthn_data.extend_from_slice(client_data);
905
906        let tx_hash = B256::ZERO;
907        let result = verify_webauthn_data_internal(&webauthn_data, &tx_hash);
908
909        assert!(result.is_err());
910        assert_eq!(
911            result.unwrap_err(),
912            "clientDataJSON missing required type field"
913        );
914    }
915
916    #[test]
917    fn test_webauthn_data_verification_invalid_challenge() {
918        // Create valid authenticatorData with UP flag
919        let mut auth_data = vec![0u8; 37];
920        auth_data[32] = 0x01; // flags byte with UP flag set
921
922        // Add clientDataJSON with wrong challenge
923        let client_data =
924            b"{\"type\":\"webauthn.get\",\"challenge\":\"wrong_challenge_value_here\"}";
925        let mut webauthn_data = auth_data;
926        webauthn_data.extend_from_slice(client_data);
927
928        let tx_hash = B256::ZERO;
929        let result = verify_webauthn_data_internal(&webauthn_data, &tx_hash);
930
931        assert!(result.is_err());
932        assert_eq!(
933            result.unwrap_err(),
934            "clientDataJSON challenge does not match transaction hash"
935        );
936    }
937
938    #[test]
939    fn test_webauthn_data_verification_valid() {
940        use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
941        use sha2::{Digest, Sha256};
942
943        // Create valid authenticatorData with UP flag
944        let mut auth_data = vec![0u8; 37];
945        auth_data[32] = 0x01; // flags byte with UP flag set
946
947        // Create a test transaction hash
948        let tx_hash = B256::from_slice(&[0xAA; 32]);
949
950        // Encode challenge as Base64URL
951        let challenge_b64url = URL_SAFE_NO_PAD.encode(tx_hash.as_slice());
952
953        // Create valid clientDataJSON with matching challenge
954        let client_data =
955            format!("{{\"type\":\"webauthn.get\",\"challenge\":\"{challenge_b64url}\"}}");
956        let mut webauthn_data = auth_data.clone();
957        webauthn_data.extend_from_slice(client_data.as_bytes());
958
959        let result = verify_webauthn_data_internal(&webauthn_data, &tx_hash);
960        assert!(
961            result.is_ok(),
962            "Valid WebAuthn data should verify successfully"
963        );
964
965        // Verify the computed message hash is correct
966        let message_hash = result.unwrap();
967
968        // Manually compute expected hash
969        let client_data_hash = Sha256::digest(client_data.as_bytes());
970
971        let mut final_hasher = Sha256::new();
972        final_hasher.update(&auth_data);
973        final_hasher.update(client_data_hash);
974        let expected_hash = final_hasher.finalize();
975
976        assert_eq!(message_hash.as_slice(), expected_hash.as_slice());
977    }
978
979    #[test]
980    fn test_p256_address_derivation() {
981        let pub_key_x =
982            hex!("1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef").into();
983        let pub_key_y =
984            hex!("fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321").into();
985
986        let addr1 = derive_p256_address(&pub_key_x, &pub_key_y);
987        let addr2 = derive_p256_address(&pub_key_x, &pub_key_y);
988
989        // Should be deterministic
990        assert_eq!(addr1, addr2);
991
992        // Should not be zero address
993        assert_ne!(addr1, Address::ZERO);
994    }
995
996    #[test]
997    fn test_p256_address_derivation_deterministic() {
998        // Test that address derivation is deterministic
999        let pub_key_x =
1000            hex!("1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef").into();
1001        let pub_key_y =
1002            hex!("fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321").into();
1003
1004        let addr1 = derive_p256_address(&pub_key_x, &pub_key_y);
1005        let addr2 = derive_p256_address(&pub_key_x, &pub_key_y);
1006
1007        assert_eq!(addr1, addr2, "Address derivation should be deterministic");
1008    }
1009
1010    #[test]
1011    fn test_p256_address_different_keys_different_addresses() {
1012        // Different keys should produce different addresses
1013        let pub_key_x1 =
1014            hex!("1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef").into();
1015        let pub_key_y1 =
1016            hex!("fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321").into();
1017
1018        let pub_key_x2 =
1019            hex!("fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321").into();
1020        let pub_key_y2 =
1021            hex!("1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef").into();
1022
1023        let addr1 = derive_p256_address(&pub_key_x1, &pub_key_y1);
1024        let addr2 = derive_p256_address(&pub_key_x2, &pub_key_y2);
1025
1026        assert_ne!(
1027            addr1, addr2,
1028            "Different keys should produce different addresses"
1029        );
1030    }
1031
1032    #[test]
1033    fn test_tempo_signature_from_bytes_secp256k1() {
1034        use super::SECP256K1_SIGNATURE_LENGTH;
1035
1036        // Secp256k1 signatures are detected by length (65 bytes), no type identifier
1037        let sig_bytes = vec![0u8; SECP256K1_SIGNATURE_LENGTH];
1038        let result = TempoSignature::from_bytes(&sig_bytes);
1039
1040        assert!(result.is_ok());
1041        if let TempoSignature::Primitive(PrimitiveSignature::Secp256k1(_)) = result.unwrap() {
1042            // Expected
1043        } else {
1044            panic!("Expected Primitive(Secp256k1) variant");
1045        }
1046    }
1047
1048    #[test]
1049    fn test_tempo_signature_from_bytes_p256() {
1050        use super::{P256_SIGNATURE_LENGTH, SIGNATURE_TYPE_P256};
1051
1052        let mut sig_bytes = vec![SIGNATURE_TYPE_P256];
1053        sig_bytes.extend_from_slice(&[0u8; P256_SIGNATURE_LENGTH]);
1054        let result = TempoSignature::from_bytes(&sig_bytes);
1055
1056        assert!(result.is_ok());
1057        if let TempoSignature::Primitive(PrimitiveSignature::P256(_)) = result.unwrap() {
1058            // Expected
1059        } else {
1060            panic!("Expected Primitive(P256) variant");
1061        }
1062    }
1063
1064    #[test]
1065    fn test_tempo_signature_from_bytes_webauthn() {
1066        use super::SIGNATURE_TYPE_WEBAUTHN;
1067
1068        let mut sig_bytes = vec![SIGNATURE_TYPE_WEBAUTHN];
1069        sig_bytes.extend_from_slice(&[0u8; 200]); // 200 bytes of WebAuthn data
1070        let result = TempoSignature::from_bytes(&sig_bytes);
1071
1072        assert!(result.is_ok());
1073        if let TempoSignature::Primitive(PrimitiveSignature::WebAuthn(_)) = result.unwrap() {
1074            // Expected
1075        } else {
1076            panic!("Expected Primitive(WebAuthn) variant");
1077        }
1078    }
1079
1080    #[test]
1081    fn test_tempo_signature_roundtrip() {
1082        use super::{
1083            P256_SIGNATURE_LENGTH, SECP256K1_SIGNATURE_LENGTH, SIGNATURE_TYPE_P256,
1084            SIGNATURE_TYPE_WEBAUTHN,
1085        };
1086
1087        // Test secp256k1 (no type identifier, detected by 65-byte length)
1088        let sig1_bytes = vec![1u8; SECP256K1_SIGNATURE_LENGTH];
1089        let sig1 = TempoSignature::from_bytes(&sig1_bytes).unwrap();
1090        let encoded1 = sig1.to_bytes();
1091        assert_eq!(encoded1.len(), SECP256K1_SIGNATURE_LENGTH); // No type identifier
1092        // Verify roundtrip
1093        let decoded1 = TempoSignature::from_bytes(&encoded1).unwrap();
1094        assert_eq!(sig1, decoded1);
1095
1096        // Test P256
1097        let mut sig2_bytes = vec![SIGNATURE_TYPE_P256];
1098        sig2_bytes.extend_from_slice(&[2u8; P256_SIGNATURE_LENGTH]);
1099        let sig2 = TempoSignature::from_bytes(&sig2_bytes).unwrap();
1100        let encoded2 = sig2.to_bytes();
1101        assert_eq!(encoded2.len(), 1 + P256_SIGNATURE_LENGTH);
1102        // Verify roundtrip
1103        let decoded2 = TempoSignature::from_bytes(&encoded2).unwrap();
1104        assert_eq!(sig2, decoded2);
1105
1106        // Test WebAuthn
1107        let mut sig3_bytes = vec![SIGNATURE_TYPE_WEBAUTHN];
1108        sig3_bytes.extend_from_slice(&[3u8; 200]);
1109        let sig3 = TempoSignature::from_bytes(&sig3_bytes).unwrap();
1110        let encoded3 = sig3.to_bytes();
1111        assert_eq!(encoded3.len(), 1 + 200);
1112        // Verify roundtrip
1113        let decoded3 = TempoSignature::from_bytes(&encoded3).unwrap();
1114        assert_eq!(sig3, decoded3);
1115    }
1116
1117    #[test]
1118    #[cfg(feature = "serde")]
1119    fn test_tempo_signature_serde_roundtrip() {
1120        // Test serde roundtrip for all signature types
1121
1122        // Test Secp256k1
1123        let r_bytes = hex!("1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef");
1124        let s_bytes = hex!("fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321");
1125        let sig = Signature::new(
1126            alloy_primitives::U256::from_be_slice(&r_bytes),
1127            alloy_primitives::U256::from_be_slice(&s_bytes),
1128            false,
1129        );
1130        let secp256k1_sig = TempoSignature::Primitive(PrimitiveSignature::Secp256k1(sig));
1131
1132        let json = serde_json::to_string(&secp256k1_sig).unwrap();
1133        let decoded: TempoSignature = serde_json::from_str(&json).unwrap();
1134        assert_eq!(secp256k1_sig, decoded, "Secp256k1 serde roundtrip failed");
1135
1136        // Test P256
1137        let p256_sig =
1138            TempoSignature::Primitive(PrimitiveSignature::P256(P256SignatureWithPreHash {
1139                r: B256::from([1u8; 32]),
1140                s: B256::from([2u8; 32]),
1141                pub_key_x: B256::from([3u8; 32]),
1142                pub_key_y: B256::from([4u8; 32]),
1143                pre_hash: true,
1144            }));
1145
1146        let json = serde_json::to_string(&p256_sig).unwrap();
1147        let decoded: TempoSignature = serde_json::from_str(&json).unwrap();
1148        assert_eq!(p256_sig, decoded, "P256 serde roundtrip failed");
1149
1150        // Verify camelCase naming
1151        assert!(
1152            json.contains("\"pubKeyX\""),
1153            "Should use camelCase for pubKeyX"
1154        );
1155        assert!(
1156            json.contains("\"pubKeyY\""),
1157            "Should use camelCase for pubKeyY"
1158        );
1159        assert!(
1160            json.contains("\"preHash\""),
1161            "Should use camelCase for preHash"
1162        );
1163
1164        // Test WebAuthn
1165        let webauthn_sig =
1166            TempoSignature::Primitive(PrimitiveSignature::WebAuthn(WebAuthnSignature {
1167                r: B256::from([5u8; 32]),
1168                s: B256::from([6u8; 32]),
1169                pub_key_x: B256::from([7u8; 32]),
1170                pub_key_y: B256::from([8u8; 32]),
1171                webauthn_data: Bytes::from(vec![9u8; 50]),
1172            }));
1173
1174        let json = serde_json::to_string(&webauthn_sig).unwrap();
1175        let decoded: TempoSignature = serde_json::from_str(&json).unwrap();
1176        assert_eq!(webauthn_sig, decoded, "WebAuthn serde roundtrip failed");
1177
1178        // Verify camelCase naming
1179        assert!(
1180            json.contains("\"pubKeyX\""),
1181            "Should use camelCase for pubKeyX"
1182        );
1183        assert!(
1184            json.contains("\"pubKeyY\""),
1185            "Should use camelCase for pubKeyY"
1186        );
1187        assert!(
1188            json.contains("\"webauthnData\""),
1189            "Should use camelCase for webauthnData"
1190        );
1191    }
1192
1193    #[test]
1194    fn test_webauthn_flag_validation() {
1195        use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
1196
1197        // Helper to build webauthn data with given flags and optional extension bytes
1198        fn build_webauthn_data(flags: u8, extension: Option<&[u8]>, tx_hash: &B256) -> Vec<u8> {
1199            let mut data = vec![0u8; 32]; // rpIdHash
1200            data.push(flags);
1201            data.extend_from_slice(&[0u8; 4]); // signCount
1202            if let Some(ext) = extension {
1203                data.extend_from_slice(ext);
1204            }
1205            let challenge = URL_SAFE_NO_PAD.encode(tx_hash.as_slice());
1206            data.extend_from_slice(
1207                format!("{{\"type\":\"webauthn.get\",\"challenge\":\"{challenge}\"}}").as_bytes(),
1208            );
1209            data
1210        }
1211
1212        let tx_hash = B256::ZERO;
1213
1214        // AT flag must be rejected for assertion signatures
1215        let data = build_webauthn_data(0x41, None, &tx_hash); // UP + AT
1216        let err = verify_webauthn_data_internal(&data, &tx_hash).unwrap_err();
1217        assert!(err.contains("AT flag"), "Should reject AT flag");
1218
1219        // ED flag must be rejected, as extensions are not supported
1220        let data = build_webauthn_data(0x81, Some(&[0xa0]), &tx_hash); // UP + ED, empty map
1221        let err = verify_webauthn_data_internal(&data, &tx_hash).unwrap_err();
1222        assert!(err.contains("ED flag"), "Should reject ED flag");
1223
1224        // Valid with only UP flag set
1225        let data = build_webauthn_data(0x01, None, &tx_hash); // UP only
1226        assert!(
1227            verify_webauthn_data_internal(&data, &tx_hash).is_ok(),
1228            "Should accept valid webauthn data with only UP flag"
1229        );
1230    }
1231}