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
13pub const SIGNATURE_TYPE_P256: u8 = 0x01;
16pub const SIGNATURE_TYPE_WEBAUTHN: u8 = 0x02;
17pub const SIGNATURE_TYPE_KEYCHAIN: u8 = 0x03;
18
19const MIN_AUTH_DATA_LEN: usize = 37;
21
22#[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#[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 pub webauthn_data: Bytes,
51}
52
53#[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 Secp256k1(Signature),
70
71 P256(P256SignatureWithPreHash),
73
74 WebAuthn(WebAuthnSignature),
76}
77
78impl PrimitiveSignature {
79 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 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 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 pub fn to_bytes(&self) -> Bytes {
141 match self {
142 Self::Secp256k1(sig) => {
143 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 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 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 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 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 Ok(sig.recover_address_from_prehash(sig_hash)?)
222 }
223 Self::P256(p256_sig) => {
224 let message_hash = if p256_sig.pre_hash {
226 B256::from_slice(&Sha256::digest(sig_hash.as_slice()))
228 } else {
229 *sig_hash
230 };
231
232 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 Ok(derive_p256_address(
244 &p256_sig.pub_key_x,
245 &p256_sig.pub_key_y,
246 ))
247 }
248 Self::WebAuthn(webauthn_sig) => {
249 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_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 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 bytes.to_compact(buf)
307 }
308
309 fn from_compact(buf: &[u8], len: usize) -> (Self, &[u8]) {
310 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#[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 pub user_address: Address,
333 pub signature: PrimitiveSignature,
335 #[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 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 pub fn key_id(
368 &self,
369 sig_hash: &B256,
370 ) -> Result<Address, alloy_consensus::crypto::RecoveryError> {
371 if let Some(cached) = self.cached_key_id.get() {
373 return Ok(*cached);
374 }
375
376 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
383impl 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#[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 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 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#[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(), })
440 }
441}
442
443#[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(PrimitiveSignature),
454
455 Keychain(KeychainSignature),
460}
461
462impl TempoSignature {
463 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 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 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 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 let primitive = PrimitiveSignature::from_bytes(data)?;
502 Ok(Self::Primitive(primitive))
503 }
504
505 pub fn to_bytes(&self) -> Bytes {
511 match self {
512 Self::Primitive(primitive_sig) => primitive_sig.to_bytes(),
513 Self::Keychain(keychain_sig) => {
514 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 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 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 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 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 keychain_sig.key_id(sig_hash)?;
572
573 Ok(keychain_sig.user_address)
575 }
576 }
577 }
578
579 pub fn is_keychain(&self) -> bool {
581 matches!(self, Self::Keychain(_))
582 }
583
584 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 bytes.to_compact(buf)
626 }
627
628 fn from_compact(buf: &[u8], len: usize) -> (Self, &[u8]) {
629 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
643pub 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 Address::from_slice(&hash[12..])
653}
654
655fn 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 let encoded_point = EncodedPoint::from_affine_coordinates(
669 pub_key_x.into(),
670 pub_key_y.into(),
671 false, );
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 verifying_key
682 .verify_prehash(message_hash.as_slice(), &signature)
683 .map_err(|_| "P256 signature verification failed")
684}
685
686fn verify_webauthn_data_internal(
695 webauthn_data: &[u8],
696 tx_hash: &B256,
697) -> Result<B256, &'static str> {
698 if webauthn_data.len() < MIN_AUTH_DATA_LEN + 32 {
700 return Err("WebAuthn data too short");
701 }
702
703 let flags = webauthn_data[32];
705 let (up_flag, at_flag, ed_flag) = (flags & 0x01, flags & 0x40, flags & 0x80);
706
707 if up_flag == 0 {
709 return Err("User Presence (UP) flag not set in authenticatorData");
710 }
711
712 if at_flag != 0 {
714 return Err("AT flag must not be set for assertion signatures");
715 }
716
717 let auth_data_len = if ed_flag == 0 {
719 MIN_AUTH_DATA_LEN
721 } else {
722 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 let json_str =
732 core::str::from_utf8(client_data_json).map_err(|_| "clientDataJSON is not valid UTF-8")?;
733
734 if !json_str.starts_with('{') || !json_str.ends_with('}') {
736 return Err("clientDataJSON is not valid JSON");
737 }
738
739 if !json_str.contains("\"type\":\"webauthn.get\"") {
741 return Err("clientDataJSON missing required type field");
742 }
743
744 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 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")]
764fn 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 let r = [0u8; 32];
781 let s = [0u8; 32];
782 let pub_key_x = [0u8; 32]; 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 let signing_key = SigningKey::random(&mut OsRng);
796 let verifying_key = signing_key.verifying_key();
797
798 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 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 let signing_key = SigningKey::random(&mut OsRng);
831 let verifying_key = signing_key.verifying_key();
832
833 let message = b"test message";
835 let message_hash = B256::from_slice(&Sha256::digest(message));
836
837 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 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 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 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 let mut auth_data = vec![0u8; 37];
878 auth_data[32] = 0x00; 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 let mut auth_data = vec![0u8; 37];
899 auth_data[32] = 0x01; 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 let mut auth_data = vec![0u8; 37];
920 auth_data[32] = 0x01; 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 let mut auth_data = vec![0u8; 37];
945 auth_data[32] = 0x01; let tx_hash = B256::from_slice(&[0xAA; 32]);
949
950 let challenge_b64url = URL_SAFE_NO_PAD.encode(tx_hash.as_slice());
952
953 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 let message_hash = result.unwrap();
967
968 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 assert_eq!(addr1, addr2);
991
992 assert_ne!(addr1, Address::ZERO);
994 }
995
996 #[test]
997 fn test_p256_address_derivation_deterministic() {
998 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 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 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 } 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 } 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]); let result = TempoSignature::from_bytes(&sig_bytes);
1071
1072 assert!(result.is_ok());
1073 if let TempoSignature::Primitive(PrimitiveSignature::WebAuthn(_)) = result.unwrap() {
1074 } 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 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); let decoded1 = TempoSignature::from_bytes(&encoded1).unwrap();
1094 assert_eq!(sig1, decoded1);
1095
1096 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 let decoded2 = TempoSignature::from_bytes(&encoded2).unwrap();
1104 assert_eq!(sig2, decoded2);
1105
1106 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 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 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 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 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 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 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 fn build_webauthn_data(flags: u8, extension: Option<&[u8]>, tx_hash: &B256) -> Vec<u8> {
1199 let mut data = vec![0u8; 32]; data.push(flags);
1201 data.extend_from_slice(&[0u8; 4]); 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 let data = build_webauthn_data(0x41, None, &tx_hash); let err = verify_webauthn_data_internal(&data, &tx_hash).unwrap_err();
1217 assert!(err.contains("AT flag"), "Should reject AT flag");
1218
1219 let data = build_webauthn_data(0x81, Some(&[0xa0]), &tx_hash); let err = verify_webauthn_data_internal(&data, &tx_hash).unwrap_err();
1222 assert!(err.contains("ED flag"), "Should reject ED flag");
1223
1224 let data = build_webauthn_data(0x01, None, &tx_hash); assert!(
1227 verify_webauthn_data_internal(&data, &tx_hash).is_ok(),
1228 "Should accept valid webauthn data with only UP flag"
1229 );
1230 }
1231}