From 4e617830085c2085825d331a2ca62aad84c1ec2a Mon Sep 17 00:00:00 2001 From: Chris Roberts Date: Wed, 10 Jan 2024 11:32:49 -0800 Subject: [PATCH 1/5] Fix autoload for `Util::Keypair` --- lib/vagrant/util.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/vagrant/util.rb b/lib/vagrant/util.rb index 1f799af3b69..5be05813ea1 100644 --- a/lib/vagrant/util.rb +++ b/lib/vagrant/util.rb @@ -36,7 +36,7 @@ module Remote autoload :IO, 'vagrant/util/io' autoload :IPV4Interfaces, 'vagrant/util/ipv4_interfaces' autoload :IsPortOpen, 'vagrant/util/is_port_open' - autoload :KeyPair, 'vagrant/util/key_pair' + autoload :Keypair, 'vagrant/util/keypair' autoload :LineBuffer, 'vagrant/util/line_buffer' autoload :LineEndingHelpers, 'vagrant/util/line_ending_helpers' autoload :LoggingFormatter, 'vagrant/util/logging_formatter' From b934bd675cc15b83440b0c2fc21553e750e9c1a5 Mon Sep 17 00:00:00 2001 From: Chris Roberts Date: Wed, 10 Jan 2024 11:33:30 -0800 Subject: [PATCH 2/5] Add new key pair types Adds ECDSA key types (256, 384, and 521) to supported types that can be generated for key replacement on guest. --- lib/vagrant/util/keypair.rb | 165 ++++++++++++++++++++++++++++++++---- 1 file changed, 150 insertions(+), 15 deletions(-) diff --git a/lib/vagrant/util/keypair.rb b/lib/vagrant/util/keypair.rb index a83b5813ccf..c3ce48603c7 100644 --- a/lib/vagrant/util/keypair.rb +++ b/lib/vagrant/util/keypair.rb @@ -10,15 +10,43 @@ module Vagrant module Util class Keypair + # Magic string header + AUTH_MAGIC = "openssh-key-v1".freeze + # Header of private key file content + PRIVATE_KEY_START = "-----BEGIN OPENSSH PRIVATE KEY-----\n".freeze + # Footer of private key file content + PRIVATE_KEY_END = "-----END OPENSSH PRIVATE KEY-----\n".freeze + + # Check if provided key is a supported key type + # + # @param [Symbol] key Key type to check + # @return [Boolean] key type is supported + def self.valid_type?(key) + VALID_TYPES.keys.include?(key) + end + + # @return [Array] list of supported key types + def self.available_types + PREFER_KEY_TYPES.values + end + + # Create a new keypair + # + # @param [String] password Password for the key or nil for no password (only supported for rsa type) + # @param [Symbol] type Key type to generate + # @return [Array] Public key, openssh private key, openssh public key with comment + def self.create(password=nil, type: :rsa) + if !VALID_TYPES.key?(type) + raise ArgumentError, + "Invalid key type requested (supported types: #{available_types.map(&:inspect).join(", ")})" + end + + VALID_TYPES[type].create(password) + end + class Ed25519 - # Magic string header - AUTH_MAGIC = "openssh-key-v1".freeze # Key type identifier KEY_TYPE = "ssh-ed25519".freeze - # Header of private key file content - PRIVATE_KEY_START = "-----BEGIN OPENSSH PRIVATE KEY-----\n".freeze - # Footer of private key file content - PRIVATE_KEY_END = "-----END OPENSSH PRIVATE KEY-----\n".freeze # Encodes given string # @@ -95,6 +123,9 @@ def self.create(password=nil) class Rsa extend Retryable + # Key type identifier + KEY_TYPE = "ssh-rsa" + # Creates an SSH keypair and returns it. # # @param [String] password Password for the key, or nil for no password. @@ -140,19 +171,123 @@ def self.create(password=nil) end end - # Supported key types. - VALID_TYPES = {ed25519: Ed25519, rsa: Rsa}.freeze - # Ordered mapping of openssh key type name to lookup name - PREFER_KEY_TYPES = {"ssh-ed25519".freeze => :ed25519, "ssh-rsa".freeze => :rsa}.freeze + # Base class for Ecdsa type keys to subclass + class Ecdsa + # Encodes given string + # + # @param [String] s String to encode + # @return [String] + def self.string(s) + [s.length].pack("N") + s + end - def self.create(password=nil, type: :rsa) - if !VALID_TYPES.key?(type) - raise ArgumentError, - "Invalid key type requested (supported types: #{VALID_TYPES.keys.map(&:inspect)})" + # Encodes given string with padding to block size + # + # @param [String] s String to encode + # @param [Integer] blocksize Defined block size + # @return [String] + def self.padded_string(s, blocksize) + pad = blocksize - (s.length % blocksize) + string(s + Array(1..pad).pack("c*")) end - VALID_TYPES[type].create(password) + # Creates an ed25519 SSH key pair + # @return [Array] Public key, openssh private key, openssh public key with comment + # @note Password support was not included as it's not actively used anywhere. If it ends up being + # something that's needed, it can be revisited + def self.create(password=nil) + if password + raise NotImplementedError, + "Ecdsa key pair generation does not support passwords" + end + + # Generate the key + base_key = OpenSSL::PKey::EC.generate(self.const_get(:OPENSSL_CURVE)) + # Define the comment used for the key + comment = "vagrant" + + # Grab the raw public key + public_key = base_key.public_key.to_bn.to_s(2) + # Encode the public key for use building the openssh private key + encoded_public_key = string(self.const_get(:KEY_TYPE)) + string(self.const_get(:OPENSSH_CURVE)) + string(public_key) + # Format the public key into the openssh public key format for writing + openssh_public_key = "#{self.const_get(:KEY_TYPE)} #{Base64.encode64(encoded_public_key).gsub("\n", "")} #{comment}" + + pk_value = base_key.private_key.to_s(2) + # Pad the start of the key if required + if pk_value.length % 8 == 0 + pk_value = "\0#{pk_value}" + end + + # Agent encoded private key is used when building the openssh private key + # (https://datatracker.ietf.org/doc/html/draft-miller-ssh-agent#section-4.2.3) + # (https://dnaeon.github.io/openssh-private-key-binary-format/) + agent_private_key = [ + ([SecureRandom.random_number((2**32)-1)] * 2).pack("NN"), # checkint, random uint32 value, twice (used for encryption verification) + encoded_public_key, # includes the key type and public key + string(pk_value), # private key + string(comment), # comment for the key + ].join + + # Build openssh private key data (https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key) + private_key = [ + AUTH_MAGIC + "\0", # Magic string + string("none"), # cipher name, no encryption, so none + string("none"), # kdf name, no encryption, so none + string(""), # kdf options/data, no encryption, so empty string + [1].pack("N"), # Number of keys (just one) + string(encoded_public_key), # The public key + padded_string(agent_private_key, 8) # Private key encoded with agent rules, padded for 8 byte block size + ].join + + # Create the openssh private key content + openssh_private_key = [ + PRIVATE_KEY_START, + Base64.encode64(private_key), + PRIVATE_KEY_END, + ].join + + return [public_key, openssh_private_key, openssh_public_key] + end + end + + class Ecdsa256 < Ecdsa + KEY_TYPE = "ecdsa-sha2-nistp256".freeze + OPENSSH_CURVE = "nistp256".freeze + OPENSSL_CURVE = "prime256v1".freeze + end + + class Ecdsa384 < Ecdsa + KEY_TYPE = "ecdsa-sha2-nistp384".freeze + OPENSSH_CURVE = "nistp384".freeze + OPENSSL_CURVE = "secp384r1".freeze end + + class Ecdsa521 < Ecdsa + KEY_TYPE = "ecdsa-sha2-nistp521".freeze + OPENSSH_CURVE = "nistp521".freeze + OPENSSL_CURVE = "secp521r1".freeze + end + + # Supported key types. + VALID_TYPES = { + ecdsa256: Ecdsa256, + ecdsa384: Ecdsa384, + ecdsa521: Ecdsa521, + ed25519: Ed25519, + rsa: Rsa + }.freeze + + # Ordered mapping of openssh key type name to lookup name. The + # order defined here is based on preference. Note that ecdsa + # ordering is based on performance + PREFER_KEY_TYPES = { + Ed25519::KEY_TYPE => :ed25519, + Ecdsa256::KEY_TYPE => :ecdsa256, + Ecdsa521::KEY_TYPE => :ecdsa521, + Ecdsa384::KEY_TYPE => :ecdsa384, + Rsa::KEY_TYPE => :rsa, + }.freeze end end end From 443ff01ab73ea08a1d3e64e8f702b61764832e70 Mon Sep 17 00:00:00 2001 From: Chris Roberts Date: Wed, 10 Jan 2024 11:37:50 -0800 Subject: [PATCH 3/5] Add key_type configuration option for ssh Adds a new `key_type` option to the Vagrantfile ssh configuration. It defaults to :auto which allows auto detection of key type to use. Otherwise it can be set to an explicit type supported by Vagrant. --- plugins/kernel_v2/config/ssh_connect.rb | 15 +++++++++++ templates/locales/en.yml | 2 ++ .../kernel_v2/config/ssh_connect_test.rb | 27 +++++++++++++++++++ .../content/docs/vagrantfile/ssh_settings.mdx | 6 +++++ 4 files changed, 50 insertions(+) diff --git a/plugins/kernel_v2/config/ssh_connect.rb b/plugins/kernel_v2/config/ssh_connect.rb index 89c8e168bdb..a305025d127 100644 --- a/plugins/kernel_v2/config/ssh_connect.rb +++ b/plugins/kernel_v2/config/ssh_connect.rb @@ -15,6 +15,7 @@ class SSHConnectConfig < Vagrant.plugin("2", :config) attr_accessor :password attr_accessor :insert_key attr_accessor :keys_only + attr_accessor :key_type attr_accessor :paranoid attr_accessor :verify_host_key attr_accessor :compression @@ -33,6 +34,7 @@ def initialize @password = UNSET_VALUE @insert_key = UNSET_VALUE @keys_only = UNSET_VALUE + @key_type = UNSET_VALUE @paranoid = UNSET_VALUE @verify_host_key = UNSET_VALUE @compression = UNSET_VALUE @@ -50,6 +52,7 @@ def finalize! @password = nil if @password == UNSET_VALUE @insert_key = true if @insert_key == UNSET_VALUE @keys_only = true if @keys_only == UNSET_VALUE + @key_type = :auto if @key_type == UNSET_VALUE @paranoid = false if @paranoid == UNSET_VALUE @verify_host_key = :never if @verify_host_key == UNSET_VALUE @compression = true if @compression == UNSET_VALUE @@ -96,6 +99,10 @@ def finalize! rescue # ignore end + + if @key_type + @key_type = @key_type.to_sym + end end # NOTE: This is _not_ a valid config validation method, since it @@ -140,6 +147,14 @@ def validate(machine) given: @connect_timeout.to_s) end + if @key_type != :auto && !Vagrant::Util::Keypair.valid_type?(@key_type) + errors << I18n.t( + "vagrant.config.ssh.connect_invalid_key_type", + given: @key_type.to_s, + supported: Vagrant::Util::Keypair.available_types.join(", ") + ) + end + errors end end diff --git a/templates/locales/en.yml b/templates/locales/en.yml index a66eabfaea1..58042eb3264 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -2065,6 +2065,8 @@ en: `%{given}` type which cannot be converted to an Integer type. connect_timeout_invalid_value: |- The `connect_timeout` key only accepts values greater than 1 (received `%{given}`) + connect_invalid_key_type: |- + Invalid SSH key type set ('%{given}'). Supported types: %{supported} triggers: bad_trigger_type: |- The type '%{type}' defined for trigger '%{trigger}' is not valid. Must be one of the following types: '%{types}' diff --git a/test/unit/plugins/kernel_v2/config/ssh_connect_test.rb b/test/unit/plugins/kernel_v2/config/ssh_connect_test.rb index a6dbdef70c3..9969a29187d 100644 --- a/test/unit/plugins/kernel_v2/config/ssh_connect_test.rb +++ b/test/unit/plugins/kernel_v2/config/ssh_connect_test.rb @@ -44,6 +44,33 @@ end end + describe "#key_type" do + it "defaults to :auto" do + subject.finalize! + expect(subject.key_type).to eq(:auto) + end + + it "should allow supported key type" do + subject.key_type = :ed25519 + subject.finalize! + errors = subject.validate(machine) + expect(errors).to be_empty + end + + it "should not allow unsupported key type" do + subject.key_type = :unknown_type + subject.finalize! + errors = subject.validate(machine) + expect(errors).not_to be_empty + end + + it "should convert string values to symbol" do + subject.key_type = "ecdsa521" + subject.finalize! + expect(subject.key_type).to eq(:ecdsa521) + end + end + describe "#config" do let(:config_file) { "/path/to/config" } diff --git a/website/content/docs/vagrantfile/ssh_settings.mdx b/website/content/docs/vagrantfile/ssh_settings.mdx index 5ec2bff7b07..605650923ff 100644 --- a/website/content/docs/vagrantfile/ssh_settings.mdx +++ b/website/content/docs/vagrantfile/ssh_settings.mdx @@ -90,6 +90,12 @@ defaults are typically fine, but you can fine tune whatever you would like. - `config.ssh.keys_only` (boolean) - Only use Vagrant-provided SSH private keys (do not use any keys stored in ssh-agent). The default value is `true`. +- `config.ssh.key_type` (string, symbol) - The SSH key type that should be used when generating + a new key to replace the default insecure key. Supported values are: `:ed25519`, `:ecdsa256`, + `:ecdsa384`, `:ecdsa521`, `:rsa`, and `:auto`. When the value is set to `:auto`, Vagrant will + automatically pick a type based on what is supported by the guest SSH server. The default + value is `:auto`. + - `config.ssh.paranoid` (boolean) - Perform strict host-key verification. The default value is `false`. From 96f2039bcdfca192411dab8b30a22c3e092067d6 Mon Sep 17 00:00:00 2001 From: Chris Roberts Date: Wed, 10 Jan 2024 11:39:47 -0800 Subject: [PATCH 4/5] Use ssh key type defined by configuration If key type is defined as :auto, detect best key type to use. If no acceptable key type is detected as supported by the server, raise an error. If unable to determine supported key types from the server, fallback to original behavior of rsa type key. If key type is defined as custom value, use that type if the server supports it, or if the supported types cannot be read. Otherwise, raise an error informing the user that the key type is not supported. --- lib/vagrant/errors.rb | 4 + plugins/communicators/ssh/communicator.rb | 85 ++++++++++++++----- templates/locales/en.yml | 8 ++ .../communicators/ssh/communicator_test.rb | 63 ++++++++++++-- 4 files changed, 131 insertions(+), 29 deletions(-) diff --git a/lib/vagrant/errors.rb b/lib/vagrant/errors.rb index d0df2f08f62..1757892566a 100644 --- a/lib/vagrant/errors.rb +++ b/lib/vagrant/errors.rb @@ -863,6 +863,10 @@ class SSHKeyTypeNotSupported < VagrantError error_key(:ssh_key_type_not_supported) end + class SSHKeyTypeNotSupportedByServer < VagrantError + error_key(:ssh_key_type_not_supported_by_server) + end + class SSHNoExitStatus < VagrantError error_key(:ssh_no_exit_status) end diff --git a/plugins/communicators/ssh/communicator.rb b/plugins/communicators/ssh/communicator.rb index 1e806015352..7e0e9af37e2 100644 --- a/plugins/communicators/ssh/communicator.rb +++ b/plugins/communicators/ssh/communicator.rb @@ -113,6 +113,8 @@ def wait_for_ready(timeout) raise rescue Vagrant::Errors::SSHKeyTypeNotSupported raise + rescue Vagrant::Errors::SSHKeyTypeNotSupportedByServer + raise rescue Vagrant::Errors::SSHKeyBadOwner raise rescue Vagrant::Errors::SSHKeyBadPermissions @@ -188,25 +190,56 @@ def ready? @machine.guest.capability?(:remove_public_key) raise Vagrant::Errors::SSHInsertKeyUnsupported if !cap - # Check for supported key type - key_type = catch(:key_type) do - begin - Vagrant::Util::Keypair::PREFER_KEY_TYPES.each do |type_name, type| - throw :key_type, type if supports_key_type?(type_name) + key_type = machine_config_ssh.key_type + + begin + # If the key type is set to `:auto` check for supported type. Otherwise + # ensure that the key type is supported by the guest + if key_type == :auto + key_type = catch(:key_type) do + begin + Vagrant::Util::Keypair::PREFER_KEY_TYPES.each do |type_name, type| + throw :key_type, type if supports_key_type?(type_name) + end + nil + rescue => err + @logger.warn("Failed to check key types server supports: #{err}") + nil + end end - nil - rescue => err - @logger.warn("Failed to check key types server supports: #{err}") - nil - end - end - @logger.debug("Detected key type for new private key: #{key_type}") + @logger.debug("Detected key type for new private key: #{key_type}") + + # If no key type was discovered, default to rsa + if key_type.nil? + @logger.debug("Failed to detect supported key type in: #{supported_key_types.join(", ")}") + available_types = supported_key_types.map { |t| + next if !Vagrant::Util::Keypair::PREFER_KEY_TYPES.key?(t) + "#{t} (#{Vagrant::Util::Keypair::PREFER_KEY_TYPES[t]})" + }.compact.join(", ") - # If no key type was discovered, default to rsa - if key_type.nil? - @logger.debug("Failed to detect supported key type, defaulting to rsa") - key_type = :rsa + raise Vagrant::Errors::SSHKeyTypeNotSupportedByServer, + requested_key_type: ":auto", + available_key_types: available_types + end + else + type_name = Vagrant::Util::Keypair::PREFER_KEY_TYPES.key(key_type) + if !supports_key_type?(type_name) + available_types = supported_key_types.map { |t| + next if !Vagrant::Util::Keypair::PREFER_KEY_TYPES.key?(t) + "#{t} (#{Vagrant::Util::Keypair::PREFER_KEY_TYPES[t]})" + }.compact.join(", ") + raise Vagrant::Errors::SSHKeyTypeNotSupportedByServer, + requested_key_type: "#{type_name} (#{key_type})", + available_key_types: available_types + end + end + rescue ServerDataError + @logger.warn("failed to load server data for key type check") + if key_type.nil? || key_type == :auto + @logger.warn("defaulting key type to :rsa due to failed server data loading") + key_type = :rsa + end end @logger.info("Creating new ssh keypair (type: #{key_type.inspect})") @@ -788,6 +821,8 @@ def machine_config_ssh protected + class ServerDataError < StandardError; end + # Check if server supports given key type # # @param [String, Symbol] type Key type @@ -798,21 +833,31 @@ def supports_key_type?(type) if @connection.nil? raise Vagrant::Errors::SSHNotReady end + + supported_key_types.include?(type.to_s) + end + + def supported_key_types + if @connection.nil? + raise Vagrant::Errors::SSHNotReady + end + server_data = @connection. transport&. algorithms&. instance_variable_get(:@server_data) if server_data.nil? @logger.warn("No server data available for key type support check") - return false + raise ServerDataError, "no data available" end if !server_data.is_a?(Hash) @logger.warn("Server data is not expected type (expecting Hash, got #{server_data.class})") - return false + raise ServerDataError, "unexpected type encountered (expecting Hash, got #{server_data.class})" end - @logger.debug("server data used for host key support check: #{server_data.inspect}") - server_data[:host_key].include?(type.to_s) + @logger.debug("server supported key type list: #{server_data[:host_key]}") + + server_data[:host_key] end end end diff --git a/templates/locales/en.yml b/templates/locales/en.yml index 58042eb3264..2bb540df468 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -1621,6 +1621,14 @@ en: sometimes keys in your ssh-agent can interfere with this as well, so verify the keys are valid there in addition to standard file paths. + ssh_key_type_not_supported_by_server: |- + The private key you are attempting to generate is not supported by + the guest SSH server. Please use one of the available key types defined + below that is supported by the guest SSH server. + + Requested: %{requested_key_type} + Available: %{available_key_types} + ssh_not_ready: |- The provider for this Vagrant-managed machine is reporting that it is not yet ready for SSH. Depending on your provider this can carry diff --git a/test/unit/plugins/communicators/ssh/communicator_test.rb b/test/unit/plugins/communicators/ssh/communicator_test.rb index 7cc161c5d05..f7700626426 100644 --- a/test/unit/plugins/communicators/ssh/communicator_test.rb +++ b/test/unit/plugins/communicators/ssh/communicator_test.rb @@ -13,6 +13,7 @@ # SSH configuration information mock let(:ssh) do double("ssh", + key_type: :auto, timeout: 1, host: nil, port: 5986, @@ -264,46 +265,48 @@ allow(guest).to receive(:capability).with(:remove_public_key) allow(connection).to receive(:transport).and_return(transport) allow(algorithms).to receive(:instance_variable_get).with(:@server_data).and_return(server_data) + allow(communicator).to receive(:supported_key_types).and_raise(described_class.const_get(:ServerDataError)) end - after{ communicator.ready? } - it "should create a new key pair" do expect(Vagrant::Util::Keypair).to receive(:create). and_return([new_public_key, new_private_key, openssh]) + communicator.ready? end it "should call the insert_public_key guest capability" do expect(guest).to receive(:capability).with(:insert_public_key, openssh) + communicator.ready? end it "should write the new private key" do expect(private_key_file).to receive(:write).with(new_private_key) + communicator.ready? end it "should call the set_ssh_key_permissions host capability" do expect(host).to receive(:capability?).with(:set_ssh_key_permissions).and_return(true) expect(host).to receive(:capability).with(:set_ssh_key_permissions, private_key_file) + communicator.ready? end it "should remove the default public key" do expect(guest).to receive(:capability).with(:remove_public_key, any_args) + communicator.ready? end context "with server algorithm support data" do - context "when no key type matches are found" do - it "should default to rsa type" do - expect(Vagrant::Util::Keypair).to receive(:create). - with(type: :rsa).and_call_original - end + before do + allow(communicator).to receive(:supported_key_types).and_call_original end context "when rsa is the only match" do - let(:valid_key_types) { ["ssh-edsca", "ssh-rsa"] } + let(:valid_key_types) { ["ssh-ecdsa", "ssh-rsa"] } it "should use rsa type" do expect(Vagrant::Util::Keypair).to receive(:create). with(type: :rsa).and_call_original + communicator.ready? end end @@ -313,27 +316,69 @@ it "should use ed25519 type" do expect(Vagrant::Util::Keypair).to receive(:create). with(type: :ed25519).and_call_original + communicator.ready? end end context "when ed25519 is the only match" do - let(:valid_key_types) { ["ssh-edsca", "ssh-ed25519"] } + let(:valid_key_types) { ["ssh-ecdsa", "ssh-ed25519"] } it "should use ed25519 type" do expect(Vagrant::Util::Keypair).to receive(:create). with(type: :ed25519).and_call_original + communicator.ready? + end + end + + context "with key_type set as :auto in configuration" do + let(:valid_key_types) { ["ssh-ed25519", "ssh-rsa"] } + before { allow(ssh).to receive(:key_type).and_return(:auto) } + + it "should use the preferred ed25519 key type" do + expect(Vagrant::Util::Keypair).to receive(:create). + with(type: :ed25519).and_call_original + communicator.ready? + end + + context "when no supported key type is detected" do + let(:valid_key_types) { ["fake-type", "other-fake-type"] } + + it "should raise an error" do + expect { communicator.ready? }.to raise_error(Vagrant::Errors::SSHKeyTypeNotSupportedByServer) + end + end + end + + context "with key_type set as :ecdsa521 in configuration" do + let(:valid_key_types) { ["ssh-ed25519", "ssh-rsa", "ecdsa-sha2-nistp521", "ecdsa-sha2-nistp256"] } + before { allow(ssh).to receive(:key_type).and_return(:ecdsa521) } + + it "should use the requested key type" do + expect(Vagrant::Util::Keypair).to receive(:create). + with(type: :ecdsa521).and_call_original + communicator.ready? + end + + context "when requested key type is not supported" do + let(:valid_key_types) { ["ssh-ed25519", "ssh-rsa", "ecdsa-sha2-nistp256"] } + + it "should raise an error" do + expect { communicator.ready? }.to raise_error(Vagrant::Errors::SSHKeyTypeNotSupportedByServer) + end end end end context "when an error is encountered getting server data" do before do + expect(communicator).to receive(:supported_key_types).and_call_original expect(connection).to receive(:transport).and_raise(StandardError) end it "should default to rsa key" do expect(Vagrant::Util::Keypair).to receive(:create). with(type: :rsa).and_call_original + communicator.ready? end end end From 2d5c9c0d1209e4979367c478951dd35b928cfc94 Mon Sep 17 00:00:00 2001 From: Chris Roberts Date: Wed, 10 Jan 2024 11:50:56 -0800 Subject: [PATCH 5/5] Patch net-ssh for ecdsa private keys This patches net-ssh so it will properly handle loading and using ecdsa private keys. Patching is restricted to tested versions. --- lib/vagrant.rb | 1 + lib/vagrant/patches/net-ssh.rb | 76 ++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 lib/vagrant/patches/net-ssh.rb diff --git a/lib/vagrant.rb b/lib/vagrant.rb index 1faeb8be0ce..a346982a302 100644 --- a/lib/vagrant.rb +++ b/lib/vagrant.rb @@ -5,6 +5,7 @@ # Add patches to log4r to support trace level require "vagrant/patches/log4r" +require "vagrant/patches/net-ssh" # Set our log levels and include trace require 'log4r/configurator' Log4r::Configurator.custom_levels(*(["TRACE"] + Log4r::Log4rConfig::LogLevels)) diff --git a/lib/vagrant/patches/net-ssh.rb b/lib/vagrant/patches/net-ssh.rb new file mode 100644 index 00000000000..c75ea14f94e --- /dev/null +++ b/lib/vagrant/patches/net-ssh.rb @@ -0,0 +1,76 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + +require "net/ssh" +require "net/ssh/buffer" + +# Set the version requirement for when net-ssh should be patched +NET_SSH_PATCH_REQUIREMENT = Gem::Requirement.new(">= 7.0.0", "< 7.2.2") + +# This patch provides support for properly loading ECDSA private keys +if NET_SSH_PATCH_REQUIREMENT.satisfied_by?(Gem::Version.new(Net::SSH::Version::STRING)) + Net::SSH::Buffer.class_eval do + def vagrant_read_private_keyblob(type) + case type + when /^ecdsa\-sha2\-(\w*)$/ + curve_name_in_type = $1 + curve_name_in_key = read_string + + unless curve_name_in_type == curve_name_in_key + raise Net::SSH::Exception, "curve name mismatched (`#{curve_name_in_key}' with `#{curve_name_in_type}')" + end + + public_key_oct = read_string + priv_key_bignum = read_bignum + begin + curvename = OpenSSL::PKey::EC::CurveNameAlias[curve_name_in_key] + group = OpenSSL::PKey::EC::Group.new(curvename) + point = OpenSSL::PKey::EC::Point.new(group, OpenSSL::BN.new(public_key_oct, 2)) + priv_bn = OpenSSL::BN.new(priv_key_bignum, 2) + asn1 = OpenSSL::ASN1::Sequence( + [ + OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(0)), + OpenSSL::ASN1::Sequence.new( + [ + OpenSSL::ASN1::ObjectId("id-ecPublicKey"), + OpenSSL::ASN1::ObjectId(curvename) + ] + ), + OpenSSL::ASN1::OctetString.new( + OpenSSL::ASN1::Sequence.new( + [ + OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(1)), + OpenSSL::ASN1::OctetString.new(priv_bn.to_s(2)), + OpenSSL::ASN1::ASN1Data.new( + [ + OpenSSL::ASN1::BitString.new(point.to_octet_string(:uncompressed)), + ], 1, :CONTEXT_SPECIFIC, + ) + ] + ).to_der + ) + ] + ) + + key = OpenSSL::PKey::EC.new(asn1.to_der) + + return key + rescue OpenSSL::PKey::ECError + raise NotImplementedError, "unsupported key type `#{type}'" + end + else + netssh_read_private_keyblob(type) + end + end + + alias_method :netssh_read_private_keyblob, :read_private_keyblob + alias_method :read_private_keyblob, :vagrant_read_private_keyblob + end + + OpenSSL::PKey::EC::Point.class_eval do + include Net::SSH::Authentication::PubKeyFingerprint + def to_pem + "#{ssh_type} #{self.to_bn.to_s(2)}" + end + end +end