chore: release v0.2.0#58
Merged
Merged
Conversation
… (Phase 1) Wire up inbound DIDComm message processing so messages are no longer silently dropped. Auto-process messages that don't need user input (pong, accept/finalize/reject) and queue interactive tasks in the inbox (relationship requests, VRC requests, VRC issued). - Add rich state structures for all main menu panels (inbox, relationships, credentials, settings) - Add sync_from_config() to rebuild display state from Config - Change InboundMessage event to carry full Message object - Create message_dispatch.rs porting logic from openvtc-cli fetch.rs Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Render inbox tasks with type indicators, DID, and timestamps. Support task list navigation (Up/Down), detail view (Enter), and actions (a=accept, r=reject, d=dismiss, Esc=back). - Add inbox action variants (accept/reject relationship, accept VRC, dismiss task) - Create inbox_actions.rs with Config/TDK handlers for each action - Update content_panel.rs with full inbox, relationships, credentials, and settings panel rendering (relationships/credentials/settings are display-only for now, interactive features in Phases 3-5) - Add content panel key event handling in MainPage Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
…se 3) List relationships with state/alias, view details, create new relationship requests, send trust-pings, and remove relationships. - Add relationship action variants (select, new request, submit, ping, remove, input update, back) - Create relationship_actions.rs porting request/ping/remove logic from openvtc-cli - Add three-mode relationships UI (list, detail, new-request form) with inline text input for DID/alias/reason - Wire all relationship actions into state handler Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
List VRCs with Received/Issued tab switching, view credential details, and request new VRCs from established relationships. - Add credential action variants (switch tab, select, detail, new request, submit, cancel, reason update) - Create credential_actions.rs porting VRC request logic from openvtc-cli - Add three-mode credentials UI (list with tabs, detail view, new-request with relationship picker and reason input) - Wire all credential actions into state handler Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
… (Phase 5) View and edit friendly name, mediator DID, and org DID with inline text editing. Export config to encrypted file with passphrase. - Add settings action variants (select, start/cancel/submit edit, edit update, export config) - Create settings_actions.rs with config save/update/export handlers - Add settings key handling with view/edit/export modes - Update settings rendering with inline edit fields, export form with masked passphrase Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Extract panel renderers from content_panel.rs (822 lines) into separate files: inbox_panel.rs, relationships_panel.rs, credentials_panel.rs, settings_panel.rs. Content_panel.rs is now a thin 80-line dispatcher. Add VRC issuance flow: when accepting an inbound VRC request from the inbox, create, sign (DataIntegrityProof), and send a VRC back to the requester via DIDComm. Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Add token management sub-screen in settings with detect tokens and factory reset capabilities. Shows protection type, detected token count, and operation status messages. - Add TokenManagementState and TokenManagement mode to SettingsState - Add token action variants (detect, factory reset, back) - Update settings view with protection type display and token management entry point (cfg-gated behind openpgp-card feature) - Update settings index layout: 0-2=editable fields, 3=persona DID, 4=protection type, 5=export, 6=token management Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
…agement Replace confusing "Plaintext" label with descriptive protection type display: "Keyring Only (no additional encryption)", "Passphrase Encrypted", or "Hardware Token (id)". Add ability to change config protection level from Settings: - Set passphrase: two-field form with confirmation and mismatch detection, derives key via Argon2id and re-saves config encrypted - Remove passphrase: reverts to keyring-only protection - Protection line in settings now shows [Enter to change] hint Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Add a scrolling activity log panel at the bottom of the main screen showing real-time events (messages received, tasks created, config saved, errors). Log entries are pushed from all action handlers. Close feature gaps with openvtc-cli: - Add reject VRC request (send rejection message back to requester) - Add remove VRC (delete from issued/received collections) - Add clear all inbox tasks - Add activity log calls throughout all action handlers - Show relationship created dates in list view Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Add a toggle in the new-relationship-request form to generate a random relationship DID (did:peer) instead of using the persona DID. This provides privacy by not exposing the persona DID to every relationship partner. - Add generate_r_did toggle (Space key) to form (field index 3) - Port create_relationship_did from openvtc-cli: derives Ed25519 and X25519 keys from BIP32 path m/3'/1'/1'/<n>', creates did:peer with mediator routing, registers secrets with TDK resolver - Falls back to persona DID when BIP32 backend not available or toggle is off Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Replace static Help panel with Status/Help showing persona DID, mediator DID, protection type, relationship/task/VRC counts, connection status, and keyboard shortcuts. - Add contact management actions (add/remove) with handlers - Add config import form in settings (validates file, advises restart) - Show VRC validity date ranges (valid_from → valid_until) in list - Import Config option at settings index 6, token mgmt moved to 7 - Reusable export/import form renderer with parameterized title Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Config was only saved to disk by settings actions. Inbox, relationship, credential, and message dispatch actions modified Config in memory but never persisted, causing data loss on crash or restart. Add save_config() calls after every mutating action handler: - 7 inbox actions (accept/reject/dismiss/clear) - 3 relationship actions (submit/ping/remove) - 2 credential actions (submit/remove) - 1 inbound message dispatch Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
…d actions Replace the 0x8000_0000 bit flag hack used to encode "open detail" in InboxSelectTask/RelationshipSelect/CredentialSelect with separate InboxOpenDetail/RelationshipOpenDetail/CredentialOpenDetail actions. Replace the SettingsEditUpdate(String) control character encoding (\x00-\x04, \t prefixes) with typed action variants: - SettingsFieldUpdate for inline text editing - SettingsFormFieldUpdate/SettingsFormTabSwitch for export/import forms - SettingsProtection* variants for protection change flow - RelationshipToggleRDid for the R-DID toggle Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Extract ~40 handler functions from the 700+ line action match block in state_handler/mod.rs. The match block is now a thin dispatcher where each arm is 1-3 lines calling a named handler function. Handlers are grouped by domain: inbox (10), relationship (9), credential (9), contact (2), settings (16 including token mgmt). Also fix clippy warnings: replace &Box<Config> with &Config in helper signatures. Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Replace Vec<String> with VecDeque<String> for the activity log. This changes log truncation from O(n) (Vec::remove(0) shifts all elements) to O(1) (VecDeque::pop_front). Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Bump all workspace crates to 0.2.0. Changes: - Full TUI main menu panels (inbox, relationships, credentials, settings, status) - Activity log panel with real-time events - R-DID generation for relationship privacy - VRC issuance from inbox - Critical fix: config persistence after all mutations - Code quality: replaced hacks with typed actions, extracted handlers, VecDeque log Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Passphrases for export/import/protection-change were stored as plain String in SettingsMode variants, which is part of State that gets cloned every tick and sent to the UI channel. Multiple copies existed in memory without zeroization. Fix: replace passphrase strings in State with length-only fields (passphrase_len, confirm_len). Actual passphrase content is now kept only in MainPage's local buffers (passphrase_buffer, confirm_buffer) which are never cloned. Passphrases are consumed via std::mem::take on submit and cleared on cancel. Also wrap token_admin_pin in Arc<SecretString> so State clones share a single allocation instead of creating independent copies. Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
- Validate message body size (1MB limit) before deserialization to prevent DoS via oversized JSON payloads - Check for duplicate task IDs before creating new tasks to prevent hijacking via crafted message IDs/thread IDs - Validate sender identity on thid-referencing messages (reject/accept) to prevent unauthorized state mutations - Add collection size bounds: max 10K tasks, 5K relationships — reject new entries when limits reached - Sanitize all untrusted display text (DIDs, aliases, reasons from remote peers) by stripping ANSI escape codes and control characters, truncating to 256 chars Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
…tion - Document key material zeroization limitation (ed25519-dalek-bip32 lacks Zeroize impl) with explicit drop() calls after use - Add unlock attempt rate limiting: max 5 attempts with exponential backoff (2s, 4s, 8s delay after 3 failures) - Redact home directory paths from user-facing error messages (replace with ~), keep full paths in debug logs - Truncate DIDs to 30 chars in activity log entries to limit information exposure on shared terminals - Validate export/import file paths: reject empty paths and path traversal (..) attempts - Add structured audit log entries for config export and import operations Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Replace all .unwrap()/.expect() in fallible contexts with proper error propagation (?, ok_or_else, map_err): - cli.rs: get_user_pin now returns Result - main.rs: signal handler, password loop, config loading - setup_sequence/config.rs: 9 expect/unwrap calls replaced - did_keys_export_show.rs: clipboard operations gracefully degrade with tracing::warn instead of panicking Additional improvements: - Add #[must_use] to pure functions (sanitize_display, shorten_did, collect_vrcs, truncate_did) - Add doc comments to State, ActivePage, MediatorStatus, ConnectionState types and variants - Replace blanket #[allow(dead_code)] with targeted per-field annotations with explanatory comments - Use explicit Arc::clone() instead of .clone() on 27 Arc values across inbox_actions, relationship_actions, credential_actions - Simplify SecretString allocation chains: remove unnecessary .to_string() in "literal".to_string().into() patterns Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Return Cow::Borrowed when the DID is short enough (<=30 chars), avoiding a String allocation on the common path. Cow::Owned is only used when truncation actually occurs. Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Add config_version field to PublicConfig (defaults to 0 for pre-existing configs via serde(default)). On load, if the version is below CONFIG_VERSION, run stepwise migrations. On save, stamp the current CONFIG_VERSION. Migration framework: each version increment gets a match arm in migrate_config(). Version 0→1 is a no-op (just adds the field). Future format changes add new arms. Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
…ates Switch from tokio::sync::mpsc::unbounded_channel to tokio::sync::watch for State communication between StateHandler and UI. The watch channel ensures the UI always sees the latest state, automatically dropping intermediate updates when the UI is slower than state changes. Benefits: - No unbounded queue growth if state changes faster than rendering - UI always renders the most recent state, never stale intermediates - Simpler mental model: "current value" vs "message queue" Updated 9 files: state_handler mod.rs, ui/mod.rs, setup_wizard.rs, setup_vta_actions.rs, setup_token_actions.rs, setup_did_actions.rs, setup_sequence/config.rs, setup_sequence/openpgp_card.rs, setup_sequence/did_keys.rs. Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
When the mediator DID is changed in settings, automatically tear down the existing DIDComm WebSocket loop and spawn a new connection with the updated mediator. Also adds an explicit SettingsReconnectMediator action for manual reconnection. The existing conn_result_rx channel is reused so the reconnection result is handled by the same select arm that handles initial connection setup. Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Add OutboundQueue that stores failed outbound messages for automatic retry when connectivity is restored. Queue is bounded (1K messages, 5 retries per message) with oldest-message eviction. On successful mediator reconnection, queued messages are automatically retried via pack_and_send. Failed retries are re-queued. Queue count is displayed in the inbox connection status. Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Split the flat ~65-variant Action enum into domain-specific sub-enums: - InboxAction (10 variants) - RelationshipAction (10 variants) - CredentialAction (10 variants) - ContactAction (2 variants) - SettingsAction (20 variants incl. token management) The main Action enum now dispatches to sub-enums via wrapper variants (Action::Inbox(InboxAction::...), etc.). Setup/VTA/WebVH actions remain flat in the main enum since they're stable. Benefits: each domain is self-contained, easier to add features, cleaner match blocks in the state handler dispatcher. Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Define a Panel trait with a render() method that unifies the four panel render signatures behind a single interface. Each panel module (inbox, relationships, credentials, settings) now exports a struct implementing the trait. The content_panel dispatcher uses dyn Panel trait objects for the four domain panels, with direct rendering for Help/Status and Quit. This establishes the pattern for incrementally moving key handling into each panel in the future. Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Add test coverage for core non-async handler functions: - outbound_queue (4 tests): enqueue/len, drain retry count, max retries drop, queue size limit - sanitize_display (5 tests): control chars, ANSI escapes, truncation, spaces, empty input - shorten_did (2 tests): short passthrough, long truncation - activity_log (1 test): bounded VecDeque behavior - validate_file_path (5 tests): empty, traversal, normal paths, dot-slash, hidden traversal - main_panel_switch (1 test): panel toggle logic Also fixes a bug in sanitize_display where ANSI escape stripping was dead code (control char filter removed ESC byte first). Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
…iom changes Add security hardening section (14 findings fixed), architectural changes (config versioning, watch channel, mediator reconnect, outbound queue, action grouping, panel trait, test harness), Rust idiom improvements, and bug fixes to the v0.2.0 release notes. Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Enable crossterm's bracketed paste mode so that paste operations arrive as a single Event::Paste(String) instead of hundreds of individual KeyEvent characters. This eliminates the per-character render cycle overhead when pasting large strings like VTA credential bundles (typically 500+ chars). The VTA credential paste page now handles Event::Paste by setting the entire input value at once via tui_input::Input::new(), making paste effectively instant instead of taking several seconds. Also adds handle_paste_event to the Component trait with a default no-op implementation, and flows paste events through AppRouter → SetupFlow → VtaCredentialPaste. Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
…patch
Tightens process_inbound_message with the two M-tier defenses called
out in the v0.2.0 review:
* Replay window — drop messages whose `created_time` is more than 48
hours old (matching our outbound 48h `expires_time` horizon) or
more than 5 minutes in the future (clock-skew tolerance), and drop
messages whose `expires_time` has already passed. Stops stale
captures from re-driving relationship/VRC state long after the
legitimate exchange has been torn down.
* Seen-message LRU — process-lifetime 1024-entry deduplicator on
message IDs. The TDK already filters unpack-level duplicates, but
mediator pickup retries and replay attempts both surface as the
same packed bytes from the mediator's perspective. Belt-and-braces
backstop, evicted FIFO at capacity.
The `from`-required check already covered the M1 anoncrypt-vs-authcrypt
concern in practice: HandlerContext.sender_did is only `Some(_)` after
the TDK confirms an authcrypt'd sender, and process_inbound_message
rejects messages with `from = None` for everything except trust-pong
(where thid linkage is the equivalent authentication channel).
Threads `&mut SeenMessages` through from the StateHandler main loop
where the LRU is instantiated once. 8 new unit tests cover both helpers.
Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Three boundary fixes flagged in the v0.2.0 review: * DID validation no longer trusts a `did:` prefix. The previous validate_did would accept anything ≥ 8 bytes that started with "did:", including newlines, spaces, and zero-width characters that downstream routing logic took as identity. The replacement parses did = "did:" method-name ":" method-specific-id per W3C DID Core 1.0 syntax and rejects malformed inputs at the message-dispatch boundary. * Inbox display-name sanitisation extends sanitize_display to strip bidi-override / isolate / zero-width / BOM codepoints in addition to ANSI / control bytes. The accept-relationship handler now runs this over `request.name` and clamps to 64 chars before persisting the value as a contact alias, so a malicious peer can't spoof a contact's displayed identity via RLO/RLM tricks. * Relationships' VTA-client fallback no longer hand-rolls challenge_response against `vta_url`. The DIDComm-only VTA case (empty vta_url, non-empty mediator_did) was failing with "vta_url is empty" because the fallback was REST-only. It now defers to build_runtime_vta_client, which already does the right thing for both transports. Six new unit tests cover the strengthened validate_did. Workspace test suite passes (243 total). Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Three CI improvements + one tracking bump: * deny.toml + new "Cargo Deny" job runs `cargo deny check advisories licenses bans sources`. Two advisories are explicitly ignored with rationale: RUSTSEC-2023-0071 (rsa Marvin Attack — no upstream fix, reachable only on local key paths, not network-observable timing) and RUSTSEC-2024-0370 (proc-macro-error unmaintained — build-time only, pulled in by the json-ld stack inside affinidi-did-resolver). Two non-OSI-but-permissive licenses are allow-listed: bzip2-1.0.6 (libbz2-rs-sys via pgp) and CDLA-Permissive-2.0 (webpki-roots). rand 0.8 is in skip-tree to suppress the multiple-versions noise the workspace already documents in Cargo.toml. * "Coverage (llvm-cov)" job runs `cargo llvm-cov --workspace --lcov` with `--include-ignored` so any future #[ignore]-gated integration tests (e.g. mock-mediator E2E) still contribute. Uploads lcov.info as an artifact for inspection or later upload to a coverage host. * MSRV check moved 1.91.0 -> 1.94.0 to match the workspace rust-version. Was the only place still pinning the old value. Side fix: openvtc -> did-git-sign workspace dep gets an explicit version = "0.1" alongside its path = "../did-git-sign", so the path dep no longer trips cargo-deny's wildcard-dependency check. Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
…'t OOM us
The DIDComm router → state-handler event channel was unbounded. A
mediator (or upstream service) that pushed messages faster than the
state handler could drain them would grow the channel without limit,
turning into a memory-exhaustion vector that needs no authentication
to trigger.
Bound the channel at 256 entries (DIDCOMM_EVENT_CHANNEL_CAPACITY).
The four router send sites switch from `tx.send(...)` to
`tx.try_send(...)`:
* On success the message reaches the dispatcher unchanged.
* On a full channel we log a warning and drop the message.
Inbound message pickup is at-least-once via the mediator —
once the dispatcher catches up and drains, the mediator
redelivers anything we shed under load. That keeps the
handler closures non-blocking and keeps the DIDCommService
progress decoupled from state-handler pace.
Capacity is sized so that bursty real operator activity doesn't ever
overflow; the cap is purely a defense against pathological volume.
The other in-process channels (action_rx, lifecycle_log_tx,
progress_tx, etc.) are deferred — they aren't attacker-controllable
and an Arc<State> migration is the higher-leverage follow-up there.
Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
setup_flow/navigation.rs is a 260-line pure function — a perfect target for exhaustive unit testing — and it had zero tests. Adds 25 tests covering every SetupEvent variant outside the openpgp-card feature, both flow branches that key off SetupState (webvh-server-pick vs manual, unlock-code-set webvh vs manual), and the structural shape of every NavResult variant. Locks in the wizard flow before the larger state-handler split refactor that will move pieces of it around. A regression in navigate() is one of the highest-impact bug classes the binary can ship — broken page routing leaves the operator stuck mid-setup with no recovery path. Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Two test additions targeted at silent-corruption regressions in the crypto stack — exactly the bug class that doesn't fail loudly: * bip32: 7 known-answer tests pin BIP32-ed25519 derivation behaviour for the production paths (m/0'/0'/0' persona, m/3'/1'/1'/0' relationship). Verifies that signing/authentication purposes share the same Ed25519 public key, that encryption goes through ed25519 -> x25519 conversion (different bytes), that distinct seeds produce distinct keys, and that invalid paths / unknown purposes return errors. A future ed25519-dalek-bip32 / affinidi-crypto bump that silently changes derivation will trip these. * config/encryption: 6 tampering tests assert the AES-256-GCM AEAD fails closed on any modification to the stored ciphertext — nonce byte flip, ciphertext byte flip, GCM tag byte flip, truncation, append, and a "frankenstein" splice (nonce of one message onto the body+tag of another). Catches silent on-disk-data-edit attacks and AEAD-construction regressions. Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
state_handler::main_loop's Action::Inbox arm was a 30-line nested match fronting nine private handlers further down in mod.rs. Both pieces now live in `inbox_actions.rs` next to the protocol-level helpers they wrap, and the main loop calls a single `inbox_actions::dispatch(action, ...)` entry point. mod.rs drops ~250 lines (2255 -> 2007). The wrapper handlers (handle_inbox_select / open_detail / accept_* / reject_* / dismiss / clear_all) plus the small `save_and_sync` and `record_error` UI helpers all move with their related protocol code. External behaviour is unchanged — same call shape, same arguments, same per-action effects. Workspace test suite still passes (250+ tests; same as before). This is the first slice of B5; Relationship / Credential / Settings will follow the same pattern. Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
…_actions Same pattern as the Inbox slice: the 60-line Action::Relationship match arm fronting 10 private handlers in mod.rs becomes a single relationship_actions::dispatch(...) call. The wrapper handlers (open_detail / start_new_request / cancel_or_back / input_update / toggle_r_did / submit / ping / remove / edit_alias / request_vrc) plus their RelationshipsMode-mutation helpers move to relationship_actions.rs next to the protocol-level helpers they wrap. The Ping arm side-effect — setting the main loop's `ping_sent_at` so the inbound pong can be correlated for RTT — is preserved by threading `&mut Option<Instant>` into the dispatch signature. Two helpers used by relationship handlers (and Credential / Settings handlers later) — `log_did` and `resolve_did_to_display` — are promoted from private fns in mod.rs to `pub(crate)` so they can be called from the per-domain modules without re-implementing. mod.rs drops another ~390 lines (2007 -> 1617). Workspace test suite still green. Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
…ions Same per-domain extraction as the Inbox and Relationship slices: the Action::Credential match in main_loop is replaced with a single credential_actions::dispatch(...) call, and the eight handler wrappers (switch_tab / open_detail / back / start_new_request / select_relationship / reason_update / submit_request / remove) move into credential_actions.rs alongside the protocol-level VRC helpers they were already calling. mod.rs drops another ~120 lines (1617 -> 1475). Workspace test suite still passes. Contact dispatch and Settings dispatch remain inline in mod.rs; Contact's match is six lines (not worth the move) and Settings is a larger refactor that's worth its own slice. Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
…gs_actions The largest of the per-domain extractions: the 163-line Action::Settings match (with two inline 30-line reconnect-listener blocks) and the 6-line Action::Contact match both move into settings_actions.rs as `dispatch` (Settings) and `dispatch_contact` (Contact). The two reconnect-listener inline blocks dedupe to a new didcomm::reconnect_persona_listener helper that returns a `ReconnectOutcome::Connected | Failed(reason)` enum; the dispatcher calls it for both the SubmitEdit-mediator-change branch and the ReconnectMediator action. The wipe-the-profile-then-exit semantic is preserved by having the dispatcher return a `SettingsOutcome` enum: `Continue` is the common case, `ExitUserInt` is returned only when the operator confirms the wipe — the main loop then drives the terminator and breaks out of main_loop with Interrupted::UserInt as before. mod.rs drops another ~660 lines (1475 -> 813); cumulative reduction since B5 started is 2255 -> 813, a 64% shrink. Workspace test suite still passes (250+ tests, same as before this refactor). That's the four big domains (Inbox, Relationship, Credential, Settings) plus Contact extracted; the action match in main_loop is now a clean five-line dispatch table. Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
…gration
The v0.2.0 review (H2) flagged that derive_passphrase_key derived its
Argon2 salt deterministically from the info label. Two operators with
the same passphrase produced the same KEK, exported config files
encrypted with the same passphrase were byte-comparable, and an
attacker could amortise a dictionary attack across all OpenVTC users.
Fix:
* derive_passphrase_key_v2(passphrase, salt) takes an explicit
per-entry random salt. Same Argon2id parameters as v1.
* derive_passphrase_key (the legacy entry point) is kept and clearly
re-documented as legacy. It's now used only on the v1 decrypt path
for backward compat.
* passphrase_encrypt_v2(pass, info, plaintext) generates a random
16-byte salt, derives a v2 key, AEAD-encrypts via the existing
unlock_code_encrypt, and prepends a magic+salt header:
[b"OPV2" : 4][salt : 16][nonce : 12][ct+tag]
* passphrase_decrypt(pass, info, blob) auto-detects format by magic
prefix:
starts with b"OPV2" -> v2 path (parse salt from blob)
otherwise -> v1 path (deterministic info-salt)
* Config::export switches to v2. Existing v1-encrypted backup files
are still readable through passphrase_decrypt (transparent
migration: an exported v1 blob re-encrypted by the operator after
upgrade lands as v2 on the next export).
The unlock-code path against the SecuredConfig keyring entry still
uses derive_passphrase_key directly (the salt has no on-disk slot
there yet); migrating that requires a versioned keyring entry format
and is intentionally deferred.
Seven new tests cover the v2 round-trip, that two encrypts of the same
plaintext under the same passphrase produce distinct blobs, that
legacy v1 ciphertexts decrypt cleanly via the new API, that wrong
passphrase fails, that v1 blobs need their original info-label to
decrypt (since info IS the salt for v1), that v2 blobs no longer
depend on info (since the salt is in the blob), and that tampering
with the salt bytes fails authentication.
Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
The v0.2.0 review's B9 line item — a real mediator binary, not a
wiremock stub — drops in cleanly because affinidi-messaging-mediator
0.14 exposes a `MediatorBuilder` that stands up the whole server
(handlers, store, secrets resolver, listener) inside the calling
process, returning a `MediatorHandle` with the bound HTTP/WS endpoints.
* `tests/common/mod.rs` adds `MockMediator::start()` — generates a
fresh `did:peer` for the mediator with a random Ed25519/X25519 key
pair, loads the secrets into a `ThreadedSecretsResolver`, picks
`MediatorBuilder::memory_store()` so nothing touches disk, binds
on `127.0.0.1:0` (ephemeral) so parallel test invocations don't
collide, and returns a guard that triggers shutdown on drop.
* `tests/mediator_smoke.rs` exercises the harness end-to-end: the
mediator binds, the `well-known/did.json` endpoint either serves
or 404s (both indicate the HTTP server is up), and the handle
reports a sensible mediator/admin DID.
* Mediator-spawning tests are marked `#[ignore]` so the default
`cargo test` stays fast. CI runs them via the existing coverage
job, which already passes `--include-ignored` to `cargo llvm-cov`.
dev-deps added on openvtc-core: affinidi-messaging-mediator (with
default features off, didcomm + memory-backend on so we don't pull
Redis), affinidi-secrets-resolver, reqwest (workspace), tokio (with
macros + rt-multi-thread), tokio-util.
Verified by running `cargo test -p openvtc-core --test mediator_smoke
-- --ignored` — the in-process mediator boots in ~150ms and the
smoke assertions pass. The full default `cargo test --workspace`
suite remains unchanged.
This is the foundation for the B11 integration tests (relationship
E2E, VRC round-trip, trust-ping over the mediator, did-git-sign git
commit E2E, full setup-wizard happy path) — each can layer on
`MockMediator` and stand up a couple of TDKProfile instances against it.
Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Lands the file-and-comment scaffolding for the first B11 integration test so the intended shape is recorded next to the working harness. The body is gated on a profile-registration helper that doesn't yet exist (the mediator's admin-account / ACL provisioning flow needs its own helper before two TDKProfiles can register with the test mediator and start exchanging routable DIDComm). The test is `#[ignore]` and runs as a no-op apart from confirming the mediator boot path is still intact. The full B11 backlog (relationship E2E, VRC issue/verify, trust-ping over the mediator, did-git-sign git commit E2E, full setup-wizard happy path) layers on the same harness once the registration helper lands. Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Lands the first working end-to-end integration test on the
[`MockMediator`] harness. Two profiles (Alice + Bob) connect to the
in-process mediator, Alice sends a typed DIDComm message to Bob, the
mediator routes it through its forwarding/pickup pipeline, and Bob's
router receives it via a capture handler. All in-process, no external
services, sub-second on a warm cache.
Required harness extensions to actually drive a real mediator:
* Pre-bind the listener so the mediator's URL is known *before* the
DID document is generated. Drop and reuse the port — the window
is microseconds and 127.0.0.1, no realistic CI collision.
* Construct the mediator's `did:peer` directly via
`affinidi-did-common`'s `DID::generate_peer` (the TDK 0.6 helper
only supports a single service entry). Publish three services:
- `dm` : DIDComm messaging endpoint (Long-form so the
SDK's `_find_endpoint` can read the JSON map)
- `#auth` : `Authentication` service URI ending in
`/authenticate` so the SDK appends the right
path; Uri form because did-authentication's
`get_uri` mishandles Long-form (round-trips
through Value::to_string and re-emits JSON
quotes around the URL).
- `#ws` : WebSocket endpoint (Long-form, ws:// scheme)
* Inject a real Ed25519 JWT signing keypair via `config_mut`. The
headless mediator config ships zero-byte JWT keys; the auth
handler 500s on `InvalidEddsaKey` the moment it tries to mint an
access token.
* Switch the mediator's global ACL defaults to the SDK's "allow_all"
preset (mediator-mode `ExplicitAllow`, per-account
`access_list_mode = ExplicitDeny`, all flags on). Default deny mode
flips `did_local` off, which trips the websocket handler's
"DID isn't local" 403, and the recipient ACL's empty allow-list
blocks all forwarded messages.
The test is `#[ignore]` (slow: ~1s mediator boot + auth handshake +
WS connect for two profiles). CI's coverage job runs all ignored
tests via `cargo llvm-cov ... -- --include-ignored`.
This unblocks the rest of the B11 integration tests (relationship E2E,
VRC issue/verify, trust-ping over the mediator, did-git-sign git
commit, full setup-wizard happy path) — each layers on the same
`MockMediator::start()` / `make_profile()` harness.
Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
… pass Two pieces of remaining v0.2.0 work: * Adds a second integration test, `relationship_request_round_trip`, that drives a real `openvtc_core::relationships::RelationshipRequestBody` through the in-process mediator and asserts the receiving side can deserialise it back into the protocol type. Same connect / wait / send / receive shape as the basic round-trip but the body is the production protocol struct, so a future serde change on either side trips the test. Refactored the original test to share a `connect_alice_and_bob` helper so subsequent integration tests can layer on without repeating the boilerplate. * CHANGELOG: the original `[0.2.0]` entry was written when the rename branch landed; the 23 review-driven commits that followed (security hardening, Argon2 salt migration, did-git-sign signing policy, state-handler split, DIDComm replay window, integration-test harness, CI hardening, doc cleanup) had no entry. Adds a "Post-release deep-review pass" subsection under `[0.2.0]` so the release notes describe what's actually in the tag. Both integration tests pass under `cargo test -p openvtc-core -- --ignored`. Default `cargo test --workspace` is unchanged. Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Adds the third B11 integration test. Two legs through the mediator:
Alice -> Bob: VrcRequest { reason } typed as VRC_REQUEST
Bob -> Alice: VRCRequestReject { reason } typed as VRC_REJECTED
Each side deserialises the inbound DIDComm body back into the
openvtc-core protocol type; a serde regression on either field name
trips this test the same way relationship_request_round_trip does for
the relationship pair. The reject's `thid` links it to the original
request id, matching how the production protocol correlates the two
legs.
That's three of the audit's five recommended integration tests
covering the harness end-to-end: basic round-trip, relationship
request, VRC request/reject. The remaining two — did-git-sign git E2E
(needs a mock VTA stack) and the full setup-wizard happy path
(needs the TUI decoupled from the wizard's setup_wizard.rs) — are
each separate infra investments and stay flagged as follow-up work
in the harness module docs.
Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
…or 0.1 Replace the hand-rolled mediator harness with the just-published `affinidi-messaging-test-mediator = "0.1"` crate. The fixture handles port pre-bind, the mediator did:peer (with dm/#auth/#ws services), the JWT signing keypair, and global ACL defaults so we no longer carry that boilerplate locally. End-user DIDs now use the **mediator's DID** (not its HTTP URL) as the DIDComm service endpoint. The mediator's routing/2.0 logic only short-circuits to local delivery when the next-hop URI string equals the mediator's DID; an HTTP URL — even one pointing back at the same mediator — is treated as remote and enqueued to FORWARD_Q. Since the mediator's DID isn't known at builder time (content-addressed off keys generated during spawn), we can't use the test-mediator's `local_dids` shortcut: instead, we pass our own `MemoryStore`, spawn the mediator, then register Alice + Bob as ALLOW_ALL accounts directly through the shared store handle. All four integration tests (smoke + 3 round-trips) pass. Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Catches up the workspace to the May 2026 upstream releases: - affinidi-tdk 0.6 → 0.7 (workspace) — accessor-method API on TDKSharedState / TDKEnvironment; field access (`.secrets_resolver`, `.environment`, `.profiles`, `.default_mediator`) now goes through methods. Affected sites: openvtc-core, openvtc, openvtc-service, robotic-maintainers. - TDKSharedState::default() (also removed in 0.6) replaced with TDKSharedState::new(TDKConfig::headless()?).await? in openvtc-service. - affinidi-messaging-didcomm-service 0.2 → 0.3 (workspace). - affinidi-messaging-test-mediator 0.1 → 0.2 (openvtc-core dev-deps). Replaces the hand-rolled MemoryStore + ALLOW_ALL registration dance with the new TestMediator::with_users(["alice", "bob"]) helper, which mints user DIDs whose service URI is the mediator's DID rather than its HTTP URL — the routing-2.0 shape that short-circuits forwards to local delivery. Drops affinidi-messaging-mediator, -mediator-common, -sdk and sha256 from dev-deps now that the workaround is gone. - tests/common/mod.rs trims from ~150 lines to ~95. Verified: `cargo build --workspace`, `cargo test --workspace`, all four mediator integration tests (`--test relationship_e2e --test mediator_smoke -- --ignored`), `cargo clippy --workspace --all-targets`. Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
… badge CHANGELOG v0.2.0 post-release deep-review pass: - Rewrote the in-process mediator harness bullet to reflect the upstream test-mediator 0.2 fixture (`TestMediator::with_users`) rather than the hand-rolled `MockMediator::start` boot-the-mediator-yourself shape that predated the published crate. - Added the integration tests bullet (smoke + 3 round-trips, ~350ms warm). - Added a "Dependency refresh" subsection covering tdk 0.6 → 0.7 (accessor-method API, TDKSharedState::default removal), didcomm-service 0.2 → 0.3, and test-mediator 0.1 → 0.2 (drops four dev-deps). Notes the IPv6 + mediator-common feature-gating follow-ups landing post-publication; neither is on openvtc's loopback path. openvtc-core README: MSRV badge 1.91.0 → 1.94.0 to match Cargo.toml's `rust-version` and the top-level README. Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Folds in @sameerchore's PR #57. `validate_profile_name` now trims leading/trailing whitespace before validating, so `" default "` is treated as `"default"`, and the empty/whitespace check runs *before* the character validation so a whitespace-only input (" ") gets a clear "cannot be empty or contain only whitespace" error instead of the confusing "Invalid profile name ' '" the alphanumeric check used to produce. Adds the three integration tests from the PR: - whitespace_only_profile_name_rejected - padded_default_profile_name_accepted - padded_valid_profile_name_accepted Closes #57. Co-authored-by: Sameer Chore <sameerchore5@gmail.com> Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
…#51, #47) Folds in @krsatyamthakur-droid's PR #51 (resolves #47). Two improvements: 1. Windows config location. `profile_dir` and `get_lock_file` now use `dirs::config_dir()` on Windows (typically `%APPDATA%\openvtc`), matching the platform convention. Unix/macOS continues to use `~/.config/openvtc/` so existing installs don't move. 2. PathBuf throughout. `get_config_path` and `get_lock_file` return `PathBuf` instead of `String`, so callers don't round-trip through a (potentially non-UTF-8) string and don't have to re-parse with `Path::new`. `create_lock_file` and `remove_lock_file` now take `impl AsRef<Path>` for ergonomic call sites. Tests in `public_config::tests` rewritten to be cross-platform — asserts use `PathBuf::push` rather than concatenated string literals, and the `OPENVTC_CONFIG_PATH` test uses platform-appropriate `C:\` or `/tmp` bases. Adds `test_get_config_path_fallback` covering the no-env-var path. Closes #51, closes #47. Co-authored-by: satyam kumar <krsatyamthakur@gmail.com> Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
…ate (#34) Folds in the structural-defence parts of @ojasshelke's PR #34. ## Tagged-variant downgrade defence (Layer 1) `SecuredConfigFormat` switches from `#[serde(untagged)]` to `#[serde(tag = "format")]` so every blob carries an explicit `"format"` discriminator on disk. Without that tag, an attacker with write access to the OS keychain (or any caller fed a crafted blob) could substitute a `PasswordEncrypted` blob with `{"text": "<plaintext>"}` and serde would silently match it as `PlainText` — bypassing AES-256-GCM and delivering raw identity material. Tagged format means any blob lacking `"format"` is rejected at parse time. ## Caller-intent cross-validation (Layer 2) New `assert_format_matches_intent` runs **before** any decrypt or re-save. Even if an attacker substitutes a *correctly-tagged* but weaker variant — e.g. a tagged `PlainText` blob where `PasswordEncrypted` is expected — this gate refuses to proceed and emits a logged "Security violation" error. ## Transparent migration `SecuredConfig::load` first tries the new tagged format; on parse failure it falls back to a `LegacySecuredConfigFormat` (kept solely for migration), then re-saves in the tagged shape on next save. The intent gate runs before the re-save, so a downgrade attack can't be laundered through migration. ## Out of scope PR #34's HKDF v2 with fixed `HKDF_SALT` constant is **superseded** by the random-per-entry-salt v2 we shipped earlier in this release (`passphrase_encrypt_v2`, `OPV2` magic prefix in `openvtc-core::config::secured_config`). Random salt is strictly stronger — it prevents rainbow tables across users with the same passphrase. We've kept our random-salt v2 path; only the serde-format defence from #34 is folded. ## Audit advisories `.cargo/audit.toml` and `deny.toml` pick up entries the May 2026 dep refresh didn't resolve: `RUSTSEC-2024-0370` (proc-macro-error unmaintained, transitive via the affinidi DID resolver's json-ld stack) and `RUSTSEC-2025-0134` (rustls-pemfile unmaintained, transitive via reqwest 0.11 and tonic). The two rustls-webpki entries from the original PR were already cleared by the May 2026 dep bumps and are not added. ## Tests Four new tests in `secured_config::tests`: - `tagged_format_writes_explicit_discriminator` - `legacy_untagged_blobs_round_trip_through_legacy_enum` - `intent_gate_rejects_plaintext_when_password_expected` - `intent_gate_accepts_matching_combinations` Closes #34. Co-authored-by: Ojas Shelke <ojasshelke733@gmail.com> Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Records the substantive folds from PRs #57 (profile-name validation), #51 (Windows AppData + PathBuf cross-platform paths, closes #47), and #34 (SecuredConfig tagged-variant downgrade defence + intent gate) under v0.2.0's "Post-release deep-review pass". Adds a "Community contributions" subsection summarising each fold with author credit. Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Addresses the small-but-actionable items from @stormer78's #52 review of #46. Each fix is independently small; bundling so the issue can be closed in one go. 1. **Dead inner condition removed** (main.rs `None` arm): the early `-Y` block returns before this arm is reached, so the nested `cli.operation.is_none()` was always true. Replaced with a comment explaining why no inner check is needed. 3. **`process::exit` comment expanded** (main.rs delegation branch): spells out that inherited stdio doesn't buffer in-process — bytes have already crossed the syscall boundary, so the `Drop`-skip is safe — and what would change that (any added `println!` between delegation and exit). 4. **`DID_GIT_SIGN_SSH_KEYGEN` README warning** (Security Model section): explicitly marks the override as test-only and explains that an attacker with write access to the env could redirect verification to a binary that always returns success. Notes the override has no effect on the signing path. 6. **`setup_git_never_writes_user_email` invariant pinned** (init.rs): captures `original_cwd` before the inner block and asserts it after, so a future edit that drops `CwdGuard` or moves the verify command inside the guard's scope is caught loudly. 7. **`did_key_id` doc-comment corrected** (config.rs): the field's comment claimed "DID#key-id to use as git user.email" — stale from the pre-#46 era. Replaced with an accurate description of what it actually does (recorded in the SSH signature's `Signed-By` principal, not written to `user.email`). Items 2 (clap catch-all for forward-compat with future Git flags) and R2 (full-loop integration test) are deferred — both larger than this fold-and-close pass should carry. Items 5 (`user.signingKey` `.json` path tracking) and R1 (active-unset of stale DID-shaped `user.email` on upgrade) are tracked for v0.3. Closes #52. Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
This was referenced May 5, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Bump all workspace crates to v0.2.0 — major release. Branch renamed from
release/v0.1.6torelease/v0.2.0mid-flight (supersedes #40).Workspace consolidation
openvtc-cli(legacy CLI) deletedopenvtc-cli2renamed toopenvtc— package and binaryopenvtc-librenamed toopenvtc-core— frees the unsuffixed name for the binary, matches Rust CLI-first conventions (uv, ruff, deno, cargo)vta-sdkswitched from local path pin to published 0.5 on crates.ioVtaAuthenticatesetup page removedDIDComm & Messaging
affinidi-messaging-didcomm-service(now 0.3)send_message_with_retryFull TUI (8 panels)
Inbox / Relationships / Credentials / Settings / VTA Service / Logs / Help-Status / Quit. Full task lifecycle, inline alias edit, R-DID toggle, trust-ping with RTT, raw VRC JSON copy, export/import, hardware-token management. Plus bracketed paste, Up/Down navigation everywhere, dynamic DID display width, timestamped activity log.
R-DID privacy
Protocol
RelationshipRequestBody.namefor friendly names; auto-set contact alias on acceptPost-release deep-review pass
After cutting v0.2.0 a multi-axis review (security, architecture, tests, CI, docs) flagged a set of findings; rather than rolling another major, they all land on this same release branch before merge. The diff stays readable because the pre-review work is already in
[0.2.0]above the### Post-release deep-review passdivider in the CHANGELOG.Security hardening
passphrase_encrypt_v2writes[OPV2 | salt(16) | nonce(12) | ct+tag]; decrypt path auto-detects the magic prefix so existing exports keep opening. Argon2id parameters bumped to OWASP "high-value KEK" floor (m=128 MiB, t=4, p=1).did-git-signsigning policy — refuses to sign unless the parent process isgit*orssh-keygen; every signing attempt (accepted or denied) lands in~/.config/did-git-sign/audit.log(mode 0600).process_inbound_message: drops messages outside ±48h / +5m skew, drops past-expires_time, dedupes via 1024-entry process-lifetime ID LRU.did.jsonlwrite path is now the resolved profile dir (was: cwd).cargo update; nine open advisories cleared.Architecture & code quality
state_handler/mod.rs2,255 → 813 lines (-64%). Per-domain matches extracted todispatch(action, ctx).awaitentry points in each sub-module.colors.rsand thedialoguerpassphrase prompt moved out ofopenvtc-coreso the daemon and automation crates no longer pull inratatui+dialoguertransitively.openvtc-core::display.openvtc-corepublic surface — dropped a dead re-export, scoped two helpers topub(crate).log_error; replaced four router-init.expect("valid route")panics with?; replaced a panic-on-log-file-open with stderr + continue.relationships.rsnow usesbuild_runtime_vta_clientinstead of REST-onlychallenge_response.Tests & CI
openvtc-core/tests/common/mod.rs) wrapsaffinidi-messaging-test-mediator0.2 viaTestMediator::with_users(["alice", "bob"]). Returns Alice + Bob as ALLOW_ALL accounts whose DIDComm service URI is the mediator's DID — the routing/2.0 shape required for forwards to short-circuit to local delivery. The migration drops ~400 lines of in-tree fixture code and four dev-deps (affinidi-messaging-mediator,-mediator-common,-sdk,sha256).RelationshipRequestBodyround-trip, and a two-leg VRC request/reject round-trip — all in ~350ms once the mediator is up. Plus a smoke test on the well-known endpoint. Marked#[ignore]; CI's coverage job runs them with--include-ignored.setup_flow/navigation(25 table-driven), BIP32 derivation (7 known-answer vectors), AES-GCM tampering (6).cargo-denyjob (advisories + licenses + bans + sources) and acargo-llvm-covcoverage job. MSRV check bumped 1.91 → 1.94 to matchCargo.toml.Dependency refresh
affinidi-tdk0.6 → 0.7 — accessor-method API onTDKSharedState/TDKEnvironment/TDKProfile.TDKSharedState::default().await(removed in tdk 0.6) replaced withTDKSharedState::new(TDKConfig::headless()?).await?inopenvtc-service.affinidi-messaging-didcomm-service0.2 → 0.3 —?-transparent via the upstreamFrom<ACLError> for ATMError.affinidi-messaging-test-mediator0.1 → 0.2 (dev-deps).Community contributions
Three open community PRs against
mainwere assessed and folded into this release; each PR is closed with a comment pointing here.Co-authored-by:trailers preserve attribution.ae7fd72).validate_profile_namenow trims leading/trailing whitespace and reorders the empty/whitespace check before the character check, replacing the confusing"Invalid profile name ' '"with a clear"cannot be empty or contain only whitespace"error. Three new integration tests pin the behaviour.674dc40).profile_dirandget_lock_fileusedirs::config_dir()on Windows (typically%APPDATA%\openvtc); Unix/macOS unchanged.get_config_pathandget_lock_filereturnPathBufend-to-end;*_lock_filehelpers takeimpl AsRef<Path>. Cross-platform tests added.SecuredConfigdowngrade defence (@ojasshelke, commite20ea83). Tagged-variant serde format (#[serde(untagged)]→#[serde(tag = "format")]) prevents silent variant-substitution attacks on the keychain blob; newassert_format_matches_intentcross-check rejects tagged-but-weaker blobs before any decrypt or re-save; transparent migration of legacy untagged blobs on first load. PlusRUSTSEC-2024-0370andRUSTSEC-2025-0134advisory entries that survived the dep refresh. The PR's HKDF v2 fixed-salt scheme was superseded by our random-per-entry-saltOPV2v2 (already in this branch) and intentionally not folded.Checklist
cargo test+ four integration tests under--ignored