Created
March 17, 2020 12:53
-
-
Save k3rni/325ff534971498d60921c1ebc70a9b38 to your computer and use it in GitHub Desktop.
Ruby script to load and validate private SSH-RSA keys
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
source 'https://rubygems.org' | |
gem 'bindata' | |
gem 'bcrypt_pbkdf', github: 'mfazekas/bcrypt_pbkdf-ruby' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#! /usr/bin/env ruby | |
require 'bindata' | |
require 'ostruct' | |
require 'openssl' | |
require 'bcrypt_pbkdf' | |
CIPHERS = { | |
'aes256-ctr' => OpenStruct.new(block_size: 16, key_len: 32, iv_len: 16, name: 'AES-256-CTR') | |
} | |
def decode_binary(data) | |
return nil if data.empty? | |
data.each_char.inject(0) { |a, b| (a << 8) | b.ord } | |
end | |
# A byte string prefixed by 32 bit length | |
class String32 < BinData::Primitive | |
endian :big | |
uint32 :len | |
string :data, read_length: :len | |
def get | |
data | |
end | |
end | |
# Bignum32, a variant of String32 that displays as a bignum. | |
class Bn32 < String32 | |
def get | |
decode_binary(data) | |
end | |
end | |
class SSHKeyHeader < BinData::Record | |
endian :big | |
stringz :format # "openssh-key-v1" | |
string32 :cipher # "none" or "aes256-ctr" in recent versions of openssh | |
string32 :kdf # "none" or "bcrypt" | |
end | |
class PrivateKey < BinData::Record | |
endian :big | |
uint64 :checksum # padding or checksum, not important | |
string32 :key_type # "ssh-rsa" | |
bn32 :n # modulus | |
bn32 :e # public exponent | |
bn32 :d # private exponent | |
bn32 :coeff # CRT helper value: q^(-1) mod p | |
bn32 :p # prime 1 | |
bn32 :q # prime 2 | |
string32 :comment # user@host by default, can be edited through ssh-keygen | |
# And some irrelevant padding | |
end | |
class PlainSSHKey < BinData::Record | |
endian :big | |
bn32 # ignore kdf_data | |
uint32 :num_keys # hardcoded to 1 | |
uint32 :pubkeys_len # length of following pubkey block | |
array :pubkeys, initial_length: :num_keys do | |
struct :pubkey do | |
string32 :key_type # "ssh-rsa" | |
bn32 :e # public exponent | |
bn32 :n # modulus | |
end | |
end | |
uint32 :privkey_len | |
private_key :privkey | |
end | |
class BcryptData < BinData::Record | |
endian :big | |
uint32 # bcdata_len, not useful | |
string32 :salt | |
uint32 :rounds | |
end | |
class EncryptedSSHKey < BinData::Record | |
mandatory_parameter :cipher | |
endian :big | |
bcrypt_data :kdf_data | |
uint32 :num_keys # hardcoded to 1 | |
uint32 :pubkeys_len # length of following pubkey block | |
array :pubkeys, initial_length: :num_keys do | |
struct :pubkey do | |
string32 :key_type # "ssh-rsa" | |
bn32 :e # public exponent | |
bn32 :n # modulus | |
end | |
end | |
uint32 :enc_len | |
# Must be a multiple of blocksize (and nonzero) | |
virtual :assert_blocklen, assert: -> { enc_len % CIPHERS[cipher.to_s].block_size == 0 } | |
string :enc_privkey, read_length: :enc_len # length of following privkey block | |
def salt | |
kdf_data.salt.to_s | |
end | |
def passphrase | |
ARGV[0] or raise 'Must provide passphrase on commandline' | |
end | |
def key_and_iv | |
key_len = cipher_params.key_len | |
key_with_iv = BCryptPbkdf.key(passphrase, salt, | |
key_len + cipher_params.iv_len, kdf_data.rounds) | |
shield_key = key_with_iv[0...key_len] | |
iv = key_with_iv[key_len..] | |
[shield_key, iv] | |
end | |
def cipher_params | |
CIPHERS[get_parameter(:cipher)] | |
end | |
def privkey | |
decoder = OpenSSL::Cipher.new(cipher_params.name).decrypt | |
decoder.key, decoder.iv = key_and_iv | |
data = decoder.update(enc_privkey.to_s) | |
PrivateKey.read(data) | |
end | |
end | |
header = SSHKeyHeader.read(STDIN) | |
cipher = header.cipher.to_s | |
key = case cipher | |
when 'none' | |
PlainSSHKey.read(STDIN) | |
else | |
# Not passing kdf, as we only support bcrypt anyway | |
EncryptedSSHKey.read(STDIN, cipher: cipher) | |
end | |
# Validating the file | |
pub = OpenStruct.new(key[:pubkeys].first) | |
priv = OpenStruct.new(key.privkey) | |
# NOTE: the `get`s are necessary, because the hash contains bindata wrapper types, not ruby types | |
fail "pubkey modulus different from privkey" unless priv.n.get == pub.n.get | |
fail "pubkey exponent different from privkey" unless priv.e.get == pub.e.get | |
fail "modulus is not a multiple of p and q" unless priv.p.get * priv.q.get == priv.n.get | |
enc = (42).pow(priv.e.get, priv.n.get) # RSA Encryption | |
dec = (enc).pow(priv.d.get, priv.n.get) # and decryption | |
fail "e and d are not public and private exponents mod n" unless dec == 42 | |
fail "coeff is not inverse of q mod p" unless (priv.q.get * priv.coeff.get) % priv.p.get == 1 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment