├── .github └── workflows │ └── ci.yml ├── .gitignore ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── lib ├── sshkey.rb └── sshkey │ └── version.rb ├── sshkey.gemspec └── test └── sshkey_test.rb /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - '**' 7 | pull_request: 8 | branches: 9 | - '**' 10 | 11 | permissions: 12 | contents: read 13 | 14 | jobs: 15 | test: 16 | runs-on: ubuntu-latest 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | ruby: ["2.5", "2.6", "2.7", "3.0", "3.1", "3.2", "3.3", "head", jruby-9.3, jruby-9.4] 21 | steps: 22 | - uses: actions/checkout@v4 23 | - uses: ruby/setup-ruby@v1 24 | with: 25 | bundler-cache: true 26 | ruby-version: ${{ matrix.ruby }} 27 | - run: bundle exec rake 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .bundle 3 | Gemfile.lock 4 | pkg/* 5 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "jruby-openssl", ">= 0.8.2", platform: :jruby 4 | 5 | gemspec 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011-2023 James Miller 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SSHKey 2 | 3 | Generate private and public SSH keys (RSA, DSA, and ECDSA supported) using pure Ruby. 4 | 5 | ## Requirements 6 | 7 | Tested / supported on CRuby 2.5+ and JRuby. 8 | 9 | ## Installation 10 | 11 | gem install sshkey 12 | 13 | ## Usage 14 | 15 | ### Generate a new key 16 | 17 | When generating a new keypair the default key type is 2048-bit RSA, but you can supply the `type` (RSA or DSA or ECDSA) and `bits` in the options. 18 | You can also (optionally) supply a `comment` or `passphrase`. 19 | 20 | ```ruby 21 | k = SSHKey.generate 22 | 23 | k = SSHKey.generate( 24 | type: "DSA", 25 | bits: 1024, 26 | comment: "foo@bar.com", 27 | passphrase: "foobar" 28 | ) 29 | ``` 30 | 31 | ### Use your existing key 32 | 33 | Return an SSHKey object from an existing RSA or DSA or ECDSA private key (provided as a string in PEM format). 34 | 35 | ```ruby 36 | f = File.read(File.expand_path("~/.ssh/id_rsa")) 37 | k = SSHKey.new(f, comment: "foo@bar.com") 38 | ``` 39 | 40 | If your existing key is in the OpenSSH format (starts with `-----BEGIN OPENSSH PRIVATE KEY-----`), you'll need to convert it to PEM format or generate a new key. 41 | 42 | Generate a new RSA key in PEM format with `ssh-keygen`: 43 | 44 | ``` 45 | ssh-keygen -t rsa -b 4096 -m PEM 46 | ``` 47 | 48 | Or convert an existing OpenSSH-formatted key with the following. This will modify the existing private key file. 49 | 50 | ``` 51 | ssh-keygen -p -N "" -m pem -f /path/to/existing/private/key 52 | ``` 53 | 54 | ### The SSHKey object 55 | 56 | #### Private and public keys 57 | 58 | Fetch the private and public keys as strings. Note that the `public_key` is the RSA or DSA or ECDSA public key, not an SSH public key. 59 | 60 | ```ruby 61 | k.private_key 62 | # => "-----BEGIN RSA PRIVATE KEY-----\nMIIEoAIBAAKCAQEAvR7l72CT7UBP6P+02Iut8gKKbKyekz/pQxnckPp1VafuaIwC\nMvYfP4ffVJTcY5IhU9mISNxZf6YDQ0TuD1aOrZYG9wsIgGY0nXhOUZxe/Q5I+V7D\nOI/hSzKF7W0cNCvaJPUSo8+soCLNSQ5mjnV3sRZ6uJwGFN30i1GulqHHKkx3vGxb\niaAL9YG58dPSbPGHFTA/epqUyd1fzCuWHyL9dHW7aw4RroNyEtVdiftAQfaK20I2\nueeDfuEtCPaxQYFQqbz5kKnXQx3fwHRpC7/84xHxsrY576evGxHw4p5EJD37scNN\ncneTG3Ly79/VVSAlrSm6ltutx0+S70scCqK0ewIDAQABAoH/MjwC15LPuDVdBIbn\ngp2XlrEWE8fGV1ainzA/ZkMg55+ztBF8hAzcQAPXTqA76jbmo18k1DWzkDSIqVWl\n5m0XeQRg1T4ZBAIh97H9G7BtispAl/yT3nJZZaAF8wsIctMzHp36VYjUUbTs0nsA\nwtZw9JkEAAVxmBlc26TWuyw9uv4fYXuR+uOsWH8jTTVPvxM9FaCCdK+dOMnswm7Y\nlOAlJj5dANkB2KPwIeE461ThyMo9GHEjpsvciMhKLuBoTSucNkhdgapAmYTSI+/1\nf1cA/KEdCMs9ANr1HFujeS01+N1Xrw/yW6EazaDN1oFHCVORtlB295Eac0Wq6y/P\npf1BAoGBAPIw4HQWsolU3f4FdIvc2POAcSJDRgt++I9Qt/QXq1SJ2dGKIveFiJgo\nZjCfHQFVZ8xl64cLzQ1WagZA1JBbbk9g5RxHDxRv7q+Kn3ogugDo9GUoQvpuuAU6\nXHoR/mLinDorJUnttL3U49xTMfrrut4qkUg+daBVptPtylpio6EDAoGBAMfnYq08\nfd/cPEQ2XPeswgtzXsKNLqA6UXBM7ZauKaFLByjy8peMMF6JPOYlBKQif5k+Egmu\nWIe8oTm8Nn5Ymt32bEd+MkHUC7kFzQeiXnM3u0oKzJMXLAvjSTs296g50YM5zJTC\nl64ACQmQOLZ9tdKorl52ZcmdbBEcZ2uwRvkpAoGAKhs5SrWPgLTSi5FjO9W/mkYg\nZTaQ/PqsOC5ubO+Yh/AXgIiln6cFon6Tlax0HIE+tJibpDT3B3SYplGrIxXiTcao\nzovEIWd8deSB6Xe7HuFhbBzd2DBbqf0FiuuJ8KM5ShuqNfovzDkxDGMic198c5eu\n/oJtbNy3Tm0vGxu/GwUCgYAgmRPXShkAq0pMmUzZups+AMdAFIO47ymelXzc6HOz\ncKevPsbefabZk6mRohG6rkF+fMe2Om8HW3QzFQUR32MJtQh9NA//+hMbTd3cU9bx\nFPJ+pXostkehfKPReyoxjZQjwQYicAUKA8l1fMYyxBclTgp5Lvd0RC5+L9KRlgJM\n2QKBgGVIWRNVpGg38dDqdq/4ue1BoTFhqoMGi6WQm3xa+NH+lyJGacdUhGRz8PxN\nhVKpIj8ljg2Rq/CA9qSgL/Z9rhn8QUMWULuAroCp0S2pMBtZ2RB+Mg2FdVFR9/Ft\nfG7co6mKUGkFPtr48EMfeKY88BRsp3yGOsROGdDsCHItjOVH\n-----END RSA PRIVATE KEY-----\n" 63 | 64 | k.public_key 65 | # => "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvR7l72CT7UBP6P+02Iut\n8gKKbKyekz/pQxnckPp1VafuaIwCMvYfP4ffVJTcY5IhU9mISNxZf6YDQ0TuD1aO\nrZYG9wsIgGY0nXhOUZxe/Q5I+V7DOI/hSzKF7W0cNCvaJPUSo8+soCLNSQ5mjnV3\nsRZ6uJwGFN30i1GulqHHKkx3vGxbiaAL9YG58dPSbPGHFTA/epqUyd1fzCuWHyL9\ndHW7aw4RroNyEtVdiftAQfaK20I2ueeDfuEtCPaxQYFQqbz5kKnXQx3fwHRpC7/8\n4xHxsrY576evGxHw4p5EJD37scNNcneTG3Ly79/VVSAlrSm6ltutx0+S70scCqK0\newIDAQAB\n-----END PUBLIC KEY-----\n" 66 | ``` 67 | 68 | Fetch the SSH public key as a string. 69 | 70 | ```ruby 71 | k.ssh_public_key 72 | # => "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC9HuXvYJPtQE/o/7TYi63yAopsrJ6TP+lDGdyQ+nVVp+5ojAIy9h8/h99UlNxjkiFT2YhI3Fl/pgNDRO4PVo6tlgb3CwiAZjSdeE5RnF79Dkj5XsM4j+FLMoXtbRw0K9ok9RKjz6ygIs1JDmaOdXexFnq4nAYU3fSLUa6WoccqTHe8bFuJoAv1gbnx09Js8YcVMD96mpTJ3V/MK5YfIv10dbtrDhGug3IS1V2J+0BB9orbQja554N+4S0I9rFBgVCpvPmQqddDHd/AdGkLv/zjEfGytjnvp68bEfDinkQkPfuxw01yd5MbcvLv39VVICWtKbqW263HT5LvSxwKorR7" 73 | ``` 74 | 75 | #### Encryption 76 | 77 | If a passphrase is set when a key is generated or by setting the `passphrase` accessor, you can 78 | fetch the encrypted version of the private key. 79 | 80 | ```ruby 81 | k.passphrase = "foo" 82 | # => "foo" 83 | 84 | k.encrypted_private_key 85 | # => "-----BEGIN RSA PRIVATE KEY-----\nProc-Type: 4,ENCRYPTED\nDEK-Info: AES-128-CBC,748B766CFB185C3BD1D7E4D31113EBDA\n\ntWbfOuAjBlSZdq3kdJTLZJ7prjNWOKuGpeesNfVZDziIaZNCUakvgnUFdX3IZZnj\nEYITfjZ1TEUY3EkemL/57txiP3A4iOMDK2JGg8lp3G45x6c9XucJ2YxgvMye/ugP\n014MzLvBNunWq8TolkFj4gbc+WCqsyFqGdpRsf/hx7PcLDd2nvS5zxjBAPno87KN\nYgEnZYrpyl01ePucwFVWlrlGJdc0+F+0Ms5gpjMds56YL3Rwv9BlWzapVtrqN29r\nZg0otylPAyuGJOQ8srDOa+pbSySXvcdoKfR6xQ9fIB0tUfGgrH3c5O0/rEW7FSiO\n6ng4ntXXOKKkQfCezXQVvqMjKtKAbcKaPYAvrB2Gp2VIPUN5tN52nKuWvQWPA0P/\nm/uKiFkvzDWj8xMEOdzDAG9/7ysX+T5angvhfT23+NEdGIlPZLDRHI3f+2Itn99f\nvVoDYUXiyd5h7VwOTn6scebbvyPY8DiWpB/5iaU8WBPr7TVTl9n2z+Gmy4eg3wS0\nTU4hGlKv7MiITO2+dOCZTVrKn9/gTgmtyiLucb4huBH88Nsj4zWnTrVjMMBWsTUD\nkzvo9081zgDKKeawcbZYdI1Tc4epV7SMTHpx1ztzIlPdQ6kRaWomwMSarQeSlhJe\naFx67cde6M3Kc3LOgE0VT+3NvVLnkDwkytwnQKLd6oT3d1kFxWXjMwqiPbSzz3bf\nkOhG01gsJDXIzAgDlOlhE+Qlsd3yc734UIH98rTFMVB00HS36WLuz3hh+Ew4rsrf\nDIuRIdxL/4GVdQ8J5WpSoN0tF5iQD1wpEMU2vUjYjj9TZkhpOpnK3UVvbKd4WPsV\n956XJT7ZDvX4+pvHc5GJq/UX5h42kycY0hftUoLapXt5Nhb/fL8mUT8Eix184uiO\n5mA3fgRP3oGJ28N653X/+kL2YhXCeTd2VjkVhKruuoex96Igyt8W7wW5y7MOPezf\nwfo8IzidcJcDR1W4OEOXr+oDlCE1CLGCzmenR+AUIisqz45yb5G076l8PQkI3NWC\nBhT1YbTds4QzrndIDZgMm65ZCaklm+FVHWV61rXd9rlugcq+flQuXAE/EnFtySMc\n3lztrzXulLXzgLrYG355JbQFddwehO7LdxKZA9LHC9/odcoVI9RBj1CzshYtlftR\nn56nxPTIxRTVjQdgCZ6VcjZhwv1I904NtGm4SZupiShXsbHzAfaeJ54GMq4PRlgN\nmH7JrI9/puBb1dLD0XNgPtmYIo18v9e7g9o+un/wDtxCTxhQtD0npPo1IuW4cW7q\n07lZPwGkN2FD2PNTBGXeQ6/EXTHxlyFn62GSr+DmXu0O8MJS827Vd4b8QmKzRTxf\nFEmtVhiD15KlrQxwajmhqfY6KHRxbBuG/w7ioRr2Vl0G9NmKwmJkQO8dM+mJ8rVE\nsWvm8xVm1bowahzDVPnyFUUjuGNi6jFElkv8zvlQUoTcjSZHPrQSHuX742f5Spph\nLLCHdGZ2Ry8UGPlqKtvd6V/z25NsBgbuit+hNkBsdIztH7MVGAhKSMgk1FgXmKzV\nmZnPigq5WAHtIvojzI9NfZxU2Avif0yymXNtOnipw0sCJ0notN8NuGdQEmyxThqW\n-----END RSA PRIVATE KEY-----\n" 86 | ``` 87 | 88 | #### Comments 89 | 90 | Keys can optionally have a comment that is shown as part of the public SSH key. Get or 91 | set the key's comment with the `comment` accessor. 92 | 93 | ```ruby 94 | k.comment 95 | # => nil 96 | 97 | k.comment = "me@me.com" 98 | # => "me@me.com" 99 | 100 | k.ssh_public_key 101 | # => "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC9HuXvYJPtQE/o/7TYi63yAopsrJ6TP+lDGdyQ+nVVp+5ojAIy9h8/h99UlNxjkiFT2YhI3Fl/pgNDRO4PVo6tlgb3CwiAZjSdeE5RnF79Dkj5XsM4j+FLMoXtbRw0K9ok9RKjz6ygIs1JDmaOdXexFnq4nAYU3fSLUa6WoccqTHe8bFuJoAv1gbnx09Js8YcVMD96mpTJ3V/MK5YfIv10dbtrDhGug3IS1V2J+0BB9orbQja554N+4S0I9rFBgVCpvPmQqddDHd/AdGkLv/zjEfGytjnvp68bEfDinkQkPfuxw01yd5MbcvLv39VVICWtKbqW263HT5LvSxwKorR7 me@me.com" 102 | 103 | k.ssh2_public_key 104 | # => "---- BEGIN SSH2 PUBLIC KEY ----\nComment: me@me.com\nAAAAB3NzaC1yc2EAAAADAQABAAABAQC9HuXvYJPtQE/o/7TYi63yAopsrJ6TP+lDGdyQ+n\nVVp+5ojAIy9h8/h99UlNxjkiFT2YhI3Fl/pgNDRO4PVo6tlgb3CwiAZjSdeE5RnF79Dkj5\nXsM4j+FLMoXtbRw0K9ok9RKjz6ygIs1JDmaOdXexFnq4nAYU3fSLUa6WoccqTHe8bFuJoA\nv1gbnx09Js8YcVMD96mpTJ3V/MK5YfIv10dbtrDhGug3IS1V2J+0BB9orbQja554N+4S0I\n9rFBgVCpvPmQqddDHd/AdGkLv/zjEfGytjnvp68bEfDinkQkPfuxw01yd5MbcvLv39VVIC\nWtKbqW263HT5LvSxwKorR7\n---- END SSH2 PUBLIC KEY ----" 105 | ``` 106 | 107 | #### Bit length 108 | 109 | Determine the strength of the key in bits as an integer. 110 | 111 | ```ruby 112 | k.bits 113 | # => 2048 114 | ``` 115 | 116 | #### Fingerprints 117 | 118 | It is often helpful to use a fingerprint to visually or programmatically check if one key 119 | matches another. Fetch an MD5, SHA1, or SHA256 fingerprint of the SSH public key. 120 | 121 | ```ruby 122 | k.md5_fingerprint 123 | # => "04:1b:d4:18:df:87:60:94:8c:83:8a:7b:5a:35:59:3d" 124 | 125 | k.sha1_fingerprint 126 | # => "e5:c2:43:9e:e4:0c:0c:47:82:7a:3b:e9:61:13:bd:9c:43:eb:4c:b7" 127 | 128 | k.sha256_fingerprint 129 | # => "x1GEnx1SRY/QwxjMAoyO6mhQlaBedDHtYLEmfeUXy3o=" 130 | ``` 131 | 132 | #### Public Key Directives 133 | 134 | Add optional directives prefixed to the public key that will be enforced when a key is authenticated. 135 | 136 | Accepts a string or an array of strings. 137 | 138 | ```ruby 139 | k.directives = "no-pty" 140 | # => ["no-pty"] 141 | 142 | k.directives = [ 143 | "no-port-forwarding", 144 | "no-X11-forwarding", 145 | "no-agent-forwarding", 146 | "no-pty", 147 | "command='/home/user/bin/authprogs'" 148 | ] 149 | # => ["no-port-forwarding", "no-X11-forwarding", "no-agent-forwarding", "no-pty", "command='/home/user/bin/authprogs'"] 150 | 151 | k.ssh_public_key 152 | # => "no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,command='/home/user/bin/authprogs' ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC9HuXvYJPtQE/o/7TYi63yAopsrJ6TP+lDGdyQ+nVVp+5ojAIy9h8/h99UlNxjkiFT2YhI3Fl/pgNDRO4PVo6tlgb3CwiAZjSdeE5RnF79Dkj5XsM4j+FLMoXtbRw0K9ok9RKjz6ygIs1JDmaOdXexFnq4nAYU3fSLUa6WoccqTHe8bFuJoAv1gbnx09Js8YcVMD96mpTJ3V/MK5YfIv10dbtrDhGug3IS1V2J+0BB9orbQja554N+4S0I9rFBgVCpvPmQqddDHd/AdGkLv/zjEfGytjnvp68bEfDinkQkPfuxw01yd5MbcvLv39VVICWtKbqW263HT5LvSxwKorR7" 153 | ``` 154 | 155 | #### Randomart 156 | 157 | Generate [OpenSSH compatible](http://www.opensource.apple.com/source/OpenSSH/OpenSSH-175/openssh/key.c) ASCII art fingerprints. 158 | 159 | ```ruby 160 | puts k.randomart 161 | +--[ RSA 2048]----+ 162 | |o+ o.. | 163 | |..+.o | 164 | | ooo | 165 | |.++. o | 166 | |+o+ + S | 167 | |.. + o . | 168 | | . + . | 169 | | . . | 170 | | Eo. | 171 | +-----------------+ 172 | ``` 173 | 174 | #### Original OpenSSL key object 175 | 176 | Return the original [OpenSSL::PKey::RSA](https://ruby-doc.org/3.2.2/exts/openssl/OpenSSL/PKey/RSA.html) or [OpenSSL::PKey::DSA](https://ruby-doc.org/3.2.2/exts/openssl/OpenSSL/PKey/DSA.html) or [OpenSSL::PKey::EC](https://ruby-doc.org/3.2.2/exts/openssl/OpenSSL/PKey/EC.html) object. 177 | 178 | ```ruby 179 | k.key_object 180 | # => -----BEGIN RSA PRIVATE KEY-----\nMIIEowI... 181 | ``` 182 | 183 | ### Existing SSH public keys 184 | 185 | #### Validation 186 | 187 | Determine if a given SSH public key is valid. Very useful to test user input of public keys to make sure they accurately copy/pasted the key. Just pass the SSH public key as a string. Returns false if the key is invalid. 188 | 189 | ```ruby 190 | SSHKey.valid_ssh_public_key? "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC9HuXvYJPtQE/o/7TYi63yAopsrJ6TP+lDGdyQ+nVVp+5ojAIy9h8/h99UlNxjkiFT2YhI3Fl/pgNDRO4PVo6tlgb3CwiAZjSdeE5RnF79Dkj5XsM4j+FLMoXtbRw0K9ok9RKjz6ygIs1JDmaOdXexFnq4nAYU3fSLUa6WoccqTHe8bFuJoAv1gbnx09Js8YcVMD96mpTJ3V/MK5YfIv10dbtrDhGug3IS1V2J+0BB9orbQja554N+4S0I9rFBgVCpvPmQqddDHd/AdGkLv/zjEfGytjnvp68bEfDinkQkPfuxw01yd5MbcvLv39VVICWtKbqW263HT5LvSxwKorR7" 191 | # => true 192 | ``` 193 | 194 | #### Bit length 195 | 196 | Determine the strength of the key in bits as an integer. Returns `SSHKey::PublicKeyError` if bits cannot be determined. 197 | 198 | ```ruby 199 | SSHKey.ssh_public_key_bits "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC9HuXvYJPtQE/o/7TYi63yAopsrJ6TP+lDGdyQ+nVVp+5ojAIy9h8/h99UlNxjkiFT2YhI3Fl/pgNDRO4PVo6tlgb3CwiAZjSdeE5RnF79Dkj5XsM4j+FLMoXtbRw0K9ok9RKjz6ygIs1JDmaOdXexFnq4nAYU3fSLUa6WoccqTHe8bFuJoAv1gbnx09Js8YcVMD96mpTJ3V/MK5YfIv10dbtrDhGug3IS1V2J+0BB9orbQja554N+4S0I9rFBgVCpvPmQqddDHd/AdGkLv/zjEfGytjnvp68bEfDinkQkPfuxw01yd5MbcvLv39VVICWtKbqW263HT5LvSxwKorR7" 200 | # => 2048 201 | ``` 202 | 203 | #### Fingerprints 204 | 205 | Fetch an MD5, SHA1, or SHA256 fingerprint of the SSH public key. 206 | Returns `SSHKey::PublicKeyError` if a fingerprint cannot be determined. 207 | 208 | ```ruby 209 | SSHKey.fingerprint "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC9HuXvYJPtQE/o/7TYi63yAopsrJ6TP+lDGdyQ+nVVp+5ojAIy9h8/h99UlNxjkiFT2YhI3Fl/pgNDRO4PVo6tlgb3CwiAZjSdeE5RnF79Dkj5XsM4j+FLMoXtbRw0K9ok9RKjz6ygIs1JDmaOdXexFnq4nAYU3fSLUa6WoccqTHe8bFuJoAv1gbnx09Js8YcVMD96mpTJ3V/MK5YfIv10dbtrDhGug3IS1V2J+0BB9orbQja554N+4S0I9rFBgVCpvPmQqddDHd/AdGkLv/zjEfGytjnvp68bEfDinkQkPfuxw01yd5MbcvLv39VVICWtKbqW263HT5LvSxwKorR7" 210 | # => "04:1b:d4:18:df:87:60:94:8c:83:8a:7b:5a:35:59:3d" 211 | SSHKey.sha1_fingerprint "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC9HuXvYJPtQE/o/7TYi63yAopsrJ6TP+lDGdyQ+nVVp+5ojAIy9h8/h99UlNxjkiFT2YhI3Fl/pgNDRO4PVo6tlgb3CwiAZjSdeE5RnF79Dkj5XsM4j+FLMoXtbRw0K9ok9RKjz6ygIs1JDmaOdXexFnq4nAYU3fSLUa6WoccqTHe8bFuJoAv1gbnx09Js8YcVMD96mpTJ3V/MK5YfIv10dbtrDhGug3IS1V2J+0BB9orbQja554N+4S0I9rFBgVCpvPmQqddDHd/AdGkLv/zjEfGytjnvp68bEfDinkQkPfuxw01yd5MbcvLv39VVICWtKbqW263HT5LvSxwKorR7" 212 | # => "e5:c2:43:9e:e4:0c:0c:47:82:7a:3b:e9:61:13:bd:9c:43:eb:4c:b7" 213 | SSHKey.sha256_fingerprint "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC9HuXvYJPtQE/o/7TYi63yAopsrJ6TP+lDGdyQ+nVVp+5ojAIy9h8/h99UlNxjkiFT2YhI3Fl/pgNDRO4PVo6tlgb3CwiAZjSdeE5RnF79Dkj5XsM4j+FLMoXtbRw0K9ok9RKjz6ygIs1JDmaOdXexFnq4nAYU3fSLUa6WoccqTHe8bFuJoAv1gbnx09Js8YcVMD96mpTJ3V/MK5YfIv10dbtrDhGug3IS1V2J+0BB9orbQja554N+4S0I9rFBgVCpvPmQqddDHd/AdGkLv/zjEfGytjnvp68bEfDinkQkPfuxw01yd5MbcvLv39VVICWtKbqW263HT5LvSxwKorR7" 214 | # => "x1GEnx1SRY/QwxjMAoyO6mhQlaBedDHtYLEmfeUXy3o=" 215 | ``` 216 | 217 | #### Convert to SSH2 Public Key 218 | 219 | Convert an existing SSH Public Key into an SSH2 Public key. Returns `SSHKey::PublicKeyError` if a valid key cannot be generated. 220 | 221 | ```ruby 222 | SSHKey.ssh_public_key_to_ssh2_public_key "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC9HuXvYJPtQE/o/7TYi63yAopsrJ6TP+lDGdyQ+nVVp+5ojAIy9h8/h99UlNxjkiFT2YhI3Fl/pgNDRO4PVo6tlgb3CwiAZjSdeE5RnF79Dkj5XsM4j+FLMoXtbRw0K9ok9RKjz6ygIs1JDmaOdXexFnq4nAYU3fSLUa6WoccqTHe8bFuJoAv1gbnx09Js8YcVMD96mpTJ3V/MK5YfIv10dbtrDhGug3IS1V2J+0BB9orbQja554N+4S0I9rFBgVCpvPmQqddDHd/AdGkLv/zjEfGytjnvp68bEfDinkQkPfuxw01yd5MbcvLv39VVICWtKbqW263HT5LvSxwKorR7 me@me.com" 223 | # => "---- BEGIN SSH2 PUBLIC KEY ----\nComment: me@me.com\nAAAAB3NzaC1yc2EAAAADAQABAAABAQC9HuXvYJPtQE/o/7TYi63yAopsrJ6TP+lDGdyQ+n\nVVp+5ojAIy9h8/h99UlNxjkiFT2YhI3Fl/pgNDRO4PVo6tlgb3CwiAZjSdeE5RnF79Dkj5\nXsM4j+FLMoXtbRw0K9ok9RKjz6ygIs1JDmaOdXexFnq4nAYU3fSLUa6WoccqTHe8bFuJoA\nv1gbnx09Js8YcVMD96mpTJ3V/MK5YfIv10dbtrDhGug3IS1V2J+0BB9orbQja554N+4S0I\n9rFBgVCpvPmQqddDHd/AdGkLv/zjEfGytjnvp68bEfDinkQkPfuxw01yd5MbcvLv39VVIC\nWtKbqW263HT5LvSxwKorR7\n---- END SSH2 PUBLIC KEY ----" 224 | ``` 225 | 226 | ## Copyright 227 | 228 | Copyright (c) 2011-2023 James Miller 229 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "rake" 2 | require "rake/testtask" 3 | 4 | desc "Default: run unit tests." 5 | task :default => :test 6 | 7 | desc "Test the sshkey gem" 8 | Rake::TestTask.new(:test) do |t| 9 | t.libs << "lib" 10 | t.libs << "test" 11 | t.test_files = FileList['test/*_test.rb'] 12 | t.verbose = true 13 | end 14 | -------------------------------------------------------------------------------- /lib/sshkey.rb: -------------------------------------------------------------------------------- 1 | require 'openssl' 2 | require 'base64' 3 | require 'digest/md5' 4 | require 'digest/sha1' 5 | require 'digest/sha2' 6 | 7 | def jruby_not_implemented(msg) 8 | raise NotImplementedError.new "jruby-openssl #{JOpenSSL::VERSION}: #{msg}" if RUBY_PLATFORM == "java" 9 | end 10 | 11 | # Monkey patch OpenSSL::PKey::EC to provide convenience methods usable in this gem 12 | class OpenSSL::PKey::EC 13 | def identifier 14 | # NOTE: Unable to find these constants within OpenSSL, so hardcode them here. 15 | # Analogous to net-ssh OpenSSL::PKey::EC::CurveNameAliasInv 16 | # https://github.com/net-ssh/net-ssh/blob/master/lib/net/ssh/transport/openssl.rb#L147-L151 17 | case group.curve_name 18 | when "prime256v1" then "nistp256" # https://stackoverflow.com/a/41953717 19 | when "secp256r1" then "nistp256" # JRuby 20 | when "secp384r1" then "nistp384" 21 | when "secp521r1" then "nistp521" 22 | else 23 | raise "Unknown curve name: #{public_key.group.curve_name}" 24 | end 25 | end 26 | 27 | def q 28 | # jruby-openssl does not currently support to_octet_string 29 | # https://github.com/jruby/jruby-openssl/issues/226 30 | jruby_not_implemented("to_octet_string is not implemented") 31 | 32 | public_key.to_octet_string(group.point_conversion_form) 33 | end 34 | end 35 | 36 | class SSHKey 37 | SSH_TYPES = { 38 | "ssh-rsa" => "rsa", 39 | "ssh-dss" => "dsa", 40 | "ssh-ed25519" => "ed25519", 41 | "ecdsa-sha2-nistp256" => "ecdsa", 42 | "ecdsa-sha2-nistp384" => "ecdsa", 43 | "ecdsa-sha2-nistp521" => "ecdsa", 44 | } 45 | 46 | SSHFP_TYPES = { 47 | "rsa" => 1, 48 | "dsa" => 2, 49 | "ecdsa" => 3, 50 | "ed25519" => 4, 51 | } 52 | 53 | ECDSA_CURVES = { 54 | 256 => "prime256v1", # https://stackoverflow.com/a/41953717 55 | 384 => "secp384r1", 56 | 521 => "secp521r1", 57 | } 58 | 59 | VALID_BITS = { 60 | "ecdsa" => ECDSA_CURVES.keys, 61 | } 62 | 63 | # Accessor methods are defined in: 64 | # - RSA: https://github.com/ruby/openssl/blob/master/ext/openssl/ossl_pkey_rsa.c 65 | # - DSA: https://github.com/ruby/openssl/blob/master/ext/openssl/ossl_pkey_dsa.c 66 | # - ECDSA: monkey patch OpenSSL::PKey::EC above 67 | SSH_CONVERSION = {"rsa" => ["e", "n"], "dsa" => ["p", "q", "g", "pub_key"], "ecdsa" => ["identifier", "q"]} 68 | 69 | SSH2_LINE_LENGTH = 70 # +1 (for line wrap '/' character) must be <= 72 70 | 71 | class << self 72 | # Generate a new keypair and return an SSHKey object 73 | # 74 | # The default behavior when providing no options will generate a 2048-bit RSA 75 | # keypair. 76 | # 77 | # ==== Parameters 78 | # * options<~Hash>: 79 | # * :type<~String> - "rsa" or "dsa", "rsa" by default 80 | # * :bits<~Integer> - Bit length 81 | # * :comment<~String> - Comment to use for the public key, defaults to "" 82 | # * :passphrase<~String> - Encrypt the key with this passphrase 83 | # 84 | def generate(options = {}) 85 | type = options[:type] || "rsa" 86 | 87 | # JRuby modulus size must range from 512 to 1024 88 | case type 89 | when "rsa" then default_bits = 2048 90 | when "ecdsa" then default_bits = 256 91 | else 92 | default_bits = 1024 93 | end 94 | 95 | bits = options[:bits] || default_bits 96 | cipher = OpenSSL::Cipher.new("AES-128-CBC") if options[:passphrase] 97 | 98 | raise "Bits must either: #{VALID_BITS[type.downcase].join(', ')}" unless VALID_BITS[type.downcase].nil? || VALID_BITS[type.downcase].include?(bits) 99 | 100 | case type.downcase 101 | when "rsa" 102 | key_object = OpenSSL::PKey::RSA.generate(bits) 103 | 104 | when "dsa" 105 | key_object = OpenSSL::PKey::DSA.generate(bits) 106 | 107 | when "ecdsa" 108 | # jruby-openssl OpenSSL::PKey::EC support isn't complete 109 | # https://github.com/jruby/jruby-openssl/issues/189 110 | jruby_not_implemented("OpenSSL::PKey::EC is not fully implemented") 111 | 112 | if OpenSSL::OPENSSL_VERSION_NUMBER >= 0x30000000 113 | # https://github.com/ruby/openssl/pull/480 114 | key_object = OpenSSL::PKey::EC.generate(ECDSA_CURVES[bits]) 115 | else 116 | key_pkey = OpenSSL::PKey::EC.new(ECDSA_CURVES[bits]) 117 | key_object = key_pkey.generate_key 118 | end 119 | 120 | else 121 | raise "Unknown key type: #{type}" 122 | end 123 | 124 | key_pem = key_object.to_pem(cipher, options[:passphrase]) 125 | new(key_pem, options) 126 | end 127 | 128 | # Validate an existing SSH public key 129 | # 130 | # Returns true or false depending on the validity of the public key provided 131 | # 132 | # ==== Parameters 133 | # * ssh_public_key<~String> - "ssh-rsa AAAAB3NzaC1yc2EA...." 134 | # 135 | def valid_ssh_public_key?(ssh_public_key) 136 | ssh_type, encoded_key = parse_ssh_public_key(ssh_public_key) 137 | sections = unpacked_byte_array(ssh_type, encoded_key) 138 | case ssh_type 139 | when "ssh-rsa", "ssh-dss" 140 | sections.size == SSH_CONVERSION[SSH_TYPES[ssh_type]].size 141 | when "ssh-ed25519" 142 | sections.size == 1 # https://tools.ietf.org/id/draft-bjh21-ssh-ed25519-00.html#rfc.section.4 143 | when "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384", "ecdsa-sha2-nistp521" 144 | sections.size == 2 # https://tools.ietf.org/html/rfc5656#section-3.1 145 | else 146 | false 147 | end 148 | rescue 149 | false 150 | end 151 | 152 | # Bits 153 | # 154 | # Returns ssh public key bits or false depending on the validity of the public key provided 155 | # 156 | # ==== Parameters 157 | # * ssh_public_key<~String> - "ssh-rsa AAAAB3NzaC1yc2EA...." 158 | # * ssh_public_key<~String> - "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTY...." 159 | # 160 | def ssh_public_key_bits(ssh_public_key) 161 | ssh_type, encoded_key = parse_ssh_public_key(ssh_public_key) 162 | sections = unpacked_byte_array(ssh_type, encoded_key) 163 | 164 | case ssh_type 165 | when "ssh-rsa", "ssh-dss", "ssh-ed25519" 166 | sections.last.num_bytes * 8 167 | 168 | when "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384", "ecdsa-sha2-nistp521" 169 | raise PublicKeyError, "invalid ECDSA key" unless sections.count == 2 170 | 171 | # https://tools.ietf.org/html/rfc5656#section-3.1 172 | identifier = sections[0].to_s(2) 173 | q = sections[1].to_s(2) 174 | ecdsa_bits(ssh_type, identifier, q) 175 | 176 | else 177 | raise PublicKeyError, "unsupported key type #{ssh_type}" 178 | end 179 | end 180 | 181 | # Fingerprints 182 | # 183 | # Accepts either a public or private key 184 | # 185 | # MD5 fingerprint for the given SSH key 186 | def md5_fingerprint(key) 187 | if key.match(/PRIVATE/) 188 | new(key).md5_fingerprint 189 | else 190 | Digest::MD5.hexdigest(decoded_key(key)).gsub(fingerprint_regex, '\1:\2') 191 | end 192 | end 193 | alias_method :fingerprint, :md5_fingerprint 194 | 195 | # SHA1 fingerprint for the given SSH key 196 | def sha1_fingerprint(key) 197 | if key.match(/PRIVATE/) 198 | new(key).sha1_fingerprint 199 | else 200 | Digest::SHA1.hexdigest(decoded_key(key)).gsub(fingerprint_regex, '\1:\2') 201 | end 202 | end 203 | 204 | # SHA256 fingerprint for the given SSH key 205 | def sha256_fingerprint(key) 206 | if key.match(/PRIVATE/) 207 | new(key).sha256_fingerprint 208 | else 209 | Base64.encode64(Digest::SHA256.digest(decoded_key(key))).gsub("\n", "") 210 | end 211 | end 212 | 213 | # SSHFP records for the given SSH key 214 | def sshfp(hostname, key) 215 | if key.match(/PRIVATE/) 216 | new(key).sshfp hostname 217 | else 218 | type, encoded_key = parse_ssh_public_key(key) 219 | format_sshfp_record(hostname, SSH_TYPES[type], Base64.decode64(encoded_key)) 220 | end 221 | end 222 | 223 | # Convert an existing SSH public key to SSH2 (RFC4716) public key 224 | # 225 | # ==== Parameters 226 | # * ssh_public_key<~String> - "ssh-rsa AAAAB3NzaC1yc2EA...." 227 | # * headers<~Hash> - The Key will be used as the header-tag and the value as the header-value 228 | # 229 | def ssh_public_key_to_ssh2_public_key(ssh_public_key, headers = nil) 230 | raise PublicKeyError, "invalid ssh public key" unless SSHKey.valid_ssh_public_key?(ssh_public_key) 231 | 232 | _source_format, source_key = parse_ssh_public_key(ssh_public_key) 233 | 234 | # Add a 'Comment' Header Field unless others are explicitly passed in 235 | if source_comment = ssh_public_key.split(source_key)[1] 236 | headers = {'Comment' => source_comment.strip} if headers.nil? && !source_comment.empty? 237 | end 238 | header_fields = build_ssh2_headers(headers) 239 | 240 | ssh2_key = "---- BEGIN SSH2 PUBLIC KEY ----\n" 241 | ssh2_key << header_fields unless header_fields.nil? 242 | ssh2_key << source_key.scan(/.{1,#{SSH2_LINE_LENGTH}}/).join("\n") 243 | ssh2_key << "\n---- END SSH2 PUBLIC KEY ----" 244 | end 245 | 246 | def format_sshfp_record(hostname, type, key) 247 | [[Digest::SHA1, 1], [Digest::SHA256, 2]].map { |f, num| 248 | fpr = f.hexdigest(key) 249 | "#{hostname} IN SSHFP #{SSHFP_TYPES[type]} #{num} #{fpr}" 250 | }.join("\n") 251 | end 252 | 253 | private 254 | 255 | def unpacked_byte_array(ssh_type, encoded_key) 256 | prefix = [ssh_type.length].pack("N") + ssh_type 257 | decoded = Base64.decode64(encoded_key) 258 | 259 | # Base64 decoding is too permissive, so we should validate if encoding is correct 260 | unless Base64.encode64(decoded).gsub("\n", "") == encoded_key && decoded.slice!(0, prefix.length) == prefix 261 | raise PublicKeyError, "validation error" 262 | end 263 | 264 | byte_count = 0 265 | data = [] 266 | until decoded.empty? 267 | front = decoded.slice!(0,4) 268 | size = front.unpack("N").first 269 | segment = decoded.slice!(0, size) 270 | byte_count += segment.length 271 | unless front.length == 4 && segment.length == size 272 | raise PublicKeyError, "byte array too short" 273 | end 274 | data << OpenSSL::BN.new(segment, 2) 275 | end 276 | 277 | 278 | if ssh_type == "ssh-ed25519" 279 | unless byte_count == 32 280 | raise PublicKeyError, "validation error, ed25519 key length not OK" 281 | end 282 | end 283 | 284 | return data 285 | end 286 | 287 | def ecdsa_bits(ssh_type, identifier, q) 288 | raise PublicKeyError, "invalid ssh type" unless ssh_type == "ecdsa-sha2-#{identifier}" 289 | 290 | len_q = q.length 291 | 292 | compression_octet = q.slice(0, 1) 293 | if compression_octet == "\x04" 294 | # Point compression is off 295 | # Summary from https://www.secg.org/sec1-v2.pdf "2.3.3 Elliptic-Curve-Point-to-Octet-String Conversion" 296 | # - the leftmost octet indicates that point compression is off 297 | # (first octet 0x04 as specified in "3.3. Output M = 04 base 16 ‖ X ‖ Y.") 298 | # - the remainder of the octet string contains the x-coordinate followed by the y-coordinate. 299 | len_x = (len_q - 1) / 2 300 | 301 | else 302 | # Point compression is on 303 | # Summary from https://www.secg.org/sec1-v2.pdf "2.3.3 Elliptic-Curve-Point-to-Octet-String Conversion" 304 | # - the compressed y-coordinate is recovered from the leftmost octet 305 | # - the x-coordinate is recovered from the remainder of the octet string 306 | raise PublicKeyError, "invalid compression octet" unless compression_octet == "\x02" || compression_octet == "\x03" 307 | len_x = len_q - 1 308 | end 309 | 310 | # https://www.secg.org/sec2-v2.pdf "2.1 Properties of Elliptic Curve Domain Parameters over Fp" defines 311 | # five discrete bit lengths: 192, 224, 256, 384, 521 312 | # These bit lengths can be ascertained from the length of the packed x-coordinate. 313 | # Alternatively, these bit lengths can be derived from their associated prime constants using Math.log2(prime).ceil 314 | # against the prime constants defined in https://www.secg.org/sec2-v2.pdf 315 | case len_x 316 | when 24 then bits = 192 317 | when 28 then bits = 224 318 | when 32 then bits = 256 319 | when 48 then bits = 384 320 | when 66 then bits = 521 321 | else 322 | raise PublicKeyError, "invalid x-coordinate length #{len_x}" 323 | end 324 | 325 | raise PublicKeyError, "invalid identifier #{identifier}" unless identifier =~ /#{bits}/ 326 | return bits 327 | end 328 | 329 | def decoded_key(key) 330 | Base64.decode64(parse_ssh_public_key(key).last) 331 | end 332 | 333 | def fingerprint_regex 334 | /(.{2})(?=.)/ 335 | end 336 | 337 | def parse_ssh_public_key(public_key) 338 | # lines starting with a '#' and empty lines are ignored as comments (as in ssh AuthorizedKeysFile) 339 | public_key = public_key.gsub(/^#.*$/, '') 340 | public_key = public_key.strip # leading and trailing whitespaces wiped out 341 | 342 | raise PublicKeyError, "newlines are not permitted between key data" if public_key =~ /\n(?!$)/ 343 | 344 | parsed = public_key.split(" ") 345 | parsed.each_with_index do |el, index| 346 | return parsed[index..(index+1)] if SSH_TYPES[el] 347 | end 348 | raise PublicKeyError, "cannot determine key type" 349 | end 350 | 351 | def build_ssh2_headers(headers = {}) 352 | return nil if headers.nil? || headers.empty? 353 | 354 | headers.keys.sort.collect do |header_tag| 355 | # header-tag must be us-ascii & <= 64 bytes and header-data must be UTF-8 & <= 1024 bytes 356 | raise PublicKeyError, "SSH2 header-tag '#{header_tag}' must be US-ASCII" unless header_tag.each_byte.all? {|b| b < 128 } 357 | raise PublicKeyError, "SSH2 header-tag '#{header_tag}' must be <= 64 bytes" unless header_tag.size <= 64 358 | raise PublicKeyError, "SSH2 header-value for '#{header_tag}' must be <= 1024 bytes" unless headers[header_tag].size <= 1024 359 | 360 | header_field = "#{header_tag}: #{headers[header_tag]}" 361 | header_field.scan(/.{1,#{SSH2_LINE_LENGTH}}/).join("\\\n") 362 | end.join("\n") << "\n" 363 | end 364 | end 365 | 366 | attr_reader :key_object, :type, :typestr 367 | attr_accessor :passphrase, :comment 368 | 369 | # Create a new SSHKey object 370 | # 371 | # ==== Parameters 372 | # * private_key - Existing RSA or DSA or ECDSA private key 373 | # * options<~Hash> 374 | # * :comment<~String> - Comment to use for the public key, defaults to "" 375 | # * :passphrase<~String> - If the key is encrypted, supply the passphrase 376 | # * :directives<~Array> - Options prefixed to the public key 377 | # 378 | def initialize(private_key, options = {}) 379 | @passphrase = options[:passphrase] 380 | @comment = options[:comment] || "" 381 | self.directives = options[:directives] || [] 382 | 383 | begin 384 | @key_object = OpenSSL::PKey::RSA.new(private_key, passphrase) 385 | @type = "rsa" 386 | @typestr = "ssh-rsa" 387 | rescue OpenSSL::PKey::RSAError 388 | @type = nil 389 | end 390 | 391 | return if @type 392 | 393 | begin 394 | @key_object = OpenSSL::PKey::DSA.new(private_key, passphrase) 395 | @type = "dsa" 396 | @typestr = "ssh-dss" 397 | rescue OpenSSL::PKey::DSAError 398 | @type = nil 399 | end 400 | 401 | return if @type 402 | 403 | @key_object = OpenSSL::PKey::EC.new(private_key, passphrase) 404 | @type = "ecdsa" 405 | bits = ECDSA_CURVES.invert[@key_object.group.curve_name] 406 | @typestr = "ecdsa-sha2-nistp#{bits}" 407 | end 408 | 409 | # Fetch the private key (PEM format) 410 | # 411 | # rsa_private_key and dsa_private_key are aliased for backward compatibility 412 | def private_key 413 | # jruby-openssl OpenSSL::PKey::EC support isn't complete 414 | # https://github.com/jruby/jruby-openssl/issues/189 415 | jruby_not_implemented("OpenSSL::PKey::EC is not fully implemented") if type == "ecdsa" 416 | 417 | key_object.to_pem 418 | end 419 | alias_method :rsa_private_key, :private_key 420 | alias_method :dsa_private_key, :private_key 421 | 422 | # Fetch the encrypted RSA/DSA private key using the passphrase provided 423 | # 424 | # If no passphrase is set, returns the unencrypted private key 425 | def encrypted_private_key 426 | return private_key unless passphrase 427 | key_object.to_pem(OpenSSL::Cipher.new("AES-128-CBC"), passphrase) 428 | end 429 | 430 | # Fetch the public key (PEM format) 431 | # 432 | # rsa_public_key and dsa_public_key are aliased for backward compatibility 433 | def public_key 434 | public_key_object.to_pem 435 | end 436 | alias_method :rsa_public_key, :public_key 437 | alias_method :dsa_public_key, :public_key 438 | 439 | def public_key_object 440 | if type == "ecdsa" 441 | return nil unless key_object 442 | return nil unless key_object.group 443 | 444 | if OpenSSL::OPENSSL_VERSION_NUMBER >= 0x30000000 && RUBY_PLATFORM != "java" 445 | 446 | # jruby-openssl does not currently support point_conversion_form 447 | # (futureproofing for if/when JRuby requires this technique to determine public key) 448 | jruby_not_implemented("point_conversion_form is not implemented") 449 | 450 | # Avoid "OpenSSL::PKey::PKeyError: pkeys are immutable on OpenSSL 3.0" 451 | # https://github.com/ruby/openssl/blob/master/History.md#version-300 452 | # https://github.com/ruby/openssl/issues/498 453 | # https://github.com/net-ssh/net-ssh/commit/4de6831dea4e922bf3052192eec143af015a3486 454 | # https://github.com/ClearlyClaire/cose-ruby/commit/28ee497fa7d9d49e72d5a5e97a567c0b58fdd822 455 | 456 | curve_name = key_object.group.curve_name 457 | return nil unless curve_name 458 | 459 | # Map to different curve_name for JRuby 460 | # (futureproofing for if/when JRuby requires this technique to determine public key) 461 | # https://github.com/jwt/ruby-jwt/issues/362#issuecomment-722938409 462 | curve_name = "prime256v1" if curve_name == "secp256r1" && RUBY_PLATFORM == "java" 463 | 464 | # Construct public key OpenSSL::PKey::EC from OpenSSL::PKey::EC::Point 465 | public_key_point = key_object.public_key # => OpenSSL::PKey::EC::Point 466 | return nil unless public_key_point 467 | 468 | asn1 = OpenSSL::ASN1::Sequence( 469 | [ 470 | OpenSSL::ASN1::Sequence( 471 | [ 472 | OpenSSL::ASN1::ObjectId("id-ecPublicKey"), 473 | OpenSSL::ASN1::ObjectId(curve_name) 474 | ] 475 | ), 476 | OpenSSL::ASN1::BitString(public_key_point.to_octet_string(key_object.group.point_conversion_form)) 477 | ] 478 | ) 479 | 480 | pub = OpenSSL::PKey::EC.new(asn1.to_der) 481 | pub 482 | 483 | else 484 | pub = OpenSSL::PKey::EC.new(key_object.group) 485 | pub.public_key = key_object.public_key 486 | pub 487 | end 488 | 489 | else 490 | key_object.public_key 491 | end 492 | end 493 | 494 | # SSH public key 495 | def ssh_public_key 496 | [directives.join(",").strip, typestr, Base64.encode64(ssh_public_key_conversion).gsub("\n", ""), comment].join(" ").strip 497 | end 498 | 499 | # SSH2 public key (RFC4716) 500 | # 501 | # ==== Parameters 502 | # * headers<~Hash> - Keys will be used as header-tags and values as header-values. 503 | # 504 | # ==== Examples 505 | # {'Comment' => '2048-bit RSA created by user@example'} 506 | # {'x-private-use-tag' => 'Private Use Value'} 507 | # 508 | def ssh2_public_key(headers = nil) 509 | self.class.ssh_public_key_to_ssh2_public_key(ssh_public_key, headers) 510 | end 511 | 512 | # Fingerprints 513 | # 514 | # MD5 fingerprint for the given SSH public key 515 | def md5_fingerprint 516 | Digest::MD5.hexdigest(ssh_public_key_conversion).gsub(/(.{2})(?=.)/, '\1:\2') 517 | end 518 | alias_method :fingerprint, :md5_fingerprint 519 | 520 | # SHA1 fingerprint for the given SSH public key 521 | def sha1_fingerprint 522 | Digest::SHA1.hexdigest(ssh_public_key_conversion).gsub(/(.{2})(?=.)/, '\1:\2') 523 | end 524 | 525 | # SHA256 fingerprint for the given SSH public key 526 | def sha256_fingerprint 527 | Base64.encode64(Digest::SHA256.digest(ssh_public_key_conversion)).gsub("\n", "") 528 | end 529 | 530 | # Determine the length (bits) of the key as an integer 531 | def bits 532 | self.class.ssh_public_key_bits(ssh_public_key) 533 | end 534 | 535 | # Randomart 536 | # 537 | # Generate OpenSSH compatible ASCII art fingerprints 538 | # See http://www.opensource.apple.com/source/OpenSSH/OpenSSH-175/openssh/key.c (key_fingerprint_randomart function) 539 | # or https://mirrors.mit.edu/pub/OpenBSD/OpenSSH/ (sshkey.c fingerprint_randomart function) 540 | # 541 | # Example: 542 | # +--[ RSA 2048]----+ 543 | # |o+ o.. | 544 | # |..+.o | 545 | # | ooo | 546 | # |.++. o | 547 | # |+o+ + S | 548 | # |.. + o . | 549 | # | . + . | 550 | # | . . | 551 | # | Eo. | 552 | # +-----------------+ 553 | def randomart(dgst_alg = "MD5") 554 | fieldsize_x = 17 555 | fieldsize_y = 9 556 | x = fieldsize_x / 2 557 | y = fieldsize_y / 2 558 | 559 | case dgst_alg 560 | when "MD5" then raw_digest = Digest::MD5.digest(ssh_public_key_conversion) 561 | when "SHA256" then raw_digest = Digest::SHA2.new(256).digest(ssh_public_key_conversion) 562 | when "SHA384" then raw_digest = Digest::SHA2.new(384).digest(ssh_public_key_conversion) 563 | when "SHA512" then raw_digest = Digest::SHA2.new(512).digest(ssh_public_key_conversion) 564 | else 565 | raise "Unknown digest algorithm: #{digest}" 566 | end 567 | 568 | augmentation_string = " .o+=*BOX@%&#/^SE" 569 | len = augmentation_string.length - 1 570 | 571 | field = Array.new(fieldsize_x) { Array.new(fieldsize_y) {0} } 572 | 573 | raw_digest.bytes.each do |byte| 574 | 4.times do 575 | x += (byte & 0x1 != 0) ? 1 : -1 576 | y += (byte & 0x2 != 0) ? 1 : -1 577 | 578 | x = [[x, 0].max, fieldsize_x - 1].min 579 | y = [[y, 0].max, fieldsize_y - 1].min 580 | 581 | field[x][y] += 1 if (field[x][y] < len - 2) 582 | 583 | byte >>= 2 584 | end 585 | end 586 | 587 | fieldsize_x_halved = fieldsize_x / 2 588 | fieldsize_y_halved = fieldsize_y / 2 589 | 590 | field[fieldsize_x_halved][fieldsize_y_halved] = len - 1 591 | field[x][y] = len 592 | 593 | type_name_length_max = 4 # Note: this will need to be extended to accomodate ed25519 594 | bits_number_length_max = (bits < 1000 ? 3 : 4) 595 | formatstr = "[%#{type_name_length_max}s %#{bits_number_length_max}u]" 596 | output = "+--#{sprintf(formatstr, type.upcase, bits)}----+\n" 597 | 598 | fieldsize_y.times do |y| 599 | output << "|" 600 | fieldsize_x.times do |x| 601 | output << augmentation_string[[field[x][y], len].min] 602 | end 603 | output << "|" 604 | output << "\n" 605 | end 606 | output << "+#{"-" * fieldsize_x}+" 607 | output 608 | end 609 | 610 | def sshfp(hostname) 611 | self.class.format_sshfp_record(hostname, @type, ssh_public_key_conversion) 612 | end 613 | 614 | def directives=(directives) 615 | @directives = Array[directives].flatten.compact 616 | end 617 | attr_reader :directives 618 | 619 | private 620 | 621 | def self.ssh_public_key_data_dsarsa(val) 622 | # Get byte-representation of absolute value of val 623 | data = val.to_s(2) 624 | 625 | first_byte = data[0,1].unpack("c").first 626 | if val < 0 627 | # For negative values, highest bit must be set 628 | data[0] = [0x80 & first_byte].pack("c") 629 | elsif first_byte < 0 630 | # For positive values where highest bit would be set, prefix with \0 631 | data = "\0" + data 632 | end 633 | 634 | data 635 | end 636 | 637 | def self.ssh_public_key_data_ecdsa(val) 638 | val 639 | end 640 | 641 | # SSH Public Key Conversion 642 | # 643 | # All data type encoding is defined in the section #5 of RFC #4251. 644 | # String and mpint (multiple precision integer) types are encoded this way: 645 | # 4-bytes word: data length (unsigned big-endian 32 bits integer) 646 | # n bytes: binary representation of the data 647 | 648 | # For instance, the "ssh-rsa" string is encoded as the following byte array 649 | # [0, 0, 0, 7, 's', 's', 'h', '-', 'r', 's', 'a'] 650 | def ssh_public_key_conversion 651 | methods = SSH_CONVERSION[type] 652 | methods.inject([typestr.length].pack("N") + typestr) do |pubkeystr, m| 653 | # Given public_key_object.class == OpenSSL::BN, public_key_object.to_s(0) 654 | # returns an MPI formatted string (length prefixed bytes). This is not 655 | # supported by JRuby, so we still have to deal with length and data separately. 656 | val = public_key_object.send(m) 657 | 658 | case type 659 | when "dsa","rsa" then data = self.class.ssh_public_key_data_dsarsa(val) 660 | when "ecdsa" then data = self.class.ssh_public_key_data_ecdsa(val) 661 | else 662 | raise "Unknown key type: #{type}" 663 | end 664 | 665 | pubkeystr + [data.length].pack("N") + data 666 | end 667 | end 668 | 669 | class PublicKeyError < StandardError; end 670 | end 671 | -------------------------------------------------------------------------------- /lib/sshkey/version.rb: -------------------------------------------------------------------------------- 1 | class SSHKey 2 | VERSION = "3.0.0" 3 | end 4 | -------------------------------------------------------------------------------- /sshkey.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require "sshkey/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "sshkey" 7 | s.version = SSHKey::VERSION 8 | s.platform = Gem::Platform::RUBY 9 | s.authors = ["James Miller"] 10 | s.email = ["bensie@gmail.com"] 11 | s.homepage = "https://github.com/bensie/sshkey" 12 | s.summary = %q{SSH private/public key generator in Ruby} 13 | s.description = %q{Generate private/public SSH keypairs using pure Ruby} 14 | s.licenses = ["MIT"] 15 | 16 | # ECDSA requires OpenSSL::PKey::EC::Point#to_octet_string 17 | # to_octet string was added in Ruby/OpenSSL 2.1.0 https://github.com/ruby/openssl/blob/master/History.md#version-210 18 | # Ruby 2.5 Updated Ruby/OpenSSL from to 2.1.0 https://github.com/ruby/ruby/blob/v2_5_0/NEWS 19 | s.required_ruby_version = '>= 2.5' 20 | 21 | s.files = `git ls-files`.split("\n") 22 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 23 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 24 | s.require_paths = ["lib"] 25 | 26 | s.add_dependency("base64") 27 | 28 | s.add_development_dependency("rake") 29 | s.add_development_dependency("test-unit") 30 | end 31 | -------------------------------------------------------------------------------- /test/sshkey_test.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | require 'sshkey' 3 | 4 | class SSHKeyTest < Test::Unit::TestCase 5 | 6 | # https://github.com/jruby/jruby-openssl/issues/189 7 | # https://github.com/jruby/jruby-openssl/issues/226 8 | def ecdsa_supported? 9 | RUBY_PLATFORM != "java" 10 | end 11 | 12 | SSH_PRIVATE_KEY1 = <<-EOF 13 | -----BEGIN RSA PRIVATE KEY----- 14 | MIIEogIBAAKCAQEArfTA/lKVR84IMc9ZzXOCHr8DVtR8hzWuEVHF6KElavRHlk14 15 | g0SZu3m908Ejm/XF3EfNHjX9wN+62IMA0QBxkBMFCuLF+U/oeUs0NoDdAEKxjj4n 16 | 6lq6Ss8aLct+anMy7D1jwvOLbcwV54w1d5JDdlZVdZ6AvHm9otwJq6rNpDgdmXY4 17 | HgC2nM9csFpuy0cDpL6fdJx9lcNL2RnkRC4+RMsIB+PxDw0j3vDi04dYLBXMGYjy 18 | eGH+mIFpL3PTPXGXwL2XDYXZ2H4SQX6bOoKmazTXq6QXuEB665njh1GxXldoIMcS 19 | shoJL0hrk3WrTOG22N2CQA+IfHgrXJ+A+QUzKQIBIwKCAQBAnLy2O+5N3s/X/I8R 20 | y9E+nrgY79Z7XRTEmrc5JelTnI+eOggwwbVxhP1dR7zE5kItPz2O4NqYGJXbY9u7 21 | V++qiri65oQMJP6Tc7ROwiYzS/jObtugMFPSpLHzwJyrMho6fTOuz3zuRH0qHiJ8 22 | 3o4WAs9I8brJqY+UQxmI56t3gfHcX4nRhueyUvmEdDG+4Mob21wED1GD5ENh9ebX 23 | UiuYkeROqd+lfBUkWoxUXi2fjRMSRt7n3bq59pyZQCwKiShIVaonciV8xAAlNvhI 24 | RBzYvXbQ47YgsTmcW4Srlv0j/Oij2/RaDhkJtaXyPkqw9k4B8oCaX3C2x4sdhcwa 25 | iLU7AoGBANb4Rmz1w4wfZSnu/HlW4G0Us+AWVEX+6zePoOartP5Pe5t3XhHW7vpi 26 | YoB4ecqhz4Y1LoYZL07cSsQZHfntUV4eh/apuo/5slrhDkk0ewJkUh6SKLOFNv6Q 27 | 7iJnmtzzRovW1MQPa0NeInsUrZYe4B4iGZmK4yEr9+c7IQCPFQvVAoGBAM8ofVgb 28 | gzDYY2uX1lvU9bGAHqA/qNJHcYZBu5AZr7bkZC1GlSKh93ppczdQhiZmj2FQr09R 29 | Z5GgKIlSWk8MYC+kYq7l5r2O42g3Unp+i1Zc5KCYUWYpyeE/jfl5IFJFQJFVtdB1 30 | JlsFxruQIF/HuTzY6D+zF8GzK/T5ZQwigBgFAoGAGJFnImU663FNY+DMZaOHXOxs 31 | VB/PHfE/dBBqKP2uSPMkEcR/x4ZHMo7mr5i9dj5g3CNVxi7Dk/vrSZx4dFWi5i9f 32 | /u7TfisqU4dvWNLMOsmi/C32BeNWvgHvVGOcq4mEZ8DH2+SBSYcZ4i4/uWKdRUW5 33 | yGek7dkjpWXX4s6GD/sCgYEAiCHr+BIUYe1Ipcotx1FuQXFzNhs0bO0et0/EZgJA 34 | RPx8WERTX+bHMy9aV4yv7VlW6C21CDzPB+zncC7NoakMAgzwZE3vZp+6AqgDAAoD 35 | ywnYEcMuLTFnaCJzPYocjdW8t0bz0iEZNIAjgpHpY4M/Np0q6Af5qyyZOpVCZw9b 36 | fX8CgYEAqFpBwetp78UfwvWyKkfN56cY8EaC7gMkwE4gnXsByrqW0f/Shf5ofpO1 37 | kCMav5GhplRYcF3mUO9xiAPx1FxWj/MjeevkmmugIrcYi5OpGu70KoaBmCmb5uV6 38 | zJLsX4h3i0JFdIOaECZEOXhPA7btQT8Vvznj8cHFeeronqdFWf0= 39 | -----END RSA PRIVATE KEY----- 40 | EOF 41 | SSH_PRIVATE_KEY2 = <<-EOF 42 | -----BEGIN RSA PRIVATE KEY----- 43 | MIIEogIBAAKCAQEAxl6TpN7uFiY/JZ8qDnD7UrxDP+ABeh2PVg8Du1LEgXNk0+YW 44 | CeP5S6oHklqaWeDlbmAs1oHsBwCMAVpMa5tgONOLvz4JgwgkiqQEbKR8ofWJ+LAD 45 | UElvqRVGmGiNEMLI6GJWeneL4sjmbb8d6U+M53c6iWG0si9XE5m7teBQSsCl0Tk3 46 | qMIkQGw5zpJeCXjZ8KpJhIJRYgexFkGgPlYRV+UYIhxpUW90t0Ra5i6JOFYwq98k 47 | 5S/6SJIZQ/A9F4JNzwLw3eVxZj0yVHWxkGz1+TyELNY1kOyMxnZaqSfGzSQJTrnI 48 | XpdweVHuYh1LtOgedRQhCyiELeSMGwio1vRPKwIBIwKCAQEAiAZWnPCjQmNegDKg 49 | fu5jMWsmzLbccP5TqLnWrFYDFvAKn+46/3fA421HBUVxJ7AoS6+pt6mL54tYsHhu 50 | 6rOvsfAlT/AGhbxw1Biyk6P9sOLiRCDsVE+dBjp5jRR9/N1WkLh10FH5hZETCW0b 51 | 0y88DG8DkWeR2UUIgngLr+pFr5jV/e4nvA5QpvbNscOwoiR7sFsMGLcMgM2fT4Hj 52 | ZZovcGQMrDr6AG+y0/Vdf9wX22j+XKj7huIqM3GZvyqGPqJnP9sOKkPcuTck8Wx3 53 | 55BX675RVdoW9OTcHbUh3qHcCND4d9WZqHarW/a7XBdIiuRmC2kBX5WBmVXnm/RF 54 | bvxoCwKBgQDqyVNWwm98gIw7LS8dR6Ec7t3kOeLNo/rtNTszo631yZ4hqdwgYR3Q 55 | q6rhjufsVVRLVzfTDXucbhZ5h+UB9wXAM49ZPxKNw+ddHsRbhCuIWUl/iO8E/Aub 56 | H3eZupo73N9JGa4STFw056ejOQrTTCMf0M316V4wgFAXOZeHEErxSQKBgQDYSuqR 57 | nr3Hdw1n/iXfKrfd9fJI++nm14uQ4mkA+9HrtQpj/RTxr66/QSj7p3r6GF4dDYY4 58 | XaqK+iCfhUKMr8+3CP7NoS/saZAUqvMnL+RCvX14sV55xRMwplaaNIwqDhQAhkmL 59 | UeOBq40kmBsunjfp06JedmWhWKHYc1eR2iPw0wKBgA1qlwxFn/h8X8jeArE3Swj3 60 | tOh4VhphJEgRq5yNAqBUqfNLiOvoSti5WjjGVmVGtFwTnMo7SOReD+mv/nUkDvUK 61 | QrSkhLeky2RoKHpCER279ZJCVs0Vt4U0/4UgmxldFBLORHYS/fRlAkPXX7RNflmW 62 | 5zKfnvt1C+QR62bNuyO7AoGBAI4imiUtzStOPAKCcKiYajLGMYBrB2vPePjPTFEa 63 | gqI1JBXSMlWt9n2uegR1X3El9LQBkrdTfrMZZeUrr2PD/Ybop3EvaKKrxRTlXfUu 64 | GagzYRTMVAbgl5T/l/7vVMst0qFCTZYRPbucnpRj9Jr6QgAOuygh6wOgpN6yMjtG 65 | NOAVAoGACIdfR5oZ4tvNIeqjtLF83HmUJARv86eMmjqgiQTFcZS3A8vk5y05STxX 66 | HU3kTCfT6sypRi9zDQafIIyqYFgaOezr2eRRFRojQZqzHjtuFUeKLrKf7R9bzwwx 67 | DPlNgYq8p4FOY5ZOL/ZOxUHW4vKRewURJttnxzw+LEy0T1FyAE0= 68 | -----END RSA PRIVATE KEY----- 69 | EOF 70 | SSH_PRIVATE_KEY3 = <<-EOF 71 | -----BEGIN DSA PRIVATE KEY----- 72 | MIIBvAIBAAKBgQC8lcuXcFcIC9wsV87L6PAwYefKgK0CwTSD1v3/aabZsu4w+UF8 73 | zsPtdsNP8+JWfOp3KFbrUTH+ODgAXF/aL4UZfpbsQe446ZFV8v6dmWqj23sk0FLX 74 | U5l2tsuJ9OdyXetVXjBvoiz+/r4k/iG/esvWlVGEHwq5eYXgQ1GfXABY3QIVAMVe 75 | c7skmkUrCR6iivgZYYe3PQPZAoGBAKnpdEVATtDGOW9w2evSf5kc1InzdTurcJOH 76 | q9qYdCaa8rlMGaIS6XFWcKqBlpj0Mv2R5ldW90bU/RllGvh1KinTIRVTsf4qtZIV 77 | Xy4vN8IYzDL1493nKndMsxsRh50rI1Snn2tssAix64eJ5VFSGlyOYEKYDMlWzHK6 78 | Jg3tVmc6AoGBAIwTRPAEcroqOzaebiVspFcmsXxDQ4wXQZQdho1ExW6FKS8s7/6p 79 | ItmZYXTvJDwLXgq2/iK1fRRcKk2PJEaSuJR7WeNGsJKfWmQ2UbOhqA3wWLDazIZt 80 | cMKjFzD0hM4E8qgjHjMvKDE6WgT6SFP+tqx3nnh7pJWwsbGjSMQexpyRAhQLhz0l 81 | GzM8qwTcXd06uIZAJdTHIQ== 82 | -----END DSA PRIVATE KEY----- 83 | EOF 84 | 85 | SSH_PRIVATE_KEY4 = <<-EOF 86 | -----BEGIN EC PRIVATE KEY----- 87 | MHcCAQEEIByjVCRawGxEd/L/VblGjnJTJeOgk6vGFYnolYWHg+JkoAoGCCqGSM49 88 | AwEHoUQDQgAEQOAmNzXT3XN5DQdHBYCgflosVlHd6MUB1n9n6CCijvVJCQGJAA0p 89 | 6+3o91ccyA0zHXuUno2eMzBUDghfNZYnHg== 90 | -----END EC PRIVATE KEY----- 91 | EOF 92 | 93 | PUBLIC_KEY1 = <<-EOF 94 | -----BEGIN PUBLIC KEY----- 95 | MIIBIDANBgkqhkiG9w0BAQEFAAOCAQ0AMIIBCAKCAQEArfTA/lKVR84IMc9ZzXOC 96 | Hr8DVtR8hzWuEVHF6KElavRHlk14g0SZu3m908Ejm/XF3EfNHjX9wN+62IMA0QBx 97 | kBMFCuLF+U/oeUs0NoDdAEKxjj4n6lq6Ss8aLct+anMy7D1jwvOLbcwV54w1d5JD 98 | dlZVdZ6AvHm9otwJq6rNpDgdmXY4HgC2nM9csFpuy0cDpL6fdJx9lcNL2RnkRC4+ 99 | RMsIB+PxDw0j3vDi04dYLBXMGYjyeGH+mIFpL3PTPXGXwL2XDYXZ2H4SQX6bOoKm 100 | azTXq6QXuEB665njh1GxXldoIMcSshoJL0hrk3WrTOG22N2CQA+IfHgrXJ+A+QUz 101 | KQIBIw== 102 | -----END PUBLIC KEY----- 103 | EOF 104 | 105 | PUBLIC_KEY2 = <<-EOF 106 | -----BEGIN PUBLIC KEY----- 107 | MIIBIDANBgkqhkiG9w0BAQEFAAOCAQ0AMIIBCAKCAQEAxl6TpN7uFiY/JZ8qDnD7 108 | UrxDP+ABeh2PVg8Du1LEgXNk0+YWCeP5S6oHklqaWeDlbmAs1oHsBwCMAVpMa5tg 109 | ONOLvz4JgwgkiqQEbKR8ofWJ+LADUElvqRVGmGiNEMLI6GJWeneL4sjmbb8d6U+M 110 | 53c6iWG0si9XE5m7teBQSsCl0Tk3qMIkQGw5zpJeCXjZ8KpJhIJRYgexFkGgPlYR 111 | V+UYIhxpUW90t0Ra5i6JOFYwq98k5S/6SJIZQ/A9F4JNzwLw3eVxZj0yVHWxkGz1 112 | +TyELNY1kOyMxnZaqSfGzSQJTrnIXpdweVHuYh1LtOgedRQhCyiELeSMGwio1vRP 113 | KwIBIw== 114 | -----END PUBLIC KEY----- 115 | EOF 116 | 117 | PUBLIC_KEY3 = <<-EOF 118 | -----BEGIN PUBLIC KEY----- 119 | MIIBuDCCASwGByqGSM44BAEwggEfAoGBALyVy5dwVwgL3CxXzsvo8DBh58qArQLB 120 | NIPW/f9pptmy7jD5QXzOw+12w0/z4lZ86ncoVutRMf44OABcX9ovhRl+luxB7jjp 121 | kVXy/p2ZaqPbeyTQUtdTmXa2y4n053Jd61VeMG+iLP7+viT+Ib96y9aVUYQfCrl5 122 | heBDUZ9cAFjdAhUAxV5zuySaRSsJHqKK+Blhh7c9A9kCgYEAqel0RUBO0MY5b3DZ 123 | 69J/mRzUifN1O6twk4er2ph0JpryuUwZohLpcVZwqoGWmPQy/ZHmV1b3RtT9GWUa 124 | +HUqKdMhFVOx/iq1khVfLi83whjMMvXj3ecqd0yzGxGHnSsjVKefa2ywCLHrh4nl 125 | UVIaXI5gQpgMyVbMcromDe1WZzoDgYUAAoGBAIwTRPAEcroqOzaebiVspFcmsXxD 126 | Q4wXQZQdho1ExW6FKS8s7/6pItmZYXTvJDwLXgq2/iK1fRRcKk2PJEaSuJR7WeNG 127 | sJKfWmQ2UbOhqA3wWLDazIZtcMKjFzD0hM4E8qgjHjMvKDE6WgT6SFP+tqx3nnh7 128 | pJWwsbGjSMQexpyR 129 | -----END PUBLIC KEY----- 130 | EOF 131 | 132 | PUBLIC_KEY4 = <<-EOF 133 | -----BEGIN PUBLIC KEY----- 134 | MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEQOAmNzXT3XN5DQdHBYCgflosVlHd 135 | 6MUB1n9n6CCijvVJCQGJAA0p6+3o91ccyA0zHXuUno2eMzBUDghfNZYnHg== 136 | -----END PUBLIC KEY----- 137 | EOF 138 | 139 | SSH_PUBLIC_KEY1 = 'AAAAB3NzaC1yc2EAAAABIwAAAQEArfTA/lKVR84IMc9ZzXOCHr8DVtR8hzWuEVHF6KElavRHlk14g0SZu3m908Ejm/XF3EfNHjX9wN+62IMA0QBxkBMFCuLF+U/oeUs0NoDdAEKxjj4n6lq6Ss8aLct+anMy7D1jwvOLbcwV54w1d5JDdlZVdZ6AvHm9otwJq6rNpDgdmXY4HgC2nM9csFpuy0cDpL6fdJx9lcNL2RnkRC4+RMsIB+PxDw0j3vDi04dYLBXMGYjyeGH+mIFpL3PTPXGXwL2XDYXZ2H4SQX6bOoKmazTXq6QXuEB665njh1GxXldoIMcSshoJL0hrk3WrTOG22N2CQA+IfHgrXJ+A+QUzKQ==' 140 | SSH_PUBLIC_KEY2 = 'AAAAB3NzaC1yc2EAAAABIwAAAQEAxl6TpN7uFiY/JZ8qDnD7UrxDP+ABeh2PVg8Du1LEgXNk0+YWCeP5S6oHklqaWeDlbmAs1oHsBwCMAVpMa5tgONOLvz4JgwgkiqQEbKR8ofWJ+LADUElvqRVGmGiNEMLI6GJWeneL4sjmbb8d6U+M53c6iWG0si9XE5m7teBQSsCl0Tk3qMIkQGw5zpJeCXjZ8KpJhIJRYgexFkGgPlYRV+UYIhxpUW90t0Ra5i6JOFYwq98k5S/6SJIZQ/A9F4JNzwLw3eVxZj0yVHWxkGz1+TyELNY1kOyMxnZaqSfGzSQJTrnIXpdweVHuYh1LtOgedRQhCyiELeSMGwio1vRPKw==' 141 | SSH_PUBLIC_KEY3 = 'AAAAB3NzaC1kc3MAAACBALyVy5dwVwgL3CxXzsvo8DBh58qArQLBNIPW/f9pptmy7jD5QXzOw+12w0/z4lZ86ncoVutRMf44OABcX9ovhRl+luxB7jjpkVXy/p2ZaqPbeyTQUtdTmXa2y4n053Jd61VeMG+iLP7+viT+Ib96y9aVUYQfCrl5heBDUZ9cAFjdAAAAFQDFXnO7JJpFKwkeoor4GWGHtz0D2QAAAIEAqel0RUBO0MY5b3DZ69J/mRzUifN1O6twk4er2ph0JpryuUwZohLpcVZwqoGWmPQy/ZHmV1b3RtT9GWUa+HUqKdMhFVOx/iq1khVfLi83whjMMvXj3ecqd0yzGxGHnSsjVKefa2ywCLHrh4nlUVIaXI5gQpgMyVbMcromDe1WZzoAAACBAIwTRPAEcroqOzaebiVspFcmsXxDQ4wXQZQdho1ExW6FKS8s7/6pItmZYXTvJDwLXgq2/iK1fRRcKk2PJEaSuJR7WeNGsJKfWmQ2UbOhqA3wWLDazIZtcMKjFzD0hM4E8qgjHjMvKDE6WgT6SFP+tqx3nnh7pJWwsbGjSMQexpyR' 142 | SSH_PUBLIC_KEY4 = 'AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEDgJjc1091zeQ0HRwWAoH5aLFZR3ejFAdZ/Z+ggoo71SQkBiQANKevt6PdXHMgNMx17lJ6NnjMwVA4IXzWWJx4=' 143 | 144 | SSH_PUBLIC_KEY_ED25519 = 'AAAAC3NzaC1lZDI1NTE5AAAAIBrNsRCISAtKXV5OVxqV6unVcdis5Uh3oiC6B7CMB7HQ' 145 | SSH_PUBLIC_KEY_ED25519_0_BYTE = 'AAAAC3NzaC1lZDI1NTE5AAAAIADK9x9t3yQQH7h4OEJpUa7l2j7mcmKf4LAsNXHxNbSm' 146 | 147 | SSH_PUBLIC_KEY_ECDSA_256 = 'AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBHJFDZ5qymZfIzoJcxYeu3C9HjJ08QAbqR28C2zSMLwcb3ZzWdRApnj6wEgRvizsBmr9zyPKb2u5Rp0vjJtQcZo=' 148 | SSH_PUBLIC_KEY_ECDSA_384 = 'AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBP+GtUCOR8aW7xTtpkbJS0qqNZ98PgbUNtTFhE+Oe+khgoFMX+o0JG5bckVuvtkRl8dr+63kUK0QPTtzP9O5yixB9CYnB8CgCgYo1FCXZuJIImf12wW5nWKglrCH4kV1Qg==' 149 | SSH_PUBLIC_KEY_ECDSA_521 = 'AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBACsunidnIZ77AjCHSDp/xknLGDW3M0Ia7nxLdImmp0XGbxtbwYm2ga5XUzV9dMO9wF9ICC3OuH6g9DtGOBNPru1PwFDjaPISGgm0vniEzWazLsvjJVLThOA3VyYLxmtjm0WfS+/DfxgWVS6oeCTnDjjoVVpwU/fDbUbYPPRZI84/hOGNA==' 150 | 151 | SSH_PUBLIC_KEY_ECDSA_256_COMPRESSED = 'AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAAAhA+YNpJJrrUsu5OLLvqGX5pAH3+x6/yEFU2AYdxb54Jk8' 152 | SSH_PUBLIC_KEY_ECDSA_384_COMPRESSED = 'AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAAAxAgMhp0cNvtzncxXF0W5nrkBCTrxJIcYqUTX4RcKWIM74FfxizmWJqP/C+looEz6dLQ==' 153 | SSH_PUBLIC_KEY_ECDSA_521_COMPRESSED = 'AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAABDAgDoeNR4bndT24BosNaTKCLOALjL6tXrpNHn0HJzHO5z30L4SvH0Gz9jvAiqehNHOgmK3/bFbwLVW1W4TJbNsp8BVA==' 154 | 155 | KEY1_MD5_FINGERPRINT = "2a:89:84:c9:29:05:d1:f8:49:79:1c:ba:73:99:eb:af" 156 | KEY2_MD5_FINGERPRINT = "3c:af:74:87:cc:cc:a1:12:05:1a:09:b7:7b:ce:ed:ce" 157 | KEY3_MD5_FINGERPRINT = "14:f6:6a:12:96:be:44:32:e6:3c:77:43:94:52:f5:7a" 158 | KEY4_MD5_FINGERPRINT = "38:0b:0f:63:36:64:b6:f0:43:94:de:32:75:eb:57:68" 159 | ED25519_MD5_FINGERPRINT = "6f:1a:8a:c1:4f:13:5c:36:6e:3f:be:eb:49:3b:8e:3e" 160 | ECDSA_256_MD5_FINGERPRINT = "d9:3a:7f:de:b2:65:04:ac:62:05:1a:1e:97:e9:2b:9d" 161 | ECDSA_384_MD5_FINGERPRINT = "b5:bb:3e:f6:eb:3b:0f:1e:18:37:1f:36:ac:7c:87:0d" 162 | ECDSA_521_MD5_FINGERPRINT = "98:8e:a9:4c:b9:aa:58:35:d1:42:65:c3:41:dd:04:e1" 163 | 164 | KEY1_SHA1_FINGERPRINT = "e4:f9:79:f2:fe:d6:be:2d:ef:2e:c2:fa:aa:f8:b0:17:34:fe:0d:c0" 165 | KEY2_SHA1_FINGERPRINT = "9a:52:78:2b:6b:cb:39:b7:85:ed:90:8a:28:62:aa:b3:98:88:e6:07" 166 | KEY3_SHA1_FINGERPRINT = "15:68:c6:72:ac:18:d1:fc:ab:a2:b7:b5:8c:d1:fe:8f:b9:ae:a9:47" 167 | KEY4_SHA1_FINGERPRINT = "aa:b5:e6:62:27:87:b8:05:f6:d6:8f:31:dc:83:81:d9:8f:f8:71:29" 168 | ED25519_SHA1_FINGERPRINT = "57:41:7c:d0:e2:53:28:87:7e:87:53:d4:69:ef:ef:63:ec:c0:0e:5e" 169 | ECDSA_256_SHA1_FINGERPRINT = "94:e8:92:2b:1b:ec:49:de:ff:85:ea:6e:10:d6:8d:87:7a:67:40:ee" 170 | ECDSA_384_SHA1_FINGERPRINT = "cc:fb:4c:d6:e9:d0:03:ae:2d:82:e1:fc:70:d8:47:98:25:e1:83:2b" 171 | ECDSA_521_SHA1_FINGERPRINT = "6b:2c:a2:6e:3a:82:6c:73:28:57:91:20:71:82:bc:8f:f8:9d:6c:41" 172 | 173 | KEY1_SHA256_FINGERPRINT = "js3llFehloxCfsVuDw5xu3NtS9AOAxcXY8WL6vkDIts=" 174 | KEY2_SHA256_FINGERPRINT = "23f/6U/LdxIFx1CQFKHylw76n+LIHYoY4nRxKcFoos4=" 175 | KEY3_SHA256_FINGERPRINT = "mPqEPQlOPGORrTJrU17sPax1jOqeutZja6MOsFIca+8=" 176 | KEY4_SHA256_FINGERPRINT = "foUpf1ox3KfG3eKgJxGoSdZFRxHPsBYJgfD+CMYky6Y=" 177 | ED25519_SHA256_FINGERPRINT = "gyzHUKl1eO8Bk1Cvn4joRgxRlXo1+1HJ3Vho/hAtKEg=" 178 | ECDSA_256_SHA256_FINGERPRINT = "ncy2crhoL44R58GCZPQ5chPRrjlQKKgu07FDNelDmdk=" 179 | ECDSA_384_SHA256_FINGERPRINT = "mrr4QcP6qD05DUS6Rwefb9f0uuvjyMcO28LSiq2283U=" 180 | ECDSA_521_SHA256_FINGERPRINT = "QnaiGMIVDZyTG47hMWK6Y1z/yUzHIcTBGpNNuUwlhAk=" 181 | 182 | KEY1_RANDOMART = <<-EOF.rstrip 183 | +--[ RSA 2048]----+ 184 | |o+ o.. | 185 | |..+.o | 186 | | ooo | 187 | |.++. o | 188 | |+o+ + S | 189 | |.. + o . | 190 | | . + . | 191 | | . . | 192 | | Eo. | 193 | +-----------------+ 194 | EOF 195 | 196 | KEY2_RANDOMART = <<-EOF.rstrip 197 | +--[ RSA 2048]----+ 198 | | ..o.. | 199 | | ..+ . | 200 | | o . | 201 | | . o | 202 | | . o S . | 203 | | + o O o | 204 | | + + O . | 205 | | = o . | 206 | | .E | 207 | +-----------------+ 208 | EOF 209 | 210 | KEY3_RANDOMART = <<-EOF.rstrip 211 | +--[ DSA 1024]----+ 212 | | .=o. | 213 | | .+.o . | 214 | | + =.o . . | 215 | | + * + . . | 216 | | + = S . E | 217 | | + = . . | 218 | | . | 219 | | | 220 | | | 221 | +-----------------+ 222 | EOF 223 | 224 | # ssh-keygen -lv -E md5 -f ./id_ecdsa_ssh_public_key4.pub 225 | KEY4_RANDOMART = <<-EOF.rstrip 226 | +--[ECDSA 256]----+ 227 | | .. | 228 | | .. . . | 229 | | ..=o . . . | 230 | | B+.... E . | 231 | | @oo.S. . | 232 | | o B o. . | 233 | | o . | 234 | | | 235 | | | 236 | +-----------------+ 237 | EOF 238 | 239 | # ssh-keygen -lv -E sha256 -f ./id_ecdsa_ssh_public_key4.pub 240 | KEY4_RANDOMART_USING_SHA256_DIGEST = <<-EOF.rstrip 241 | +--[ECDSA 256]----+ 242 | | .. o++B+ | 243 | | .. ...* | 244 | | . ...o o o | 245 | | . =o.o .= . | 246 | | +o+oS o.= . .| 247 | | o .oo =.. + +.| 248 | | E o +.+ = o| 249 | | ..=.+ . | 250 | | oo . | 251 | +-----------------+ 252 | EOF 253 | 254 | # ssh-keygen -lv -E sha384 -f ./id_ecdsa_ssh_public_key4.pub 255 | KEY4_RANDOMART_USING_SHA384_DIGEST = <<-EOF.rstrip 256 | +--[ECDSA 256]----+ 257 | | o++. | 258 | | . *oo. . | 259 | |o .o+B.o.. | 260 | |+o ooB+O *..| 261 | |.=+ .SB== ^.+.| 262 | |+ o +o .O Xo.| 263 | | . ... .. + .o| 264 | | . E. o + + +..| 265 | | .... . o..Bo..| 266 | +-----------------+ 267 | EOF 268 | 269 | # ssh-keygen -lv -E sha512 -f ./id_ecdsa_ssh_public_key4.pub 270 | KEY4_RANDOMART_USING_SHA512_DIGEST = <<-EOF.rstrip 271 | +--[ECDSA 256]----+ 272 | | +*+o oo| 273 | | . .o o . +| 274 | | . o. oo oo| 275 | |.. .+ . .*.o+ | 276 | |..Bo.* S ..=o..| 277 | | .+X+ Oo ...+ | 278 | | +o.B*+=o .+ +| 279 | |+=+O.+=+.+. +.o+.| 280 | |@**EB*O++=o+ =o.+| 281 | +-----------------+ 282 | EOF 283 | 284 | KEY1_SSHFP = <<-EOF.rstrip 285 | localhost IN SSHFP 1 1 e4f979f2fed6be2def2ec2faaaf8b01734fe0dc0 286 | localhost IN SSHFP 1 2 8ecde59457a1968c427ec56e0f0e71bb736d4bd00e03171763c58beaf90322db 287 | EOF 288 | 289 | KEY2_SSHFP = <<-EOF.rstrip 290 | localhost IN SSHFP 1 1 9a52782b6bcb39b785ed908a2862aab39888e607 291 | localhost IN SSHFP 1 2 db77ffe94fcb771205c7509014a1f2970efa9fe2c81d8a18e2747129c168a2ce 292 | EOF 293 | 294 | KEY3_SSHFP = <<-EOF.rstrip 295 | localhost IN SSHFP 2 1 1568c672ac18d1fcaba2b7b58cd1fe8fb9aea947 296 | localhost IN SSHFP 2 2 98fa843d094e3c6391ad326b535eec3dac758cea9ebad6636ba30eb0521c6bef 297 | EOF 298 | 299 | SSH2_PUBLIC_KEY1 = <<-EOF.rstrip 300 | ---- BEGIN SSH2 PUBLIC KEY ---- 301 | Comment: me@example.com 302 | AAAAB3NzaC1yc2EAAAABIwAAAQEArfTA/lKVR84IMc9ZzXOCHr8DVtR8hzWuEVHF6KElav 303 | RHlk14g0SZu3m908Ejm/XF3EfNHjX9wN+62IMA0QBxkBMFCuLF+U/oeUs0NoDdAEKxjj4n 304 | 6lq6Ss8aLct+anMy7D1jwvOLbcwV54w1d5JDdlZVdZ6AvHm9otwJq6rNpDgdmXY4HgC2nM 305 | 9csFpuy0cDpL6fdJx9lcNL2RnkRC4+RMsIB+PxDw0j3vDi04dYLBXMGYjyeGH+mIFpL3PT 306 | PXGXwL2XDYXZ2H4SQX6bOoKmazTXq6QXuEB665njh1GxXldoIMcSshoJL0hrk3WrTOG22N 307 | 2CQA+IfHgrXJ+A+QUzKQ== 308 | ---- END SSH2 PUBLIC KEY ---- 309 | EOF 310 | 311 | SSH2_PUBLIC_KEY2 = <<-EOF.rstrip 312 | ---- BEGIN SSH2 PUBLIC KEY ---- 313 | AAAAB3NzaC1yc2EAAAABIwAAAQEAxl6TpN7uFiY/JZ8qDnD7UrxDP+ABeh2PVg8Du1LEgX 314 | Nk0+YWCeP5S6oHklqaWeDlbmAs1oHsBwCMAVpMa5tgONOLvz4JgwgkiqQEbKR8ofWJ+LAD 315 | UElvqRVGmGiNEMLI6GJWeneL4sjmbb8d6U+M53c6iWG0si9XE5m7teBQSsCl0Tk3qMIkQG 316 | w5zpJeCXjZ8KpJhIJRYgexFkGgPlYRV+UYIhxpUW90t0Ra5i6JOFYwq98k5S/6SJIZQ/A9 317 | F4JNzwLw3eVxZj0yVHWxkGz1+TyELNY1kOyMxnZaqSfGzSQJTrnIXpdweVHuYh1LtOgedR 318 | QhCyiELeSMGwio1vRPKw== 319 | ---- END SSH2 PUBLIC KEY ---- 320 | EOF 321 | 322 | SSH2_PUBLIC_KEY3 = <<-EOF.rstrip 323 | ---- BEGIN SSH2 PUBLIC KEY ---- 324 | Comment: 1024-bit DSA with provided comment 325 | x-private-use-header: some value that is long enough to go to wrap aro\\ 326 | und to a new line. 327 | AAAAB3NzaC1kc3MAAACBALyVy5dwVwgL3CxXzsvo8DBh58qArQLBNIPW/f9pptmy7jD5QX 328 | zOw+12w0/z4lZ86ncoVutRMf44OABcX9ovhRl+luxB7jjpkVXy/p2ZaqPbeyTQUtdTmXa2 329 | y4n053Jd61VeMG+iLP7+viT+Ib96y9aVUYQfCrl5heBDUZ9cAFjdAAAAFQDFXnO7JJpFKw 330 | keoor4GWGHtz0D2QAAAIEAqel0RUBO0MY5b3DZ69J/mRzUifN1O6twk4er2ph0JpryuUwZ 331 | ohLpcVZwqoGWmPQy/ZHmV1b3RtT9GWUa+HUqKdMhFVOx/iq1khVfLi83whjMMvXj3ecqd0 332 | yzGxGHnSsjVKefa2ywCLHrh4nlUVIaXI5gQpgMyVbMcromDe1WZzoAAACBAIwTRPAEcroq 333 | OzaebiVspFcmsXxDQ4wXQZQdho1ExW6FKS8s7/6pItmZYXTvJDwLXgq2/iK1fRRcKk2PJE 334 | aSuJR7WeNGsJKfWmQ2UbOhqA3wWLDazIZtcMKjFzD0hM4E8qgjHjMvKDE6WgT6SFP+tqx3 335 | nnh7pJWwsbGjSMQexpyR 336 | ---- END SSH2 PUBLIC KEY ---- 337 | EOF 338 | 339 | def setup 340 | @key1 = SSHKey.new(SSH_PRIVATE_KEY1, :comment => "me@example.com") 341 | @key2 = SSHKey.new(SSH_PRIVATE_KEY2, :comment => "me@example.com") 342 | @key3 = SSHKey.new(SSH_PRIVATE_KEY3, :comment => "me@example.com") 343 | @key4 = SSHKey.new(SSH_PRIVATE_KEY4, :comment => "me@example.com") 344 | @key_without_comment = SSHKey.new(SSH_PRIVATE_KEY1) 345 | end 346 | 347 | def test_generator_with_no_args 348 | assert_kind_of SSHKey, SSHKey.generate 349 | end 350 | 351 | def test_generator_with_comment 352 | assert_equal "foo", SSHKey.generate(:comment => "foo").comment 353 | end 354 | 355 | def test_generator_with_type 356 | assert_equal "rsa", SSHKey.generate(:type => "rsa").type 357 | assert_equal "dsa", SSHKey.generate(:type => "dsa").type 358 | 359 | if ecdsa_supported? 360 | assert_equal "ecdsa", SSHKey.generate(:type => "ecdsa").type 361 | else 362 | assert_raises(NotImplementedError) { SSHKey.generate(:type => "ecdsa").type } 363 | end 364 | end 365 | 366 | def test_generator_with_passphrase 367 | assert_equal "password", SSHKey.generate(:passphrase => "password").passphrase 368 | end 369 | 370 | def test_private_key1 371 | assert_equal SSH_PRIVATE_KEY1, @key1.private_key 372 | assert_equal SSH_PRIVATE_KEY1, @key1.rsa_private_key 373 | end 374 | 375 | def test_private_key2 376 | assert_equal SSH_PRIVATE_KEY2, @key2.private_key 377 | assert_equal SSH_PRIVATE_KEY2, @key2.rsa_private_key 378 | end 379 | 380 | def test_private_key3 381 | assert_equal SSH_PRIVATE_KEY3, @key3.private_key 382 | assert_equal SSH_PRIVATE_KEY3, @key3.dsa_private_key 383 | end 384 | 385 | def test_private_key4 386 | if ecdsa_supported? 387 | assert_equal SSH_PRIVATE_KEY4, @key4.private_key 388 | else 389 | assert_raises(NotImplementedError) { @key4.private_key } 390 | end 391 | end 392 | 393 | def test_public_key_1 394 | assert_equal PUBLIC_KEY1, @key1.public_key 395 | end 396 | 397 | def test_public_key_2 398 | assert_equal PUBLIC_KEY2, @key2.public_key 399 | end 400 | 401 | def test_public_key_3 402 | assert_equal PUBLIC_KEY3, @key3.public_key 403 | end 404 | 405 | def test_public_key_4 406 | assert_equal PUBLIC_KEY4, @key4.public_key 407 | end 408 | 409 | def test_ssh_public_key_decoded1 410 | assert_equal Base64.decode64(SSH_PUBLIC_KEY1), @key1.send(:ssh_public_key_conversion) 411 | end 412 | 413 | def test_ssh_public_key_decoded2 414 | assert_equal Base64.decode64(SSH_PUBLIC_KEY2), @key2.send(:ssh_public_key_conversion) 415 | end 416 | 417 | def test_ssh_public_key_decoded3 418 | assert_equal Base64.decode64(SSH_PUBLIC_KEY3), @key3.send(:ssh_public_key_conversion) 419 | end 420 | 421 | def test_ssh_public_key_decoded4 422 | if ecdsa_supported? 423 | assert_equal Base64.decode64(SSH_PUBLIC_KEY4), @key4.send(:ssh_public_key_conversion) 424 | else 425 | assert_raises(NotImplementedError) { @key4.send(:ssh_public_key_conversion) } 426 | end 427 | end 428 | 429 | def test_ssh_public_key_encoded1 430 | assert_equal SSH_PUBLIC_KEY1, Base64.encode64(@key1.send(:ssh_public_key_conversion)).gsub("\n", "") 431 | end 432 | 433 | def test_ssh_public_key_encoded2 434 | assert_equal SSH_PUBLIC_KEY2, Base64.encode64(@key2.send(:ssh_public_key_conversion)).gsub("\n", "") 435 | end 436 | 437 | def test_ssh_public_key_encoded3 438 | assert_equal SSH_PUBLIC_KEY3, Base64.encode64(@key3.send(:ssh_public_key_conversion)).gsub("\n", "") 439 | end 440 | 441 | def test_ssh_public_key_encoded4 442 | if ecdsa_supported? 443 | assert_equal SSH_PUBLIC_KEY4, Base64.encode64(@key4.send(:ssh_public_key_conversion)).gsub("\n", "") 444 | else 445 | assert_raises(NotImplementedError) { Base64.encode64(@key4.send(:ssh_public_key_conversion)) } 446 | end 447 | end 448 | 449 | def test_ssh_public_key_output 450 | expected1 = "ssh-rsa #{SSH_PUBLIC_KEY1} me@example.com" 451 | expected2 = "ssh-rsa #{SSH_PUBLIC_KEY2} me@example.com" 452 | expected3 = "ssh-dss #{SSH_PUBLIC_KEY3} me@example.com" 453 | expected4 = "ecdsa-sha2-nistp256 #{SSH_PUBLIC_KEY4} me@example.com" 454 | expected1b = "ssh-rsa #{SSH_PUBLIC_KEY1}" 455 | assert_equal expected1, @key1.ssh_public_key 456 | assert_equal expected2, @key2.ssh_public_key 457 | assert_equal expected3, @key3.ssh_public_key 458 | 459 | if ecdsa_supported? 460 | assert_equal expected4, @key4.ssh_public_key 461 | else 462 | assert_raises(NotImplementedError) { @key4.ssh_public_key } 463 | end 464 | 465 | assert_equal expected1b, @key_without_comment.ssh_public_key 466 | end 467 | 468 | def test_ssh2_public_key_output 469 | expected1 = SSH2_PUBLIC_KEY1 470 | expected2 = SSH2_PUBLIC_KEY2 471 | expected3 = SSH2_PUBLIC_KEY3 472 | 473 | assert_equal expected1, @key1.ssh2_public_key 474 | assert_equal expected2, @key2.ssh2_public_key({}) 475 | assert_equal expected3, @key3.ssh2_public_key({'Comment' => '1024-bit DSA with provided comment', 476 | 'x-private-use-header' => 'some value that is long enough to go to wrap around to a new line.'}) 477 | end 478 | 479 | def test_ssh_public_key_output_from_generated 480 | generated_rsa = SSHKey.generate(:type => "rsa", :comment => "rsa key") 481 | generated_dsa = SSHKey.generate(:type => "dsa", :comment => "dsa key") 482 | generated_ecdsa = SSHKey.generate(:type => "ecdsa", :comment => "ecdsa key") if ecdsa_supported? 483 | 484 | encoded_rsa = Base64.encode64(generated_rsa.send(:ssh_public_key_conversion)).gsub("\n", "") 485 | encoded_dsa = Base64.encode64(generated_dsa.send(:ssh_public_key_conversion)).gsub("\n", "") 486 | encoded_ecdsa = Base64.encode64(generated_ecdsa.send(:ssh_public_key_conversion)).gsub("\n", "") if ecdsa_supported? 487 | 488 | expected_rsa = "ssh-rsa #{encoded_rsa} rsa key" 489 | expected_dsa = "ssh-dss #{encoded_dsa} dsa key" 490 | expected_ecdsa = "ecdsa-sha2-nistp256 #{encoded_ecdsa} ecdsa key" 491 | 492 | assert_equal expected_rsa, generated_rsa.ssh_public_key 493 | assert_equal expected_dsa, generated_dsa.ssh_public_key 494 | assert_equal expected_ecdsa, generated_ecdsa.ssh_public_key if ecdsa_supported? 495 | end 496 | 497 | def test_public_key_directives 498 | assert_equal [], SSHKey.generate.directives 499 | 500 | @key1.directives = "no-pty" 501 | assert_equal ["no-pty"], @key1.directives 502 | 503 | @key1.directives = ["no-pty"] 504 | assert_equal ["no-pty"], @key1.directives 505 | 506 | @key1.directives = [ 507 | "no-port-forwarding", 508 | "no-X11-forwarding", 509 | "no-agent-forwarding", 510 | "no-pty", 511 | "command='/home/user/bin/authprogs'" 512 | ] 513 | expected1 = "no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,command='/home/user/bin/authprogs' ssh-rsa #{SSH_PUBLIC_KEY1} me@example.com" 514 | assert_equal expected1, @key1.ssh_public_key 515 | assert SSHKey.valid_ssh_public_key?(expected1) 516 | 517 | @key2.directives = "no-pty" 518 | expected2 = "no-pty ssh-rsa #{SSH_PUBLIC_KEY2} me@example.com" 519 | assert_equal expected2, @key2.ssh_public_key 520 | assert SSHKey.valid_ssh_public_key?(expected2) 521 | end 522 | 523 | def test_ssh_public_key_validation 524 | expected1 = "ssh-rsa #{SSH_PUBLIC_KEY1} me@example.com" 525 | expected2 = "ssh-rsa #{SSH_PUBLIC_KEY2} me@example.com" 526 | expected3 = "ssh-dss #{SSH_PUBLIC_KEY3} me@example.com" 527 | expected4 = "ssh-rsa #{SSH_PUBLIC_KEY1}" 528 | expected5 = %Q{from="trusted.eng.cam.ac.uk",no-port-forwarding,no-pty ssh-rsa #{SSH_PUBLIC_KEY1}} 529 | invalid1 = "ssh-rsa #{SSH_PUBLIC_KEY1}= me@example.com" 530 | invalid2 = "ssh-rsa #{SSH_PUBLIC_KEY2}= me@example.com" 531 | invalid3 = "ssh-dss #{SSH_PUBLIC_KEY3}= me@example.com" 532 | invalid4 = "ssh-rsa A#{SSH_PUBLIC_KEY1}" 533 | invalid5 = "ssh-rsa #{SSH_PUBLIC_KEY3} me@example.com" 534 | 535 | assert SSHKey.valid_ssh_public_key?(expected1) 536 | assert SSHKey.valid_ssh_public_key?(expected2) 537 | assert SSHKey.valid_ssh_public_key?(expected3) 538 | assert SSHKey.valid_ssh_public_key?(expected4) 539 | assert SSHKey.valid_ssh_public_key?(expected5) 540 | 541 | assert !SSHKey.valid_ssh_public_key?(invalid1) 542 | assert !SSHKey.valid_ssh_public_key?(invalid2) 543 | assert !SSHKey.valid_ssh_public_key?(invalid3) 544 | assert !SSHKey.valid_ssh_public_key?(invalid4) 545 | assert !SSHKey.valid_ssh_public_key?(invalid5) 546 | end 547 | 548 | def test_ssh_public_key_validation_elliptic 549 | assert SSHKey.valid_ssh_public_key?("ssh-ed25519 #{SSH_PUBLIC_KEY_ED25519} me@example.com") 550 | assert SSHKey.valid_ssh_public_key?("ssh-ed25519 #{SSH_PUBLIC_KEY_ED25519_0_BYTE} me@example.com") 551 | assert SSHKey.valid_ssh_public_key?("ecdsa-sha2-nistp256 #{SSH_PUBLIC_KEY_ECDSA_256}") 552 | assert SSHKey.valid_ssh_public_key?("ecdsa-sha2-nistp384 #{SSH_PUBLIC_KEY_ECDSA_384} me@example.com") 553 | assert SSHKey.valid_ssh_public_key?(%Q{from="trusted.eng.cam.ac.uk",no-port-forwarding,no-pty ecdsa-sha2-nistp521 #{SSH_PUBLIC_KEY_ECDSA_521} me@example.com}) 554 | 555 | assert !SSHKey.valid_ssh_public_key?("ssh-ed25519 #{SSH_PUBLIC_KEY_ED25519}= me@example.com") # bad base64 556 | assert !SSHKey.valid_ssh_public_key?("ssh-ed25519 #{SSH_PUBLIC_KEY_ECDSA_384} me@example.com") # mismatched key format 557 | assert !SSHKey.valid_ssh_public_key?("ecdsa-sha2-nistp256 #{SSH_PUBLIC_KEY_ECDSA_384} me@example.com") # mismatched key format 558 | assert !SSHKey.valid_ssh_public_key?("ssh-ed25519 asdf me@example.com") # gibberish key data 559 | assert !SSHKey.valid_ssh_public_key?("ecdsa-sha2-nistp256 asdf me@example.com") # gibberish key data 560 | end 561 | 562 | def test_ssh_public_key_validation_with_newlines 563 | expected1 = "ssh-rsa #{SSH_PUBLIC_KEY1}\n" 564 | expected2 = "ssh-ed25519 #{SSH_PUBLIC_KEY_ED25519} me@example.com\n" 565 | invalid1 = "ssh-rsa #{SSH_PUBLIC_KEY1}\nme@example.com" 566 | invalid2 = "ssh-rsa #{SSH_PUBLIC_KEY1}\n me@example.com" 567 | invalid3 = "ssh-rsa #{SSH_PUBLIC_KEY1} \nme@example.com" 568 | invalid4 = "ecdsa-sha2-nistp256 #{SSH_PUBLIC_KEY_ECDSA_256}\nme@example.com" 569 | 570 | assert SSHKey.valid_ssh_public_key?(expected1) 571 | assert SSHKey.valid_ssh_public_key?(expected2) 572 | 573 | assert !SSHKey.valid_ssh_public_key?(invalid1) 574 | assert !SSHKey.valid_ssh_public_key?(invalid2) 575 | assert !SSHKey.valid_ssh_public_key?(invalid3) 576 | assert !SSHKey.valid_ssh_public_key?(invalid4) 577 | end 578 | 579 | def test_ssh_public_key_validation_with_comments 580 | expected1 = "# Comment\nssh-rsa #{SSH_PUBLIC_KEY1}" 581 | expected2 = "# First comment\n\n# Second comment\n\nssh-ed25519 #{SSH_PUBLIC_KEY_ED25519} me@example.com" 582 | invalid1 = "No starting hash # Valid comment\nssh-rsa #{SSH_PUBLIC_KEY1} me@example.com" 583 | invalid2 = "# First comment\n\nSecond comment without hash\n\necdsa-sha2-nistp256 #{SSH_PUBLIC_KEY_ECDSA_256}\nme@example.com" 584 | 585 | assert SSHKey.valid_ssh_public_key?(expected1) 586 | assert SSHKey.valid_ssh_public_key?(expected2) 587 | 588 | assert !SSHKey.valid_ssh_public_key?(invalid1) 589 | assert !SSHKey.valid_ssh_public_key?(invalid2) 590 | end 591 | 592 | def test_ssh_public_key_sshfp 593 | assert_equal KEY1_SSHFP, SSHKey.sshfp("localhost", "ssh-rsa #{SSH_PUBLIC_KEY1}\n") 594 | assert_equal KEY2_SSHFP, SSHKey.sshfp("localhost", "ssh-rsa #{SSH_PUBLIC_KEY2}\n") 595 | assert_equal KEY3_SSHFP, SSHKey.sshfp("localhost", "ssh-dss #{SSH_PUBLIC_KEY3}\n") 596 | assert_equal KEY1_SSHFP, SSHKey.sshfp("localhost", SSH_PRIVATE_KEY1) 597 | assert_equal KEY2_SSHFP, SSHKey.sshfp("localhost", SSH_PRIVATE_KEY2) 598 | assert_equal KEY3_SSHFP, SSHKey.sshfp("localhost", SSH_PRIVATE_KEY3) 599 | end 600 | 601 | def test_ssh_public_key_bits 602 | expected1 = "ssh-rsa #{SSH_PUBLIC_KEY1} me@example.com" 603 | expected2 = "ssh-rsa #{SSH_PUBLIC_KEY2} me@example.com" 604 | expected3 = "ssh-dss #{SSH_PUBLIC_KEY3} me@example.com" 605 | expected4 = "ssh-rsa #{SSH_PUBLIC_KEY1}" 606 | expected5 = %Q{from="trusted.eng.cam.ac.uk",no-port-forwarding,no-pty ssh-rsa #{SSH_PUBLIC_KEY1}} 607 | invalid1 = "#{SSH_PUBLIC_KEY1} me@example.com" 608 | ecdsa256 = "ecdsa-sha2-nistp256 #{SSH_PUBLIC_KEY_ECDSA_256}" 609 | ecdsa384 = "ecdsa-sha2-nistp384 #{SSH_PUBLIC_KEY_ECDSA_384}" 610 | ecdsa521 = "ecdsa-sha2-nistp521 #{SSH_PUBLIC_KEY_ECDSA_521}" 611 | ecdsa256_compressed = "ecdsa-sha2-nistp256 #{SSH_PUBLIC_KEY_ECDSA_256_COMPRESSED}" 612 | ecdsa384_compressed = "ecdsa-sha2-nistp384 #{SSH_PUBLIC_KEY_ECDSA_384_COMPRESSED}" 613 | ecdsa521_compressed = "ecdsa-sha2-nistp521 #{SSH_PUBLIC_KEY_ECDSA_521_COMPRESSED}" 614 | 615 | assert_equal 2048, SSHKey.ssh_public_key_bits(expected1) 616 | assert_equal 2048, SSHKey.ssh_public_key_bits(expected2) 617 | assert_equal 1024, SSHKey.ssh_public_key_bits(expected3) 618 | assert_equal 2048, SSHKey.ssh_public_key_bits(expected4) 619 | assert_equal 2048, SSHKey.ssh_public_key_bits(expected5) 620 | assert_equal 512, SSHKey.ssh_public_key_bits(SSHKey.generate(:bits => 512).ssh_public_key) 621 | assert_equal 256, SSHKey.ssh_public_key_bits(ecdsa256) 622 | assert_equal 384, SSHKey.ssh_public_key_bits(ecdsa384) 623 | assert_equal 521, SSHKey.ssh_public_key_bits(ecdsa521) 624 | assert_equal 256, SSHKey.ssh_public_key_bits(ecdsa256_compressed) 625 | assert_equal 384, SSHKey.ssh_public_key_bits(ecdsa384_compressed) 626 | assert_equal 521, SSHKey.ssh_public_key_bits(ecdsa521_compressed) 627 | 628 | exception1 = assert_raises(SSHKey::PublicKeyError) { SSHKey.ssh_public_key_bits( expected1.gsub('A','.') ) } 629 | exception2 = assert_raises(SSHKey::PublicKeyError) { SSHKey.ssh_public_key_bits( expected1[0..-20] ) } 630 | exception3 = assert_raises(SSHKey::PublicKeyError) { SSHKey.ssh_public_key_bits(invalid1) } 631 | 632 | assert_equal( "validation error", exception1.message ) 633 | assert_equal( "byte array too short", exception2.message ) 634 | assert_equal( "cannot determine key type", exception3.message ) 635 | end 636 | 637 | def test_ssh2_public_key_bits 638 | public_key1 = "ssh-rsa #{SSH_PUBLIC_KEY1} me@example.com" 639 | public_key2 = "ssh-rsa #{SSH_PUBLIC_KEY2}" 640 | public_key3 = "ssh-dss #{SSH_PUBLIC_KEY3} 1024-bit DSA with provided comment" 641 | 642 | assert_equal(SSH2_PUBLIC_KEY1, SSHKey.ssh_public_key_to_ssh2_public_key(public_key1)) 643 | assert_equal(SSH2_PUBLIC_KEY2, SSHKey.ssh_public_key_to_ssh2_public_key(public_key2)) 644 | assert_equal(SSH2_PUBLIC_KEY2, SSHKey.ssh_public_key_to_ssh2_public_key(public_key2, {})) 645 | assert_equal(SSH2_PUBLIC_KEY3, SSHKey.ssh_public_key_to_ssh2_public_key(public_key3, {'Comment' => '1024-bit DSA with provided comment', 'x-private-use-header' => 'some value that is long enough to go to wrap around to a new line.'})) 646 | end 647 | 648 | def test_exponent 649 | assert_equal 35, @key1.key_object.e.to_i 650 | assert_equal 35, @key2.key_object.e.to_i 651 | end 652 | 653 | def test_modulus 654 | assert_equal 21959919395955180268707532246136630338880737002345156586705317733493418045367765414088155418090419238250026039981229751319343545922377196559932805781226688384973919515037364518167604848468288361633800200593870224270802677578686553567598208927704479575929054501425347794297979215349516030584575472280923909378896367886007339003194417496761108245404573433556449606964806956220743380296147376168499567508629678037211105349574822849913423806275470761711930875368363589001630573570236600319099783704171412637535837916991323769813598516411655563604244942820475880695152610674934239619752487880623016350579174487901241422633, @key1.key_object.n.to_i 655 | assert_equal 25041821909255634338594631709409930006900629565221199423527442992482865961613864019776541767273966885435978473182530882048894721263421597979360058644777295324028381353356143013803778109979347540540538361684778724178534886189535456555760676722894784592989232554962714835255398111716791175503010379276254975882143986862239829255392231575481418297073759441882528318940783011390002193682320028951031205422825662402426266933458263786546846123394508656926946338411182471843223455365249418245551220933173115037201835242811305615780499842939975996344432437312062643436832423359634116147870328774728910949553186982115987967787, @key2.key_object.n.to_i 656 | end 657 | 658 | def test_fingerprint 659 | assert_equal KEY1_MD5_FINGERPRINT, @key1.md5_fingerprint 660 | assert_equal KEY1_MD5_FINGERPRINT, @key1.fingerprint # Aliased method 661 | assert_equal KEY2_MD5_FINGERPRINT, @key2.md5_fingerprint 662 | assert_equal KEY3_MD5_FINGERPRINT, @key3.md5_fingerprint 663 | 664 | if ecdsa_supported? 665 | assert_equal KEY4_MD5_FINGERPRINT, @key4.md5_fingerprint 666 | else 667 | assert_raises(NotImplementedError) { @key4.md5_fingerprint } 668 | end 669 | 670 | assert_equal KEY1_SHA1_FINGERPRINT, @key1.sha1_fingerprint 671 | assert_equal KEY2_SHA1_FINGERPRINT, @key2.sha1_fingerprint 672 | assert_equal KEY3_SHA1_FINGERPRINT, @key3.sha1_fingerprint 673 | 674 | if ecdsa_supported? 675 | assert_equal KEY4_SHA1_FINGERPRINT, @key4.sha1_fingerprint 676 | else 677 | assert_raises(NotImplementedError) { @key4.sha1_fingerprint } 678 | end 679 | 680 | assert_equal KEY1_SHA256_FINGERPRINT, @key1.sha256_fingerprint 681 | assert_equal KEY2_SHA256_FINGERPRINT, @key2.sha256_fingerprint 682 | assert_equal KEY3_SHA256_FINGERPRINT, @key3.sha256_fingerprint 683 | 684 | if ecdsa_supported? 685 | assert_equal KEY4_SHA256_FINGERPRINT, @key4.sha256_fingerprint 686 | else 687 | assert_raises(NotImplementedError) { @key4.sha256_fingerprint } 688 | end 689 | 690 | assert_equal KEY1_MD5_FINGERPRINT, SSHKey.md5_fingerprint(SSH_PRIVATE_KEY1) 691 | assert_equal KEY1_MD5_FINGERPRINT, SSHKey.md5_fingerprint("ssh-rsa #{SSH_PUBLIC_KEY1}") 692 | assert_equal KEY2_MD5_FINGERPRINT, SSHKey.md5_fingerprint(SSH_PRIVATE_KEY2) 693 | assert_equal KEY2_MD5_FINGERPRINT, SSHKey.md5_fingerprint("ssh-rsa #{SSH_PUBLIC_KEY2} me@me.com") 694 | assert_equal KEY3_MD5_FINGERPRINT, SSHKey.md5_fingerprint(SSH_PRIVATE_KEY3) 695 | assert_equal KEY3_MD5_FINGERPRINT, SSHKey.md5_fingerprint("ssh-dss #{SSH_PUBLIC_KEY3}") 696 | assert_equal KEY4_MD5_FINGERPRINT, SSHKey.md5_fingerprint("ecdsa-sha2-nistp256 #{SSH_PUBLIC_KEY4}") 697 | assert_equal ED25519_MD5_FINGERPRINT, SSHKey.md5_fingerprint("ssh-ed25519 #{SSH_PUBLIC_KEY_ED25519}") 698 | assert_equal ECDSA_256_MD5_FINGERPRINT, SSHKey.md5_fingerprint("ecdsa-sha2-nistp256 #{SSH_PUBLIC_KEY_ECDSA_256} me@me.com") 699 | assert_equal ECDSA_384_MD5_FINGERPRINT, SSHKey.md5_fingerprint("ecdsa-sha2-nistp384 #{SSH_PUBLIC_KEY_ECDSA_384} me@me.com") 700 | assert_equal ECDSA_521_MD5_FINGERPRINT, SSHKey.md5_fingerprint("ecdsa-sha2-nistp521 #{SSH_PUBLIC_KEY_ECDSA_521} me@me.com") 701 | 702 | assert_equal KEY1_SHA1_FINGERPRINT, SSHKey.sha1_fingerprint(SSH_PRIVATE_KEY1) 703 | assert_equal KEY1_SHA1_FINGERPRINT, SSHKey.sha1_fingerprint("ssh-rsa #{SSH_PUBLIC_KEY1}") 704 | assert_equal KEY2_SHA1_FINGERPRINT, SSHKey.sha1_fingerprint(SSH_PRIVATE_KEY2) 705 | assert_equal KEY2_SHA1_FINGERPRINT, SSHKey.sha1_fingerprint("ssh-rsa #{SSH_PUBLIC_KEY2} me@me.com") 706 | assert_equal KEY3_SHA1_FINGERPRINT, SSHKey.sha1_fingerprint(SSH_PRIVATE_KEY3) 707 | assert_equal KEY3_SHA1_FINGERPRINT, SSHKey.sha1_fingerprint("ssh-dss #{SSH_PUBLIC_KEY3}") 708 | assert_equal KEY4_SHA1_FINGERPRINT, SSHKey.sha1_fingerprint("ecdsa-sha2-nistp256 #{SSH_PUBLIC_KEY4}") 709 | assert_equal ED25519_SHA1_FINGERPRINT, SSHKey.sha1_fingerprint("ssh-ed25519 #{SSH_PUBLIC_KEY_ED25519}") 710 | assert_equal ECDSA_256_SHA1_FINGERPRINT, SSHKey.sha1_fingerprint("ecdsa-sha2-nistp256 #{SSH_PUBLIC_KEY_ECDSA_256} me@me.com") 711 | assert_equal ECDSA_384_SHA1_FINGERPRINT, SSHKey.sha1_fingerprint("ecdsa-sha2-nistp384 #{SSH_PUBLIC_KEY_ECDSA_384} me@me.com") 712 | assert_equal ECDSA_521_SHA1_FINGERPRINT, SSHKey.sha1_fingerprint("ecdsa-sha2-nistp521 #{SSH_PUBLIC_KEY_ECDSA_521} me@me.com") 713 | 714 | assert_equal KEY1_SHA256_FINGERPRINT, SSHKey.sha256_fingerprint(SSH_PRIVATE_KEY1) 715 | assert_equal KEY1_SHA256_FINGERPRINT, SSHKey.sha256_fingerprint("ssh-rsa #{SSH_PUBLIC_KEY1}") 716 | assert_equal KEY2_SHA256_FINGERPRINT, SSHKey.sha256_fingerprint(SSH_PRIVATE_KEY2) 717 | assert_equal KEY2_SHA256_FINGERPRINT, SSHKey.sha256_fingerprint("ssh-rsa #{SSH_PUBLIC_KEY2} me@me.com") 718 | assert_equal KEY3_SHA256_FINGERPRINT, SSHKey.sha256_fingerprint(SSH_PRIVATE_KEY3) 719 | assert_equal KEY3_SHA256_FINGERPRINT, SSHKey.sha256_fingerprint("ssh-dss #{SSH_PUBLIC_KEY3}") 720 | assert_equal KEY4_SHA256_FINGERPRINT, SSHKey.sha256_fingerprint("ecdsa-sha2-nistp256 #{SSH_PUBLIC_KEY4}") 721 | assert_equal ED25519_SHA256_FINGERPRINT, SSHKey.sha256_fingerprint("ssh-ed25519 #{SSH_PUBLIC_KEY_ED25519}") 722 | assert_equal ECDSA_256_SHA256_FINGERPRINT, SSHKey.sha256_fingerprint("ecdsa-sha2-nistp256 #{SSH_PUBLIC_KEY_ECDSA_256} me@me.com") 723 | assert_equal ECDSA_384_SHA256_FINGERPRINT, SSHKey.sha256_fingerprint("ecdsa-sha2-nistp384 #{SSH_PUBLIC_KEY_ECDSA_384} me@me.com") 724 | assert_equal ECDSA_521_SHA256_FINGERPRINT, SSHKey.sha256_fingerprint("ecdsa-sha2-nistp521 #{SSH_PUBLIC_KEY_ECDSA_521} me@me.com") 725 | end 726 | 727 | def test_bits 728 | assert_equal 2048, @key1.bits 729 | assert_equal 2048, @key2.bits 730 | assert_equal 1024, @key3.bits 731 | 732 | if ecdsa_supported? 733 | assert_equal 256, @key4.bits 734 | else 735 | assert_raises(NotImplementedError) { @key4.bits } 736 | end 737 | 738 | assert_equal 512, SSHKey.generate(:bits => 512).bits 739 | end 740 | 741 | def test_randomart 742 | assert_equal KEY1_RANDOMART, @key1.randomart 743 | assert_equal KEY2_RANDOMART, @key2.randomart 744 | assert_equal KEY3_RANDOMART, @key3.randomart 745 | 746 | if ecdsa_supported? 747 | assert_equal KEY4_RANDOMART, @key4.randomart 748 | else 749 | assert_raises(NotImplementedError) { @key4.randomart } 750 | end 751 | 752 | if ecdsa_supported? 753 | assert_equal KEY4_RANDOMART_USING_SHA256_DIGEST, @key4.randomart("SHA256") 754 | assert_equal KEY4_RANDOMART_USING_SHA384_DIGEST, @key4.randomart("SHA384") 755 | assert_equal KEY4_RANDOMART_USING_SHA512_DIGEST, @key4.randomart("SHA512") 756 | end 757 | 758 | end 759 | 760 | def test_sshfp 761 | assert_equal KEY1_SSHFP, @key1.sshfp("localhost") 762 | assert_equal KEY2_SSHFP, @key2.sshfp("localhost") 763 | assert_equal KEY3_SSHFP, @key3.sshfp("localhost") 764 | end 765 | 766 | end 767 | 768 | class SSHKeyEncryptedTest < Test::Unit::TestCase 769 | 770 | ENCRYPTED_PRIVATE_KEY = <<-EOF 771 | -----BEGIN RSA PRIVATE KEY----- 772 | Proc-Type: 4,ENCRYPTED 773 | DEK-Info: AES-128-CBC,3514D8812B519059944A811726594515 774 | 775 | fSr1v51I65MZrSs7u12nype6RH6NS15xN5FDPaPKV++EBPxysEzicU5QHDt/aHa3 776 | t87nXkra1M400+zNgFcfi5Ga7w5SBmqEjdNgwhUV2/j1Yqqlr5c7l804OfIPxdE6 777 | ELoWH7pen72JnlZe6gXq495W96QTg3IzIWdiKEbKJlEwrNBligqT7GB2mup8nY1D 778 | o71R07dIrvfDy3xVgCoRjX4LKUilO6nRnwVCFRgVQTEVKclqt8NiSFGMjzv3iekR 779 | f1fJ8Wm6CiST8zdetIXgMnHEK1KELhNeMhI/42Tn/gHPDsckBiKLtM+85OOT92wh 780 | L9o/KUySdcsb/ld0yT/kAc99/wqNitHAqUEcLshIWDVhqoT1XK46hEuRN782AN2U 781 | shQKirF8QFopYF+u9K2Q0mr1EsYaBWOFFBR7EiwFvEYOx+ad6qGQGPcxWhbf6eCU 782 | D///9g1g5q8nWb80UH9Hw1aMhIA+VTlIasM6XJKmGr1LapxlrYsqRovPwkgOQg01 783 | jhSV1fy10bbaFBwd9qTdTTVqa368/e3/TxF2VKhDaqoy5lqvRqKzGJxi3ubzDuz9 784 | m3qRTCgy1v3XI5DgcjWt5xC5gZLHjKf79fQKRJjuEnWALahpDVWQ6PRCuqPfyph5 785 | /vVqGHqvA53HJ9pmXz4J9qtQQ2gkYRj1m2tlRJjtGRMqnAj7bpcDKIrdLudOiWB0 786 | FXwmsXljzPaf/SPUa+tGg7jbh+Jq+72vdpo1ijJtLXhWQAJasIbvSXOVHbZ6YhJj 787 | vES98gJPzevqemS//C1DMrr0ci6pM9ciT2szkrg71zRacnfqrjeZUI7qHKAsRbD5 788 | 258Jj6BeFPeQSrUg8sqQdoPxVTNnVr2bOB5SNfh7gqLanPksi6Kr7XNIzsYP5Wzf 789 | ADAElPdcRRwYc2kLVqugZTMLSn1r8rQjEyQ8/TT2QefI4ma8mCrBKgqYX+SDbhMJ 790 | +KUrah0jCgj86z6fSNkNHaKuzvGCovZsJHXt2SoIWVYWVUz91IHPKXXycIqvf3Yj 791 | 9HFpJRAPh30MYBgiImCJjmk8tqKGn0Tc80vOsrKMlVuMIIu2eNrddrnHUzSbjIHr 792 | tTtSDvsJ/Bn/6+Ox77U/FKg6s6/6PxOA1a+ffKkBXB/g4jS5CfGZl1owb3kX91C/ 793 | a+bcNWp07DsaTaZd/0UEL2gIvaEuCULgyIvmnBPCOY6Pc5GGegWEJne/sk7j9W4I 794 | 59YtVfcSXiovYR/QEywytfN/tfPxKfUoqNMIxLkukjFYz4Zzk5kXEeI+1lcry398 795 | UQSaOboSDKY6boX4rWgiiqyn5LN+47eAIZPO+zsWXky16F04JpT7V7XqZPXQn7vI 796 | pMAoPCkT4qE9Gp2CcSj2l2CoZ3ZA5lOs6Wvxuz0q1zd0uSe8O81/3rnw28DthDQO 797 | SuzrY0HinPEFomwMGbfhosB5kOmBXEk7XbSWWHhK0QG63CYqp5caUst2Mie21b0Y 798 | FgFTrS1VqUiqDjCmt8F8UCPQS89aFm096wqtmwDO+VWKanuHUUShtTPlYyLe1RTm 799 | wqh1BBa05ydM0Vf1NagFB4JNT1qSIL5x4XtkOFwqcdXWYvwYfT8PkZjX/kz+W7jb 800 | -----END RSA PRIVATE KEY----- 801 | EOF 802 | 803 | DECRYPTED_PRIVATE_KEY = <<-EOF 804 | -----BEGIN RSA PRIVATE KEY----- 805 | MIIEpAIBAAKCAQEA33H9u4rG0SVMzK8PFyIi30kVHvogmpKVOQL2g2tozi2GipkA 806 | imzoCW9jIx1jo6zo0kMKCbMdJCNUc93tQxkJAAWA07WYwE9z+J6MC/3urb4Q0UAZ 807 | orlNyN3pPP8ATFIQomd0wW/EHwbmknlJOIZY4MNUQpNJunnBAAzZyLef6+sbLzQZ 808 | wc0/vgtmatoCxh4mZWFuFMopkMqScYDKxL30xXXPRGfJvAVWZDnY3ErJSe7uyefI 809 | Zo/lp4tsH5XGMcFm+nLs++nJjYZ6Ud/ie1g+ZCJv5Qit1m7zCVjiJ4RVMC/4yTb7 810 | CO8n4PhX8gR9d1DRPVvlKa1sZfQeULEIXMniKQIDAQABAoIBAQDEXS74s5rJjhgS 811 | AP4n/E3dICK5mGMytAMDmUD+eVQfbQ7BmnhJLjA0qnjbESbRXlE1Bsk5gPjpG0tK 812 | kAvEXan1JOD0LLDSwIBQSzUUDNLGSTQKUGS3BlX/YlVoz0h5ydzofDa1D/2wrqXO 813 | r1vTmu1ciQvxffLbN8iOvLxfkk+uSMhqnhf/q3WVinu+VALPg8e/v3p4VbnSfP4D 814 | eClBiMKEKRFdsa9xBxShijcX1HxjIvp3zDgb127fo2iFw09PIHUZCUo6M77iGrQY 815 | mscoA+5q1qSyD6Btw0EkKK55ytNMC2T+KfGYV0ySwhadmM+5+G64etvNGCn/j7CU 816 | rhuRhMlpAoGBAPiJlcWaNVw3ttlea+jllxnaCEOmG29NGWxw080XRvjSIrltPAiE 817 | 8e4FynAMx49aXKsaB8Df2WspdKBxHJgv1U0sapADwxlMO8erj2eDSRqy6bwnI4CT 818 | T+vvo2vdmkvkV0D9RskXCi5tgTO5FYnf7CjON6JLkg83V3vzyjsKlvDzAoGBAOYn 819 | iC50OdZ84U58wQUsvwXFuyzMQX9r+h0jL2tIDv/yYlMWg9tNt0HkGUOo7H1ZVhdL 820 | 9Z1B0Ztl2qoJipcQvhzfwdo4XwuLk7D0bOAfZo9YMbU5Jqy+rqE5yv/P4wa7ba4S 821 | uUQYvSuv54CtiOZDFyK8dU7y9mm+no3Fvrd9RwdzAoGANzlLACctqBnxFQd37r3k 822 | /yeFIpLsEaUN+xxu02lSqcL3WEA/UJ1JrFu5CYCtbtrjMFmOU3rpsnf5pBS+B8rJ 823 | GGbAHtPXK+3Wcp1aNePkAHy0lswThWQ2I/SRWUxaFnbcNGKSsefeqUZHqRh9Aq+w 824 | p7h6gCNOhvcDB1W6H7hQpaUCgYEAyBRDygaWJUVI5N+FOUduBMmhb09d/TTUKTJm 825 | TcBF8fE30v12wVZtYqW15ODcPhhExFnverc2Tf6cukczKSKP8y/+KQPqdHHxgdrr 826 | L2d81E6aX+4AFhpqW5SPShXiSf70WWjDkFRlV65C9dVmdq6KVVM6M9j5qHHjCmKG 827 | 6qLI9csCgYBuhFwwI9DiYvJPR1LJZnJtE0qZiTwpmCjU2LoBRsywvuBeyXtwpmIM 828 | 5IgfgXXLK5qK/+cp9047T5rzT6ndu5fNZINPzynA8tNhTtHXK8l/GT/iq8Rd6AcM 829 | WJmIe8EnUDhHqg7Z2h5tGpX1QPMSA4G8RGPPyrcd3v0G/PZ6pFALlQ== 830 | -----END RSA PRIVATE KEY----- 831 | EOF 832 | 833 | DECRYPTED_KEY_FINGERPRINT = "2e:ba:06:b1:c7:13:37:24:6f:2d:3a:ba:45:e2:b4:78" 834 | 835 | def setup 836 | @key_encrypted = SSHKey.new(ENCRYPTED_PRIVATE_KEY, :passphrase => "password") 837 | end 838 | 839 | def test_encrypted_private_key_can_be_decrypted 840 | assert_equal DECRYPTED_PRIVATE_KEY, @key_encrypted.private_key 841 | end 842 | 843 | def test_encrypted_private_key_matches_when_reencrypted 844 | key = SSHKey.new(@key_encrypted.encrypted_private_key, :passphrase => "password") 845 | assert_equal DECRYPTED_PRIVATE_KEY, key.private_key 846 | assert_equal DECRYPTED_KEY_FINGERPRINT, key.md5_fingerprint 847 | end 848 | end 849 | --------------------------------------------------------------------------------