Skip to content

Client-side extension response parsing rejects non-query extension responses from spec-compliant agents #2

@sfriedenberg-etsy

Description

@sfriedenberg-etsy

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:

  1. 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.

  2. 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

  1. Extension::decode should read details as raw remaining bytes (reader.read_to_end()), not as a length-prefixed string
  2. 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
  3. 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:

  1. Forward ecdh@joyent.com to pivy-agent and relay the response back to pivy-box
  2. Forward ecdh-rebox@joyent.com to pivy-agent and relay the response back to pivy-box
  3. piggy show through the mux should decrypt on the first attempt without falling back to a PIN prompt

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions