In crates/xmtp_cryptography/src/basic_credential.rs:120-130, the from_raw function constructs a SigningKey from caller-supplied private and public key bytes without verifying they correspond:
rust
fn from_raw(private: &[u8], public: &[u8]) -> Result<Self, ed25519_dalek::SignatureError> {
let keypair = Zeroizing::new({
let mut keypair = [0u8; 64];
keypair[0..32].copy_from_slice(private);
keypair[32..].copy_from_slice(public);
keypair
});
let signing_key = SigningKey::from_keypair_bytes(&keypair)?;
Ok(Self(Box::new(signing_key)))
}
SigningKey::from_keypair_bytes stores the public bytes verbatim without verifying that public == private * G (i.e., that the public key is the correct generator multiplied by the private key). A caller can supply (private_key_A, public_key_B) as a mismatched pair. The credential will sign with private_key_A but report public_key_B via verifying_key().
This is reachable through the serde Deserialize impl at line 267-269 of the same file.
Impact: An attacker who knows a victim's public key but not their private key can create a credential that signs with their own private key but claims the victim's public key. Any downstream verification that trusts the reported verifying_key() would accept signatures from the wrong party.
Suggested fix: After constructing the SigningKey, verify that signing_key.verifying_key().as_bytes() == public. If they don't match, return an error.
rust
let signing_key = SigningKey::from_keypair_bytes(&keypair)?;
if signing_key.verifying_key().as_bytes() != public {
return Err(SignatureError::new());
}
In crates/xmtp_cryptography/src/basic_credential.rs:120-130, the from_raw function constructs a SigningKey from caller-supplied private and public key bytes without verifying they correspond: