class SSHKey
Constants
- SSH2_LINE_LENGTH
- SSH_CONVERSION
- SSH_TYPES
- VERSION
Attributes
Public Class Methods
Generate a new keypair and return an SSHKey object
The default behavior when providing no options will generate a 2048-bit RSA keypair.
Parameters¶ ↑
-
options<~Hash>:
-
:type<~String> - “rsa” or “dsa”, “rsa” by default
-
:bits<~Integer> - Bit length
-
:comment<~String> - Comment to use for the public key, defaults to “”
-
:passphrase<~String> - Encrypt the key with this passphrase
-
# File lib/sshkey.rb, line 27 def generate(options = {}) type = options[:type] || "rsa" # JRuby modulus size must range from 512 to 1024 default_bits = type == "rsa" ? 2048 : 1024 bits = options[:bits] || default_bits cipher = OpenSSL::Cipher::Cipher.new("AES-128-CBC") if options[:passphrase] case type.downcase when "rsa" then new(OpenSSL::PKey::RSA.generate(bits).to_pem(cipher, options[:passphrase]), options) when "dsa" then new(OpenSSL::PKey::DSA.generate(bits).to_pem(cipher, options[:passphrase]), options) else raise "Unknown key type: #{type}" end end
Fingerprints
Accepts either a public or private key
MD5 fingerprint for the given SSH key
# File lib/sshkey.rb, line 74 def md5_fingerprint(key) if key.match(/PRIVATE/) new(key).md5_fingerprint else Digest::MD5.hexdigest(decoded_key(key)).gsub(fingerprint_regex, '\1:\2') end end
Create a new SSHKey object
Parameters¶ ↑
-
#private_key - Existing RSA or DSA private key
-
options<~Hash>
-
:comment<~String> - Comment to use for the public key, defaults to “”
-
:passphrase<~String> - If the key is encrypted, supply the passphrase
-
:directives<~Array> - Options prefixed to the public key
-
# File lib/sshkey.rb, line 184 def initialize(private_key, options = {}) @passphrase = options[:passphrase] @comment = options[:comment] || "" self.directives = options[:directives] || [] begin @key_object = OpenSSL::PKey::RSA.new(private_key, passphrase) @type = "rsa" rescue @key_object = OpenSSL::PKey::DSA.new(private_key, passphrase) @type = "dsa" end end
SHA1 fingerprint for the given SSH key
# File lib/sshkey.rb, line 84 def sha1_fingerprint(key) if key.match(/PRIVATE/) new(key).sha1_fingerprint else Digest::SHA1.hexdigest(decoded_key(key)).gsub(fingerprint_regex, '\1:\2') end end
Bits
Returns ssh public key bits or false depending on the validity of the public key provided
Parameters¶ ↑
-
#ssh_public_key<~String> - “ssh-rsa AAAAB3NzaC1yc2EA.…”
# File lib/sshkey.rb, line 65 def ssh_public_key_bits(ssh_public_key) unpacked_byte_array( *parse_ssh_public_key(ssh_public_key) ).last.num_bytes * 8 end
Convert an existing SSH public key to SSH2 (RFC4716) public key
Parameters¶ ↑
-
#ssh_public_key<~String> - “ssh-rsa AAAAB3NzaC1yc2EA.…”
-
headers<~Hash> - The Key will be used as the header-tag and the value as the header-value
# File lib/sshkey.rb, line 98 def ssh_public_key_to_ssh2_public_key(ssh_public_key, headers = nil) raise PublicKeyError, "invalid ssh public key" unless SSHKey.valid_ssh_public_key?(ssh_public_key) source_format, source_key = parse_ssh_public_key(ssh_public_key) # Add a 'Comment' Header Field unless others are explicitly passed in if source_comment = ssh_public_key.split(source_key)[1] headers = {'Comment' => source_comment.strip} if headers.nil? && !source_comment.empty? end header_fields = build_ssh2_headers(headers) ssh2_key = "---- BEGIN SSH2 PUBLIC KEY ----\n" ssh2_key << header_fields unless header_fields.nil? ssh2_key << source_key.scan(/.{1,#{SSH2_LINE_LENGTH}}/).join("\n") ssh2_key << "\n---- END SSH2 PUBLIC KEY ----" end
Validate an existing SSH public key
Returns true or false depending on the validity of the public key provided
Parameters¶ ↑
-
#ssh_public_key<~String> - “ssh-rsa AAAAB3NzaC1yc2EA.…”
# File lib/sshkey.rb, line 51 def valid_ssh_public_key?(ssh_public_key) ssh_type, encoded_key = parse_ssh_public_key(ssh_public_key) SSH_CONVERSION[SSH_TYPES.invert[ssh_type]].size == unpacked_byte_array(ssh_type, encoded_key).size rescue false end
Private Class Methods
# File lib/sshkey.rb, line 157 def build_ssh2_headers(headers = {}) return nil if headers.nil? || headers.empty? headers.keys.sort.collect do |header_tag| # header-tag must be us-ascii & <= 64 bytes and header-data must be UTF-8 & <= 1024 bytes raise PublicKeyError, "SSH2 header-tag '#{header_tag}' must be US-ASCII" unless header_tag.each_byte.all? {|b| b < 128 } raise PublicKeyError, "SSH2 header-tag '#{header_tag}' must be <= 64 bytes" unless header_tag.size <= 64 raise PublicKeyError, "SSH2 header-value for '#{header_tag}' must be <= 1024 bytes" unless headers[header_tag].size <= 1024 header_field = "#{header_tag}: #{headers[header_tag]}" header_field.scan(/.{1,#{SSH2_LINE_LENGTH}}/).join("\\\n") end.join("\n") << "\n" end
# File lib/sshkey.rb, line 139 def decoded_key(key) Base64.decode64(parse_ssh_public_key(key).last) end
# File lib/sshkey.rb, line 143 def fingerprint_regex /(.{2})(?=.)/ end
# File lib/sshkey.rb, line 147 def parse_ssh_public_key(public_key) raise PublicKeyError, "newlines are not permitted between key data" if public_key =~ /\n(?!$)/ parsed = public_key.split(" ") parsed.each_with_index do |el, index| return parsed[index..(index+1)] if SSH_TYPES.invert[el] end raise PublicKeyError, "cannot determine key type" end
# File lib/sshkey.rb, line 117 def unpacked_byte_array(ssh_type, encoded_key) prefix = [7].pack("N") + ssh_type decoded = Base64.decode64(encoded_key) # Base64 decoding is too permissive, so we should validate if encoding is correct unless Base64.encode64(decoded).gsub("\n", "") == encoded_key && decoded.slice!(0, prefix.length) == prefix raise PublicKeyError, "validation error" end data = [] until decoded.empty? front = decoded.slice!(0,4) size = front.unpack("N").first segment = decoded.slice!(0, size) unless front.length == 4 && segment.length == size raise PublicKeyError, "byte array too short" end data << OpenSSL::BN.new(segment, 2) end return data end
Public Instance Methods
Determine the length (bits) of the key as an integer
# File lib/sshkey.rb, line 255 def bits self.class.ssh_public_key_bits(ssh_public_key) end
# File lib/sshkey.rb, line 316 def directives=(directives) @directives = Array[directives].flatten.compact end
Fetch the encrypted RSA/DSA private key using the passphrase provided
If no passphrase is set, returns the unencrypted private key
# File lib/sshkey.rb, line 209 def encrypted_private_key return private_key unless passphrase key_object.to_pem(OpenSSL::Cipher::Cipher.new("AES-128-CBC"), passphrase) end
Fingerprints
MD5 fingerprint for the given SSH public key
# File lib/sshkey.rb, line 244 def md5_fingerprint Digest::MD5.hexdigest(ssh_public_key_conversion).gsub(/(.{2})(?=.)/, '\1:\2') end
Fetch the RSA/DSA private key
#rsa_private_key and #dsa_private_key are aliased for backward compatibility
# File lib/sshkey.rb, line 200 def private_key key_object.to_pem end
Fetch the RSA/DSA public key
#rsa_public_key and #dsa_public_key are aliased for backward compatibility
# File lib/sshkey.rb, line 217 def public_key key_object.public_key.to_pem end
Randomart
Generate OpenSSH compatible ASCII art fingerprints See www.opensource.apple.com/source/OpenSSH/OpenSSH-175/openssh/key.c (key_fingerprint_randomart function)
Example: +–[ RSA 2048]—-+ |o+ o.. | |..+.o | | ooo
| |.++. o | |o
+ S | |.. + o . | |
. + . | | . . | | Eo. |
-----------------
# File lib/sshkey.rb, line 276 def randomart fieldsize_x = 17 fieldsize_y = 9 x = fieldsize_x / 2 y = fieldsize_y / 2 raw_digest = Digest::MD5.digest(ssh_public_key_conversion) num_bytes = raw_digest.bytesize field = Array.new(fieldsize_x) { Array.new(fieldsize_y) {0} } raw_digest.bytes.each do |byte| 4.times do x += (byte & 0x1 != 0) ? 1 : -1 y += (byte & 0x2 != 0) ? 1 : -1 x = [[x, 0].max, fieldsize_x - 1].min y = [[y, 0].max, fieldsize_y - 1].min field[x][y] += 1 if (field[x][y] < num_bytes - 2) byte >>= 2 end end field[fieldsize_x / 2][fieldsize_y / 2] = num_bytes - 1 field[x][y] = num_bytes augmentation_string = " .o+=*BOX@%&#/^SE" output = "+--#{sprintf("[%4s %4u]", type.upcase, bits)}----+\n" fieldsize_y.times do |y| output << "|" fieldsize_x.times do |x| output << augmentation_string[[field[x][y], num_bytes].min] end output << "|" output << "\n" end output << "+#{"-" * fieldsize_x}+" output end
SHA1 fingerprint for the given SSH public key
# File lib/sshkey.rb, line 250 def sha1_fingerprint Digest::SHA1.hexdigest(ssh_public_key_conversion).gsub(/(.{2})(?=.)/, '\1:\2') end
SSH2 public key (RFC4716)
Parameters¶ ↑
-
headers<~Hash> - Keys will be used as header-tags and values as header-values.
Examples¶ ↑
{'Comment' => '2048-bit RSA created by user@example'} {'x-private-use-tag' => 'Private Use Value'}
# File lib/sshkey.rb, line 237 def ssh2_public_key(headers = nil) self.class.ssh_public_key_to_ssh2_public_key(ssh_public_key, headers) end
SSH public key
# File lib/sshkey.rb, line 224 def ssh_public_key [directives.join(",").strip, SSH_TYPES[type], Base64.encode64(ssh_public_key_conversion).gsub("\n", ""), comment].join(" ").strip end
Private Instance Methods
For instance, the “ssh-rsa” string is encoded as the following byte array
- 0, 0, 0, 7, 's', 's', 'h', '-', 'r', 's', 'a'
# File lib/sshkey.rb, line 332 def ssh_public_key_conversion typestr = SSH_TYPES[type] methods = SSH_CONVERSION[type] pubkey = key_object.public_key methods.inject([7].pack("N") + typestr) do |pubkeystr, m| # Given pubkey.class == OpenSSL::BN, pubkey.to_s(0) returns an MPI # formatted string (length prefixed bytes). This is not supported by # JRuby, so we still have to deal with length and data separately. val = pubkey.send(m) # Get byte-representation of absolute value of val data = val.to_s(2) first_byte = data[0,1].unpack("c").first if val < 0 # For negative values, highest bit must be set data[0] = [0x80 & first_byte].pack("c") elsif first_byte < 0 # For positive values where highest bit would be set, prefix with \0 data = "\0" + data end pubkeystr + [data.length].pack("N") + data end end