Class: SshTresor::Agent
- Inherits:
-
Object
- Object
- SshTresor::Agent
- 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
-
.bit_length(bytes) ⇒ Integer
Returns the bit length of a big-endian SSH integer.
-
.connect ⇒ SshTresor::Agent
Opens the SSH agent named by
SSH_AUTH_SOCK. -
.format_key_type(blob) ⇒ String
Formats a public-key blob into a human-readable key type.
Instance Method Summary collapse
-
#find_key(fingerprint) ⇒ SshTresor::AgentKey
Finds a key by full SHA-256 fingerprint or unambiguous prefix.
-
#find_key_by_fingerprint_bytes(fingerprint_bytes) ⇒ SshTresor::AgentKey
Finds a key by the raw SHA-256 fingerprint bytes stored in a tresor slot.
-
#first_key ⇒ SshTresor::AgentKey
Returns the first available key.
-
#initialize(socket) ⇒ Agent
constructor
Creates an agent client over an already-open socket.
-
#list_keys ⇒ Array<SshTresor::AgentKey>
Lists public keys available through the agent.
-
#sign(key, data) ⇒ String
Signs arbitrary data with an agent key and returns only the raw signature bytes from the SSH agent response.
Constructor Details
#initialize(socket) ⇒ Agent
Creates an agent client over an already-open 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.
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 |
.connect ⇒ SshTresor::Agent
Opens the SSH agent named by SSH_AUTH_SOCK.
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.}" end |
.format_key_type(blob) ⇒ String
Formats a public-key blob into a 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.
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.
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_key ⇒ SshTresor::AgentKey
Returns the first available key.
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_keys ⇒ Array<SshTresor::AgentKey>
Lists public keys available through the agent.
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.
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 |