Summary
ssh-agent-lib's client cannot parse extension responses from agents that follow the IETF spec / OpenSSH wire format. This is the inverse of #1 (which covers the server encoding direction). Together, these two issues mean ssh-agent-lib agents cannot interoperate with spec-compliant agents in either direction.
Context
#1 identified that ssh-agent-lib's Extension encoding adds two extra u32 length frames when producing responses. This issue covers the opposite direction: ssh-agent-lib's client-side Extension decoding expects those same extra frames, so it rejects valid responses from agents that follow the spec (OpenSSH, pivy-agent, etc.).
How this manifests
In ssh-agent-mux, the catch-all extension handler forwards requests to upstream agents using ssh-agent-lib's client:
// ssh-agent-mux/src/lib.rs — catch-all extension handler
_ => {
for sock_path in &self.socket_paths {
let mut client = self.connect_upstream_agent(sock_path).await?;
let result = client.extension(request.clone()).await;
match result {
Ok(v) => return Ok(v), // first success wins
Err(AgentError::Failure) => continue, // try next upstream
Err(e) => { log::error!(...); continue; }
}
}
Err(AgentError::Failure) // all upstreams failed
}
When pivy-agent responds to ecdh-rebox@joyent.com (or ecdh@joyent.com), ssh-agent-lib's client decoder tries to parse the response. The response is valid per the spec, but ssh-agent-lib expects its own non-standard framing, so parsing fails. The mux sees Err(AgentError::Failure) from every upstream and returns SSH_AGENT_FAILURE (code 5) to the calling client.
Observed error
.pivy-box-unwrapped: warning: failed to unlock ebox with agent
Caused by SSHAgentError: SSH agent returned message code 5 to rebox request
in piv_box_open_agent() at src/piv.c:7085
pivy-box falls back to a direct PIN prompt (second attempt via PCSC), so decrypt works end-to-end but with degraded UX.
Wire format comparison
Spec / OpenSSH / pivy-agent response to a data-bearing extension
Per draft-ietf-sshm-ssh-agent §3.8:
byte SSH_AGENT_EXTENSION_RESPONSE (29)
string extension type ← echo of the request name
byte[] extension response-specific contents ← raw bytes to end of message
Or for no-data responses (pivy currently uses this for ecdh extensions):
byte SSH_AGENT_SUCCESS (6)
What ssh-agent-lib's client decoder expects
ssh-agent-lib decodes extension responses through Extension::decode, which expects:
byte SSH_AGENT_EXTENSION_RESPONSE (29)
string extension name ← u32 len + name bytes
string details blob ← u32 len + payload bytes ← EXTRA
The details field is decoded as an SSH string (u32 length-prefixed blob), but the spec says byte[] — raw remaining bytes with no length prefix. This means:
-
If the upstream responds with type 29 (spec-compliant data-bearing response), ssh-agent-lib reads the extension name echo correctly, then tries to read a u32 length prefix for the details blob. It interprets the first 4 bytes of the actual payload as a length, getting garbage.
-
If the upstream responds with type 6 (SSH_AGENT_SUCCESS, used by pivy for ecdh extensions), ssh-agent-lib doesn't recognize it as an extension response at all — it maps to Response::Success, which has no payload, so the ecdh response data is lost.
Relationship to other issues
Root cause
Same structural issue as #1 but in the decode direction. Extension's Decode impl reads details as a length-prefixed string, but the spec's byte[] extension response-specific contents means "all remaining bytes after the extension name" — no length prefix.
Additionally, the client needs to handle both type 29 (data-bearing) and type 6 (no-data) extension responses. Some agents (including pivy, currently) use type 6 for extensions that do return data — this is non-spec but exists in the wild and needs to be handled for interoperability.
Suggested fix
Extension::decode should read details as raw remaining bytes (reader.read_to_end()), not as a length-prefixed string
- The client-side response handling should recognize that a type-6 response to an extension request may carry no data but still indicates success — the client should return
Ok(None) rather than discarding the response
- Optionally, handle type-6 responses that DO carry a trailing payload (pivy's legacy format) by reading remaining bytes as the extension response body
Verification plan
After fixing, ssh-agent-mux should be able to:
- Forward
ecdh@joyent.com to pivy-agent and relay the response back to pivy-box
- Forward
ecdh-rebox@joyent.com to pivy-agent and relay the response back to pivy-box
piggy show through the mux should decrypt on the first attempt without falling back to a PIN prompt
Summary
ssh-agent-lib's client cannot parse extension responses from agents that follow the IETF spec / OpenSSH wire format. This is the inverse of #1 (which covers the server encoding direction). Together, these two issues mean ssh-agent-lib agents cannot interoperate with spec-compliant agents in either direction.
Context
#1 identified that ssh-agent-lib's
Extensionencoding adds two extrau32length frames when producing responses. This issue covers the opposite direction: ssh-agent-lib's client-sideExtensiondecoding expects those same extra frames, so it rejects valid responses from agents that follow the spec (OpenSSH, pivy-agent, etc.).How this manifests
In ssh-agent-mux, the catch-all extension handler forwards requests to upstream agents using ssh-agent-lib's client:
When pivy-agent responds to
ecdh-rebox@joyent.com(orecdh@joyent.com), ssh-agent-lib's client decoder tries to parse the response. The response is valid per the spec, but ssh-agent-lib expects its own non-standard framing, so parsing fails. The mux seesErr(AgentError::Failure)from every upstream and returnsSSH_AGENT_FAILURE(code 5) to the calling client.Observed error
pivy-box falls back to a direct PIN prompt (second attempt via PCSC), so decrypt works end-to-end but with degraded UX.
Wire format comparison
Spec / OpenSSH / pivy-agent response to a data-bearing extension
Per draft-ietf-sshm-ssh-agent §3.8:
Or for no-data responses (pivy currently uses this for ecdh extensions):
What ssh-agent-lib's client decoder expects
ssh-agent-lib decodes extension responses through
Extension::decode, which expects:The
detailsfield is decoded as an SSHstring(u32 length-prefixed blob), but the spec saysbyte[]— raw remaining bytes with no length prefix. This means:If the upstream responds with type 29 (spec-compliant data-bearing response), ssh-agent-lib reads the extension name echo correctly, then tries to read a
u32length prefix for the details blob. It interprets the first 4 bytes of the actual payload as a length, getting garbage.If the upstream responds with type 6 (
SSH_AGENT_SUCCESS, used by pivy for ecdh extensions), ssh-agent-lib doesn't recognize it as an extension response at all — it maps toResponse::Success, which has no payload, so the ecdh response data is lost.Relationship to other issues
Root cause
Same structural issue as #1 but in the decode direction.
Extension'sDecodeimpl readsdetailsas a length-prefixedstring, but the spec'sbyte[] extension response-specific contentsmeans "all remaining bytes after the extension name" — no length prefix.Additionally, the client needs to handle both type 29 (data-bearing) and type 6 (no-data) extension responses. Some agents (including pivy, currently) use type 6 for extensions that do return data — this is non-spec but exists in the wild and needs to be handled for interoperability.
Suggested fix
Extension::decodeshould readdetailsas raw remaining bytes (reader.read_to_end()), not as a length-prefixed stringOk(None)rather than discarding the responseVerification plan
After fixing, ssh-agent-mux should be able to:
ecdh@joyent.comto pivy-agent and relay the response back to pivy-boxecdh-rebox@joyent.comto pivy-agent and relay the response back to pivy-boxpiggy showthrough the mux should decrypt on the first attempt without falling back to a PIN prompt