Implement SSH identity authentication

This commit is contained in:
2026-08-25 19:52:47 +02:00
parent f636f3b551
commit 5dbda4bbd2
14 changed files with 1964 additions and 60 deletions

View File

@@ -70,16 +70,35 @@ The HTTPS account name is stored inside the protected credential record, not in
TOML. OpenPGP passphrases are addressed by the resolved primary fingerprint.
SSH remotes use either `ssh://[user@]host[:port]/path` or scp-like
`[user@]host:path` syntax and omit the HTTPS credential fields:
`[user@]host:path` syntax and omit the HTTPS credential fields. They select
exactly one private-key file or one already-running SSH-agent identity:
```toml
[[git.remotes]]
name = "origin"
url = "git@git.example.test:alice/password-store.git"
ssh_identity_file = "keys/id_ed25519"
ssh_known_hosts_file = "known_hosts"
```
Relative paths are resolved against `config.toml`. If
`ssh_known_hosts_file` is omitted, `~/.ssh/known_hosts` is used. Agent
authentication replaces `ssh_identity_file` with an exact SHA-256 fingerprint
and may name a socket explicitly:
```toml
ssh_agent_fingerprint = "SHA256:47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU"
ssh_agent_socket = "/run/user/1000/ssh-agent.socket"
```
Private-key passphrases are stored by fingerprint in the operating-system
secret store and never appear in TOML. IronStorage does not read OpenSSH
configuration, try additional keys, prompt for passwords or
keyboard-interactive authentication, launch an agent, or invoke proxy/helper
commands.
The typed endpoint model is always available so an SSH remote remains readable
in configuration even when the binary was built without SSH. Such a build
through the Rust API even when the binary was built without SSH. Such a build
returns a typed unsupported-transport error before connection or repository
mutation. The optional storage `ssh` feature contains `russh` 0.63.1 and Tokio;
`russh` default features are disabled and the Ring backend plus RSA key support
@@ -94,6 +113,12 @@ bytes, credentials, queries, fragments, ambiguous unbracketed IPv6 or colon
paths, leading-option paths, local paths, URL rewrites, separate push URLs,
helper transports, and unknown schemes fail closed.
Server identity is checked before authentication against the configured
known-hosts file. Exact, hashed, and non-default-port host entries are
supported. Unknown keys return their host, algorithm, and SHA-256 fingerprint
for explicit confirmation; confirming appends the key atomically. A changed
key is a hard failure and is never replaced by that confirmation API.
`clipboard_timeout_seconds` controls the native clipboard presentation lease.
It defaults to 45 seconds for upstream `pass` compatibility and must be between
1 and 300 seconds. The CLI remains alive for the lease so Linux can serve its

View File

@@ -32,10 +32,7 @@ repository mutation instead of treating its configuration as malformed.
## HTTPS transport
HTTPS credentials are requested with the configured server ID and application
ID and remain outside Git configuration. The SSH session, host-verification,
authentication, and pack-protocol implementations are separate milestone work;
until those layers are present, network operations on SSH endpoints return the
typed unsupported-transport result.
ID and remain outside Git configuration.
Fetch uses the embedded Rust smart-HTTP client with an explicit credential
callback, so Git's credential cascade is never entered. Push implements the
@@ -45,6 +42,24 @@ requests `report-status`, and accepts the update only after both unpack and ref
status succeed. HTTP redirects are disabled so authorization cannot cross an
origin boundary.
## SSH authentication
With the optional `ssh` feature, storage opens an in-process Russh client and
verifies the server key before any authentication or command. Known-host files
are read with size and line bounds; exact, hashed, and bracketed non-default
port entries are supported. Unknown keys require an explicit call to persist
the confirmed key atomically. Changed keys always fail and are never replaced.
Authentication uses one configured OpenSSH private-key file (Ed25519, ECDSA,
or RSA) or one exact SHA-256 fingerprint from an already-running SSH agent.
Encrypted-key passphrases come from `SecretBytes` in the operating-system
secret store. Identity attempts are bounded and deterministic; IronStorage
does not spray keys, use passwords or keyboard-interactive authentication,
read OpenSSH configuration, start/probe an agent process, or run proxy/helper
commands. SHA-1 host signatures and `ssh-rsa` authentication are excluded.
Cancellation interrupts connection and authentication without changing Git,
known hosts, or secure storage.
Pull refuses a dirty worktree. It fast-forwards when possible and otherwise
uses the embedded three-way tree merge. Unresolved paths are returned as typed
`MergeConflicts`; no conflict markers or partial checkout are written. Checkout

View File

@@ -5,12 +5,13 @@ password-store repositories, Git configuration, command arguments, and logs
contain only opaque identifiers; passphrases, tokens, and HTTPS passwords are
stored by the operating system.
`SecretReference` has two validated forms. OpenPGP passphrases are keyed by the
`SecretReference` has three validated forms. OpenPGP passphrases are keyed by the
primary fingerprint. HTTPS Git credentials are keyed by purpose, server ID,
application ID, and account. The account is kept inside the protected record,
so a configured server/application pair can retrieve it without adding an
account or secret value to TOML. References, locators, store state, and errors
all use redacted `Debug` output.
account or secret value to TOML. SSH private-key passphrases are keyed by the
key's SHA-256 fingerprint. References, locators, store state, and errors all use
redacted `Debug` output.
Stored values use a small versioned binary envelope containing their reference
and secret bytes. Retrieval validates the envelope and exact reference before
@@ -47,8 +48,8 @@ if the platform lock operation reports an error. Caching is disabled unless a
caller explicitly selects `SecretCachePolicy::Timed`. Timed policies are capped
at 128 entries and 15 minutes, expire lazily, and are always cleared on lock.
The same unlocked store implements the OpenPGP `SecretProvider` and HTTPS Git
`GitCredentialProvider`. The CLI uses it for terminal `show` and embedded `git
fetch`, proving that protected keys and remote authentication are resolved only
through opaque references. Tests inject a memory backend and never access a
developer or CI user keyring.
The same unlocked store implements the OpenPGP `SecretProvider`, HTTPS Git
`GitCredentialProvider`, and SSH `SshPassphraseProvider`. The CLI uses it for
terminal `show` and embedded Git, proving that protected keys and remote
authentication are resolved only through opaque references. Tests inject a
memory backend and never access a developer or CI user keyring.