From b934bd675cc15b83440b0c2fc21553e750e9c1a5 Mon Sep 17 00:00:00 2001 From: Chris Roberts Date: Wed, 10 Jan 2024 11:33:30 -0800 Subject: [PATCH] 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 | 167 ++++++++++++++++++++++++++++++++---- 1 file changed, 151 insertions(+), 16 deletions(-) diff --git a/lib/vagrant/util/keypair.rb b/lib/vagrant/util/keypair.rb index a83b5813c..c3ce48603 100644 --- a/lib/vagrant/util/keypair.rb +++ b/lib/vagrant/util/keypair.rb @@ -10,15 +10,43 @@ require "vagrant/util/retryable" 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 @@ module Vagrant 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 @@ module Vagrant 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)})" + # 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 - VALID_TYPES[type].create(password) + # 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 + + # 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