#42234 [BC-Insight] Missing Match Arm in to_single_key_authenticators() Allows WebAuthn Signatures Despite WEBAUTHN_SIGNATURE Being Disabled
Submitted on Mar 21st 2025 at 22:43:33 UTC by @jovi for Attackathon | Movement Labs
Report ID: #42234
Report Type: Blockchain/DLT
Report severity: Insight
Target: https://github.com/immunefi-team/attackathon-movement-aptos-core/tree/main
Impacts:
A bug in the respective layer 0/1/2 network code that results in unintended smart contract behavior with no concrete funds at direct risk
Description
Summary
A mismatch between the top‐level TransactionAuthenticator
variants (which can be multi‐agent or fee payer–based) and the helper function to_single_key_authenticators()
leads to WebAuthn signatures being silently ignored when the WEBAUTHN_SIGNATURE
feature is off. Instead of rejecting transactions that contain WebAuthn signatures, the validator code never even detects them, effectively bypassing the feature gating.
Vulnerability Details
TransactionAuthenticator
Enum
TransactionAuthenticator
EnumThe chain supports several transaction authentication modes, including single Ed25519, multi‐Ed25519, multi‐agent, or fee‐payer transactions:
#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub enum TransactionAuthenticator {
/// Single Ed25519 signature
Ed25519 {
public_key: Ed25519PublicKey,
signature: Ed25519Signature,
},
/// K-of-N multisignature
MultiEd25519 {
public_key: MultiEd25519PublicKey,
signature: MultiEd25519Signature,
},
/// Multi-agent transaction.
MultiAgent {
sender: AccountAuthenticator,
secondary_signer_addresses: Vec<AccountAddress>,
secondary_signers: Vec<AccountAuthenticator>,
},
/// Optional Multi-agent transaction with a fee payer.
FeePayer {
sender: AccountAuthenticator,
secondary_signer_addresses: Vec<AccountAddress>,
secondary_signers: Vec<AccountAuthenticator>,
fee_payer_address: AccountAddress,
fee_payer_signer: AccountAuthenticator,
},
SingleSender {
sender: AccountAuthenticator,
},
}
to_single_key_authenticators()
Snippet
to_single_key_authenticators()
SnippetBelow is the function used to convert high‐level signers into a list of SingleKeyAuthenticator
objects:
pub fn to_single_key_authenticators(&self) -> Result<Vec<SingleKeyAuthenticator>> {
let account_authenticators = self.all_signers();
let mut single_key_authenticators: Vec<SingleKeyAuthenticator> =
Vec::with_capacity(MAX_NUM_OF_SIGS);
for account_authenticator in account_authenticators {
match account_authenticator {
AccountAuthenticator::Ed25519 { public_key, signature } => {
let authenticator = SingleKeyAuthenticator {
public_key: AnyPublicKey::ed25519(public_key.clone()),
signature: AnySignature::ed25519(signature.clone()),
};
single_key_authenticators.push(authenticator);
},
AccountAuthenticator::MultiEd25519 { public_key, signature } => {
let public_keys = MultiKey::from(public_key);
let signatures: Vec<AnySignature> = signature
.signatures()
.iter()
.map(|sig| AnySignature::ed25519(sig.clone()))
.collect();
let signatures_bitmap = aptos_bitvec::BitVec::from(signature.bitmap().to_vec());
let authenticator = MultiKeyAuthenticator {
public_keys,
signatures,
signatures_bitmap,
};
single_key_authenticators
.extend(authenticator.to_single_key_authenticators()?);
},
AccountAuthenticator::SingleKey { authenticator } => {
single_key_authenticators.push(authenticator);
},
AccountAuthenticator::MultiKey { authenticator } => {
single_key_authenticators
.extend(authenticator.to_single_key_authenticators()?);
},
};
}
Ok(single_key_authenticators)
}
Problem: If a FeePayer/MultiAgent authenticator appears, there is no match
arm for it. If the code compiles and runs without error, that variant may be silently dropped or never converted, effectively bypassing the check for WebAuthn.
Underlying Gating Check
When WEBAUTHN_SIGNATURE
is disabled, the validator code attempts to reject any WebAuthn usage:
if !self.features().is_enabled(FeatureFlag::WEBAUTHN_SIGNATURE) {
if let Ok(sk_authenticators) = transaction
.authenticator_ref()
.to_single_key_authenticators()
{
for authenticator in sk_authenticators {
if let AnySignature::WebAuthn { .. } = authenticator.signature() {
return VMValidatorResult::error(StatusCode::FEATURE_UNDER_GATING);
}
}
} else {
return VMValidatorResult::error(StatusCode::INVALID_SIGNATURE);
}
}
But since to_single_key_authenticators()
never produces any item with WebAuthn
(due to missing match logic), no WebAuthn signature is “found.” The transaction is incorrectly allowed.
Impact
Users can include WebAuthn signatures (or other unhandled signatures) in multi‐agent or fee payer transactions, even with
WEBAUTHN_SIGNATURE
off.It circumvents a chain policy that is meant to block WebAuthn.
Proof of Concept
Disable
WEBAUTHN_SIGNATURE
via on‐chain config or local node settings.Submit a multi‐agent or fee‐payer transaction carrying an unknown or hypothetical
WebAuthn
variant for the signers.Observe that
to_single_key_authenticators()
does not parse or flag this, and the loop checking forAnySignature::WebAuthn { .. }
never sees it.The transaction is accepted instead of returning
FEATURE_UNDER_GATING
.
Was this helpful?