feat!: siegel key manager for Safe Smart Account 🧧#355
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7ba38efb96
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
NnnOooPppEee
left a comment
There was a problem hiding this comment.
Security review of the Siegel integration. Two inline notes below. One additional pre-existing observation that doesn't touch this PR's diff:
EoaSigner::new at signer.rs L260-L267 — pre-existing in this file (not from this PR): hex::decode(private_key) returns a plain Vec<u8> with the raw 32 key bytes, and Vec::drop doesn't zero memory — so after new() returns, those bytes sit in the allocator's free list until something reuses the slot. Same pattern as TOB-Parity-022 (p.23). One-line fix: Zeroizing::new(hex::decode(private_key)?). Worth folding in here since EoaSigner will need the same treatment when it migrates to SmartAccountKeyManager anyway.
Full write-up and the equivalent upstream Siegel hardening PR are tracked separately.
| fi | ||
| shift | ||
| ;; | ||
| --help | -h) |
There was a problem hiding this comment.
my ide now formats bash files, looks reasonable
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit f22b83f. Configure here.
|
|
||
| // Read the key once to validate it and derive the EOA address. | ||
| // The session is consumed and zeroized inside `read_once`. | ||
| let siegel = key_manager.get_eoa_private_key(); |
There was a problem hiding this comment.
Will get_eoa_private_key() eventually go through biometric or some sort of user presence check on device? If so, that prompt would be triggered at construction instead of at signing, which is going to feel off for the user. I will let @NnnOooPppEee weigh in here as I think just in time secret access is a principle we want to hold to. I do see the value of what we're doing here though, failing early if we can't load the secret and checking the public address.
There was a problem hiding this comment.
This is a decision to be made on world app. I believe the best approach is to have it when unlocked if the user doesn't enable Face ID or require Face ID for the initial access id the user has Face ID protection in the app
| shopt -s nullglob | ||
| swift_files=("$BASE_PATH/$SOURCES_PATH_NAME"/*.swift) | ||
| shopt -u nullglob | ||
| if [ ${#swift_files[@]} -eq 0 ]; then | ||
| echo -e "${RED}✗ Could not find any generated Swift bindings in: $BASE_PATH/$SOURCES_PATH_NAME${NC}" |
There was a problem hiding this comment.
Just a side comment, I would like to advocate for doing scripts in a higher level language, nodejs, python, rust even. Bash is famlously hard to understand or review.
There was a problem hiding this comment.
will have this in the back burner to migrate
There was a problem hiding this comment.
Pull request overview
Introduces Siegel-backed ephemeral key handling for Safe Smart Account signing by replacing long-lived private-key material in Rust with per-operation SiegelSession reads supplied by a foreign SmartAccountKeyManager (UniFFI foreign trait). This is a breaking UniFFI API change (privateKey → keyManager) and adjusts Rust, Swift, and Kotlin tests/build tooling accordingly.
Changes:
- Redesign Safe signing to fetch the EOA key via one-shot
SiegelSession::read_oncefrom aSmartAccountKeyManager, caching only the derived EOA address. - Add test-only helpers (
from_private_key_hex, in-memory key managers) and update Rust/Swift/Kotlin tests to use the new key-manager constructor flow. - Wire
siegel-uniffiinto the build/bindgen pipeline and ensure generated Swift/Kotlin bindings are copied/compiled for both bedrock + siegel.
Reviewed changes
Copilot reviewed 29 out of 30 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| swift/tests/BedrockTests/BedrockSolMacroTests.swift | Updates SafeSmartAccount initialization in Swift tests to use TestKeyManager. |
| swift/tests/BedrockTests/BedrockSmartAccountTests.swift | Adds Swift test SmartAccountKeyManager implementation and switches tests to new constructor. |
| swift/test_swift.sh | Copies all generated Swift binding files (bedrock + siegel) into the Swift test package. |
| swift/build_swift.sh | Moves all generated UniFFI Swift sources/headers and concatenates modulemaps to support multiple linked UniFFI crates. |
| kotlin/bedrock-tests/src/test/kotlin/bedrock/TestKeyManager.kt | Adds Kotlin test SmartAccountKeyManager using JNA to call siegel_fill. |
| kotlin/bedrock-tests/src/test/kotlin/bedrock/BedrockSolMacroTests.kt | Updates SafeSmartAccount initialization in Kotlin tests to use TestKeyManager. |
| kotlin/bedrock-tests/src/test/kotlin/bedrock/BedrockSmartAccountTests.kt | Refactors Kotlin tests to use a helper that constructs accounts via TestKeyManager. |
| kotlin/bedrock-tests/build.gradle.kts | Adds generated uniffi/siegel_uniffi sources to the Kotlin test source set. |
| Cargo.lock | Records new siegel and siegel-uniffi dependencies. |
| bedrock/tests/test_smart_account_world_gift_manager_gift_redeem.rs | Migrates tests to SafeSmartAccount::from_private_key_hex. |
| bedrock/tests/test_smart_account_world_gift_manager_gift_cancel.rs | Migrates tests to SafeSmartAccount::from_private_key_hex. |
| bedrock/tests/test_smart_account_wld_vault.rs | Migrates tests to SafeSmartAccount::from_private_key_hex. |
| bedrock/tests/test_smart_account_wa_get_user_operation_receipt.rs | Migrates tests to SafeSmartAccount::from_private_key_hex. |
| bedrock/tests/test_smart_account_usd_vault.rs | Migrates tests to SafeSmartAccount::from_private_key_hex. |
| bedrock/tests/test_smart_account_transfer.rs | Migrates tests to SafeSmartAccount::from_private_key_hex. |
| bedrock/tests/test_smart_account_sign_typed_data.rs | Migrates tests to SafeSmartAccount::from_private_key_hex. |
| bedrock/tests/test_smart_account_personal_sign.rs | Migrates tests to SafeSmartAccount::from_private_key_hex. |
| bedrock/tests/test_smart_account_permit2_transfer.rs | Migrates tests to SafeSmartAccount::from_private_key_hex. |
| bedrock/tests/test_smart_account_morpho.rs | Migrates tests to SafeSmartAccount::from_private_key_hex. |
| bedrock/tests/test_smart_account_erc4337_transaction_execution.rs | Migrates tests to SafeSmartAccount::from_private_key_hex. |
| bedrock/tests/test_smart_account_bundler_sponsored.rs | Migrates tests to SafeSmartAccount::from_private_key_hex. |
| bedrock/tests/test_permit2_approval_processor.rs | Migrates processor test to SafeSmartAccount::from_private_key_hex. |
| bedrock/src/transactions/mod.rs | Updates docs/examples to reflect new key-manager based constructor signature. |
| bedrock/src/test_utils.rs | Adds InMemoryKeyManager test helper backed by SiegelSession + siegel_fill. |
| bedrock/src/smart_account/transaction_4337.rs | Updates unit tests to use from_private_key_hex. |
| bedrock/src/smart_account/signer.rs | Changes signing to build a one-shot signer from siegel bytes per signing call. |
| bedrock/src/smart_account/mod.rs | Replaces stored signer with SmartAccountKeyManager + cached EOA address; introduces error mapping for Siegel session failures; adds from_private_key_hex test constructor and the foreign trait definition. |
| bedrock/src/siwe/test.rs | Updates SIWE tests to use from_private_key_hex. |
| bedrock/src/lib.rs | Re-exports UniFFI scaffolding for siegel bindings. |
| bedrock/Cargo.toml | Adds a pinned dependency on siegel-uniffi. |
| pub const fn new(private_key_hex: String) -> Self { | ||
| // Normally we'd verify the sk length here, but because we have tests that require | ||
| // passing invalid secrets, we don't enforce it. | ||
| Self { | ||
| hex_bytes: private_key_hex.into_bytes(), | ||
| } |

Introduces Siegel to securely pass the secret bytes of the EOA private key to Rust for one-off signature operations. This ensures the private key exists in application memory solely for the duration of the signature process and then zeroized (and while the key is in memory there's protections against under/overflows).
Outstanding
Other places still pass secrets to Rust via the UniFFI boundary (e.g. backup manager) and must be updated.
Note
High Risk
Breaking UniFFI constructor and redesigned private-key lifecycle for all Safe signing; incorrect platform KeyManager/siegel_fill behavior could break signatures or widen secret exposure.
Overview
Safe Smart Account no longer accepts a hex private key at construction. Callers must supply a
SmartAccountKeyManager(UniFFI foreign trait) that returns a freshSiegelSession(64-byte ASCII hex EOA key) per use; Rust validates the key once at init (caches EOA address), then signs via one-shotread_oncesessions that zeroize the secret after each operation.Signing paths (
personal_sign, typed data, Safe txs, ERC-4337) now load the key throughsign_hash_syncinstead of a long-lived in-process signer.siegel-uniffiis wired into bedrock (dependency, UniFFI re-export).SafeSmartAccountError::SiegelSessionmaps siegel session failures. Tests and Swift/Kotlin harnesses useInMemoryKeyManager/TestKeyManager+from_private_key_hex; mobile build scripts copy all generated UniFFI Swift sources/headers (bedrock + siegel).This is a breaking foreign API change (
privateKey→keyManager).EoaSignerand other UniFFI secret paths are unchanged in this PR.Reviewed by Cursor Bugbot for commit 6e32ec8. Bugbot is set up for automated code reviews on this repo. Configure here.