diff options
author | danielsdeleo <dan@opscode.com> | 2013-10-29 16:02:38 -0700 |
---|---|---|
committer | danielsdeleo <dan@getchef.com> | 2014-03-19 22:32:30 -0700 |
commit | cc2307a9b7774c1b8a70066c84961ecbfd05d5d5 (patch) | |
tree | aeb4778ef163c00f04d071618d0c7c40941a5a9e | |
parent | 09f22372674b88e6e3ee7bb9aff406862ae0f27c (diff) | |
download | chef-cc2307a9b7774c1b8a70066c84961ecbfd05d5d5.tar.gz |
Add SSL check and certificate fetching commands to knife
Fixes CHEF-4711
-rw-r--r-- | CHANGELOG.md | 1 | ||||
-rw-r--r-- | DOC_CHANGES.md | 36 | ||||
-rw-r--r-- | RELEASE_NOTES.md | 54 | ||||
-rw-r--r-- | lib/chef/knife.rb | 1 | ||||
-rw-r--r-- | lib/chef/knife/ssl_check.rb | 213 | ||||
-rw-r--r-- | lib/chef/knife/ssl_fetch.rb | 145 | ||||
-rw-r--r-- | spec/unit/knife/ssl_check_spec.rb | 187 | ||||
-rw-r--r-- | spec/unit/knife/ssl_fetch_spec.rb | 151 |
8 files changed, 788 insertions, 0 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 28fcb97360..f3ba5d85eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,7 @@ * Don't destructively merge subhashes in hash_only_merge!. (CHEF-4918) * Display correct host name in knife ssh error message (CHEF-5029) * Knife::UI#confirm now has a default_choice option. (CHEF-5057) +* Add knife 'ssl check' and 'ssl fetch' commands for debugging SSL errors (CHEF-4711) ## Last Release: 11.10.0 (02/06/2014) diff --git a/DOC_CHANGES.md b/DOC_CHANGES.md index 688d11e1d5..f54ebdffbf 100644 --- a/DOC_CHANGES.md +++ b/DOC_CHANGES.md @@ -28,3 +28,39 @@ Option similar to `-o` which sets or changes the run_list of a node permanently. ### OHAI 7 Upgrade Unless there are major issues, 11.12.0 will include OHAI 7. We already have ohai 7 docs in place. We probably need to add some notes to ohai 6 notes that one should now use the newer version when possible. + +### New knife command: `knife ssl check [URI]` + +The `knife ssl check` command is used to check or troubleshoot SSL +configuration. When run without arguments, it tests whether chef/knife +can verify the Chef server's SSL certificate. Otherwise it connects to +the server specified by the given URL. + +Examples: + +* Check knife's configuration against the chef-server: `knife ssl check` +* Check chef-client's configuration: `knife ssl check -c /etc/chef/client.rb` +* Check whether an external server's SSL certificate can be verified: + `knife ssl check https://www.getchef.com` + +### New knife command: `knife ssl fetch [URI]` + +The `knife ssl fetch` command is used to copy certificates from an HTTPS +server to the `trusted_certs_dir` of knife or `chef-client`. If the +certificates match the hostname of the remote server, this command is +all that is required for knife or chef-client to verify the remote +server in the future. WARNING: `knife` has no way to determine whether +the certificates were tampered with in transit. If that happens, +knife/chef-client will trust potentially forged/malicious certificates +until they are deleted from the `trusted_certs_dir`. Users are *VERY STRONGLY* +encouraged to verify the authenticity of the certificates downloaded +with `knife fetch` by some trustworthy means. + +Examples: + +* Fetch the chef server's certificates for use with knife: + `knife ssl fetch` +* Fetch the chef server's certificates for use with chef-client: + `knife ssl fetch -c /etc/chef/client.rb` +* Fetch the certificates from an arbitrary server: + `knife ssl fetch https://www.getchef.com` diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index faf68846a3..53166e188a 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -8,6 +8,60 @@ Details about the thing that changed that needs to get included in the Release N --> # Chef Client Release Notes: +#### `knife ssl check` and `knife ssl fetch` Commands + +As part of our process to transition to verifying SSL certificates by +default, we've added knife commands to help you test (and fix, if +needed) your SSL configuration. + +`knife ssl check` makes an SSL connection to your Chef server or any +other HTTPS server and tells you if the server presents a valid +certificate. If the certificate is not valid, knife will give further +information about the cause and some instructions on how to remedy the +issue. For example, if your Chef server uses an untrusted self-signed +certificate: + +``` +ERROR: The SSL certificate of chefserver.test could not be +verified +Certificate issuer data: +/C=US/ST=WA/L=Seattle/O=YouCorp/OU=Operations/CN=chefserver.test/emailAddress=you@example.com + +Configuration Info: + +OpenSSL Configuration: +* Version: OpenSSL 1.0.1e 11 Feb 2013 +* Certificate file: /usr/local/etc/openssl/cert.pem +* Certificate directory: /usr/local/etc/openssl/certs +Chef SSL Configuration: +* ssl_ca_path: nil +* ssl_ca_file: nil +* trusted_certs_dir: "/Users/ddeleo/.chef/trusted_certs" + +TO FIX THIS ERROR: + +If the server you are connecting to uses a self-signed certificate, you +must +configure chef to trust that server's certificate. + +By default, the certificate is stored in the following location on the +host +where your chef-server runs: + + /var/opt/chef-server/nginx/ca/SERVER_HOSTNAME.crt + +Copy that file to you trusted_certs_dir (currently: /home/user/.chef/trusted_certs) +using SSH/SCP or some other secure method, then re-run this command to confirm +that the server's certificate is now trusted. +``` + +`knife ssl fetch` allows you to automatically fetch a server's +certificates to your trusted certs directory. This provides an easy way +to configure chef to trust your self-signed certificates. Note that +knife cannot verify that the certificates haven't been tampered with, so +you should verify their content after downloading. + + #### Chef Solo Missing Dependency Warning ([CHEF-4367](https://tickets.opscode.com/browse/CHEF-4367)) Chef 11.0 introduced ordered evaluation of non-recipe files in diff --git a/lib/chef/knife.rb b/lib/chef/knife.rb index eb2c321cab..5cbc968980 100644 --- a/lib/chef/knife.rb +++ b/lib/chef/knife.rb @@ -421,6 +421,7 @@ class Chef # Don't try to load a knife.rb if it wasn't specified. if config[:config_file] + Chef::Config.config_file = config[:config_file] fetcher = Chef::ConfigFetcher.new(config[:config_file], Chef::Config.config_file_jail) if fetcher.config_missing? ui.error("Specified config file #{config[:config_file]} does not exist#{Chef::Config.config_file_jail ? " or is not under config file jail #{Chef::Config.config_file_jail}" : ""}!") diff --git a/lib/chef/knife/ssl_check.rb b/lib/chef/knife/ssl_check.rb new file mode 100644 index 0000000000..e98469d5aa --- /dev/null +++ b/lib/chef/knife/ssl_check.rb @@ -0,0 +1,213 @@ +# +# Author:: Daniel DeLeo (<dan@getchef.com>) +# Copyright:: Copyright (c) 2014 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 'chef/knife' +require 'chef/config' + +class Chef + class Knife + class SslCheck < Chef::Knife + + deps do + require 'pp' + require 'socket' + require 'uri' + require 'chef/http/ssl_policies' + require 'openssl' + end + + banner "knife ssl check [URL] (options)" + + def initialize(*args) + @host = nil + @verify_peer_socket = nil + @ssl_policy = HTTP::DefaultSSLPolicy + super + end + + def uri + @uri ||= begin + Chef::Log.debug("Checking SSL cert on #{given_uri}") + URI.parse(given_uri) + end + end + + def given_uri + (name_args[0] or Chef::Config.chef_server_url) + end + + def host + uri.host + end + + def port + uri.port + end + + def validate_uri + unless host && port + invalid_uri! + end + rescue URI::Error + invalid_uri! + end + + def invalid_uri! + ui.error("Given URI: `#{given_uri}' is invalid") + show_usage + exit 1 + end + + + def verify_peer_socket + @verify_peer_socket ||= begin + tcp_connection = TCPSocket.new(host, port) + OpenSSL::SSL::SSLSocket.new(tcp_connection, verify_peer_ssl_context) + end + end + + def verify_peer_ssl_context + @verify_peer_ssl_context ||= begin + verify_peer_context = OpenSSL::SSL::SSLContext.new + @ssl_policy.apply_to(verify_peer_context) + verify_peer_context.verify_mode = OpenSSL::SSL::VERIFY_PEER + verify_peer_context + end + end + + def noverify_socket + @noverify_socket ||= begin + tcp_connection = TCPSocket.new(host, port) + OpenSSL::SSL::SSLSocket.new(tcp_connection, noverify_peer_ssl_context) + end + end + + def noverify_peer_ssl_context + @noverify_peer_ssl_context ||= begin + noverify_peer_context = OpenSSL::SSL::SSLContext.new + @ssl_policy.apply_to(noverify_peer_context) + noverify_peer_context.verify_mode = OpenSSL::SSL::VERIFY_NONE + noverify_peer_context + end + end + + def verify_cert + ui.msg("Connecting to host #{host}:#{port}") + verify_peer_socket.connect + true + rescue OpenSSL::SSL::SSLError => e + ui.error "The SSL certificate of #{host} could not be verified" + Chef::Log.debug e.message + debug_invalid_cert + false + end + + def verify_cert_host + verify_peer_socket.post_connection_check(host) + true + rescue OpenSSL::SSL::SSLError => e + ui.error "The SSL cert is signed by a trusted authority but is not valid for the given hostname" + Chef::Log.debug(e) + debug_invalid_host + false + end + + def debug_invalid_cert + noverify_socket.connect + issuer_info = noverify_socket.peer_cert.issuer + ui.msg("Certificate issuer data: #{issuer_info}") + + ui.msg("\n#{ui.color("Configuration Info:", :bold)}\n\n") + debug_ssl_settings + debug_chef_ssl_config + + ui.err(<<-ADVICE) + +#{ui.color("TO FIX THIS ERROR:", :bold)} + +If the server you are connecting to uses a self-signed certificate, you must +configure chef to trust that server's certificate. + +By default, the certificate is stored in the following location on the host +where your chef-server runs: + + /var/opt/chef-server/nginx/ca/SERVER_HOSTNAME.crt + +Copy that file to you trusted_certs_dir (currently: #{configuration.trusted_certs_dir}) +using SSH/SCP or some other secure method, then re-run this command to confirm +that the server's certificate is now trusted. + +ADVICE + end + + def debug_invalid_host + noverify_socket.connect + subject = noverify_socket.peer_cert.subject + cn_field_tuple = subject.to_a.find {|field| field[0] == "CN" } + cn = cn_field_tuple[1] + + ui.error("You are attempting to connect to: '#{host}'") + ui.error("The server's certificate belongs to '#{cn}'") + ui.err(<<-ADVICE) + +#{ui.color("TO FIX THIS ERROR:", :bold)} + +The solution for this issue depends on your networking configuration. If you +are able to connect to this server using the hostname #{cn} +instead of #{host}, then you can resolve this issue by updating chef_server_url +in your configuration file. + +If you are not able to connect to the server using the hostname #{cn} +you will have to update the certificate on the server to use the correct hostname. +ADVICE + end + + def debug_ssl_settings + ui.err "OpenSSL Configuration:" + ui.err "* Version: #{OpenSSL::OPENSSL_VERSION}" + ui.err "* Certificate file: #{OpenSSL::X509::DEFAULT_CERT_FILE}" + ui.err "* Certificate directory: #{OpenSSL::X509::DEFAULT_CERT_DIR}" + end + + def debug_chef_ssl_config + ui.err "Chef SSL Configuration:" + ui.err "* ssl_ca_path: #{configuration.ssl_ca_path.inspect}" + ui.err "* ssl_ca_file: #{configuration.ssl_ca_file.inspect}" + ui.err "* trusted_certs_dir: #{configuration.trusted_certs_dir.inspect}" + end + + def configuration + Chef::Config + end + + def run + validate_uri + if verify_cert && verify_cert_host + ui.msg "Successfully verified certificates from `#{host}'" + else + exit 1 + end + end + + end + end +end + + + + diff --git a/lib/chef/knife/ssl_fetch.rb b/lib/chef/knife/ssl_fetch.rb new file mode 100644 index 0000000000..5626a5610d --- /dev/null +++ b/lib/chef/knife/ssl_fetch.rb @@ -0,0 +1,145 @@ +# +# Author:: Daniel DeLeo (<dan@getchef.com>) +# Copyright:: Copyright (c) 2014 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 'chef/knife/ssl_fetch' +require 'chef/config' + +class Chef + class Knife + class SslFetch < Chef::Knife + + deps do + require 'pp' + require 'socket' + require 'uri' + require 'openssl' + end + + banner "knife ssl fetch [URL] (options)" + + def initialize(*args) + super + @uri = nil + end + + def uri + @uri ||= begin + Chef::Log.debug("Checking SSL cert on #{given_uri}") + URI.parse(given_uri) + end + end + + def given_uri + (name_args[0] or Chef::Config.chef_server_url) + end + + def host + uri.host + end + + def port + uri.port + end + + def validate_uri + unless host && port + invalid_uri! + end + rescue URI::Error + invalid_uri! + end + + def invalid_uri! + ui.error("Given URI: `#{given_uri}' is invalid") + show_usage + exit 1 + end + + def remote_cert_chain + tcp_connection = TCPSocket.new(host, port) + shady_ssl_connection = OpenSSL::SSL::SSLSocket.new(tcp_connection, noverify_peer_ssl_context) + shady_ssl_connection.connect + shady_ssl_connection.peer_cert_chain + end + + def noverify_peer_ssl_context + @noverify_peer_ssl_context ||= begin + noverify_peer_context = OpenSSL::SSL::SSLContext.new + noverify_peer_context.verify_mode = OpenSSL::SSL::VERIFY_NONE + noverify_peer_context + end + end + + + def cn_of(certificate) + subject = certificate.subject + cn_field_tuple = subject.to_a.find {|field| field[0] == "CN" } + cn_field_tuple[1] + end + + # Convert the CN of a certificate into something that will work well as a + # filename. To do so, all `*` characters are converted to the string + # "wildcard" and then all characters other than alphanumeric and hypen + # characters are converted to underscores. + # NOTE: There is some confustion about what the CN will contain when + # using internationalized domain names. RFC 6125 mandates that the ascii + # representation be used, but it is not clear whether this is followed in + # practice. + # https://tools.ietf.org/html/rfc6125#section-6.4.2 + def normalize_cn(cn) + cn.gsub("*", "wildcard").gsub(/[^[:alnum:]\-]/, '_') + end + + def configuration + Chef::Config + end + + def trusted_certs_dir + configuration.trusted_certs_dir + end + + def write_cert(cert) + FileUtils.mkdir_p(trusted_certs_dir) + cn = cn_of(cert) + filename = File.join(trusted_certs_dir, "#{normalize_cn(cn)}.crt") + ui.msg("Adding certificate for #{cn} in #{filename}") + File.open(filename, File::CREAT|File::TRUNC|File::RDWR, 0644) do |f| + f.print(cert.to_s) + end + end + + def run + validate_uri + ui.warn(<<-TRUST_TRUST) +Certificates from #{host} will be fetched and placed in your trusted_cert +directory (#{trusted_certs_dir}). + +Knife has no means to verify these are the correct certificates. You should +verify the authenticity of these certificates after downloading. + +TRUST_TRUST + remote_cert_chain.each do |cert| + write_cert(cert) + end + end + + + end + end +end + diff --git a/spec/unit/knife/ssl_check_spec.rb b/spec/unit/knife/ssl_check_spec.rb new file mode 100644 index 0000000000..32405a5977 --- /dev/null +++ b/spec/unit/knife/ssl_check_spec.rb @@ -0,0 +1,187 @@ +# +# Author:: Daniel DeLeo (<dan@getchef.com>) +# Copyright:: Copyright (c) 2014 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 "spec_helper" +require 'stringio' + +describe Chef::Knife::SslCheck do + + let(:name_args) { [] } + let(:stdout_io) { StringIO.new } + let(:stderr_io) { StringIO.new } + + def stderr + stderr_io.string + end + + def stdout + stdout_io.string + end + + subject(:ssl_check) do + s = Chef::Knife::SslCheck.new + s.ui.stub(:stdout).and_return(stdout_io) + s.ui.stub(:stderr).and_return(stderr_io) + s.name_args = name_args + s + end + + before do + Chef::Config.chef_server_url = "https://example.com:8443/chef-server" + end + + context "when no arguments are given" do + it "uses the chef_server_url as the host to check" do + expect(ssl_check.host).to eq("example.com") + expect(ssl_check.port).to eq(8443) + end + end + + context "when a specific URI is given" do + let(:name_args) { %w{https://example.test:10443/foo} } + + it "checks the SSL configuration against the given host" do + expect(ssl_check.host).to eq("example.test") + expect(ssl_check.port).to eq(10443) + end + end + + context "when an invalid URI is given" do + + let(:name_args) { %w{foo.test} } + + it "prints an error and exits" do + expect { ssl_check.run }.to raise_error(SystemExit) + expected_stdout=<<-E +USAGE: knife ssl check [URL] (options) +E + expected_stderr=<<-E +ERROR: Given URI: `foo.test' is invalid +E + expect(stdout_io.string).to eq(expected_stdout) + expect(stderr_io.string).to eq(expected_stderr) + end + + context "and its malformed enough to make URI.parse barf" do + + let(:name_args) { %w{ftp://lkj\\blah:example.com/blah} } + + it "prints an error and exits" do + expect { ssl_check.run }.to raise_error(SystemExit) + expected_stdout=<<-E +USAGE: knife ssl check [URL] (options) +E + expected_stderr=<<-E +ERROR: Given URI: `#{name_args[0]}' is invalid +E + expect(stdout_io.string).to eq(expected_stdout) + expect(stderr_io.string).to eq(expected_stderr) + end + end + end + + describe "verifying the remote certificate" do + let(:name_args) { %w{https://foo.example.com:8443} } + + let(:tcp_socket) { double(TCPSocket) } + let(:ssl_socket) { double(OpenSSL::SSL::SSLSocket) } + + before do + TCPSocket.should_receive(:new).with("foo.example.com", 8443).and_return(tcp_socket) + OpenSSL::SSL::SSLSocket.should_receive(:new).with(tcp_socket, ssl_check.verify_peer_ssl_context).and_return(ssl_socket) + end + + def run + ssl_check.run + rescue Exception + #puts "OUT: #{stdout_io.string}" + #puts "ERR: #{stderr_io.string}" + raise + end + + context "when the remote host's certificate is valid" do + + before do + ssl_socket.should_receive(:connect) # no error + ssl_socket.should_receive(:post_connection_check).with("foo.example.com") # no error + end + + it "prints a success message" do + ssl_check.run + expect(stdout_io.string).to include("Successfully verified certificates from `foo.example.com'") + end + end + + describe "and the certificate is not valid" do + + let(:tcp_socket_for_debug) { double(TCPSocket) } + let(:ssl_socket_for_debug) { double(OpenSSL::SSL::SSLSocket) } + + let(:self_signed_crt_path) { File.join(CHEF_SPEC_DATA, "trusted_certs", "example.crt") } + let(:self_signed_crt) { OpenSSL::X509::Certificate.new(File.read(self_signed_crt_path)) } + + before do + trap(:INT, "DEFAULT") + + TCPSocket.should_receive(:new). + with("foo.example.com", 8443). + and_return(tcp_socket_for_debug) + OpenSSL::SSL::SSLSocket.should_receive(:new). + with(tcp_socket_for_debug, ssl_check.noverify_peer_ssl_context). + and_return(ssl_socket_for_debug) + end + + context "when the certificate's CN does not match the hostname" do + before do + ssl_socket.should_receive(:connect) # no error + ssl_socket.should_receive(:post_connection_check). + with("foo.example.com"). + and_raise(OpenSSL::SSL::SSLError) + ssl_socket_for_debug.should_receive(:connect) + ssl_socket_for_debug.should_receive(:peer_cert).and_return(self_signed_crt) + end + + it "shows the CN used by the certificate and prints an error" do + expect { run }.to raise_error(SystemExit) + expect(stderr).to include("The SSL cert is signed by a trusted authority but is not valid for the given hostname") + expect(stderr).to include("You are attempting to connect to: 'foo.example.com'") + expect(stderr).to include("The server's certificate belongs to 'example.local'") + end + + end + + context "when the cert is not signed by any trusted authority" do + before do + ssl_socket.should_receive(:connect). + and_raise(OpenSSL::SSL::SSLError) + ssl_socket_for_debug.should_receive(:connect) + ssl_socket_for_debug.should_receive(:peer_cert).and_return(self_signed_crt) + end + + it "shows the CN used by the certificate and prints an error" do + expect { run }.to raise_error(SystemExit) + expect(stderr).to include("The SSL certificate of foo.example.com could not be verified") + end + + end + end + + end + +end + diff --git a/spec/unit/knife/ssl_fetch_spec.rb b/spec/unit/knife/ssl_fetch_spec.rb new file mode 100644 index 0000000000..0d3c8913f7 --- /dev/null +++ b/spec/unit/knife/ssl_fetch_spec.rb @@ -0,0 +1,151 @@ +# +# Author:: Daniel DeLeo (<dan@getchef.com>) +# Copyright:: Copyright (c) 2014 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 'spec_helper' +require 'chef/knife/ssl_fetch' + +describe Chef::Knife::SslFetch do + + let(:name_args) { [] } + let(:stdout_io) { StringIO.new } + let(:stderr_io) { StringIO.new } + + def stderr + stderr_io.string + end + + def stdout + stdout_io.string + end + + subject(:ssl_fetch) do + s = Chef::Knife::SslFetch.new + s.name_args = name_args + s.ui.stub(:stdout).and_return(stdout_io) + s.ui.stub(:stderr).and_return(stderr_io) + s + end + + context "when no arguments are given" do + + before do + Chef::Config.chef_server_url = "https://example.com:8443/chef-server" + end + + it "uses the chef_server_url as the host to fetch" do + expect(ssl_fetch.host).to eq("example.com") + expect(ssl_fetch.port).to eq(8443) + end + end + + context "when a specific URI is given" do + let(:name_args) { %w{https://example.test:10443/foo} } + + it "fetchs the SSL configuration against the given host" do + expect(ssl_fetch.host).to eq("example.test") + expect(ssl_fetch.port).to eq(10443) + end + end + + context "when an invalid URI is given" do + + let(:name_args) { %w{foo.test} } + + it "prints an error and exits" do + expect { ssl_fetch.run }.to raise_error(SystemExit) + expected_stdout=<<-E +USAGE: knife ssl fetch [URL] (options) +E + expected_stderr=<<-E +ERROR: Given URI: `foo.test' is invalid +E + expect(stdout_io.string).to eq(expected_stdout) + expect(stderr_io.string).to eq(expected_stderr) + end + + context "and its malformed enough to make URI.parse barf" do + + let(:name_args) { %w{ftp://lkj\\blah:example.com/blah} } + + it "prints an error and exits" do + expect { ssl_fetch.run }.to raise_error(SystemExit) + expected_stdout=<<-E +USAGE: knife ssl fetch [URL] (options) +E + expected_stderr=<<-E +ERROR: Given URI: `#{name_args[0]}' is invalid +E + expect(stdout_io.string).to eq(expected_stdout) + expect(stderr_io.string).to eq(expected_stderr) + end + end + end + + describe "normalizing CNs for use as paths" do + + it "normalizes '*' to 'wildcard'" do + expect(ssl_fetch.normalize_cn("*.example.com")).to eq("wildcard_example_com") + end + + it "normalizes non-alnum and hyphen characters to underscores" do + expect(ssl_fetch.normalize_cn("Billy-Bob's Super Awesome CA!")).to eq("Billy-Bob_s_Super_Awesome_CA_") + end + + end + + describe "fetching the remote cert chain" do + + let(:name_args) { %w{https://foo.example.com:8443} } + + let(:tcp_socket) { double(TCPSocket) } + let(:ssl_socket) { double(OpenSSL::SSL::SSLSocket) } + + let(:self_signed_crt_path) { File.join(CHEF_SPEC_DATA, "trusted_certs", "example.crt") } + let(:self_signed_crt) { OpenSSL::X509::Certificate.new(File.read(self_signed_crt_path)) } + + let(:trusted_certs_dir) { Dir.mktmpdir } + + def run + ssl_fetch.run + rescue Exception + puts "OUT: #{stdout_io.string}" + puts "ERR: #{stderr_io.string}" + raise + end + + before do + Chef::Config.trusted_certs_dir = trusted_certs_dir + + TCPSocket.should_receive(:new).with("foo.example.com", 8443).and_return(tcp_socket) + OpenSSL::SSL::SSLSocket.should_receive(:new).with(tcp_socket, ssl_fetch.noverify_peer_ssl_context).and_return(ssl_socket) + ssl_socket.should_receive(:connect) + ssl_socket.should_receive(:peer_cert_chain).and_return([self_signed_crt]) + end + + after do + FileUtils.rm_rf(trusted_certs_dir) + end + + it "fetches the cert chain and writes the certs to the trusted_certs_dir" do + run + stored_cert_path = File.join(trusted_certs_dir, "example_local.crt") + expect(File).to exist(stored_cert_path) + expect(File.read(stored_cert_path)).to eq(File.read(self_signed_crt_path)) + end + end +end |