diff --git a/package/yast2-registration.changes b/package/yast2-registration.changes index df5cb2ef1..d662a391a 100644 --- a/package/yast2-registration.changes +++ b/package/yast2-registration.changes @@ -1,3 +1,12 @@ +------------------------------------------------------------------- +Mon Feb 25 12:32:35 UTC 2019 - lslezak@suse.cz + +- Better handle the SSL certificates signed by an uknown CA + (bsc#1124992) + - Display details in a scrollable widget + - Display hints how to install the certificate manually +- 4.1.18 + ------------------------------------------------------------------- Mon Feb 18 13:11:12 UTC 2019 - lslezak@suse.cz diff --git a/package/yast2-registration.spec b/package/yast2-registration.spec index 45d128a0a..628f65a3b 100644 --- a/package/yast2-registration.spec +++ b/package/yast2-registration.spec @@ -17,7 +17,7 @@ Name: yast2-registration -Version: 4.1.17 +Version: 4.1.18 Release: 0 BuildRoot: %{_tmppath}/%{name}-%{version}-build @@ -84,6 +84,7 @@ source (mirror) automatically. %files %defattr(-,root,root) +%{yast_ybindir}/* %{yast_desktopdir}/*.desktop %{yast_clientdir}/*.rb %{yast_ydatadir}/registration diff --git a/src/bin/install_ssl_certificates b/src/bin/install_ssl_certificates new file mode 100755 index 000000000..76302e877 --- /dev/null +++ b/src/bin/install_ssl_certificates @@ -0,0 +1,48 @@ +#! /usr/bin/env ruby + +# ------------------------------------------------------------------------------ +# Copyright (c) 2019 SUSE LLC +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of version 2 of the GNU General Public License as published by the +# Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# ------------------------------------------------------------------------------ +# + +# This is a helper script which to import the SSL certificates into inst-sys +# during installation. (But is should work also in installed system.) +# +# It is intended for user convenience, this script just call the YaST +# functions, it not used by YaST itself. + +require "English" +require "yast" +require "registration/ssl_certificate" + +dir = Registration::SslCertificate::INSTSYS_CERT_DIR +if Dir.empty?(dir) + puts "ERROR: Empty #{dir} directory, put your SSL certificate there." + exit 1 +end + +# in installed system just call the update-ca-certificates script +if ENV["YAST_IS_RUNNING"] != "instsys" + puts "Updating the installed SSL certificates..." + system("/usr/sbin/update-ca-certificates") + puts $CHILD_STATUS.success? ? "Done" : "Failed!" + exit $CHILD_STATUS.exitstatus +end + +# import into the inst-sys +puts "Updating the inst-sys SSL certificates..." +if Registration::SslCertificate.update_instsys_ca + puts "Done" +else + puts "Failed!" + exit 1 +end diff --git a/src/data/registration/certificate_error.erb b/src/data/registration/certificate_error.erb index 0321f731c..18f056d1a 100644 --- a/src/data/registration/certificate_error.erb +++ b/src/data/registration/certificate_error.erb @@ -2,15 +2,48 @@ textdomain "registration" %> -<%# dialog heading %> +<%# TRANSLATORS: dialog heading %>

<%= _("Secure Connection Error") %>

- <%# label followed by error details %> - <%= _("Details:") %> <%= _(OPENSSL_ERROR_MESSAGES[Storage::SSLErrors.instance.ssl_error_code]) %> + <%# TRANSLATORS: label followed by error details %> + <%= _("Details:") %> <%= h(@url) %>: <%= h(@msg) %>

-<%# dialog sub-heading %> +<%# display a special help with description how to install the certificate manually %> +<% if error_code == SslErrorCodes::NO_LOCAL_ISSUER_CERTIFICATE %> + +

+ <%# TRANSLATORS: error description %> + <%= _("The issuer certificate cannot be found, "\ + "it needs to be installed manually.") %> +

+ +

+

+

+ +
+ +<% end %> + +<%# TRANSLATORS: dialog sub-heading %>

<%= _("Failed Certificate Details") %>

<%= SslCertificateDetails.new(certificate).richtext_summary %> \ No newline at end of file diff --git a/src/data/registration/certificate_summary.erb b/src/data/registration/certificate_summary.erb index be0b713d0..4e0ad874d 100644 --- a/src/data/registration/certificate_summary.erb +++ b/src/data/registration/certificate_summary.erb @@ -42,7 +42,7 @@ textdomain "registration" <%# label followed by the certificate serial number (in HEX format, e.g. AB:CD:42:FF...) %> <%= _("Serial Number: ") %><%= h(certificate.serial) %>
<%= _("SHA1 Fingerprint: ") %> - <%= h(certificate.fingerprint(::Registration::Fingerprint::SHA1).value) %> + <%= h(certificate.fingerprint(::Registration::Fingerprint::SHA1).value) %>
<%= _("SHA256 Fingerprint: ") %> <%= h(certificate.fingerprint(::Registration::Fingerprint::SHA256).value) %>

diff --git a/src/lib/registration/connect_helpers.rb b/src/lib/registration/connect_helpers.rb index 758d3e5c9..5bf814c8a 100644 --- a/src/lib/registration/connect_helpers.rb +++ b/src/lib/registration/connect_helpers.rb @@ -25,14 +25,16 @@ require "suse/connect" require "ui/text_helpers" -require "registration/helpers" require "registration/exceptions" -require "registration/storage" +require "registration/helpers" require "registration/smt_status" require "registration/ssl_certificate" require "registration/ssl_certificate_details" -require "registration/url_helpers" +require "registration/ssl_error_codes" +require "registration/storage" require "registration/ui/import_certificate_dialog" +require "registration/ui/failed_certificate_popup" +require "registration/url_helpers" module Registration # FIXME: change to a module and include it in the clients @@ -41,11 +43,6 @@ class ConnectHelpers extend ::UI::TextHelpers extend Yast::I18n - # openSSL error codes for which the import SSL certificate dialog is shown, - # for the other error codes just the error message is displayed - # (importing the certificate would not help) - IMPORT_ERROR_CODES = UI::ImportCertificateDialog::OPENSSL_ERROR_MESSAGES.keys - textdomain "registration" Yast.import "Mode" @@ -191,9 +188,9 @@ def self.handle_ssl_error(error, certificate_imported) expected_cert_type = Storage::Config.instance.reg_server_cert_fingerprint_type # in non-AutoYast mode ask the user to import the certificate - if !Yast::Mode.autoinst && cert && IMPORT_ERROR_CODES.include?(error_code) + if !Yast::Mode.autoinst && cert && SslErrorCodes::IMPORT_ERROR_CODES.include?(error_code) # retry after successfull import - return true if ask_import_ssl_certificate(cert) + return true if ask_import_ssl_certificate(cert, error_code) # in AutoYast mode check whether the certificate fingerprint match # the configured value (if present) elsif Yast::Mode.autoinst && cert && expected_cert_type && !expected_cert_type.empty? @@ -207,28 +204,21 @@ def self.handle_ssl_error(error, certificate_imported) return true end - report_ssl_error(error.message, cert) + report_ssl_error(error.message, cert, error_code) else # error message Yast::Report.Error(_("Received SSL Certificate does not match " \ "the expected certificate.")) end else - report_ssl_error(error.message, cert) + report_ssl_error(error.message, cert, error_code) end false end - def self.ssl_error_details(cert) - return "" if cert.nil? - - details = SslCertificateDetails.new(cert) - details.summary - end - - def self.ask_import_ssl_certificate(cert) + def self.ask_import_ssl_certificate(cert, error_code) # run the import dialog, check the user selection - if UI::ImportCertificateDialog.run(cert) != :import + if UI::ImportCertificateDialog.run(cert, error_code) != :import log.info "Certificate import rejected" return false end @@ -270,20 +260,8 @@ def self.import_ssl_certificate(cert) result end - def self.report_ssl_error(message, cert) - # try to use a translatable message first, if not found then use - # the original error message from openSSL - error_code = Storage::SSLErrors.instance.ssl_error_code - msg = UI::ImportCertificateDialog::OPENSSL_ERROR_MESSAGES[error_code] - msg = msg ? _(msg) : Storage::SSLErrors.instance.ssl_error_msg - msg = message if msg.nil? || msg.empty? - - url = UrlHelpers.registration_url || SUSE::Connect::YaST::DEFAULT_URL - msg = url + ": " + msg # workaround after string freeze - - Yast::Report.Error( - error_with_details(_("Secure connection error: %s") % msg, ssl_error_details(cert)) - ) + def self.report_ssl_error(message, cert, error_code) + UI::FailedCertificatePopup.show(message, cert, error_code) end # Check whether the registration server provides the old NCC API, @@ -359,7 +337,7 @@ def self.add_update_hint(error_msg) error_msg << msg end - private_class_method :report_error, :error_with_details, :ssl_error_details, - :import_ssl_certificate, :report_ssl_error, :check_smt_api, :handle_network_error + private_class_method :report_error, :error_with_details, :import_ssl_certificate, + :report_ssl_error, :check_smt_api, :handle_network_error end end diff --git a/src/lib/registration/ssl_error_codes.rb b/src/lib/registration/ssl_error_codes.rb new file mode 100644 index 000000000..60a25a48f --- /dev/null +++ b/src/lib/registration/ssl_error_codes.rb @@ -0,0 +1,56 @@ +# ------------------------------------------------------------------------------ +# Copyright (c) 2019 SUSE LLC +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of version 2 of the GNU General Public License as published by the +# Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# ------------------------------------------------------------------------------ +# + +require "yast" + +module Registration + # This class defines constants and translations for the most common OpenSSL errors + # @see https://www.openssl.org/docs/apps/verify.html + # @see https://github.com/openssl/openssl/blob/2c75f03b39de2fa7d006bc0f0d7c58235a54d9bb/include/openssl/x509_vfy.h#L99-L189 + class SslErrorCodes + extend Yast::I18n + textdomain "registration" + + # "certificate has expired" + EXPIRED = 10 + # "self signed certificate" + SELF_SIGNED_CERT = 18 + # "self signed certificate in certificate chain" + SELF_SIGNED_CERT_IN_CHAIN = 19 + # "unable to get local issuer certificate" + NO_LOCAL_ISSUER_CERTIFICATE = 20 + + # openSSL error codes for which the import SSL certificate dialog is shown, + # for the other error codes just the error message is displayed + # (importing the certificate would not help) + IMPORT_ERROR_CODES = [ + SELF_SIGNED_CERT, + SELF_SIGNED_CERT_IN_CHAIN + ].freeze + + # error code => translatable error message + # @note the text messages need to be translated at runtime via _() call + # @note we do not translate every possible OpenSSL error message, just the most common ones + OPENSSL_ERROR_MESSAGES = { + # TRANSLATORS: SSL error message + EXPIRED => N_("Certificate has expired"), + # TRANSLATORS: SSL error message + SELF_SIGNED_CERT => N_("Self signed certificate"), + # TRANSLATORS: SSL error message + SELF_SIGNED_CERT_IN_CHAIN => N_("Self signed certificate in certificate chain"), + # TRANSLATORS: SSL error message + NO_LOCAL_ISSUER_CERTIFICATE => N_("Unable to get local issuer certificate") + }.freeze + end +end diff --git a/src/lib/registration/ui/failed_certificate_popup.rb b/src/lib/registration/ui/failed_certificate_popup.rb new file mode 100644 index 000000000..1feed4ef4 --- /dev/null +++ b/src/lib/registration/ui/failed_certificate_popup.rb @@ -0,0 +1,87 @@ +# ------------------------------------------------------------------------------ +# Copyright (c) 2019 SUSE LLC +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of version 2 of the GNU General Public License as published by the +# Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# ------------------------------------------------------------------------------ +# + +require "erb" +require "yast" + +require "registration/helpers" +require "registration/ssl_certificate" +require "registration/ssl_certificate_details" +require "registration/ssl_error_codes" +require "registration/url_helpers" + +module Registration + module UI + # This class displays a popup with a SSL certificate error + class FailedCertificatePopup + include Yast::I18n + include ERB::Util + + attr_accessor :certificate, :error_code, :message + + Yast.import "Report" + Yast.import "Stage" + Yast.import "Directory" + + # create a display the error popup + # @param cert [Registration::SslCertitificate] certificate to display + def self.show(msg, cert, error_code) + popup = FailedCertificatePopup.new(msg, cert, error_code) + popup.show + end + + # the constructor + # @param msg [String,nil] the original OpenSSL error message + # (used as a fallback when a translated message is not found) + # @param cert [Registration::SslCertitificate] certificate to display + # @param error_code [Integer] OpenSSL error code + def initialize(msg, cert, error_code) + textdomain "registration" + + @certificate = cert + @message = msg + @error_code = error_code + end + + # display the popup and wait for clicking the [OK] button + def show + # this uses a RichText message format + Yast::Report.LongError(ssl_error_message) + end + + private + + # Build the message displayed in the popup + # @return [String] message in RichText format + def ssl_error_message + # try to use a translatable message first, if not found then use + # the original error message from openSSL + @url = UrlHelpers.registration_url || SUSE::Connect::YaST::DEFAULT_URL + @msg = _(SslErrorCodes::OPENSSL_ERROR_MESSAGES[error_code]) || message + + Helpers.render_erb_template("certificate_error.erb", binding) + end + + # the command which needs to be called to import the SSL certificate + # @return [String] command + def import_command + if Yast::Stage.initial + File.join(Yast::Directory.bindir, "install_ssl_certificates") + else + "update-ca-certificates" + end + end + end + end +end diff --git a/src/lib/registration/ui/import_certificate_dialog.rb b/src/lib/registration/ui/import_certificate_dialog.rb index fbf3b3035..7287cefb9 100644 --- a/src/lib/registration/ui/import_certificate_dialog.rb +++ b/src/lib/registration/ui/import_certificate_dialog.rb @@ -1,31 +1,23 @@ +require "erb" require "yast" require "registration/fingerprint" require "registration/ssl_certificate_details" +require "registration/ssl_error_codes" +require "registration/url_helpers" module Registration module UI # this class displays and runs the dialog for importing a SSL certificate class ImportCertificateDialog + include ERB::Util include Yast::Logger include Yast::I18n extend Yast::I18n include Yast::UIShortcuts - attr_accessor :certificate - - # error code => translatable error message - # @see https://www.openssl.org/docs/apps/verify.html - # @note the text messages need to be translated at runtime via _() call - OPENSSL_ERROR_MESSAGES = { - # SSL error message - 10 => N_("Certificate has expired"), - # SSL error message - 18 => N_("Self signed certificate"), - # SSL error message - 19 => N_("Self signed certificate in certificate chain") - }.freeze + attr_accessor :certificate, :error_code Yast.import "UI" Yast.import "Label" @@ -33,16 +25,17 @@ class ImportCertificateDialog # create a new dialog for importing a SSL certificate and run it # @param cert [Registration::SslCertitificate] certificate to display # @return [Symbol] user input (:import, :cancel) - def self.run(cert) - dialog = ImportCertificateDialog.new(cert) + def self.run(cert, error_code) + dialog = ImportCertificateDialog.new(cert, error_code) dialog.run end # the constructor # @param cert [Registration::SslCertitificate] certificate to display - def initialize(cert) + def initialize(cert, error_code) textdomain "registration" @certificate = cert + @error_code = error_code end # display the dialog and wait for a button click @@ -89,7 +82,7 @@ def import_dialog_content hide_help = displayinfo["TextMode"] && displayinfo["Width"] < 105 window_height = displayinfo["Height"] - window_height = 25 if window_height > 25 + window_height = 26 if window_height > 26 HBox( VSpacing(window_height), @@ -111,8 +104,14 @@ def handle_dialog # render Richtext description with the certificate details def certificate_description + msg = _(SslErrorCodes::OPENSSL_ERROR_MESSAGES[error_code]) + url = UrlHelpers.registration_url || SUSE::Connect::YaST::DEFAULT_URL details = SslCertificateDetails.new(certificate) - details.richtext_summary + + "

#{_("Secure Connection Error")}

\n" \ + "

#{_("Details:")} #{h(url)}: #{h(msg)}

\n" \ + "

#{_("Failed Certificate Details")}

\n" + + details.richtext_summary end # inline help text displayed in the import dialog diff --git a/test/import_certificate_dialog_test.rb b/test/import_certificate_dialog_test.rb index 12609a20e..e12778d97 100644 --- a/test/import_certificate_dialog_test.rb +++ b/test/import_certificate_dialog_test.rb @@ -5,6 +5,7 @@ describe Registration::UI::ImportCertificateDialog do describe ".run" do it "displays the certificate details and returns the user input" do + allow(Registration::UrlHelpers).to receive(:registration_url) # generic UI mocks expect(Yast::UI).to receive(:CloseDialog) # "Cancel" button must be the default @@ -25,7 +26,11 @@ end cert = Registration::SslCertificate.load_file(fixtures_file("test.pem")) - expect(Registration::UI::ImportCertificateDialog.run(cert)).to eq(:import) + expect( + Registration::UI::ImportCertificateDialog.run( + cert, Registration::SslErrorCodes::SELF_SIGNED_CERT + ) + ).to eq(:import) end end end diff --git a/test/registration/ui/failed_certificate_popup_test.rb b/test/registration/ui/failed_certificate_popup_test.rb new file mode 100644 index 000000000..be48313dc --- /dev/null +++ b/test/registration/ui/failed_certificate_popup_test.rb @@ -0,0 +1,67 @@ +#!/usr/bin/env rspec +# ------------------------------------------------------------------------------ +# Copyright (c) 2018 SUSE LLC, All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of version 2 of the GNU General Public License as published by the +# Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# ------------------------------------------------------------------------------ + +require_relative "../../spec_helper" +require "registration/ui/failed_certificate_popup" + +describe Registration::UI::FailedCertificatePopup do + + let(:ssl_error) do + "SSL_connect returned=1 errno=0 state=error: certificate verify failed " \ + "(unable to get local issuer certificate)" + end + + let(:error_code) { Registration::SslErrorCodes::NO_LOCAL_ISSUER_CERTIFICATE } + + let(:ssl_cert) do + Registration::SslCertificate.load_file(fixtures_file("test.pem")) + end + + subject do + Registration::UI::FailedCertificatePopup.new(ssl_error, ssl_cert, error_code) + end + + before do + allow(Yast::Report).to receive(:LongError) + allow(Yast::Stage).to receive(:initial).and_return(false) + end + + # the instance method + describe "#show" do + it "displays the certificate details" do + expect(Yast::Report).to receive(:LongError).with(/Organization \(O\): .*WebYaST/) + subject.show + end + + it "displays the certificate import hints" do + expect(Yast::Report).to receive(:LongError) + .with(/Save the server certificate in PEM format to file/) + subject.show + end + + it "suggests to call the install_ssl_certificates script in inst-sys" do + expect(Yast::Stage).to receive(:initial).and_return(true) + expect(Yast::Report).to receive(:LongError) + .with(/install_ssl_certificates/) + subject.show + end + end + + # the class method + describe ".show" do + it "displays the failed certificate popup" do + expect_any_instance_of(Registration::UI::FailedCertificatePopup).to receive(:show) + Registration::UI::FailedCertificatePopup.show(ssl_error, ssl_cert, error_code) + end + end +end diff --git a/test/registration/ui/not_installed_products_dialog.rb b/test/registration/ui/not_installed_products_dialog_test.rb similarity index 100% rename from test/registration/ui/not_installed_products_dialog.rb rename to test/registration/ui/not_installed_products_dialog_test.rb