Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions contracts/smart-account/src/account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,9 @@ impl SmartAccountInterface for SmartAccount {
if let Signer::Multisig(ref multisig, _) = signer {
Self::validate_multisig(env, multisig)?;
}
if let Signer::Webauthn(ref webauthn, _) = signer {
Self::validate_webauthn_key_id(&webauthn.key_id)?;
}
Self::validate_signer_expiration(env, &signer)?;

// Validate: Some(empty_vec) is not allowed — use None for no policies
Expand Down Expand Up @@ -186,6 +189,9 @@ impl SmartAccountInterface for SmartAccount {
if let Signer::Multisig(ref multisig, _) = signer {
Self::validate_multisig(env, multisig)?;
}
if let Signer::Webauthn(ref webauthn, _) = signer {
Self::validate_webauthn_key_id(&webauthn.key_id)?;
}
Self::validate_signer_expiration(env, &signer)?;

// Validate: Some(empty_vec) is not allowed — use None for no policies
Expand Down Expand Up @@ -414,6 +420,18 @@ impl SmartAccount {
Ok(())
}

/// Bounds WebAuthn credential ID length. FIDO2/CTAP2 caps the CredentialID
/// at 1023 bytes, and real-world authenticators stay well under that
/// (typically ≤ 128 bytes). Unbounded key_ids inflate persistent storage
/// rent and signature proof payloads with no upside, so we reject them.
fn validate_webauthn_key_id(key_id: &soroban_sdk::Bytes) -> Result<(), SmartAccountError> {
const MAX_WEBAUTHN_KEY_ID_LEN: u32 = 1023;
if key_id.len() > MAX_WEBAUTHN_KEY_ID_LEN {
return Err(SmartAccountError::InvalidPolicy);
}
Ok(())
}

/// Validates multisig signer configuration
fn validate_multisig(env: &Env, multisig: &MultisigSigner) -> Result<(), SmartAccountError> {
if multisig.members.is_empty() || multisig.threshold == 0 {
Expand Down
77 changes: 77 additions & 0 deletions contracts/smart-account/src/tests/webauthn_signer_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,80 @@ fn test_webauthn_end_to_end_auth() {
)
.unwrap();
}

// ============================================================================
// key_id length bound: anything over 1023 bytes is rejected at registration
// (matches the FIDO2/CTAP2 CredentialID cap).
// ============================================================================

#[test]
fn test_webauthn_oversize_key_id_rejected() {
use crate::account::SmartAccount;
use smart_account_interfaces::{Signer, SmartAccountInterface as _, WebauthnSigner};
use soroban_sdk::testutils::Address as _;

let env = setup();
env.mock_all_auths();

// Deploy with a valid admin first.
let admin = crate::tests::test_utils::WebauthnTestSigner::generate(SignerRole::Admin);
let contract_id = env.register(
SmartAccount,
(
vec![&env, admin.into_signer(&env)],
Vec::<Address>::new(&env),
),
);

// Build a Webauthn signer whose key_id is 1024 bytes — over the cap.
let oversize = Bytes::from_slice(&env, &[0xAAu8; 1024]);
let pk_bytes = [0x04u8; 65];
let bad_signer = Signer::Webauthn(
WebauthnSigner {
key_id: oversize,
public_key: BytesN::from_array(&env, &pk_bytes),
},
SignerRole::Standard(None, 0),
);

let err = env
.as_contract(&contract_id, || {
SmartAccount::add_signer(&env, bad_signer)
})
.unwrap_err();
assert_eq!(err, Error::InvalidPolicy);
}

#[test]
fn test_webauthn_max_size_key_id_accepted() {
use crate::account::SmartAccount;
use smart_account_interfaces::{Signer, SmartAccountInterface as _, WebauthnSigner};

let env = setup();
env.mock_all_auths();

let admin = crate::tests::test_utils::WebauthnTestSigner::generate(SignerRole::Admin);
let contract_id = env.register(
SmartAccount,
(
vec![&env, admin.into_signer(&env)],
Vec::<Address>::new(&env),
),
);

// Exactly 1023 bytes — the FIDO2/CTAP2 cap — must pass.
let exact = Bytes::from_slice(&env, &[0xBBu8; 1023]);
let pk_bytes = [0x04u8; 65];
let ok_signer = Signer::Webauthn(
WebauthnSigner {
key_id: exact,
public_key: BytesN::from_array(&env, &pk_bytes),
},
SignerRole::Standard(None, 0),
);

env.as_contract(&contract_id, || {
SmartAccount::add_signer(&env, ok_signer)
})
.unwrap();
}
Loading