Class: SSHKey

Inherits:
Object
  • Object
show all
Defined in:
lib/sshkey.rb,
lib/sshkey/version.rb

Defined Under Namespace

Classes: PublicKeyError

Constant Summary collapse

SSH_TYPES =
{
  "ssh-rsa" => "rsa",
  "ssh-dss" => "dsa",
  "ssh-ed25519" => "ed25519",
  "ecdsa-sha2-nistp256" => "ecdsa",
  "ecdsa-sha2-nistp384" => "ecdsa",
  "ecdsa-sha2-nistp521" => "ecdsa",
}
SSHFP_TYPES =
{
  "rsa"     => 1,
  "dsa"     => 2,
  "ecdsa"   => 3,
  "ed25519" => 4,
}
SSH_CONVERSION =
{"rsa" => ["e", "n"], "dsa" => ["p", "q", "g", "pub_key"]}
SSH2_LINE_LENGTH =

+1 (for line wrap ‘/’ character) must be <= 72

70
VERSION =
"2.0.0"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(private_key, options = {}) ⇒ SSHKey

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



242
243
244
245
246
247
248
249
250
251
252
253
# File 'lib/sshkey.rb', line 242

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

Instance Attribute Details

#commentObject

Returns the value of attribute comment.



231
232
233
# File 'lib/sshkey.rb', line 231

def comment
  @comment
end

#directivesObject

Returns the value of attribute directives.



386
387
388
# File 'lib/sshkey.rb', line 386

def directives
  @directives
end

#key_objectObject (readonly)

Returns the value of attribute key_object.



230
231
232
# File 'lib/sshkey.rb', line 230

def key_object
  @key_object
end

#passphraseObject

Returns the value of attribute passphrase.



231
232
233
# File 'lib/sshkey.rb', line 231

def passphrase
  @passphrase
end

#typeObject (readonly)

Returns the value of attribute type.



230
231
232
# File 'lib/sshkey.rb', line 230

def type
  @type
end

Class Method Details

.fingerprintObject

Fingerprints

Accepts either a public or private key

MD5 fingerprint for the given SSH key



103
104
105
106
107
108
109
# File 'lib/sshkey.rb', line 103

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

.format_sshfp_record(hostname, type, key) ⇒ Object



156
157
158
159
160
161
# File 'lib/sshkey.rb', line 156

def format_sshfp_record(hostname, type, key)
  [[Digest::SHA1, 1], [Digest::SHA256, 2]].map { |f, num|
    fpr = f.hexdigest(key)
    "#{hostname} IN SSHFP #{SSHFP_TYPES[type]} #{num} #{fpr}"
  }.join("\n")
end

.generate(options = {}) ⇒ Object

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



39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/sshkey.rb', line 39

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

.md5_fingerprint(key) ⇒ Object

Fingerprints

Accepts either a public or private key

MD5 fingerprint for the given SSH key



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

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

.sha1_fingerprint(key) ⇒ Object

SHA1 fingerprint for the given SSH key



106
107
108
109
110
111
112
# File 'lib/sshkey.rb', line 106

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

.sha256_fingerprint(key) ⇒ Object

SHA256 fingerprint for the given SSH key



115
116
117
118
119
120
121
# File 'lib/sshkey.rb', line 115

def sha256_fingerprint(key)
  if key.match(/PRIVATE/)
    new(key).sha256_fingerprint
  else
    Base64.encode64(Digest::SHA256.digest(decoded_key(key))).gsub("\n", "")
  end
end

.ssh_public_key_bits(ssh_public_key) ⇒ Object

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.…”



87
88
89
# File 'lib/sshkey.rb', line 87

def ssh_public_key_bits(ssh_public_key)
  unpacked_byte_array( *parse_ssh_public_key(ssh_public_key) ).last.num_bytes * 8
end

.ssh_public_key_to_ssh2_public_key(ssh_public_key, headers = nil) ⇒ Object

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

Raises:



139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/sshkey.rb', line 139

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

.sshfp(hostname, key) ⇒ Object

SSHFP records for the given SSH key



124
125
126
127
128
129
130
131
# File 'lib/sshkey.rb', line 124

def sshfp(hostname, key)
  if key.match(/PRIVATE/)
    new(key).sshfp hostname
  else
    type, encoded_key = parse_ssh_public_key(key)
    format_sshfp_record(hostname, SSH_TYPES[type], Base64.decode64(encoded_key))
  end
end

.valid_ssh_public_key?(ssh_public_key) ⇒ Boolean

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.…”

Returns:

  • (Boolean)


63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# File 'lib/sshkey.rb', line 63

def valid_ssh_public_key?(ssh_public_key)
  ssh_type, encoded_key = parse_ssh_public_key(ssh_public_key)
  sections = unpacked_byte_array(ssh_type, encoded_key)
  case ssh_type
    when "ssh-rsa", "ssh-dss"
      sections.size == SSH_CONVERSION[SSH_TYPES[ssh_type]].size
    when "ssh-ed25519"
      sections.size == 1                                # https://tools.ietf.org/id/draft-bjh21-ssh-ed25519-00.html#rfc.section.4
    when "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384", "ecdsa-sha2-nistp521"
      sections.size == 2                                # https://tools.ietf.org/html/rfc5656#section-3.1
    else
      false
  end
rescue
  false
end

Instance Method Details

#bitsObject

Determine the length (bits) of the key as an integer



318
319
320
# File 'lib/sshkey.rb', line 318

def bits
  self.class.ssh_public_key_bits(ssh_public_key)
end

#encrypted_private_keyObject

Fetch the encrypted RSA/DSA private key using the passphrase provided

If no passphrase is set, returns the unencrypted private key



267
268
269
270
# File 'lib/sshkey.rb', line 267

def encrypted_private_key
  return private_key unless passphrase
  key_object.to_pem(OpenSSL::Cipher.new("AES-128-CBC"), passphrase)
end

#md5_fingerprintObject Also known as: fingerprint

Fingerprints

MD5 fingerprint for the given SSH public key



302
303
304
# File 'lib/sshkey.rb', line 302

def md5_fingerprint
  Digest::MD5.hexdigest(ssh_public_key_conversion).gsub(/(.{2})(?=.)/, '\1:\2')
end

#private_keyObject Also known as: rsa_private_key, dsa_private_key

Fetch the RSA/DSA private key

rsa_private_key and dsa_private_key are aliased for backward compatibility



258
259
260
# File 'lib/sshkey.rb', line 258

def private_key
  key_object.to_pem
end

#public_keyObject Also known as: rsa_public_key, dsa_public_key

Fetch the RSA/DSA public key

rsa_public_key and dsa_public_key are aliased for backward compatibility



275
276
277
# File 'lib/sshkey.rb', line 275

def public_key
  key_object.public_key.to_pem
end

#randomartObject

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



339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
# File 'lib/sshkey.rb', line 339

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_fingerprintObject

SHA1 fingerprint for the given SSH public key



308
309
310
# File 'lib/sshkey.rb', line 308

def sha1_fingerprint
  Digest::SHA1.hexdigest(ssh_public_key_conversion).gsub(/(.{2})(?=.)/, '\1:\2')
end

#sha256_fingerprintObject

SHA256 fingerprint for the given SSH public key



313
314
315
# File 'lib/sshkey.rb', line 313

def sha256_fingerprint
  Base64.encode64(Digest::SHA256.digest(ssh_public_key_conversion)).gsub("\n", "")
end

#ssh2_public_key(headers = nil) ⇒ Object

SSH2 public key (RFC4716)

Parameters

  • headers<~Hash> - Keys will be used as header-tags and values as header-values.

Examples

=> ‘2048-bit RSA created by user@example’ => ‘Private Use Value’



295
296
297
# File 'lib/sshkey.rb', line 295

def ssh2_public_key(headers = nil)
  self.class.ssh_public_key_to_ssh2_public_key(ssh_public_key, headers)
end

#ssh_public_keyObject

SSH public key



282
283
284
# File 'lib/sshkey.rb', line 282

def ssh_public_key
  [directives.join(",").strip, SSH_TYPES.invert[type], Base64.encode64(ssh_public_key_conversion).gsub("\n", ""), comment].join(" ").strip
end

#sshfp(hostname) ⇒ Object



379
380
381
# File 'lib/sshkey.rb', line 379

def sshfp(hostname)
  self.class.format_sshfp_record(hostname, @type, ssh_public_key_conversion)
end