Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
67d2585
Implement initial question to import certificate
jreidinger Apr 11, 2025
31b1e37
Merge remote-tracking branch 'origin/master' into initial_certificates
jreidinger Apr 15, 2025
72873ff
experiment with paragraphs
jreidinger Apr 16, 2025
abf9ff0
add fingerprint storage and use it in registration
jreidinger Apr 16, 2025
65ceb8d
service: add data to certificate question
joseivanlopez Apr 16, 2025
5946bbb
allow registration url be redefined and also add ssl fingerprints
jreidinger Apr 16, 2025
930f816
update dbus doc and fix typo
jreidinger Apr 17, 2025
a57e474
add proxies for security
jreidinger Apr 17, 2025
7b65d88
Add registration url to rust part
jreidinger Apr 17, 2025
6901bae
implement for security in rust part that communicate with dbus
jreidinger Apr 19, 2025
48e5991
service: fix registration certificate question
joseivanlopez Apr 22, 2025
4cb3cf9
web: add question for registration certificate
joseivanlopez Apr 22, 2025
41663b2
Merge remote-tracking branch 'origin/master' into initial_certificates
jreidinger Apr 22, 2025
90d795d
add web part of security
jreidinger Apr 22, 2025
d8c0b16
rename key in profile
jreidinger Apr 23, 2025
9354ceb
apply clippy suggestion
jreidinger Apr 23, 2025
f1b0180
make rubocop happy
jreidinger Apr 23, 2025
6d0d98a
fix rust unit test
jreidinger Apr 23, 2025
64ba358
adjust dbus doc to make ci happy
jreidinger Apr 23, 2025
9214f64
Apply suggestions from code review
jreidinger Apr 23, 2025
caa3559
changes from code review
jreidinger Apr 23, 2025
6696dee
changes from review
jreidinger Apr 24, 2025
22a03c9
cargo fmt
jreidinger Apr 24, 2025
6eb69ae
changes
jreidinger Apr 24, 2025
2fa3f3d
Apply suggestions from code review
jreidinger Apr 24, 2025
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
177 changes: 136 additions & 41 deletions service/lib/agama/registration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,19 @@
require "agama/cmdline_args"
require "agama/errors"
require "agama/registered_addon"
require "agama/ssl/certificate"
require "agama/ssl/certificate_details"
require "agama/ssl/errors"
require "agama/ssl/storage"

Yast.import "Arch"
Yast.import "Pkg"

module Agama
# Handles everything related to registration of system to SCC, RMT or similar.
class Registration
include Yast::I18n

# NOTE: identical and keep in sync with Software::Manager::TARGET_DIR
TARGET_DIR = "/run/agama/zypp"
private_constant :TARGET_DIR
Expand Down Expand Up @@ -84,54 +90,58 @@ def initialize(software_manager, logger)
def register(code, email: "")
return if product.nil? || reg_code

reg_params = connect_params(token: code, email: email)
catch_registration_errors do
reg_params = connect_params(token: code, email: email)

@login, @password = SUSE::Connect::YaST.announce_system(reg_params, target_distro)
# write the global credentials
# TODO: check if we can do it in memory for libzypp
SUSE::Connect::YaST.create_credentials_file(@login, @password, GLOBAL_CREDENTIALS_PATH)
@login, @password = SUSE::Connect::YaST.announce_system(reg_params, target_distro)
# write the global credentials
# TODO: check if we can do it in memory for libzypp
SUSE::Connect::YaST.create_credentials_file(@login, @password, GLOBAL_CREDENTIALS_PATH)

activate_params = {}
service = SUSE::Connect::YaST.activate_product(base_target_product, activate_params, email)
process_service(service)
activate_params = {}
service = SUSE::Connect::YaST.activate_product(base_target_product, activate_params, email)
process_service(service)

@reg_code = code
@email = email
run_on_change_callbacks
@reg_code = code
@email = email
run_on_change_callbacks
end
end

def register_addon(name, version, code)
register_version = if version.empty?
# version is not specified, find it automatically
find_addon_version(name)
else
# use the explicitly required version
version
catch_registration_errors do
register_version = if version.empty?
# version is not specified, find it automatically
find_addon_version(name)
else
# use the explicitly required version
version
end

if @registered_addons.any? { |a| a.name == name && a.version == register_version }
@logger.info "Addon #{name}-#{register_version} already registered, skipping registration"
return
end

