Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add keypair support for ed25519 #13219

Merged
merged 7 commits into from
Jun 27, 2023
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
15 changes: 14 additions & 1 deletion keys/README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,22 @@
# Insecure Keypair
# Insecure Keypairs

These keys are the "insecure" public/private keypair we offer to
[base box creators](https://www.vagrantup.com/docs/boxes/base.html) for use in their base boxes so that
vagrant installations can automatically SSH into the boxes.

# Vagrant Keypairs

There are currently two "insecure" public/private keypairs for
Vagrant. One keypair was generated using the older RSA algorithm
and the other keypair was generated using the more recent ED25519
algorithm.

The `vagrant.pub` file includes the public key for both keypairs. It
is important for box creators to include both keypairs as versions of
Vagrant prior to 2.3.8 will only use the RSA private key.

# Custom Keys

If you're working with a team or company or with a custom box and
you want more secure SSH, you should create your own keypair
and configure the private key in the Vagrantfile with
Expand Down
7 changes: 7 additions & 0 deletions keys/vagrant.key.ed25519
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACDdWHcQaTZc8Q6nycsP0CqMNRfsLxvYVxqKosrHyTp+WAAAAJj2TBMT9kwT
EwAAAAtzc2gtZWQyNTUxOQAAACDdWHcQaTZc8Q6nycsP0CqMNRfsLxvYVxqKosrHyTp+WA
AAAEAveRHRHSCjIxbNKHDRzezD0U3R3UEEmS7R33fzvPQAD91YdxBpNlzxDqfJyw/QKow1
F+wvG9hXGoqiysfJOn5YAAAAEHNwb3hAdmFncmFudC1kZXYBAgMEBQ==
-----END OPENSSH PRIVATE KEY-----
27 changes: 27 additions & 0 deletions keys/vagrant.key.rsa
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEA6NF8iallvQVp22WDkTkyrtvp9eWW6A8YVr+kz4TjGYe7gHzI
w+niNltGEFHzD8+v1I2YJ6oXevct1YeS0o9HZyN1Q9qgCgzUFtdOKLv6IedplqoP
kcmF0aYet2PkEDo3MlTBckFXPITAMzF8dJSIFo9D8HfdOV0IAdx4O7PtixWKn5y2
hMNG0zQPyUecp4pzC6kivAIhyfHilFR61RGL+GPXQ2MWZWFYbAGjyiYJnAmCP3NO
Td0jMZEnDkbUvxhMmBYSdETk1rRgm+R4LOzFUGaHqHDLKLX+FIPKcF96hrucXzcW
yLbIbEgE98OHlnVYCzRdK8jlqm8tehUc9c9WhQIBIwKCAQEA4iqWPJXtzZA68mKd
ELs4jJsdyky+ewdZeNds5tjcnHU5zUYE25K+ffJED9qUWICcLZDc81TGWjHyAqD1
Bw7XpgUwFgeUJwUlzQurAv+/ySnxiwuaGJfhFM1CaQHzfXphgVml+fZUvnJUTvzf
TK2Lg6EdbUE9TarUlBf/xPfuEhMSlIE5keb/Zz3/LUlRg8yDqz5w+QWVJ4utnKnK
iqwZN0mwpwU7YSyJhlT4YV1F3n4YjLswM5wJs2oqm0jssQu/BT0tyEXNDYBLEF4A
sClaWuSJ2kjq7KhrrYXzagqhnSei9ODYFShJu8UWVec3Ihb5ZXlzO6vdNQ1J9Xsf
4m+2ywKBgQD6qFxx/Rv9CNN96l/4rb14HKirC2o/orApiHmHDsURs5rUKDx0f9iP
cXN7S1uePXuJRK/5hsubaOCx3Owd2u9gD6Oq0CsMkE4CUSiJcYrMANtx54cGH7Rk
EjFZxK8xAv1ldELEyxrFqkbE4BKd8QOt414qjvTGyAK+OLD3M2QdCQKBgQDtx8pN
CAxR7yhHbIWT1AH66+XWN8bXq7l3RO/ukeaci98JfkbkxURZhtxV/HHuvUhnPLdX
3TwygPBYZFNo4pzVEhzWoTtnEtrFueKxyc3+LjZpuo+mBlQ6ORtfgkr9gBVphXZG
YEzkCD3lVdl8L4cw9BVpKrJCs1c5taGjDgdInQKBgHm/fVvv96bJxc9x1tffXAcj
3OVdUN0UgXNCSaf/3A/phbeBQe9xS+3mpc4r6qvx+iy69mNBeNZ0xOitIjpjBo2+
dBEjSBwLk5q5tJqHmy/jKMJL4n9ROlx93XS+njxgibTvU6Fp9w+NOFD/HvxB3Tcz
6+jJF85D5BNAG3DBMKBjAoGBAOAxZvgsKN+JuENXsST7F89Tck2iTcQIT8g5rwWC
P9Vt74yboe2kDT531w8+egz7nAmRBKNM751U/95P9t88EDacDI/Z2OwnuFQHCPDF
llYOUI+SpLJ6/vURRbHSnnn8a/XG+nzedGH5JGqEJNQsz+xT2axM0/W/CRknmGaJ
kda/AoGANWrLCz708y7VYgAtW2Uf1DPOIYMdvo6fxIB5i9ZfISgcJ/bbCUkFrhoH
+vq/5CIWxCPp0f85R4qxxQ5ihxJ0YDQT9Jpx4TMss4PSavPaBH3RXow5Ohe+bYoQ
NE5OgEXk2wVfZczCZpigBKbKZHNYcelXtTt/nP3rsCuGcM4h53s=
-----END RSA PRIVATE KEY-----
1 change: 1 addition & 0 deletions keys/vagrant.pub
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA6NF8iallvQVp22WDkTkyrtvp9eWW6A8YVr+kz4TjGYe7gHzIw+niNltGEFHzD8+v1I2YJ6oXevct1YeS0o9HZyN1Q9qgCgzUFtdOKLv6IedplqoPkcmF0aYet2PkEDo3MlTBckFXPITAMzF8dJSIFo9D8HfdOV0IAdx4O7PtixWKn5y2hMNG0zQPyUecp4pzC6kivAIhyfHilFR61RGL+GPXQ2MWZWFYbAGjyiYJnAmCP3NOTd0jMZEnDkbUvxhMmBYSdETk1rRgm+R4LOzFUGaHqHDLKLX+FIPKcF96hrucXzcWyLbIbEgE98OHlnVYCzRdK8jlqm8tehUc9c9WhQ== vagrant insecure public key
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIN1YdxBpNlzxDqfJyw/QKow1F+wvG9hXGoqiysfJOn5Y vagrant insecure public key
1 change: 1 addition & 0 deletions keys/vagrant.pub.ed25519
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIN1YdxBpNlzxDqfJyw/QKow1F+wvG9hXGoqiysfJOn5Y vagrant insecure public key
1 change: 1 addition & 0 deletions keys/vagrant.pub.rsa
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA6NF8iallvQVp22WDkTkyrtvp9eWW6A8YVr+kz4TjGYe7gHzIw+niNltGEFHzD8+v1I2YJ6oXevct1YeS0o9HZyN1Q9qgCgzUFtdOKLv6IedplqoPkcmF0aYet2PkEDo3MlTBckFXPITAMzF8dJSIFo9D8HfdOV0IAdx4O7PtixWKn5y2hMNG0zQPyUecp4pzC6kivAIhyfHilFR61RGL+GPXQ2MWZWFYbAGjyiYJnAmCP3NOTd0jMZEnDkbUvxhMmBYSdETk1rRgm+R4LOzFUGaHqHDLKLX+FIPKcF96hrucXzcWyLbIbEgE98OHlnVYCzRdK8jlqm8tehUc9c9WhQ== vagrant insecure public key
56 changes: 49 additions & 7 deletions lib/vagrant/environment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,11 @@ class Environment
# The path where the plugins are stored (gems)
attr_reader :gems_path

# The path to the default private key
attr_reader :default_private_key_path
# The path to the default private keys directory
attr_reader :default_private_keys_directory

# The paths for each of the default private keys
attr_reader :default_private_key_paths

# Initializes a new environment with the given options. The options
# is a hash where the main available key is `cwd`, which defines where
Expand Down Expand Up @@ -174,7 +177,12 @@ def initialize(opts=nil)

# Setup the default private key
@default_private_key_path = @home_path.join("insecure_private_key")
copy_insecure_private_key
@default_private_keys_directory = @home_path.join("insecure_private_keys")
if !@default_private_keys_directory.directory?
@default_private_keys_directory.mkdir
end
@default_private_key_paths = []
copy_insecure_private_keys

# Initialize localized plugins
plugins = Vagrant::Plugin::Manager.instance.localize!(self)
Expand All @@ -196,6 +204,13 @@ def initialize(opts=nil)
hook(:environment_load, runner: Action::PrimaryRunner.new(env: self))
end

# The path to the default private key
# NOTE: deprecated, used default_private_keys_directory instead
def default_private_key_path
# TODO(spox): Add deprecation warning
@default_private_key_path
end

# Return a human-friendly string for pretty printed or inspected
# instances.
#
Expand Down Expand Up @@ -1053,14 +1068,18 @@ def process_configured_plugins
end
end

# This method copies the private key into the home directory if it
# doesn't already exist.
# This method copies the private keys into the home directory if they
# do not already exist. The `default_private_key_path` references the
# original rsa based private key and is retained for compatibility. The
# `default_private_keys_directory` contains the list of valid private
# keys supported by Vagrant.
#
# This must be done because `ssh` requires that the key is chmod
# NOTE: The keys are copied because `ssh` requires that the key is chmod
# 0600, but if Vagrant is installed as a separate user, then the
# effective uid won't be able to read the key. So the key is copied
# to the home directory and chmod 0600.
def copy_insecure_private_key
def copy_insecure_private_keys
# First setup the deprecated single key path
if !@default_private_key_path.exist?
@logger.info("Copying private key to home directory")

Expand All @@ -1084,6 +1103,29 @@ def copy_insecure_private_key
@default_private_key_path.chmod(0600)
end
end

# Now setup the key directory
Dir.glob(File.expand_path("keys/vagrant.key.*", Vagrant.source_root)).each do |source|
destination = default_private_keys_directory.join(File.basename(source))
default_private_key_paths << destination
next if File.exist?(destination)
begin
FileUtils.cp(source, destination)
rescue Errno::EACCES
raise Errors::CopyPrivateKeyFailed,
source: source,
destination: destination
end
end

if !Util::Platform.windows?
default_private_key_paths.each do |key_path|
if Util::FileMode.from_octal(key_path.stat.mode) != "600"
@logger.info("Changing permissions on private key (#{key_path}) to 0600")
key_path.chmod(0600)
end
end
end
end

# Finds the Vagrantfile in the given directory.
Expand Down
2 changes: 1 addition & 1 deletion lib/vagrant/machine.rb
Original file line number Diff line number Diff line change
Expand Up @@ -502,7 +502,7 @@ def ssh_info
if @config.ssh.private_key_path
info[:private_key_path] = @config.ssh.private_key_path
else
info[:private_key_path] = @env.default_private_key_path
info[:private_key_path] = @env.default_private_key_paths
end
end

Expand Down
171 changes: 135 additions & 36 deletions lib/vagrant/util/keypair.rb
Original file line number Diff line number Diff line change
@@ -1,55 +1,154 @@
require "base64"
require "openssl"
require "ed25519"
require "securerandom"

require "vagrant/util/retryable"

module Vagrant
module Util
class Keypair
extend Retryable

# Creates an SSH keypair and returns it.
#
# @param [String] password Password for the key, or nil for no password.
# @return [Array<String, String, String>] PEM-encoded public and private key,
# respectively. The final element is the OpenSSH encoded public
# key.
def self.create(password=nil)
# This sometimes fails with RSAError. It is inconsistent and strangely
# sleeps seem to fix it. We just retry this a few times. See GH-5056
rsa_key = nil
retryable(on: OpenSSL::PKey::RSAError, sleep: 2, tries: 5) do
rsa_key = OpenSSL::PKey::RSA.new(2048)
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-----".freeze

# Encodes given string
#
# @param [String] s String to encode
# @return [String]
def self.string(s)
[s.length].pack("N") + s
end

# 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

public_key = rsa_key.public_key
private_key = rsa_key.to_pem
# Creates an ed25519 SSH key pair
# @return [Array<String, String, String>] 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,
"Ed25519 key pair generation does not support passwords"
end

# Generate the key
base_key = ::Ed25519::SigningKey.generate
# Define the comment used for the key
comment = "vagrant"

# Grab the raw public key
public_key = base_key.verify_key.to_bytes
# Encode the public key for use building the openssh private key
encoded_public_key = string(KEY_TYPE) + string(public_key)
# Format the public key into the openssh public key format for writing
openssh_public_key = "#{KEY_TYPE} #{Base64.encode64(encoded_public_key).gsub("\n", "")} #{comment}"

if password
cipher = OpenSSL::Cipher.new('des3')
private_key = rsa_key.to_pem(cipher, password)
# 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(base_key.seed + public_key), # private key with public key concatenated
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 Rsa
extend Retryable

# Generate the binary necessary for the OpenSSH public key.
binary = [7].pack("N")
binary += "ssh-rsa"
["e", "n"].each do |m|
val = public_key.send(m)
data = val.to_s(2)

first_byte = data[0,1].unpack("c").first
if val < 0
data[0] = [0x80 & first_byte].pack("c")
elsif first_byte < 0
data = 0.chr + data
# Creates an SSH keypair and returns it.
#
# @param [String] password Password for the key, or nil for no password.
# @return [Array<String, String, String>] PEM-encoded public and private key,
# respectively. The final element is the OpenSSH encoded public
# key.
def self.create(password=nil)
# This sometimes fails with RSAError. It is inconsistent and strangely
# sleeps seem to fix it. We just retry this a few times. See GH-5056
rsa_key = nil
retryable(on: OpenSSL::PKey::RSAError, sleep: 2, tries: 5) do
rsa_key = OpenSSL::PKey::RSA.new(2048)
end

binary += [data.length].pack("N") + data
public_key = rsa_key.public_key
private_key = rsa_key.to_pem

if password
cipher = OpenSSL::Cipher.new('des3')
private_key = rsa_key.to_pem(cipher, password)
end

# Generate the binary necessary for the OpenSSH public key.
binary = [7].pack("N")
binary += "ssh-rsa"
["e", "n"].each do |m|
val = public_key.send(m)
data = val.to_s(2)

first_byte = data[0,1].unpack("c").first
if val < 0
data[0] = [0x80 & first_byte].pack("c")
elsif first_byte < 0
data = 0.chr + data
end

binary += [data.length].pack("N") + data
end

openssh_key = "ssh-rsa #{Base64.encode64(binary).gsub("\n", "")} vagrant"
public_key = public_key.to_pem
return [public_key, private_key, openssh_key]
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

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)})"
end

openssh_key = "ssh-rsa #{Base64.encode64(binary).gsub("\n", "")} vagrant"
public_key = public_key.to_pem
return [public_key, private_key, openssh_key]
VALID_TYPES[type].create(password)
end
end
end
Expand Down
Loading