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 support for ecdsa ssh keys #13327

Merged
merged 5 commits into from
Jan 11, 2024
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: 1 addition & 0 deletions lib/vagrant.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
4 changes: 4 additions & 0 deletions lib/vagrant/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
76 changes: 76 additions & 0 deletions lib/vagrant/patches/net-ssh.rb
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion lib/vagrant/util.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
165 changes: 150 additions & 15 deletions lib/vagrant/util/keypair.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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<Symbol>] 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<String, String, String>] 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
#
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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<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,
"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
Loading