Skip to content

Release v0.8.0-preview.3: ssh-agent backend + quinn-proto security fix - #160

Merged
p1024k merged 20 commits into
masterfrom
develop
Aug 7, 2026
Merged

Release v0.8.0-preview.3: ssh-agent backend + quinn-proto security fix#160
p1024k merged 20 commits into
masterfrom
develop

Conversation

@p1024k

@p1024k p1024k commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Merges develop into master with two main changes:

1. ok agent — ssh-agent protocol backend (#159)

  • SSH identity loading with whitelist filtering, ED25519 / ECDSA (nistp256/384/521) / RSA signers (honoring sha2 flags)
  • ssh-agent protocol server over unix socket; serves multiple requests per connection for ssh interop
  • Process protections (ptrace/prctl) in run_agent for parity with TUI
  • Audit of successful SSH signatures (SshSign operation)
  • Graceful shutdown, pidfile, idle-lock, CLI lock repositioning, dedicated agent instance lock
  • Docs: ok agent README expanded into a usage guide (EN + ZH)

2. Dependabot security fix (GHSA-4w2j-m93h-cj5j)

Test Plan

  • cargo check --locked passes for root crate and tools/okb-gen
  • CI coverage gate enforced

p1024k and others added 20 commits July 22, 2026 17:49
Introduce `ok agent` (`--only`/`--allow`/`--idle-lock`): prompt for the
master password via rpassword (spawn_blocking), unlock the file-backed
SQLCipher vault mirroring executor::vault::handle_unlock, and serve the
ssh-agent over a Unix socket; prints SSH_AUTH_SOCK before serving.

- main.rs: clap Parser with optional subcommand; no subcommand == unchanged
  TUI path; --version handled natively (ok <version>, empty stderr).
- agent/cli.rs: AgentArgs + run() (tty) + unlock_and_serve() (tty-free
  testable core) + unlock_vault(); SecureStr password, never logged.
- agent/paths.rs: pure socket_path() resolver (XDG_RUNTIME_DIR > TMPDIR >
  /tmp); 0700 parent enforced by server.serve().
- idle_lock parsed/accepted, NOT enforced (Task 11) — loud startup warn.
- tests/agent_e2e_test.rs: real ssh-add -l/-L interop against the testable
  core over a file-backed vault (multi-thread runtime to avoid deadlock).

cargo fmt clean; clippy -D warnings clean; cargo test 3152 passed/0 failed.
Add RsaSigner producing RFC 8332 rsa-sha2-256 / rsa-sha2-512 PKCS#1 v1.5
signatures, mirroring Ed25519Signer's structure and secret hygiene. SHA-512
wins when both flags are set (matches OpenSSH); no flags defaults to SHA-256
(SHA-1 ssh-rsa is a spec Non-Goal — modern ssh refuses it).

Bypass ssh-key 0.6.7's buggy TryFrom<&RsaKeypair> (passes p twice instead of
p,q) by building rsa::RsaPrivateKey::from_components(n,e,d,[p,q]) directly;
re-implement the 2048-bit minimum locally.

Server: rename sign_with_ed25519 -> sign and add an RSA dispatch arm; wire
flags SSH_AGENT_SIGN_RSA_SHA2_256 (0x02) / _512 (0x04) map into SignFlags
before the algo match. ed25519 ignores the RSA bits (no regression).

Tests: 12 RSA signer tests + 3 RSA protocol-level roundtrips, both SHA-2
flag paths + default + encrypted-key path, all verifying with the rsa crate.
ed25519 + e2e suites still green (36/36 agent tests; 2628/2628 lib tests).
Add EcdsaSigner mirroring Ed25519Signer/RsaSigner: from_openssh maps
KeypairData::Ecdsa to the curve crate's SigningKey (p256/p384/p521),
sign produces the SSH wire blob `string ecdsa-sha2-nistp{256,384,521}`
+ `string <DER ECDSA sig>` with the curve's SHA-2 digest. ECDSA ignores
SignFlags. Dispatch wired into server.rs sign(); identity.rs now maps
NistP521 (previously rejected) so the agent advertises P-521 keys.

Signatures are RFC 6979 deterministic (the ecdsa crate's default Signer
impl), consistent with the deterministic ed25519/RSA-PKCS1v15 paths;
they verify identically to OpenSSH's randomized signatures.

p521 0.13 (features=["ecdsa"]) added and ssh-key "p521" feature enabled.
TDD: per-curve sign+verify (p256/p384/p521), passphrase path, cross-key
rejection, flags-ignored, determinism, and protocol SIGN_REQUEST
roundtrips for p256 and p521. cargo fmt + clippy -D warnings clean.
After a successful SIGN_REQUEST, write one AuditOperation::SshSign row
(record_id + name + resolved wire algorithm, e.g. ssh-ed25519 /
rsa-sha2-256 / ecdsa-sha2-nistp256). All three algorithm families
(ed25519/RSA/ECDSA) write SshSign.

