Skip to content

Instantly share code, notes, and snippets.

@k3rni
Created March 17, 2020 12:53
Show Gist options
  • Save k3rni/325ff534971498d60921c1ebc70a9b38 to your computer and use it in GitHub Desktop.
Save k3rni/325ff534971498d60921c1ebc70a9b38 to your computer and use it in GitHub Desktop.
Ruby script to load and validate private SSH-RSA keys
source 'https://rubygems.org'
gem 'bindata'
gem 'bcrypt_pbkdf', github: 'mfazekas/bcrypt_pbkdf-ruby'
#! /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