Class: SshTresor::Agent

Inherits:
Object
  • Object
show all
Defined in:
lib/ssh_tresor/agent.rb

Overview

Minimal SSH agent protocol client.

The agent is used as a private-key signing oracle: ssh-tresor-ruby sends a stored random challenge to the agent and derives wrapping keys from the returned signature bytes. The private key itself never leaves the agent.

Constant Summary collapse

SSH_AGENT_FAILURE =
5
SSH_AGENTC_REQUEST_IDENTITIES =
11
SSH_AGENT_IDENTITIES_ANSWER =
12
SSH_AGENTC_SIGN_REQUEST =
13
SSH_AGENT_SIGN_RESPONSE =
14
SSH_AGENT_SIGN_REQUEST_RSA_SHA2_256 =
2

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(socket) ⇒ Agent

Creates an agent client over an already-open socket.

Parameters:

  • socket (#read, #write)

    connected SSH agent socket.



148
149
150
# File 'lib/ssh_tresor/agent.rb', line 148

def initialize(socket)
  @socket = socket
end

Class Method Details

.bit_length(bytes) ⇒ Integer

Returns the bit length of a big-endian SSH integer.

Parameters:

  • bytes (String)

    big-endian integer bytes.

Returns:

  • (Integer)


138
139
140
141
142
143
# File 'lib/ssh_tresor/agent.rb', line 138

def self.bit_length(bytes)
  trimmed = bytes.b.sub(/\A\x00+/n, "")
  return 0 if trimmed.empty?

  ((trimmed.bytesize - 1) * 8) + trimmed.getbyte(0).bit_length
end

.connectSshTresor::Agent

Opens the SSH agent named by SSH_AUTH_SOCK.

Returns:

Raises:



96
97
98
99
100
101
102
103
# File 'lib/ssh_tresor/agent.rb', line 96

def self.connect
  socket_path = ENV["SSH_AUTH_SOCK"]
  raise AgentError, "SSH agent not available\nHint: Is SSH_AUTH_SOCK set? Try running: eval $(ssh-agent) && ssh-add" if socket_path.nil? || socket_path.empty?

  new(UNIXSocket.new(socket_path))
rescue SystemCallError => e
  raise AgentError, "Failed to connect to SSH agent: #{e.message}"
end

.format_key_type(blob) ⇒ String

Formats a public-key blob into a human-readable key type.

Parameters:

  • blob (String)

    SSH wire-format public-key blob.

Returns:

  • (String)

    human-readable key type.



109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/ssh_tresor/agent.rb', line 109

def self.format_key_type(blob)
  reader = SSHEncoding::Reader.new(blob)
  type = reader.string

  case type
  when "ssh-ed25519"
    "ED25519"
  when "ssh-rsa"
    reader.string
    n = reader.string
    "RSA-#{bit_length(n)}"
  when /\Aecdsa-sha2-/
    curve = reader.string
    "ECDSA-#{curve.delete_prefix("nistp")}"
  when "sk-ssh-ed25519@openssh.com"
    "SK-ED25519"
  when "sk-ecdsa-sha2-nistp256@openssh.com"
    "SK-ECDSA-256"
  else
    type.upcase
  end
rescue Error
  "UNKNOWN"
end

Instance Method Details

#find_key(fingerprint) ⇒ SshTresor::AgentKey

Finds a key by full SHA-256 fingerprint or unambiguous prefix.

Parameters:

  • fingerprint (String)

    fingerprint with or without the SHA256: prefix.

Returns:

Raises:



185
186
187
188
189
190
191
192
193
194
195
196
# File 'lib/ssh_tresor/agent.rb', line 185

def find_key(fingerprint)
  matches = list_keys.select { |key| key.matches_fingerprint?(fingerprint) }

  case matches.length
  when 0
    raise KeyNotFound, "Key not found: #{fingerprint}\nHint: Use 'ssh-tresor list-keys' to see available keys"
  when 1
    matches.first
  else
    raise KeyNotFound, "Key not found: #{fingerprint} (ambiguous: #{matches.length} keys match this prefix, please be more specific)"
  end
end

#find_key_by_fingerprint_bytes(fingerprint_bytes) ⇒ SshTresor::AgentKey

Finds a key by the raw SHA-256 fingerprint bytes stored in a tresor slot.

Parameters:

  • fingerprint_bytes (String)

    32-byte SHA-256 fingerprint digest.

Returns:

Raises:



203
204
205
206
# File 'lib/ssh_tresor/agent.rb', line 203

def find_key_by_fingerprint_bytes(fingerprint_bytes)
  list_keys.find { |key| key.fingerprint_bytes == fingerprint_bytes } ||
    raise(KeyNotFound, "Key not found: SHA256:#{Base64.strict_encode64(fingerprint_bytes).delete("=")}")
end

#first_keySshTresor::AgentKey

Returns the first available key.

Returns:

Raises:



176
177
178
# File 'lib/ssh_tresor/agent.rb', line 176

def first_key
  list_keys.first || raise(KeyNotFound, "No keys available in SSH agent\nHint: Try running: ssh-add")
end

#list_keysArray<SshTresor::AgentKey>

Lists public keys available through the agent.

Returns:

Raises:



156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/ssh_tresor/agent.rb', line 156

def list_keys
  response = request(SSHEncoding.byte(SSH_AGENTC_REQUEST_IDENTITIES))
  reader = SSHEncoding::Reader.new(response)
  type = reader.byte
  raise AgentError, "SSH agent refused identity request" if type == SSH_AGENT_FAILURE
  raise AgentError, "Unexpected SSH agent response type #{type}" unless type == SSH_AGENT_IDENTITIES_ANSWER

  count = reader.uint32
  Array.new(count) do
    blob = reader.string
    comment = reader.string.force_encoding(Encoding::UTF_8)
    comment = comment.valid_encoding? ? comment : comment.b.inspect
    AgentKey.new(blob: blob, comment: comment)
  end
end

#sign(key, data) ⇒ String

Signs arbitrary data with an agent key and returns only the raw signature bytes from the SSH agent response.

RSA keys are requested with the RSA/SHA-256 signature flag for modern OpenSSH compatibility.

Parameters:

  • key (SshTresor::AgentKey)

    key returned by this agent.

  • data (String)

    challenge bytes to sign.

Returns:

  • (String)

    raw signature bytes.

Raises:



218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
# File 'lib/ssh_tresor/agent.rb', line 218

def sign(key, data)
  flags = key.ssh_type == "ssh-rsa" ? SSH_AGENT_SIGN_REQUEST_RSA_SHA2_256 : 0
  payload = SSHEncoding.byte(SSH_AGENTC_SIGN_REQUEST) +
            SSHEncoding.string(key.blob) +
            SSHEncoding.string(data) +
            SSHEncoding.uint32(flags)

  response = request(payload)
  reader = SSHEncoding::Reader.new(response)
  type = reader.byte
  raise AgentError, "SSH agent refused signing request" if type == SSH_AGENT_FAILURE
  raise AgentError, "Unexpected SSH agent response type #{type}" unless type == SSH_AGENT_SIGN_RESPONSE

  signature_blob = reader.string
  signature_reader = SSHEncoding::Reader.new(signature_blob)
  signature_reader.string
  signature_reader.string
end