The audit write is best-effort: on failure it is logged with
tracing::warn! and the successful SIGN_RESPONSE is still returned,
so an audit failure never blocks a successful signature.

Decrypt-audit decision: the sign path now decrypts key material via
decrypt_field_no_audit (new), so each sign writes ONLY SshSign.
RecordViewPassword is misleading here (the user never "viewed" a
password; the agent used the private key internally to sign). The
internal decrypt_field_with_audit(_, _, None) hook already existed;
decrypt_field_no_audit exposes it with caller-owns-audit semantics.

Adds SshAlgo::wire_name(flags) to resolve the precise wire algorithm
name (incl. RSA SHA-2 variant) for the audit detail.
Add AgentLock (src/agent/lock.rs) mirroring InstanceLock but using a
distinct .agent.lock file, so the SSH agent daemon and the TUI hold
independent advisory locks on independent inodes within the same data_dir.

- AgentLock::acquire uses fs4 try_lock_exclusive on <data_dir>/.agent.lock;
  AgentLockError{AlreadyRunning, Io} via thiserror mirrors InstanceLockError.
- Acquire the lock in cli::unlock_and_serve before unlocking the vault or
  binding the socket; hold it for the daemon lifetime (drops on return).
  AlreadyRunning surfaces via the new AgentCliError::Lock variant and exits
  loud through main.rs::run_agent (no silent second instance).
- Declare pub mod lock in agent/mod.rs.

Tests (TDD RED->GREEN):
- Inline unit tests: first acquire ok, second acquire -> AlreadyRunning,
  drop releases (reacquire ok), nested data_dir created, AgentLock +
  InstanceLock coexist on the same dir (distinct .agent.lock / .instance.lock).
- Integration (agent_e2e_test.rs): while a real agent daemon runs through
  unlock_and_serve on a file-backed SQLCipher vault, a second AgentLock on
  the same dir fails AlreadyRunning, InstanceLock succeeds (runtime
  coexistence), and AgentLock is reacquirable after the daemon releases it.
Task 11 (OKI-0014 / OKS-0014 / OKP-0014).

- serve() now races the accept loop against SIGTERM/SIGINT (cfg(unix)) and
  an optional idle-lock timer via tokio::select!. On any shutdown source it
  removes the socket + pidfile and drops the vault session (keys cleared by
  existing zeroize-on-drop).
- Add pidfile lifecycle: paths::pidfile_path() + pidfile_for_socket(); the
  pidfile is written before bind (closes a socket-vs-pidfile race) and removed
  on every shutdown path, including early bind error.
- --idle-lock <secs> is now enforced: an idle timer resets on each successful
  SIGN_REQUEST (try_send into an mpsc activity channel from the sync dispatch
  scope); no sign for <secs> triggers the same graceful cleanup. None disables
  the timer. The old 'NOT enforced' startup warning is removed.
- Reposition AgentLock acquire ABOVE the password prompt in run() (Task 10
  Minor-1): a second 'ok agent' fails with AlreadyRunning before asking for
  the password. unlock_and_serve (test path) acquires then delegates to a new
  unlock_and_serve_with_lock core that takes the guard; production run()
  acquires early and calls the same core.
- Add AgentServerError::Pidfile variant (fail loud on pidfile write failure).
- New tests/agent_shutdown_test.rs: SIGTERM, SIGINT, and idle-lock all trigger
  graceful shutdown with socket + pidfile removal; pidfile written at startup
  with the running PID. Serialized via a tokio::sync::Mutex to keep the
  process-wide signal receivers from racing.

cargo fmt && cargo clippy --all-targets -- -D warnings clean.
All agent suites green (shutdown 3, lib agent:: 35, e2e 3, protocol 10).
Add CHANGELOG entry and a short usage section to README/README-ZH/
INSTALL/INSTALL-ZH for the `ok agent` ssh-agent backend. No version
change (managed by the version-management skill); no code change.

Task 12 (final) of OKI-0014 / OKS-0014 / OKP-0014.
feat(agent): SSH agent backend (ok agent) — OKI-0014
Fix Dependabot alerts #4 and #5: remote memory exhaustion in quinn-proto
from unbounded out-of-order stream reassembly. Transitive via opendal ->
reqwest (http3). 0.11.15 is compatible with quinn 0.11.9.
@p1024k
p1024k merged commit 6336b98 into master Aug 7, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant