Class: SSHKey
- Inherits:
-
Object
- Object
- SSHKey
- Defined in:
- lib/sshkey/version.rb,
lib/sshkey.rb
Defined Under Namespace
Classes: PublicKeyError
Constant Summary collapse
- VERSION =
"3.0.0"
- 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, }
- ECDSA_CURVES =
{ 256 => "prime256v1", # https://stackoverflow.com/a/41953717 384 => "secp384r1", 521 => "secp521r1", }
- VALID_BITS =
{ "ecdsa" => ECDSA_CURVES.keys, }
- SSH_CONVERSION =
Accessor methods are defined in:
-
RSA: github.com/ruby/openssl/blob/master/ext/openssl/ossl_pkey_rsa.c
-
DSA: github.com/ruby/openssl/blob/master/ext/openssl/ossl_pkey_dsa.c
-
ECDSA: monkey patch OpenSSL::PKey::EC above
-
{"rsa" => ["e", "n"], "dsa" => ["p", "q", "g", "pub_key"], "ecdsa" => ["identifier", "q"]}
- SSH2_LINE_LENGTH =
+1 (for line wrap ‘/’ character) must be <= 72
70
Instance Attribute Summary collapse
-
#comment ⇒ Object
Returns the value of attribute comment.
-
#directives ⇒ Object
Returns the value of attribute directives.
-
#key_object ⇒ Object
readonly
Returns the value of attribute key_object.
-
#passphrase ⇒ Object
Returns the value of attribute passphrase.
-
#type ⇒ Object
readonly
Returns the value of attribute type.
-
#typestr ⇒ Object
readonly
Returns the value of attribute typestr.
Class Method Summary collapse
-
.fingerprint ⇒ Object
Fingerprints.
- .format_sshfp_record(hostname, type, key) ⇒ Object
-
.generate(options = {}) ⇒ Object
Generate a new keypair and return an SSHKey object.
-
.md5_fingerprint(key) ⇒ Object
Fingerprints.
-
.sha1_fingerprint(key) ⇒ Object
SHA1 fingerprint for the given SSH key.
-
.sha256_fingerprint(key) ⇒ Object
SHA256 fingerprint for the given SSH key.
-
.ssh_public_key_bits(ssh_public_key) ⇒ Object
Bits.
-
.ssh_public_key_to_ssh2_public_key(ssh_public_key, headers = nil) ⇒ Object
Convert an existing SSH public key to SSH2 (RFC4716) public key.
-
.sshfp(hostname, key) ⇒ Object
SSHFP records for the given SSH key.
-
.valid_ssh_public_key?(ssh_public_key) ⇒ Boolean
Validate an existing SSH public key.
Instance Method Summary collapse
-
#bits ⇒ Object
Determine the length (bits) of the key as an integer.
-
#encrypted_private_key ⇒ Object
Fetch the encrypted RSA/DSA private key using the passphrase provided.
-
#initialize(private_key, options = {}) ⇒ SSHKey
constructor
Create a new SSHKey object.
-
#md5_fingerprint ⇒ Object
(also: #fingerprint)
Fingerprints.
-
#private_key ⇒ Object
(also: #rsa_private_key, #dsa_private_key)
Fetch the private key (PEM format).
-
#public_key ⇒ Object
(also: #rsa_public_key, #dsa_public_key)
Fetch the public key (PEM format).
- #public_key_object ⇒ Object
-
#randomart(dgst_alg = "MD5") ⇒ Object
Randomart.
-
#sha1_fingerprint ⇒ Object
SHA1 fingerprint for the given SSH public key.
-
#sha256_fingerprint ⇒ Object
SHA256 fingerprint for the given SSH public key.
-
#ssh2_public_key(headers = nil) ⇒ Object
SSH2 public key (RFC4716).
-
#ssh_public_key ⇒ Object
SSH public key.
- #sshfp(hostname) ⇒ Object
Constructor Details
#initialize(private_key, options = {}) ⇒ SSHKey
Create a new SSHKey object
Parameters
-
private_key - Existing RSA or DSA or ECDSA 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
-
378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 |
# File 'lib/sshkey.rb', line 378 def initialize(private_key, = {}) @passphrase = [:passphrase] @comment = [:comment] || "" self.directives = [:directives] || [] begin @key_object = OpenSSL::PKey::RSA.new(private_key, passphrase) @type = "rsa" @typestr = "ssh-rsa" rescue OpenSSL::PKey::RSAError @type = nil end return if @type begin @key_object = OpenSSL::PKey::DSA.new(private_key, passphrase) @type = "dsa" @typestr = "ssh-dss" rescue OpenSSL::PKey::DSAError @type = nil end return if @type @key_object = OpenSSL::PKey::EC.new(private_key, passphrase) @type = "ecdsa" bits = ECDSA_CURVES.invert[@key_object.group.curve_name] @typestr = "ecdsa-sha2-nistp#{bits}" end |
Instance Attribute Details
#comment ⇒ Object
Returns the value of attribute comment.
367 368 369 |
# File 'lib/sshkey.rb', line 367 def comment @comment end |
#directives ⇒ Object
Returns the value of attribute directives.
617 618 619 |
# File 'lib/sshkey.rb', line 617 def directives @directives end |
#key_object ⇒ Object (readonly)
Returns the value of attribute key_object.
366 367 368 |
# File 'lib/sshkey.rb', line 366 def key_object @key_object end |
#passphrase ⇒ Object
Returns the value of attribute passphrase.
367 368 369 |
# File 'lib/sshkey.rb', line 367 def passphrase @passphrase end |
#type ⇒ Object (readonly)
Returns the value of attribute type.
366 367 368 |
# File 'lib/sshkey.rb', line 366 def type @type end |
#typestr ⇒ Object (readonly)
Returns the value of attribute typestr.
366 367 368 |
# File 'lib/sshkey.rb', line 366 def typestr @typestr end |
Class Method Details
.fingerprint ⇒ Object
Fingerprints
Accepts either a public or private key
MD5 fingerprint for the given SSH key
193 194 195 196 197 198 199 |
# File 'lib/sshkey.rb', line 193 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
246 247 248 249 250 251 |
# File 'lib/sshkey.rb', line 246 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
-
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 |
# File 'lib/sshkey.rb', line 84 def generate( = {}) type = [:type] || "rsa" # JRuby modulus size must range from 512 to 1024 case type when "rsa" then default_bits = 2048 when "ecdsa" then default_bits = 256 else default_bits = 1024 end bits = [:bits] || default_bits cipher = OpenSSL::Cipher.new("AES-128-CBC") if [:passphrase] raise "Bits must either: #{VALID_BITS[type.downcase].join(', ')}" unless VALID_BITS[type.downcase].nil? || VALID_BITS[type.downcase].include?(bits) case type.downcase when "rsa" key_object = OpenSSL::PKey::RSA.generate(bits) when "dsa" key_object = OpenSSL::PKey::DSA.generate(bits) when "ecdsa" # jruby-openssl OpenSSL::PKey::EC support isn't complete # https://github.com/jruby/jruby-openssl/issues/189 jruby_not_implemented("OpenSSL::PKey::EC is not fully implemented") if OpenSSL::OPENSSL_VERSION_NUMBER >= 0x30000000 # https://github.com/ruby/openssl/pull/480 key_object = OpenSSL::PKey::EC.generate(ECDSA_CURVES[bits]) else key_pkey = OpenSSL::PKey::EC.new(ECDSA_CURVES[bits]) key_object = key_pkey.generate_key end else raise "Unknown key type: #{type}" end key_pem = key_object.to_pem(cipher, [:passphrase]) new(key_pem, ) end |
.md5_fingerprint(key) ⇒ Object
Fingerprints
Accepts either a public or private key
MD5 fingerprint for the given SSH key
186 187 188 189 190 191 192 |
# File 'lib/sshkey.rb', line 186 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
196 197 198 199 200 201 202 |
# File 'lib/sshkey.rb', line 196 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
205 206 207 208 209 210 211 |
# File 'lib/sshkey.rb', line 205 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.…”
-
ssh_public_key<~String> - “ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTY.…”
160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 |
# File 'lib/sshkey.rb', line 160 def ssh_public_key_bits(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", "ssh-ed25519" sections.last.num_bytes * 8 when "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384", "ecdsa-sha2-nistp521" raise PublicKeyError, "invalid ECDSA key" unless sections.count == 2 # https://tools.ietf.org/html/rfc5656#section-3.1 identifier = sections[0].to_s(2) q = sections[1].to_s(2) ecdsa_bits(ssh_type, identifier, q) else raise PublicKeyError, "unsupported key type #{ssh_type}" end 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
229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 |
# File 'lib/sshkey.rb', line 229 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
214 215 216 217 218 219 220 221 |
# File 'lib/sshkey.rb', line 214 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.…”
135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 |
# File 'lib/sshkey.rb', line 135 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
#bits ⇒ Object
Determine the length (bits) of the key as an integer
531 532 533 |
# File 'lib/sshkey.rb', line 531 def bits self.class.ssh_public_key_bits(ssh_public_key) end |
#encrypted_private_key ⇒ Object
Fetch the encrypted RSA/DSA private key using the passphrase provided
If no passphrase is set, returns the unencrypted private key
425 426 427 428 |
# File 'lib/sshkey.rb', line 425 def encrypted_private_key return private_key unless passphrase key_object.to_pem(OpenSSL::Cipher.new("AES-128-CBC"), passphrase) end |
#md5_fingerprint ⇒ Object Also known as: fingerprint
Fingerprints
MD5 fingerprint for the given SSH public key
515 516 517 |
# File 'lib/sshkey.rb', line 515 def md5_fingerprint Digest::MD5.hexdigest(ssh_public_key_conversion).gsub(/(.{2})(?=.)/, '\1:\2') end |
#private_key ⇒ Object Also known as: rsa_private_key, dsa_private_key
Fetch the private key (PEM format)
rsa_private_key and dsa_private_key are aliased for backward compatibility
412 413 414 415 416 417 418 |
# File 'lib/sshkey.rb', line 412 def private_key # jruby-openssl OpenSSL::PKey::EC support isn't complete # https://github.com/jruby/jruby-openssl/issues/189 jruby_not_implemented("OpenSSL::PKey::EC is not fully implemented") if type == "ecdsa" key_object.to_pem end |
#public_key ⇒ Object Also known as: rsa_public_key, dsa_public_key
Fetch the public key (PEM format)
rsa_public_key and dsa_public_key are aliased for backward compatibility
433 434 435 |
# File 'lib/sshkey.rb', line 433 def public_key public_key_object.to_pem end |
#public_key_object ⇒ Object
439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 |
# File 'lib/sshkey.rb', line 439 def public_key_object if type == "ecdsa" return nil unless key_object return nil unless key_object.group if OpenSSL::OPENSSL_VERSION_NUMBER >= 0x30000000 && RUBY_PLATFORM != "java" # jruby-openssl does not currently support point_conversion_form # (futureproofing for if/when JRuby requires this technique to determine public key) jruby_not_implemented("point_conversion_form is not implemented") # Avoid "OpenSSL::PKey::PKeyError: pkeys are immutable on OpenSSL 3.0" # https://github.com/ruby/openssl/blob/master/History.md#version-300 # https://github.com/ruby/openssl/issues/498 # https://github.com/net-ssh/net-ssh/commit/4de6831dea4e922bf3052192eec143af015a3486 # https://github.com/ClearlyClaire/cose-ruby/commit/28ee497fa7d9d49e72d5a5e97a567c0b58fdd822 curve_name = key_object.group.curve_name return nil unless curve_name # Map to different curve_name for JRuby # (futureproofing for if/when JRuby requires this technique to determine public key) # https://github.com/jwt/ruby-jwt/issues/362#issuecomment-722938409 curve_name = "prime256v1" if curve_name == "secp256r1" && RUBY_PLATFORM == "java" # Construct public key OpenSSL::PKey::EC from OpenSSL::PKey::EC::Point public_key_point = key_object.public_key # => OpenSSL::PKey::EC::Point return nil unless public_key_point asn1 = OpenSSL::ASN1::Sequence( [ OpenSSL::ASN1::Sequence( [ OpenSSL::ASN1::ObjectId("id-ecPublicKey"), OpenSSL::ASN1::ObjectId(curve_name) ] ), OpenSSL::ASN1::BitString(public_key_point.to_octet_string(key_object.group.point_conversion_form)) ] ) pub = OpenSSL::PKey::EC.new(asn1.to_der) pub else pub = OpenSSL::PKey::EC.new(key_object.group) pub.public_key = key_object.public_key pub end else key_object.public_key end end |
#randomart(dgst_alg = "MD5") ⇒ Object
Randomart
Generate OpenSSH compatible ASCII art fingerprints See www.opensource.apple.com/source/OpenSSH/OpenSSH-175/openssh/key.c (key_fingerprint_randomart function) or mirrors.mit.edu/pub/OpenBSD/OpenSSH/ (sshkey.c fingerprint_randomart function)
Example: –[ RSA 2048]—- |o+ o.. | |..+.o | | ooo | |.++. o | |o
+ S | |.. + o . | | . + . | | . . | | Eo. | -----------------
553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 |
# File 'lib/sshkey.rb', line 553 def randomart(dgst_alg = "MD5") fieldsize_x = 17 fieldsize_y = 9 x = fieldsize_x / 2 y = fieldsize_y / 2 case dgst_alg when "MD5" then raw_digest = Digest::MD5.digest(ssh_public_key_conversion) when "SHA256" then raw_digest = Digest::SHA2.new(256).digest(ssh_public_key_conversion) when "SHA384" then raw_digest = Digest::SHA2.new(384).digest(ssh_public_key_conversion) when "SHA512" then raw_digest = Digest::SHA2.new(512).digest(ssh_public_key_conversion) else raise "Unknown digest algorithm: #{digest}" end augmentation_string = " .o+=*BOX@%&#/^SE" len = augmentation_string.length - 1 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] < len - 2) byte >>= 2 end end fieldsize_x_halved = fieldsize_x / 2 fieldsize_y_halved = fieldsize_y / 2 field[fieldsize_x_halved][fieldsize_y_halved] = len - 1 field[x][y] = len type_name_length_max = 4 # Note: this will need to be extended to accomodate ed25519 bits_number_length_max = (bits < 1000 ? 3 : 4) formatstr = "[%#{type_name_length_max}s %#{bits_number_length_max}u]" output = "+--#{sprintf(formatstr, type.upcase, bits)}----+\n" fieldsize_y.times do |y| output << "|" fieldsize_x.times do |x| output << augmentation_string[[field[x][y], len].min] end output << "|" output << "\n" end output << "+#{"-" * fieldsize_x}+" output end |
#sha1_fingerprint ⇒ Object
SHA1 fingerprint for the given SSH public key
521 522 523 |
# File 'lib/sshkey.rb', line 521 def sha1_fingerprint Digest::SHA1.hexdigest(ssh_public_key_conversion).gsub(/(.{2})(?=.)/, '\1:\2') end |
#sha256_fingerprint ⇒ Object
SHA256 fingerprint for the given SSH public key
526 527 528 |
# File 'lib/sshkey.rb', line 526 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’
508 509 510 |
# File 'lib/sshkey.rb', line 508 def ssh2_public_key(headers = nil) self.class.ssh_public_key_to_ssh2_public_key(ssh_public_key, headers) end |
#ssh_public_key ⇒ Object
SSH public key
495 496 497 |
# File 'lib/sshkey.rb', line 495 def ssh_public_key [directives.join(",").strip, typestr, Base64.encode64(ssh_public_key_conversion).gsub("\n", ""), comment].join(" ").strip end |
#sshfp(hostname) ⇒ Object
610 611 612 |
# File 'lib/sshkey.rb', line 610 def sshfp(hostname) self.class.format_sshfp_record(hostname, @type, ssh_public_key_conversion) end |