Skip to content

Commit 6757a25

Browse files
authored
fix: bump 26.1.4 with normalize p256 (#1513)
* bumps to `26.1.4` ensuring that new P256/webauthn signatures are normalized: https://github.com/Vectorized/solady/blob/cbcfe0009477aa329574f17e8db0a05703bb8bdd/src/utils/WebAuthn.sol#L141-L142
1 parent 07132d9 commit 6757a25

File tree

10 files changed

+158
-12
lines changed

10 files changed

+158
-12
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "relay"
3-
version = "26.1.3"
3+
version = "26.1.4"
44
edition = "2024"
55
publish = false
66
license = "MIT OR Apache-2.0"

src/estimation/arb.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ mod tests {
9494
};
9595

9696
#[tokio::test]
97+
#[ignore]
9798
async fn test_arb_gas_estimate_l1() {
9899
let provider =
99100
ProviderBuilder::new().connect("https://arb1.arbitrum.io/rpc").await.unwrap().erased();

src/signers/dyn.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
//!
33
//! A signer abstracted over multiple underlying signers.
44
use super::Eip712PayLoadSigner;
5+
use crate::types::KeyType;
56
use alloy::{
67
network::{FullSigner, TxSigner},
78
primitives::{Address, B256, Bytes, Signature},
@@ -80,6 +81,10 @@ impl Deref for DynSigner {
8081

8182
#[async_trait::async_trait]
8283
impl Eip712PayLoadSigner for DynSigner {
84+
fn key_type(&self) -> KeyType {
85+
KeyType::Secp256k1
86+
}
87+
8388
async fn sign_payload_hash(&self, payload_hash: B256) -> eyre::Result<Bytes> {
8489
Ok(self.sign_hash(&payload_hash).await?.as_bytes().into())
8590
}

src/signers/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,14 @@ pub use p256::{P256Key, P256Signer};
1010
mod webauthn;
1111
pub use webauthn::WebAuthnSigner;
1212

13+
use crate::types::KeyType;
14+
1315
/// Trait for a [EIP-712] payload signer.
1416
#[async_trait::async_trait]
1517
pub trait Eip712PayLoadSigner: std::fmt::Debug + Send + Sync {
18+
/// Returns the key type.
19+
fn key_type(&self) -> KeyType;
20+
1621
/// Signs the [EIP-712] payload hash.
1722
///
1823
/// Returns [`Bytes`].

src/signers/p256.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//! P256 signer type with webauthn capabilities used for gas estimation and testing.
22
33
use super::Eip712PayLoadSigner;
4+
use crate::types::KeyType;
45
use alloy::primitives::{B256, Bytes};
56
use p256::ecdsa::{SigningKey, signature::hazmat::PrehashSigner};
67
use std::sync::Arc;
@@ -43,6 +44,10 @@ impl P256Signer {
4344

4445
#[async_trait::async_trait]
4546
impl Eip712PayLoadSigner for P256Signer {
47+
fn key_type(&self) -> KeyType {
48+
KeyType::P256
49+
}
50+
4651
async fn sign_payload_hash(&self, payload_hash: B256) -> eyre::Result<Bytes> {
4752
Ok(self.sign_prehash(payload_hash.as_slice())?.to_bytes().to_vec().into())
4853
}

src/signers/webauthn.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use super::{Eip712PayLoadSigner, p256::P256Key};
2-
use crate::types::WebAuthnP256;
2+
use crate::types::{KeyType, WebAuthnP256};
33
use alloy::{
44
primitives::{B256, Bytes, U256, bytes},
55
signers::k256::sha2::{Digest, Sha256},
@@ -32,6 +32,10 @@ impl P256Key for WebAuthnSigner {
3232

3333
#[async_trait::async_trait]
3434
impl Eip712PayLoadSigner for WebAuthnSigner {
35+
fn key_type(&self) -> KeyType {
36+
KeyType::WebAuthnP256
37+
}
38+
3539
async fn sign_payload_hash(&self, payload_hash: B256) -> eyre::Result<Bytes> {
3640
// ID || UserPresent Flag || SignatureCounter
3741
let authenticator_data = bytes!(

src/types/intent/mod.rs

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use super::{Call, IDelegation::authorizeCall, Key, MerkleLeafInfo};
1+
use super::{Call, IDelegation::authorizeCall, Key, MerkleLeafInfo, key::normalize_p256_s};
22
use crate::{
33
error::{IntentError, MerkleError, RelayError},
44
types::{
@@ -444,11 +444,17 @@ impl IntentKey<Key> {
444444
pub fn wrap_signature(&self, signature: Bytes, prehash: bool) -> Bytes {
445445
match self {
446446
IntentKey::EoaRootKey => signature,
447-
IntentKey::StoredKey(key) => {
448-
Signature { innerSignature: signature, keyHash: key.key_hash(), prehash }
449-
.abi_encode_packed()
450-
.into()
447+
IntentKey::StoredKey(key) => Signature {
448+
innerSignature: if key.keyType.is_webauthn() || key.keyType.is_p256() {
449+
normalize_p256_s(signature)
450+
} else {
451+
signature
452+
},
453+
keyHash: key.key_hash(),
454+
prehash,
451455
}
456+
.abi_encode_packed()
457+
.into(),
452458
}
453459
}
454460
}
@@ -493,7 +499,11 @@ impl IntentKey<CallKey> {
493499
match self {
494500
IntentKey::EoaRootKey => signature,
495501
IntentKey::StoredKey(key) => Signature {
496-
innerSignature: signature,
502+
innerSignature: if key.key_type.is_webauthn() || key.key_type.is_p256() {
503+
normalize_p256_s(signature)
504+
} else {
505+
signature
506+
},
497507
keyHash: key.key_hash(),
498508
prehash: key.prehash,
499509
}

src/types/key.rs

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use super::{
22
super::signers::{DynSigner, Eip712PayLoadSigner, P256Key, P256Signer, WebAuthnSigner},
3-
U40,
3+
U40, WebAuthnP256,
44
rpc::{AuthorizeKey, CallKey, Permission, RevokeKey},
55
};
66
use IDelegation::getKeysReturn;
@@ -364,6 +364,15 @@ impl KeyWith712Signer {
364364
}
365365
}
366366

367+
/// Wraps signer to produce high-S signatures for testing P256 normalization.
368+
pub fn with_high_s_signature(self) -> Self {
369+
Self {
370+
key: self.key,
371+
signer: Arc::new(HighSSignerWrapper(self.signer)),
372+
permissions: self.permissions,
373+
}
374+
}
375+
367376
/// Returns [`KeyWith712Signer`] with additional permissions.
368377
pub fn with_permissions(mut self, permissions: Vec<Permission>) -> Self {
369378
self.permissions = permissions;
@@ -404,6 +413,10 @@ impl KeyWith712Signer {
404413

405414
#[async_trait::async_trait]
406415
impl Eip712PayLoadSigner for KeyWith712Signer {
416+
fn key_type(&self) -> KeyType {
417+
self.signer.key_type()
418+
}
419+
407420
async fn sign_payload_hash(&self, payload_hash: B256) -> eyre::Result<Bytes> {
408421
Ok(self.signer.sign_payload_hash(payload_hash).await?)
409422
}
@@ -426,6 +439,68 @@ pub const ITHACA_ACCOUNT_STORAGE_SLOT: u128 = 1264628507133665080054;
426439
/// contract.
427440
pub const ITHACA_KEY_STORAGE_SLOT_OFFSET: u128 = 3;
428441

442+
const P256_N: U256 =
443+
alloy::uint!(0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551_U256);
444+
const P256_HALF_N: U256 =
445+
alloy::uint!(0x7fffffff800000007fffffffffffffffde737d56d38bcf4279dce5617e3192a8_U256);
446+
447+
/// Wrapper that converts signatures to high-S for testing normalization.
448+
#[derive(Debug)]
449+
struct HighSSignerWrapper(Arc<dyn Eip712PayLoadSigner>);
450+
451+
#[async_trait::async_trait]
452+
impl Eip712PayLoadSigner for HighSSignerWrapper {
453+
fn key_type(&self) -> KeyType {
454+
self.0.key_type()
455+
}
456+
457+
async fn sign_payload_hash(&self, payload_hash: B256) -> eyre::Result<Bytes> {
458+
let sig = self.0.sign_payload_hash(payload_hash).await?;
459+
match self.0.key_type() {
460+
KeyType::P256 => {
461+
let s = U256::from_be_slice(&sig[32..]);
462+
if s < P256_HALF_N {
463+
let mut out = sig[..32].to_vec();
464+
out.extend_from_slice(B256::from(P256_N - s).as_slice());
465+
return Ok(out.into());
466+
}
467+
}
468+
KeyType::WebAuthnP256 => {
469+
if let Ok(mut w) = WebAuthnP256::abi_decode(&sig) {
470+
let s: U256 = w.s.into();
471+
if s < P256_HALF_N {
472+
w.s = (P256_N - s).into();
473+
return Ok(w.abi_encode().into());
474+
}
475+
}
476+
}
477+
_ => {}
478+
}
479+
Ok(sig)
480+
}
481+
}
482+
483+
/// Normalizes P256 signature S value to lower half of curve.
484+
///
485+
/// Handles both raw P256 (64 bytes) and ABI-encoded WebAuthnP256 formats.
486+
pub fn normalize_p256_s(signature: Bytes) -> Bytes {
487+
if signature.len() == 64 {
488+
let s = U256::from_be_slice(&signature[32..]);
489+
if s > P256_HALF_N {
490+
let mut out = signature[..32].to_vec();
491+
out.extend_from_slice(B256::from(P256_N - s).as_slice());
492+
return out.into();
493+
}
494+
} else if let Ok(mut w) = WebAuthnP256::abi_decode(&signature) {
495+
let s: U256 = w.s.into();
496+
if s > P256_HALF_N {
497+
w.s = (P256_N - s).into();
498+
return WebAuthnP256::abi_encode(&w).into();
499+
}
500+
}
501+
signature
502+
}
503+
429504
#[cfg(test)]
430505
mod tests {
431506
use super::{Key, KeyType};

tests/e2e/cases/keys.rs

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@ use crate::e2e::{
33
cases::upgrade_account_eagerly, environment::Environment, run_e2e,
44
};
55
use alloy::{
6-
primitives::{U64, U256},
6+
primitives::{Address, B256, U64, U256},
77
sol_types::SolCall,
88
};
99
use relay::{
1010
rpc::RelayApiClient,
1111
signers::Eip712PayLoadSigner,
1212
types::{
13-
CallPermission,
13+
Call, CallPermission,
1414
IthacaAccount::SpendPeriod,
1515
KeyType, KeyWith712Signer,
1616
rpc::{
@@ -434,3 +434,44 @@ async fn get_keys_multichain_three_chains_two_have_session() -> eyre::Result<()>
434434

435435
Ok(())
436436
}
437+
438+
/// Test high-S normalization for P256/WebAuthn keys (precall + normal call).
439+
#[tokio::test(flavor = "multi_thread")]
440+
async fn high_s_signature() -> eyre::Result<()> {
441+
let webauthn =
442+
KeyWith712Signer::random_admin(KeyType::WebAuthnP256)?.unwrap().with_high_s_signature();
443+
let p256 = KeyWith712Signer::random_admin(KeyType::P256)?.unwrap().with_high_s_signature();
444+
445+
run_e2e(|env| {
446+
vec![
447+
// WebAuthn signs precall, P256 signs main tx
448+
TxContext {
449+
authorization_keys: vec![&webauthn],
450+
expected: ExpectedOutcome::Pass,
451+
pre_calls: vec![TxContext {
452+
authorization_keys: vec![&p256],
453+
key: Some(&webauthn),
454+
nonce: Some(U256::from_be_bytes(*B256::random()) << 64),
455+
..Default::default()
456+
}],
457+
calls: vec![Call::transfer(env.erc20, Address::random(), U256::from(10))],
458+
key: Some(&p256),
459+
..Default::default()
460+
},
461+
// P256 signs precall, WebAuthn signs main tx
462+
TxContext {
463+
expected: ExpectedOutcome::Pass,
464+
pre_calls: vec![TxContext {
465+
authorization_keys: vec![&p256],
466+
key: Some(&p256),
467+
nonce: Some(U256::from_be_bytes(*B256::random()) << 64),
468+
..Default::default()
469+
}],
470+
calls: vec![Call::transfer(env.erc20, Address::random(), U256::from(10))],
471+
key: Some(&webauthn),
472+
..Default::default()
473+
},
474+
]
475+
})
476+
.await
477+
}

0 commit comments

Comments
 (0)