All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog.
- Derive unlock key via Argon2id in non-openpgp import path — the non-
openpgp-cardbuild was passing the raw passphrase toSecuredConfig::save()as the AES-256 key, bypassing the Argon2id KDF and making the saved config unrecoverable sinceUnlockCode::from_string()always applies Argon2id at load time - Reject path-traversal characters in profile name —
--profileandOPENVTC_CONFIG_PROFILEwere spliced verbatim into lock-file paths, config paths, and OS keyring account names; now restricted to[A-Za-z0-9._-]with no..component - Redact armored private key block in
DIDKeysExportStateDebug— the derivedDebugimpl could dump the full PGP-armored private key through any{:?}ofState(panic backtrace, tracing, debug print) - Warn that
--unlock-codeis visible in the process list — the flag exposes the passphrase viaps//proc/<pid>/cmdlineand shell history; help text now documents this and a runtime warning nudges users toward the interactive prompt - Restore terminal on panic via panic hook — panics inside the render loop, key handlers, or spawned tasks no longer leave the TTY in raw mode on the alternate screen
- Drop exported private-key armor from
Stateafter use — the armored PGP private key block was cloned through the state broadcast channel on every tick for the remainder of the setup wizard - Avoid OOB panic on stale token-list index — unplugging or re-enumerating tokens no longer panics the TUI when a retained selection index exceeds the new bounds
- Clear private-key clipboard when leaving export page — the ASCII-armored PGP private key block placed on the OS clipboard by
[C]is now cleared on continue (unless the user has copied something else in the meantime) - Clear
ConfigImportpassphraseInputbuffers after dispatch — both passphrase inputs are now reset once wrapped inSecretStringand dispatched, matching the other secret-input pages
- Full TUI main menu panels in
openvtc— 8 panels: Inbox, Relationships, Credentials, Settings, VTA Service, Logs, Help/Status, Quit - Inbox panel with real-time task processing: auto-handles trust-pongs, relationship finalization, and rejections; queues interactive tasks; detail views for all task types (inbound/outbound requests, VRCs, pings, informational)
- Relationships panel with list/detail/new-request views, inline alias editing ('e' key), R-DID privacy toggle, trust-ping with RTT latency
- Credentials panel with Received/Issued tabs, raw VRC JSON in detail view, clipboard copy ('c' key), VRC request and removal
- Settings panel with inline editing, config export/import, passphrase protection management, hardware token detection and factory reset
- VTA Service panel showing VTA URL, DID, credential DID, key count, and backend type
- Logs panel with scrollable timestamped activity log, selected entry copy ('c'), copy all ('a')
- Activity log panel at bottom of screen showing real-time timestamped events (
[HH:MM:SS] message) - Status/Help panel with DID clipboard copy hotkeys ([1] persona, [2] mediator), visual feedback on copy
- R-DID generation for both BIP32 and VTA backends — VTA path authenticates and creates keys via API; both sender and receiver can use R-DIDs
- Dynamic R-DID listeners — automatically added when creating R-DIDs (sender or receiver), enabling message delivery to relationship-specific DIDs
- VRC issuance from inbox with DataIntegrityProof signing; VRC rejection with message back to requester
- Friendly name in relationship requests — sender's name included in request body, auto-set as contact alias on accept, R-DID recommendation shown when sender uses one
- DIDComm service integration (
affinidi-messaging-didcomm-service0.2) — replaces manual messaging with Router-based dispatch, automatic reconnection, message pickup, and multi-DID listener support - Periodic keepalive ping (60s) with live RTT latency in connection status header
- Inbox task count badge on menu item ("Inbox (3)" in red when tasks pending)
- Bracketed paste for all 21 text input fields — paste is instant regardless of string length
- Up/Down arrow navigation in all multi-field forms alongside Tab
- Config versioning with stepwise migration framework
- Panel trait for content panels — unified render interface
- Outbound message retry via
DIDCommService::send_message_with_retry - Auto-reconnect mediator on DID change in settings
- 15 unit tests covering core functions
- Contact management actions (add/remove)
- Trust-pings only responded to from mediator DID or established relationships — prevents presence leakage
- Passphrases removed from cloned State — length-only fields in UI, consumed via
mem::take - Token admin PIN wrapped in
Arc<SecretString>for shared allocation - Inbound message body size validation (1MB limit), task ID deduplication, sender verification
- Collection bounds (10K tasks, 5K relationships), untrusted display text sanitization
- Unlock rate limiting (5 attempts, exponential backoff), path redaction, file path validation
- Key material explicit drop with documented zeroization limitation
- Structured audit log entries for security-relevant operations
- R-DID message routing — acceptance, finalize, VRC, and ping messages now use relationship DID instead of persona DID when R-DID exists
- Config persistence — all mutating actions save to disk
- Setup → main transition —
sync_from_config()now called after setup wizard completes - VRC "From:" blank — extract remote DID from relationship for VRC tasks
- Alias on accept — sender's name set as contact alias, existing alias-less contacts updated
- Backspace to empty in relationship form fields
- Tab after backspace fix — dedicated
FocusFieldaction for field switching - DIDComm listener secrets — pass DID secrets to listeners for mediator authentication
- All
.unwrap()/.expect()replaced with proper error propagation - Clipboard graceful degradation,
sanitize_displayANSI stripping order
- Workspace consolidation — renamed the active CLI package and binary
openvtc-cli2→openvtc, and renamed the supporting libraryopenvtc-lib→openvtc-core. The unsuffixed name now belongs to the user-facing binary, matching the convention used by uv, ruff, deno, and cargo. The library ispublish = false, so no external consumers are affected. vta-sdk0.5 consumed from crates.io — dropped the temporary../verifiable-trust-infrastructure/vta-sdkpath pin so the workspace no longer requires a sibling checkout to build.- Replaced manual messaging layer with
affinidi-messaging-didcomm-service— deleted messaging/mod.rs (~280 lines) and outbound_queue.rs (~90 lines), added didcomm.rs (~260 lines) with Router, listeners, and send_message_with_retry - Grouped ~65-variant Action enum into 5 domain sub-enums
tokio::sync::watchreplaces mpsc for State updates- Panel trait with per-panel structs implementing unified render interface
- Dynamic DID display width (
shorten_did(did, max_width)— 60 chars default, full if fits) Cow<str>for zero-alloc DID truncation- Explicit
Arc::clone(),#[must_use]on pure functions, doc comments on State types VecDeque<String>for O(1) bounded activity logRelationshipRequestBody.nameprotocol field for friendly names
- Legacy
openvtc-clicrate — the original prompt-driven CLI was phased out in favour of the TUI. All ongoing work lives inopenvtc. - Dead
VtaAuthenticatesetup page — online provisioning emitsVtaAuthCompleteddirectly fromVtaProvisioning, so the legacy authenticate screen was unreachable.
After cutting the v0.2.0 branch a multi-axis review (code quality, security, tests, docs) flagged a set of findings that landed on the same release branch before merge. They're listed separately so the diff between v0.1.x and v0.2.0 stays readable.
- Per-entry random Argon2 salt with transparent v1→v2 migration.
derive_passphrase_keypreviously used a deterministic salt = SHA-256(info), so two operators with the same passphrase produced the same KEK and exported backups were byte-comparable. The newpassphrase_encrypt_v2/passphrase_decryptAPI inopenvtc-core::config::secured_configwrites a magic-prefixed[OPV2 | salt(16) | nonce(12) | ct+tag]blob with a fresh random salt; the decrypt path auto-detects v1/v2 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. The proxy now refuses to sign unless the parent process name starts withgitorssh-keygen, and writes every signing attempt — accepted or denied — to~/.config/did-git-sign/audit.log(mode 0600) with parent PID/name, namespace, buffer path and SHA-256. Blocks the "malicious build script obtains a signature with namespace=git over attacker-chosen content" pivot.- DIDComm replay window + seen-message LRU in
process_inbound_message: drop messages withcreated_timeoutside ±48h / +5m skew, drop messages whoseexpires_timealready passed, dedupe on a 1024-entry process-lifetime ID LRU. - DID validation uses a real W3C DID Core 1.0 syntax parser instead of a
did:prefix check; rejects bidi-override / zero-width chars in DID fields. - Inbox display-name sanitisation strips bidi-override / isolate / zero-width / BOM unicode (Cf class) plus ANSI escapes / control chars, and clamps inbound contact aliases to 64 chars before persistence.
- Bounded DIDComm event channel (256-entry capacity) so a noisy mediator can't grow memory without limit; overflow logs and drops, mediator pickup redelivers when we drain.
did.jsonlwrite path is now the resolved profile dir, not the current working directory.- Dependabot: transitive openssl/rustls-webpki/rand bumped via
cargo updateto clear nine open advisories.pgpwas already at the patched 0.19. - Tagged-variant downgrade defence on
SecuredConfigFormat. Switched the on-disk variant tag from#[serde(untagged)]to#[serde(tag = "format")]so every blob carries an explicit"format"discriminator. Without it, an attacker with write access to the OS keychain could substitute aPasswordEncryptedblob with{"text": "<plaintext>"}and serde would silently match it asPlainText, bypassing AES-256-GCM. Newassert_format_matches_intentcross-validation gate adds a second defence layer — a tagged-but-weaker blob is rejected before any decrypt or re-save. Old (untagged) blobs migrate transparently on first load. Folded from @ojasshelke's PR #34; the PR's HKDF v2 fixed-salt variant is superseded by our random-per-entry-salt v2 (OPV2magic prefix) above.
Three community PRs against main were assessed and folded into the release. Each PR's substantive value is preserved with Co-authored-by: trailers; the corresponding PRs are closed with a comment pointing here.
- #57 — profile-name validation hardening (@sameerchore).
validate_profile_namenow trims leading/trailing whitespace before validating, and the empty/whitespace check runs before the character check (so" "gets a clear "cannot be empty or contain only whitespace" error instead of the confusing "Invalid profile name ' '"). Three new integration tests pin the behaviour. - #51 — cross-platform config paths (@krsatyamthakur-droid, closes #47).
profile_dirandget_lock_filenow usedirs::config_dir()on Windows (typically%APPDATA%\openvtc); Unix/macOS continues to use~/.config/openvtc/so existing installs don't move.get_config_pathandget_lock_filereturnPathBufinstead ofStringend-to-end. - #34 —
SecuredConfigserde-format hardening (@ojasshelke). Tagged-variant downgrade defence + intent-gate cross-validation, described under Security above. The PR's HKDF v2 fixed-salt scheme was superseded by our random-salt OPV2 v2 and intentionally not folded.
- State-handler split.
state_handler/mod.rswas 2,255 lines with a 500-linetokio::select!arm; it's now 813 lines (-64%). Each per-domain match (Inbox, Relationship, Credential, Settings, Contact) was extracted to adispatch(action, ctx).awaitentry point in the corresponding sub-module. - Layering: moved
colors.rsand thedialoguerpassphrase prompt out ofopenvtc-coreso the daemon (openvtc-service) and automation (robotic-maintainers) crates no longer pull inratatui+dialoguertransitively. - Lifted four DID-truncation helpers into a single
openvtc-core::displaymodule (truncate_did,truncate_did_centered). - Tightened
openvtc-corepublic surface — dropped a deadpub usere-export and scoped two helpers topub(crate). - Fixed silent failures in the state handler: surfaced previously-swallowed
save_config/remove_listener/ inbox-task errors vialog_error. Replaced four.expect("valid route")panics in DIDComm router init with?. Replacedpanic!("Cannot create log file …")with stderr + continue. - Fixed DIDComm-only VTA fallback in
relationships.rs(usedbuild_runtime_vta_clientinstead of REST-onlychallenge_response).
- In-process mediator harness (
openvtc-core/tests/common/mod.rs): wraps the upstreamaffinidi-messaging-test-mediator0.2 fixture viaTestMediator::with_users(["alice", "bob"]), which boots a realaffinidi-messaging-mediatoron an ephemeral loopback port (memory-backed store, generateddid:peeridentity advertisingdm/#auth/#ws, Ed25519 JWT signing keypair) and 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 instead of being enqueued for external forwarding. The previous in-tree harness predated the test-mediator crate; the migration drops ~400 lines of fixture code and four dev-deps (affinidi-messaging-mediator,-mediator-common,-sdk,sha256). - End-to-end integration tests (
relationship_e2e.rs): drive a real Alice→Mediator→Bob DIDComm round-trip, a productionRelationshipRequestBodyround-trip, and a two-leg VRC request/reject round-trip — all in ~350ms once the mediator is up. Plus a smoke test (mediator_smoke.rs) that asserts the well-known endpoint serves a DID Document. Marked#[ignore](each spawns the mediator, ~1s); CI's coverage job runs them with--include-ignored. - 38 new unit tests across
setup_flow/navigation(25 table-driven), BIP32 derivation (7 known-answer vectors), AES-GCM tampering (6) — locking the wizard flow, derivation contract, and AEAD failure modes before the v0.3.0 work begins. - CI adds a
cargo-denyjob (advisories + licenses + bans + sources, with documentedRUSTSEC-2023-0071rsa Marvin-Attack andRUSTSEC-2024-0370proc-macro-error ignores) and acargo-llvm-covcoverage job (uploadslcov.infoartifact, runs ignored tests). MSRV check bumped 1.91 → 1.94 to matchCargo.toml.
Picks up the May 2026 Affinidi-stack releases. All bumps cleared on crates.io; build, full test suite, and integration tests pass.
affinidi-tdk0.6 → 0.7 — accessor-method API onTDKSharedState/TDKEnvironment/TDKProfile. Field accesses (.secrets_resolver,.environment,.profiles,.default_mediator,.ssl_certificate_paths) are now method calls.TDKSharedState::default().await(removed in tdk 0.6) replaced withTDKSharedState::new(TDKConfig::headless()?).await?inopenvtc-service.affinidi-messaging-didcomm-service0.2 → 0.3 — version bump driven by the upstreamMediatorACLSeterror-type relocation; downstream impact is?-transparent thanks toFrom<ACLError> for ATMError.affinidi-messaging-test-mediator0.1 → 0.2 (dev-deps only) —TestMediator::with_users(["alice", "bob"])replaces our hand-rolledMemoryStore+ ALLOW_ALL registration dance. Dropsaffinidi-messaging-mediator,-mediator-common,-sdkandsha256from dev-deps.- Working with the upstream maintainers, this branch's review of the May 2026 test-mediator changes also surfaced two follow-ups landing post-publication: an IPv6 routing-classification fix and
mediator-commonfeature-gating to keep the SDK light. Neither is on the path used by openvtc tests (loopback over127.0.0.1).
- README, CONTRIBUTING, SECURITY, CLAUDE.md aligned to the post-rename workspace shape (
openvtcbinary +openvtc-corelib). - CHANGELOG
[0.2.0]entry above describes the release as it actually shipped.
- Upgraded
pgp0.18 → 0.19, resolving 3 Dependabot alerts: parser crash on crafted RSA secret key packets (CVE-2026-21895), crash from deeply nested messages, and integrity protection not always checked on encrypted data
- Hardware token touch prompt overlay in
openvtc-cli2— a centered popup now appears when a YubiKey (or other OpenPGP card) requires physical touch confirmation, and auto-dismisses when the touch completes - Progress feedback during VTA credential validation in
openvtc-cli2setup wizard - Unit tests for
MessageTypeandKeyPurposeinopenvtc-lib - GitHub Discussions guidance in
CONTRIBUTING.md
- Upgraded
secrecy0.8 → 0.10 (SecretVec<u8>replaced withSecretBox<Vec<u8>>,SecretString::new()API updated) - Upgraded
openpgp-card0.5 → 0.6 andopenpgp-card-rpgp0.6 → 0.7 - Migrated pgp 0.19 API changes:
EncryptionKey/DecryptionKeytraits,SubpacketData::IssuerKeyId,Timestamptypes
- Stale
openvtc-cli2/did.jsonltest artifact
- Removed legacy SHA-256+HKDF encryption — existing configs must be recreated with
openvtc setup UnlockCode::from_string()now returnsResultand enforces minimum 8-character passphrasederive_passphrase_key()now returnsResult— callers must handle the error
- Replaced
rand::thread_rng()withOsRngin all cryptographic key generation paths (BIP39 entropy, PGP export, DID key generation) - Hardened Argon2id parameters: 64 MiB memory / 3 iterations (up from default 19 MiB / 2 iterations) per OWASP recommendations
- Added
#![deny(unsafe_code)]toopenvtc-lib— no unsafe code in production paths - Added DID format validation for
OPENVTC_MEDIATOR_DIDandOPENVTC_ORG_DIDenvironment variable overrides - Replaced all production
unwrap()calls with proper error handling in setup wizard, clipboard operations, and service initialization - Replaced ~15 silent
let _ =error discards withdebug!/warn!logging in state handler, service, and robotic-maintainers
- Argon2id as sole KDF (removed legacy fallback)
- Profile name validation (alphanumeric, hyphens, underscores only)
- Rate limiting to
openvtc-service(50 msg/sec with throttle logging) - Graceful shutdown signal handling (SIGINT/SIGTERM) in
openvtc-service - Criterion benchmarks for
derive_passphrase_keyandunlock_code_encrypt/unlock_code_decrypt - Integration tests for profile validation, relationships, VRCs, tasks, and logs (38 new tests)
CODE_OF_CONDUCT.md(Contributor Covenant v2.1)- Windows to CI test matrix
- MSRV verification (Rust 1.91.0) in CI pipeline
- API documentation for public modules (relationships, VRCs, tasks, logs, config)
- All Clippy warnings (migrated deprecated Protocols API, collapsible-if, items-after-test-module)
- Corrected valid-until prompt handling for VRC issuance in
openvtc-cli(PR #23)
A standalone CLI tool for signing git commits using DID Ed25519 keys managed by a VTA. Acts as a git SSH signing proxy — no private key material ever touches disk.
- Git SSH signing proxy via
gpg.ssh.programintegration - VTA authentication with token caching in OS keyring
- Credential private key stored in OS keyring (macOS Keychain / Linux Secret Service)
- Ed25519 signing key fetched from VTA at sign-time and zeroized after use
- SSH signature output in PROTOCOL.sshsig format
initcommand — configures git and sets up allowed_signers for verificationstatuscommand — displays current signing configuration and keyring stateverifycommand — end-to-end test of keyring, VTA auth, key fetch, and signing- Config validation: rejects non-HTTPS VTA URLs, empty credentials, non-Ed25519 keys
- Retry logic for VTA authentication (up to 2 attempts on transient failures)
didwebvh-rs0.1 → 0.4affinidi-tdk0.5 → 0.6 (affinidi-messaging-didcomm0.12 → 0.13)affinidi-data-integrity0.4 → 0.5dtg-credentialsswitched from local path to crates.io (0.1)vta-sdkupdated to 0.3 (health.versionis nowOption<String>,VtaClient::set_tokenno longer requires&mut self,CreateDidWebvhRequesthas new optional fields)- All transitive dependencies updated to latest compatible versions via
cargo update
- Replaced manual
DIDWebVHState::default()+create_log_entry()pattern with the newcreate_did(CreateDIDConfig)API in bothopenvtc-libandopenvtc-cli create_initial_webvh_did()is now async (required bycreate_did)- Added
LogEntryMethodstrait import forget_did_document()access
DataIntegrityProof::sign_jcs_data()is now async — added.awaitinopenvtc-cli,robotic-maintainers, anddtg-credentialsDTGCredential::sign()is now asyncCreateDidWebvhRequest.server_idchanged fromStringtoOption<String>CreateDidWebvhRequestnow requiresurl: Option<String>field and new optional fields (did_document,did_log,signing_key_id,ka_key_id,set_primary)CreateDidWebvhResultBody.mnemonicchanged toOption<String>Message::pack_encrypted()removed — replaced withATM::pack_encrypted(&msg, to, from, sign_by)Message.type_field renamed toMessage.typdidcomm::error::Errorreplaced bydidcomm::DIDCommErrorPackEncryptedOptionsremoved — encryption options are now implicit in the pack function choiceUnpackMetadatamoved fromdidcommtomessaging::messages::compatVtaClient::set_token()no longer requires&mut selfHealthResponse.versionchanged fromStringtoOption<String>
- Custom
Debugimplementations forPersonaDIDKeysandKeyInfothat redact secret material - Replaced debug logging of full
SecuredConfigstruct with safe summary - Fixed
unwrap()in SSH signature encoding path withexpect()and context - VTA URL validation — rejects plain HTTP (except localhost for development)
- Ed25519 key type validation when fetching signing keys from VTA
- Empty access token rejection after VTA authentication
- Extracted 11 hardcoded protocol URLs to
protocol_urlsconstants module inopenvtc-lib - Added
mediator_did()andorg_did()helper functions with environment variable overrides (OPENVTC_MEDIATOR_DID,OPENVTC_ORG_DID) - Updated
MessageTypeFrom/TryFromimpls and VRC message builders to use protocol URL constants - Removed unused
consoleandcrosstermdependencies fromopenvtc-lib
- openvtc-lib: Added 14 new tests (2 → 16 total)
- Encrypt/decrypt roundtrip, wrong key rejection, empty data, large data, different key ciphertext divergence, corrupted data detection, zeroize verification
- Protected config save/load roundtrip, wrong seed rejection, serialization, contacts find/remove, credential seed determinism and divergence
- did-git-sign: Added 6 new tests (5 → 11 total)
- Config validation (empty URL, HTTP rejection, HTTPS acceptance, localhost exception, empty key ID rejection, seed material zeroization)
- Added
did-git-sign/README.mdwith setup instructions, architecture diagram, security model, and config format reference - Added workspace crates table and DID Git Signing section to root
README.md
- Fixed deterministic encryption vulnerability in
unlock_code_encrypt/unlock_code_decrypt(openvtc-lib). The previous implementation used a seeded PRNG to derive both the AES-256-GCM key and nonce from the unlock code, producing identical ciphertext for the same password and plaintext. The fix uses HKDF-SHA256 for key derivation with a random nonce (viaOsRng), ensuring each encryption produces unique output. Existing configs encrypted with the old format are transparently decrypted via a legacy fallback and re-encrypted with the secure format on the next save.
- CLI interface for
openvtc-servicewith--config/-cflag to specify an alternate configuration file path (default:conf/config.json). --helpand--versionflags foropenvtc-service.- Comprehensive operator documentation for
openvtc-service: configuration schema, logging (RUST_LOG), runtime behavior, and protocol context.
- Unused
chronoandranddependencies fromopenvtc-service.
- Aligned documented minimum Rust version with workspace
rust-version(1.91.0) in root README,openvtc-lib, andopenvtc-serviceREADMEs. - Removed duplicate introductory paragraph and repeated bullet in Decentralised Identity section.
- Fixed typo "Remove" to "Remote" in Private Configuration section.
- Changed incorrect
htmlcode fence totextfor a URL example under Host Your DID Document. - Updated README badges to link to current repository (
OpenVTC/openvtc).