@logger.info "Registering addon #{name}-#{register_version}"
# do not log the code, but at least log if it is empty
@logger.info "Using empty registration code" if code.empty?

target_product = OpenStruct.new(
arch: Yast::Arch.rpm_arch,
identifier: name,
version: register_version
)
activate_params = { token: code }
service = SUSE::Connect::YaST.activate_product(target_product, activate_params, @email)
process_service(service)

@registered_addons << RegisteredAddon.new(name, register_version, !version.empty?, code)
# select the products to install
@software.addon_products(find_addon_products)

run_on_change_callbacks
end

if @registered_addons.any? { |a| a.name == name && a.version == register_version }
@logger.info "Addon #{name}-#{register_version} already registered, skipping registration"
return
end

@logger.info "Registering addon #{name}-#{register_version}"
# do not log the code, but at least log if it is empty
@logger.info "Using empty registration code" if code.empty?

target_product = OpenStruct.new(
arch: Yast::Arch.rpm_arch,
identifier: name,
version: register_version
)
activate_params = { token: code }
service = SUSE::Connect::YaST.activate_product(target_product, activate_params, @email)
process_service(service)

@registered_addons << RegisteredAddon.new(name, register_version, !version.empty?, code)
# select the products to install
@software.addon_products(find_addon_products)

run_on_change_callbacks
end

# Deregisters the selected product.
Expand Down Expand Up @@ -266,9 +276,94 @@ def credentials_path(file)
def connect_params(params = {})
default_params = {}
default_params[:url] = registration_url if registration_url
default_params[:verify_callback] = verify_callback
default_params.merge(params)
end

# returns SSL verify callback
def verify_callback
lambda do |verify_ok, context|

# we cannot raise an exception with details here (all exceptions in
# verify_callback are caught and ignored), we need to store the error
# details in a global instance
store_ssl_error(context) unless verify_ok

verify_ok
rescue StandardError => e
@logger.error "Exception in SSL verify callback: #{e.class}: #{e.message} : #{e.backtrace}"
# the exception will be ignored, but reraise anyway...
raise e

end
end

def store_ssl_error(context)
@logger.error "SSL verification failed: #{context.error}: #{context.error_string}"
SSL::Errors.instance.ssl_error_code = context.error
SSL::Errors.instance.ssl_error_msg = context.error_string
SSL::Errors.instance.ssl_failed_cert =
context.current_cert ? SSL::Certificate.load(context.current_cert) : nil
end

def catch_registration_errors(&block)
# import the SSL certificate just once to avoid an infinite loop
certificate_imported = false
begin
# reset the previous SSL errors
Agama::SSL::Errors.instance.reset

block.call

true
rescue OpenSSL::SSL::SSLError => e
@logger.error "OpenSSL error: #{e}"
should_retry = handle_ssl_error(e, certificate_imported)
puts "handle ssl error #{should_retry}"
if should_retry
certificate_imported = true
SSL::Errors.instance.ssl_failed_cert.import
retry
end
raise e
end
end

def handle_ssl_error(_error, certificate_imported)
return false if certificate_imported

cert = SSL::Errors.instance.ssl_failed_cert
return false unless cert

# Import certificate if it matches predefined fingerprint.
return true if SSL::Storage.instance.fingerprints.any? { |f| cert.match_fingerprint?(f) }

error_code = SSL::Errors.instance.ssl_error_code
return false unless SSL::ErrorCodes::IMPORT_ERROR_CODES.include?(error_code)

details = SSL::CertificateDetails.new(cert)

question = Agama::Question.new(
qclass: "registration.certificate",
text: _("Secure connection error. Import certificate?"),
options: [:Import, :Abort],
default_option: :Abort,
data: {
url: registration_url || "https://scc.suse.com",
error: SSL::ErrorCodes::OPENSSL_ERROR_MESSAGES[error_code],
subject: details.subject,
issuer: details.issuer,
summary: details.summary,
fingerprints: SSL::Storage.instance.fingerprints
}
)

questions_client = Agama::DBus::Clients::Questions.new(logger: @logger)
questions_client.ask(question) do |question_client|
return question_client.answer == :Import
end
end

# Returns the URL of the registration server
#
# At this point, it just checks the kernel's command-line.
Expand Down
Loading
Loading