1- require 'net/ssh'
1+ # frozen_string_literal: true
2+
23require 'openssl'
34require 'digest'
45require '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
0 commit comments