fix!: protect evm_wallet integrity and bind key ciphertext to wallet address
Some checks failed
ci/woodpecker/pr/server-audit Pipeline was successful
ci/woodpecker/pr/server-vet Pipeline failed
ci/woodpecker/pr/server-lint Pipeline failed
ci/woodpecker/pr/server-test Pipeline failed

Three independent failures allowed an offline attacker with DB write access
to sign transactions using a different wallet's private key:
1. evm_wallet had no HMAC envelope — aead_encrypted_id could be swapped silently.
2. AEAD used a static tag as AAD — any valid ciphertext decrypted as any wallet key.
3. No post-decryption check that the derived address matched the requested wallet.

Fix: sign_entity covers (address, aead_encrypted_id) in a single transaction;
CreateNew/Decrypt take caller-provided AAD (wallet address bytes); after decryption
signer.address() is verified against the requested wallet address.
This commit is contained in:
CleverWild
2026-06-22 17:53:34 +02:00
parent b7ab5c79b7
commit 00ddf99d77
5 changed files with 127 additions and 35 deletions

View File

@@ -14,6 +14,8 @@ use kameo::actor::{ActorRef, Spawn as _};
use std::collections::{HashMap, HashSet};
use tokio::task::JoinSet;
const TEST_AAD: &[u8] = b"test-aad";
async fn write_concurrently(
actor: ActorRef<Vault>,
prefix: &'static str,
@@ -27,6 +29,7 @@ async fn write_concurrently(
let id = actor
.ask(CreateNew {
plaintext: SafeCell::new(plaintext.clone()),
aad: TEST_AAD.to_vec(),
})
.await
.unwrap();
@@ -120,7 +123,7 @@ async fn insert_failure_does_not_create_partial_row() {
drop(conn);
let err = actor
.create_new(SafeCell::new(b"should fail".to_vec()))
.create_new(SafeCell::new(b"should fail".to_vec()), TEST_AAD.to_vec())
.await
.unwrap_err();
assert!(matches!(err, Error::DatabaseTransaction(_)));
@@ -171,7 +174,7 @@ async fn decrypt_roundtrip_after_high_concurrency() {
.unwrap();
for (id, plaintext) in expected {
let mut decrypted = decryptor.decrypt(id).await.unwrap();
let mut decrypted = decryptor.decrypt(id, TEST_AAD.to_vec()).await.unwrap();
assert_eq!(*decrypted.read(), plaintext);
}
}