Skip to content

Commit

Permalink
Merge knife-windows into chef
Browse files Browse the repository at this point in the history
Ship the knife-windows plugin directly in Chef.

Signed-off-by: Tim Smith <tsmith@chef.io>
  • Loading branch information
tas50 committed Feb 26, 2020
1 parent 63633cc commit 0c45e0b
Show file tree
Hide file tree
Showing 19 changed files with 2,384 additions and 0 deletions.
8 changes: 8 additions & 0 deletions Gemfile.lock
Expand Up @@ -60,6 +60,8 @@ PATH
train-winrm (>= 0.2.5)
tty-screen (~> 0.6)
uuidtools (~> 2.1.5)
winrm (~> 2.1)
winrm-elevated (~> 1.0)
chef (16.0.83-universal-mingw32)
addressable
bcrypt_pbkdf (~> 1.0)
Expand Down Expand Up @@ -104,6 +106,8 @@ PATH
win32-process (~> 0.8.2)
win32-service (>= 2.1.5, < 3.0)
win32-taskscheduler (~> 2.0)
winrm (~> 2.1)
winrm-elevated (~> 1.0)
wmi-lite (~> 1.0)

PATH
Expand Down Expand Up @@ -439,6 +443,10 @@ GEM
logging (>= 1.6.1, < 3.0)
nori (~> 2.0)
rubyntlm (~> 0.6.0, >= 0.6.1)
winrm-elevated (1.2.1)
erubi (~> 1.8)
winrm (~> 2.0)
winrm-fs (~> 1.0)
winrm-fs (1.3.3)
erubi (~> 1.8)
logging (>= 1.6.1, < 3.0)
Expand Down
2 changes: 2 additions & 0 deletions chef.gemspec
Expand Up @@ -19,6 +19,8 @@ Gem::Specification.new do |s|
s.add_dependency "chef-utils", "= #{Chef::VERSION}"
s.add_dependency "train-core", "~> 3.1"
s.add_dependency "train-winrm", ">= 0.2.5"
s.add_dependency "winrm", "~> 2.1"
s.add_dependency "winrm-elevated", "~> 1.0"

s.add_dependency "license-acceptance", "~> 1.0", ">= 1.0.5"
s.add_dependency "mixlib-cli", ">= 2.1.1", "< 3.0"
Expand Down
34 changes: 34 additions & 0 deletions lib/chef/knife/knife_windows_base.rb
@@ -0,0 +1,34 @@
#
# Author:: Aliasgar Batterywala (<aliasgar.batterywala@clogeny.com>)
# Copyright:: Copyright (c) 2015-2016 Chef Software, Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

require_relative "../knife"

class Chef
class Knife
module KnifeWindowsBase

def locate_config_value(key)
key = key.to_sym
value = config[key] || Chef::Config[:knife][key] || default_config[key]
Chef::Log.debug("Looking for key #{key} and found value #{value}")
value
end

end
end
end
158 changes: 158 additions & 0 deletions lib/chef/knife/windows_cert_generate.rb
@@ -0,0 +1,158 @@
# Author:: Mukta Aphale (<mukta.aphale@clogeny.com>)
# Copyright:: Copyright (c) 2014-2016 Chef Software, Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

require_relative "../knife"
require_relative "winrm_base"

class Chef
class Knife
class WindowsCertGenerate < Knife

attr_accessor :thumbprint, :hostname

banner "knife windows cert generate FILE_PATH (options)"

deps do
require "openssl"
require "socket"
end

option :hostname,
short: "-H HOSTNAME",
long: "--hostname HOSTNAME",
description: "Use to specify the hostname for the listener.
For example, --hostname something.mydomain.com or *.mydomain.com.",
required: true

option :output_file,
short: "-o PATH",
long: "--output-file PATH",
description: "Specifies the file path at which to generate the 3 certificate files of type .pfx, .b64, and .pem. The default is './winrmcert'.",
default: "winrmcert"

option :key_length,
short: "-k LENGTH",
long: "--key-length LENGTH",
description: "Default is 2048",
default: "2048"

option :cert_validity,
short: "-cv MONTHS",
long: "--cert-validity MONTHS",
description: "Default is 24 months",
default: "24"

option :cert_passphrase,
short: "-cp PASSWORD",
long: "--cert-passphrase PASSWORD",
description: "Password for certificate."

def generate_keypair
OpenSSL::PKey::RSA.new(config[:key_length].to_i)
end

def prompt_for_passphrase
passphrase = ""
begin
print "Passphrases do not match. Try again.\n" unless passphrase.empty?
print "Enter certificate passphrase (empty for no passphrase):"
passphrase = STDIN.gets
return passphrase.strip if passphrase == "\n"

print "Enter same passphrase again:"
confirm_passphrase = STDIN.gets
end until passphrase == confirm_passphrase
passphrase.strip
end

def generate_certificate(rsa_key)
@hostname = config[:hostname] if config[:hostname]

# Create a self-signed X509 certificate from the rsa_key (unencrypted)
cert = OpenSSL::X509::Certificate.new
cert.version = 2
cert.serial = Random.rand(65534) + 1 # 2 digit byte range random number for better security aspect

cert.subject = OpenSSL::X509::Name.parse "/CN=#{@hostname}"
cert.issuer = cert.subject
cert.public_key = rsa_key.public_key
cert.not_before = Time.now
cert.not_after = cert.not_before + 2 * 365 * config[:cert_validity].to_i * 60 * 60 # 2 years validity
ef = OpenSSL::X509::ExtensionFactory.new
ef.subject_certificate = cert
ef.issuer_certificate = cert
cert.add_extension(ef.create_extension("subjectKeyIdentifier", "hash", false))
cert.add_extension(ef.create_extension("authorityKeyIdentifier", "keyid:always", false))
cert.add_extension(ef.create_extension("extendedKeyUsage", "1.3.6.1.5.5.7.3.1", false))
cert.sign(rsa_key, OpenSSL::Digest::SHA1.new)
@thumbprint = OpenSSL::Digest::SHA1.new(cert.to_der)
cert
end

def write_certificate_to_file(cert, file_path, rsa_key)
File.open(file_path + ".pem", "wb") { |f| f.print cert.to_pem }
config[:cert_passphrase] = prompt_for_passphrase unless config[:cert_passphrase]
pfx = OpenSSL::PKCS12.create("#{config[:cert_passphrase]}", "winrmcert", rsa_key, cert)
File.open(file_path + ".pfx", "wb") { |f| f.print pfx.to_der }
File.open(file_path + ".b64", "wb") { |f| f.print Base64.strict_encode64(pfx.to_der) }
end

def certificates_already_exist?(file_path)
certs_exists = false
%w{pem pfx b64}.each do |extn|
unless Dir.glob("#{file_path}.*#{extn}").empty?
certs_exists = true
break
end
end

if certs_exists
begin
confirm("Do you really want to overwrite existing certificates")
rescue SystemExit # Need to handle this as confirming with N/n raises SystemExit exception
exit!
end
end
end

def run
STDOUT.sync = STDERR.sync = true

# takes user specified first cli value as a destination file path for generated cert.
file_path = @name_args.empty? ? config[:output_file].sub(/\.(\w+)$/, "") : @name_args.first

# check if certs already exists at given file path
certificates_already_exist? file_path

begin
filename = File.basename(file_path)
rsa_key = generate_keypair
cert = generate_certificate rsa_key
write_certificate_to_file cert, file_path, rsa_key
ui.info "Generated Certificates:"
ui.info "- #{filename}.pfx - PKCS12 format key pair. Contains public and private keys, can be used with an SSL server."
ui.info "- #{filename}.b64 - Base64 encoded PKCS12 key pair. Contains public and private keys, used by some cloud provider API's to configure SSL servers."
ui.info "- #{filename}.pem - Base64 encoded public certificate only. Required by the client to connect to the server."
ui.info "Certificate Thumbprint: #{@thumbprint.to_s.upcase}"
rescue => e
puts "ERROR: + #{e}"
end
end

end
end
end
68 changes: 68 additions & 0 deletions lib/chef/knife/windows_cert_install.rb
@@ -0,0 +1,68 @@
# Author:: Mukta Aphale (<mukta.aphale@clogeny.com>)
# Copyright:: Copyright (c) 2014-2016 Chef Software, Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

require_relative "../knife"
require_relative "winrm_base"

class Chef
class Knife
class WindowsCertInstall < Knife

banner "knife windows cert install CERT [CERT] (options)"

option :cert_passphrase,
short: "-cp PASSWORD",
long: "--cert-passphrase PASSWORD",
description: "Password for certificate."

def get_cert_passphrase
print "Enter given certificate's passphrase (empty for no passphrase):"
passphrase = STDIN.gets
passphrase.strip
end

def run
STDOUT.sync = STDERR.sync = true

if Chef::Platform.windows?
if @name_args.empty?
ui.error "Please specify the certificate path. e.g- 'knife windows cert install <path>"
exit 1
end
file_path = @name_args.first
config[:cert_passphrase] = get_cert_passphrase unless config[:cert_passphrase]

begin
ui.info "Adding certificate to the Windows Certificate Store..."
result = `powershell.exe -Command " '#{config[:cert_passphrase]}' | certutil -importPFX '#{file_path}' AT_KEYEXCHANGE"`
if $?.exitstatus == 0
ui.info "Certificate added to Certificate Store"
else
ui.info "Error adding the certificate. Use -VV option for details"
end
Chef::Log.debug "#{result}"
rescue => e
puts "ERROR: + #{e}"
end
else
ui.error "Certificate can be installed on Windows system only"
exit 1
end
end
end
end
end

0 comments on commit 0c45e0b

Please sign in to comment.