Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ gem 'loggregator_emitter', '~> 5.0'
gem 'mime-types', '~> 3.7'
gem 'multipart-parser'
gem 'netaddr', '>= 2.0.4'
gem 'net-ssh'
gem 'newrelic_rpm'
gem 'nokogiri', '>=1.10.5'
gem 'oj'
Expand Down
2 changes: 0 additions & 2 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,6 @@ GEM
mutex_m (0.3.0)
mysql2 (0.5.7)
bigdecimal
net-ssh (7.3.1)
netaddr (2.0.6)
netrc (0.11.0)
newrelic_rpm (10.2.0)
Expand Down Expand Up @@ -646,7 +645,6 @@ DEPENDENCIES
mock_redis
multipart-parser
mysql2 (~> 0.5.7)
net-ssh
netaddr (>= 2.0.4)
newrelic_rpm
nokogiri (>= 1.10.5)
Expand Down
71 changes: 58 additions & 13 deletions lib/cloud_controller/diego/ssh_key.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require 'net/ssh'
# frozen_string_literal: true

require 'openssl'
require 'digest'
require 'base64'
Expand All @@ -17,14 +18,14 @@ def private_key

def authorized_key
@authorized_key ||= begin
type = ssh_type_for(key)
type = ssh_type
data = [public_key_blob].pack('m0') # Base64 without newlines
"#{type} #{data}"
end
end

def fingerprint
@fingerprint ||= colon_hex(OpenSSL::Digest::SHA1.digest(public_key_blob)) # 3)
@fingerprint ||= colon_separated_hex(OpenSSL::Digest::SHA1.digest(public_key_blob))
end

def sha256_fingerprint
Expand All @@ -37,26 +38,70 @@ def key
@key ||= OpenSSL::PKey::RSA.new(@bits)
end

# Builds the SSH public key blob for an RSA key.
#
# SSH wire format for RSA public key (RFC 4253 section 6.6):
# string "ssh-rsa"
# mpint e (public exponent)
# mpint n (modulus)
#
# Data types defined in RFC 4251 section 5:
# - string: uint32 length + raw bytes
# - mpint: uint32 length + big-endian bytes (leading zero if high bit set)
#
# References:
# - https://www.rfc-editor.org/rfc/rfc4251#section-5
# - https://www.rfc-editor.org/rfc/rfc4253#section-6.6
# - https://ruby-doc.org/3.3.0/packed_data_rdoc.html
def public_key_blob
@public_key_blob ||= begin
b = Net::SSH::Buffer.new
b.write_string(ssh_type_for(key)) # key type
b.write_bignum(key.e) # public exponent (e)
b.write_bignum(key.n) # modulus (n)
b.to_s
@public_key_blob ||=
ssh_string(ssh_type) +
ssh_mpint(key.e) +
ssh_mpint(key.n)
end

# Encodes a string in SSH wire format: 4-byte length prefix + raw bytes
def ssh_string(string)
uint32_big_endian(string.bytesize) + string
end

# Encodes a bignum as an SSH "mpint" (multiple precision integer).
# Format: 4-byte length prefix + big-endian bytes.
# If high bit is set, prepends a zero byte to indicate positive number.
def ssh_mpint(bignum)
return uint32_big_endian(0) if bignum.zero?

bytes = bignum.to_s(2) # big-endian binary representation (OpenSSL::BN)

if high_bit_set?(bytes)
uint32_big_endian(bytes.bytesize + 1) + zero_byte + bytes
else
uint32_big_endian(bytes.bytesize) + bytes
end
end

def ssh_type_for(key)
def uint32_big_endian(number)
[number].pack('N') # 4-byte unsigned integer, big-endian (network byte order)
end

def zero_byte
[0].pack('C') # 8-bit unsigned integer
end

def high_bit_set?(bytes)
bytes.getbyte(0).anybits?(0x80) # binary: 10000000 - used to check if high bit is set
end

def ssh_type
case key
when OpenSSL::PKey::RSA then 'ssh-rsa' # net-ssh doesn’t publish a constant for this
when OpenSSL::PKey::RSA then 'ssh-rsa'
else
raise NotImplementedError.new("Unsupported key type: #{key.class}")
end
end

def colon_hex(bytes)
bytes.unpack('C*').map { |b| sprintf('%02x', b) }.join(':') # byte-wise hex with colons
def colon_separated_hex(bytes)
bytes.unpack('C*').map { |byte| sprintf('%02x', byte) }.join(':')
end
end
end
Expand Down
14 changes: 10 additions & 4 deletions spec/unit/lib/cloud_controller/diego/ssh_key_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,16 @@ module Diego
expect(type).to eq('ssh-rsa')

blob = Base64.strict_decode64(b64)
blob_buffer = Net::SSH::Buffer.new(blob)
blob_type = blob_buffer.read_string
e = blob_buffer.read_bignum # public exponent
n = blob_buffer.read_bignum # modulus
# Parse SSH public key blob (replaces Net::SSH::Buffer)
offset = 0
len = blob[offset, 4].unpack1('N')
blob_type = blob[offset + 4, len]
offset += 4 + len
len = blob[offset, 4].unpack1('N')
e = OpenSSL::BN.new(blob[offset + 4, len], 2) # public exponent
offset += 4 + len
len = blob[offset, 4].unpack1('N')
n = OpenSSL::BN.new(blob[offset + 4, len], 2) # modulus
expect(blob_type).to eq('ssh-rsa')

pk = OpenSSL::PKey::RSA.new(ssh_key.private_key)
Expand Down
Loading