Skip to content

Commit 989ced5

Browse files
committed
Remove net-ssh gem dependency from SSHKey
Replace Net::SSH::Buffer with native Ruby implementation for SSH wire format encoding. The new implementation uses standard Ruby pack/unpack directives to encode SSH public key blobs per RFC 4251 and RFC 4253. Changes: - Implement ssh_string() for SSH string encoding (length-prefixed) - Implement ssh_mpint() for SSH multiple precision integer encoding - Add helper methods for readability (uint32_big_endian, zero_byte, etc.) - Update test to parse SSH blob without Net::SSH::Buffer - Remove net-ssh from Gemfile Manually tested all combinations to ensure compatibility: - Old implementation + old test (baseline) - Old implementation + new test - New implementation + old test - New implementation + new test All combinations pass, confirming the new implementation produces identical SSH wire format output.
1 parent 203594c commit 989ced5

File tree

4 files changed

+68
-20
lines changed

4 files changed

+68
-20
lines changed

Gemfile

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ gem 'loggregator_emitter', '~> 5.0'
1717
gem 'mime-types', '~> 3.7'
1818
gem 'multipart-parser'
1919
gem 'netaddr', '>= 2.0.4'
20-
gem 'net-ssh'
2120
gem 'newrelic_rpm'
2221
gem 'nokogiri', '>=1.10.5'
2322
gem 'oj'

Gemfile.lock

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,6 @@ GEM
326326
mutex_m (0.3.0)
327327
mysql2 (0.5.7)
328328
bigdecimal
329-
net-ssh (7.3.0)
330329
netaddr (2.0.6)
331330
netrc (0.11.0)
332331
newrelic_rpm (10.2.0)
@@ -650,7 +649,6 @@ DEPENDENCIES
650649
mock_redis
651650
multipart-parser
652651
mysql2 (~> 0.5.7)
653-
net-ssh
654652
netaddr (>= 2.0.4)
655653
newrelic_rpm
656654
nokogiri (>= 1.10.5)

lib/cloud_controller/diego/ssh_key.rb

Lines changed: 58 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
require 'net/ssh'
1+
# frozen_string_literal: true
2+
23
require 'openssl'
34
require 'digest'
45
require 'base64'
@@ -17,14 +18,14 @@ def private_key
1718

1819
def authorized_key
1920
@authorized_key ||= begin
20-
type = ssh_type_for(key)
21+
type = ssh_type
2122
data = [public_key_blob].pack('m0') # Base64 without newlines
2223
"#{type} #{data}"
2324
end
2425
end
2526

2627
def fingerprint
27-
@fingerprint ||= colon_hex(OpenSSL::Digest::SHA1.digest(public_key_blob)) # 3)
28+
@fingerprint ||= colon_separated_hex(OpenSSL::Digest::SHA1.digest(public_key_blob))
2829
end
2930

3031
def sha256_fingerprint
@@ -37,26 +38,70 @@ def key
3738
@key ||= OpenSSL::PKey::RSA.new(@bits)
3839
end
3940

41+
# Builds the SSH public key blob for an RSA key.
42+
#
43+
# SSH wire format for RSA public key (RFC 4253 section 6.6):
44+
# string "ssh-rsa"
45+
# mpint e (public exponent)
46+
# mpint n (modulus)
47+
#
48+
# Data types defined in RFC 4251 section 5:
49+
# - string: uint32 length + raw bytes
50+
# - mpint: uint32 length + big-endian bytes (leading zero if high bit set)
51+
#
52+
# References:
53+
# - https://www.rfc-editor.org/rfc/rfc4251#section-5
54+
# - https://www.rfc-editor.org/rfc/rfc4253#section-6.6
55+
# - https://ruby-doc.org/3.3.0/packed_data_rdoc.html
4056
def public_key_blob
41-
@public_key_blob ||= begin
42-
b = Net::SSH::Buffer.new
43-
b.write_string(ssh_type_for(key)) # key type
44-
b.write_bignum(key.e) # public exponent (e)
45-
b.write_bignum(key.n) # modulus (n)
46-
b.to_s
57+
@public_key_blob ||=
58+
ssh_string(ssh_type) +
59+
ssh_mpint(key.e) +
60+
ssh_mpint(key.n)
61+
end
62+
63+
# Encodes a string in SSH wire format: 4-byte length prefix + raw bytes
64+
def ssh_string(string)
65+
uint32_big_endian(string.bytesize) + string
66+
end
67+
68+
# Encodes a bignum as an SSH "mpint" (multiple precision integer).
69+
# Format: 4-byte length prefix + big-endian bytes.
70+
# If high bit is set, prepends a zero byte to indicate positive number.
71+
def ssh_mpint(bignum)
72+
return uint32_big_endian(0) if bignum.zero?
73+
74+
bytes = bignum.to_s(2) # big-endian binary representation (OpenSSL::BN)
75+
76+
if high_bit_set?(bytes)
77+
uint32_big_endian(bytes.bytesize + 1) + zero_byte + bytes
78+
else
79+
uint32_big_endian(bytes.bytesize) + bytes
4780
end
4881
end
4982

50-
def ssh_type_for(key)
83+
def uint32_big_endian(number)
84+
[number].pack('N') # 4-byte unsigned integer, big-endian (network byte order)
85+
end
86+
87+
def zero_byte
88+
[0].pack('C') # 8-bit unsigned integer
89+
end
90+
91+
def high_bit_set?(bytes)
92+
bytes.getbyte(0).anybits?(0x80) # binary: 10000000 - used to check if high bit is set
93+
end
94+
95+
def ssh_type
5196
case key
52-
when OpenSSL::PKey::RSA then 'ssh-rsa' # net-ssh doesn’t publish a constant for this
97+
when OpenSSL::PKey::RSA then 'ssh-rsa'
5398
else
5499
raise NotImplementedError.new("Unsupported key type: #{key.class}")
55100
end
56101
end
57102

58-
def colon_hex(bytes)
59-
bytes.unpack('C*').map { |b| sprintf('%02x', b) }.join(':') # byte-wise hex with colons
103+
def colon_separated_hex(bytes)
104+
bytes.unpack('C*').map { |byte| sprintf('%02x', byte) }.join(':')
60105
end
61106
end
62107
end

spec/unit/lib/cloud_controller/diego/ssh_key_spec.rb

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,16 @@ module Diego
2727
expect(type).to eq('ssh-rsa')
2828

2929
blob = Base64.strict_decode64(b64)
30-
blob_buffer = Net::SSH::Buffer.new(blob)
31-
blob_type = blob_buffer.read_string
32-
e = blob_buffer.read_bignum # public exponent
33-
n = blob_buffer.read_bignum # modulus
30+
# Parse SSH public key blob (replaces Net::SSH::Buffer)
31+
offset = 0
32+
len = blob[offset, 4].unpack1('N')
33+
blob_type = blob[offset + 4, len]
34+
offset += 4 + len
35+
len = blob[offset, 4].unpack1('N')
36+
e = OpenSSL::BN.new(blob[offset + 4, len], 2) # public exponent
37+
offset += 4 + len
38+
len = blob[offset, 4].unpack1('N')
39+
n = OpenSSL::BN.new(blob[offset + 4, len], 2) # modulus
3440
expect(blob_type).to eq('ssh-rsa')
3541

3642
pk = OpenSSL::PKey::RSA.new(ssh_key.private_key)

0 commit comments

Comments
 (0)