diff --git a/Gemfile b/Gemfile
index be4a9756b..8e167a091 100644
--- a/Gemfile
+++ b/Gemfile
@@ -5,6 +5,7 @@ gem 'active_interaction', '~> 3.8'
gem 'apipie-rails', '~> 0.5.18'
gem 'bootsnap', '>= 1.1.0', require: false
gem 'iso8601', '0.12.1' # for dates and times
+gem 'mimemagic', '~> 0.3.7'
gem 'rails', '~> 6.0'
gem 'rest-client'
gem 'uglifier'
@@ -39,6 +40,7 @@ gem 'devise', '~> 4.7'
# registry specfic
gem 'data_migrate', '~> 6.1'
+gem 'dnsruby', '~> 1.61'
gem 'isikukood' # for EE-id validation
gem 'simpleidn', '0.1.1' # For punycode
gem 'money-rails'
diff --git a/Gemfile.lock b/Gemfile.lock
index 563c39fdc..04fe467c9 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -54,13 +54,22 @@ GIT
GIT
remote: https://github.com/internetee/omniauth-tara.git
- revision: cec845ec3794532144c4976104a07e206d759aa6
+ revision: 75c369dd86a5fe1a2ee4c2a6246252d79431071c
+ branch: extended-logging
specs:
omniauth-tara (0.3.0)
addressable (~> 2.5)
omniauth (~> 1.3)
openid_connect (~> 1.1)
+GIT
+ remote: https://github.com/karlerikounapuu/dnsruby.git
+ revision: f54986268ba3109ecf7a7af0d41a78cddfce9847
+ branch: master
+ specs:
+ dnsruby (1.61.3)
+ simpleidn (~> 0.1)
+
GIT
remote: https://github.com/tarmotalu/digidoc_client.git
revision: 1645e83a5a548addce383f75703b0275c5310c32
@@ -283,7 +292,9 @@ GEM
mime-types (3.3.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2021.0225)
- mimemagic (0.3.5)
+ mimemagic (0.3.10)
+ nokogiri (~> 1)
+ rake
mini_mime (1.0.2)
mini_portile2 (2.4.0)
minitest (5.14.4)
@@ -519,6 +530,7 @@ DEPENDENCIES
devise (~> 4.7)
digidoc_client!
directo!
+ dnsruby!
domain_name
e_invoice!
epp!
@@ -531,6 +543,7 @@ DEPENDENCIES
jquery-ui-rails (= 5.0.5)
kaminari
lhv!
+ mimemagic (~> 0.3.7)
minitest (~> 5.14)
money-rails
nokogiri (~> 1.10.0)
diff --git a/app/jobs/csync_job.rb b/app/jobs/csync_job.rb
index c35059223..32fc1abcd 100644
--- a/app/jobs/csync_job.rb
+++ b/app/jobs/csync_job.rb
@@ -2,18 +2,78 @@
class CsyncJob < Que::Job
def run(generate: false)
- @logger = Logger.new(STDOUT)
- generate ? generate_scanner_input : scanner_results
+ @store = {}
+ @input_store = { secure: {}, insecure: {} }
+ @results = {}
+ @logger = Rails.env.test? ? Rails.logger : Logger.new(STDOUT)
+ generate ? generate_scanner_input : process_scanner_results
+
+ @logger.info 'CsyncJob: Finished.'
+ end
+
+ def qualified_for_monitoring?(domain, data)
+ result_types = data[:ns].map { |ns| ns[:type] }.uniq
+ ns_ok = redundant_data_for?(nameserver: true, input: result_types)
+ key_ok = redundant_data_for?(nameserver: false, input: data)
+
+ return true if ns_ok && key_ok
+
+ @logger.info "CsyncJob: #{domain}: Reseting state. Reason: " +
+ unqualification_reason(ns_ok, key_ok, result_types)
+
+ CsyncRecord.where(domain: Domain.where(name: domain)).delete_all
+
+ false
+ end
+
+ def redundant_data_for?(nameserver: false, input:)
+ if nameserver
+ input.size == 1 && (input & %w[secure insecure]).any?
+ else
+ input[:ns].map { |ns| ns[:cdnskey] }.uniq.size == 1
+ end
+ end
+
+ def unqualification_reason(nss, key, result_types)
+ return 'no CDNSKEY / nameservers reported different CDNSKEYs' unless key
+
+ if result_types.include? 'untrustworthy'
+ return 'current DNSSEC config invalid (required for rollover/delete)'
+ end
+
+ "Nameserver(s) not reachable / invalid data (#{result_types.join(', ')})" unless nss
+ end
+
+ def process_scanner_results
+ scanner_results
+
+ @results.keys.each do |domain|
+ next unless qualified_for_monitoring?(domain, @results[domain])
+
+ CsyncRecord.by_domain_name(domain)&.record_new_scan(@results[domain][:ns].first)
+ end
end
def scanner_results
+ scanner_line_results.each do |fetch|
+ domain_name = fetch[:domain]
+ @results[domain_name] = { ns: [] } unless @results[domain_name]
+ @results[domain_name][:ns] << fetch.except(:domain)
+ end
+ end
+
+ def scanner_line_results
records = []
File.open(ENV['cdns_scanner_output_file'], 'r').each_line do |line|
# Input type, NS host, NS IP, Domain name, Key type, Protocol, Algorithm, Public key
data = line.strip.split(' ')
- type, ns, ns_ip, domain, key_bit, proto, alg, pub = data
+ if data[0] == 'secure'
+ type, domain, key_bit, proto, alg, pub, ns, ns_ip = data
+ else
+ type, ns, ns_ip, domain, key_bit, proto, alg, pub = data
+ end
cdnskey = key_bit && proto && alg && pub ? "#{key_bit} #{proto} #{alg} #{pub}" : nil
- record = { domain: domain, type: type, ns: ns, ns_ip: ns_ip, key_bit: key_bit, proto: proto,
+ record = { domain: domain, type: type, ns: ns, ns_ip: ns_ip, flags: key_bit, proto: proto,
alg: alg, pub: pub, cdnskey: cdnskey }
records << record
end
@@ -23,38 +83,39 @@ class CsyncJob < Que::Job
# From this point we're working on generating input for cdnskey-scanner
def gather_pollable_domains
@logger.info 'CsyncJob Generate: Gathering current domain(s) data'
- @store = { secure: {}, insecure: {} }
Nameserver.select(:hostname, :domain_id).all.each do |ns|
- @store[:secure][ns.hostname] = [] unless @store[:secure].key? ns.hostname
- @store[:insecure][ns.hostname] = [] unless @store[:insecure].key? ns.hostname
-
- Domain.where(id: ns.domain_id).all.each do |domain|
- state = domain.dnskeys.any? ? :secure : :insecure
- @store[state][ns.hostname].push domain.name
+ %i[secure insecure].each do |i|
+ @input_store[i][ns.hostname] = [] unless @input_store[i].key? ns.hostname
end
+
+ append_domains_to_list(ns)
+ end
+ end
+
+ def append_domains_to_list(nameserver)
+ Domain.where(id: nameserver.domain_id).all.each do |domain|
+ @input_store[domain.dnskeys.any? ? :secure : :insecure][nameserver.hostname].push domain.name
end
end
def generate_scanner_input
+ @logger.info 'CsyncJob Generate: Gathering current domain(s) data'
gather_pollable_domains
- @logger.info 'CsyncJob Generate: Writing input for cdnskey-scanner to ' \
- "#{ENV['cdns_scanner_input_file']}"
out_file = File.new(ENV['cdns_scanner_input_file'], 'w+')
- out_file.puts '[secure]'
- create_input_lines(out_file, secure: true)
- out_file.puts '[insecure]'
- create_input_lines(out_file, secure: false)
+ %i[secure insecure].each do |state|
+ out_file.puts "[#{state}]"
+ create_input_lines(out_file, state)
+ end
out_file.close
- @logger.info 'CsyncJob Generate: Finished writing output.'
+ @logger.info 'CsyncJob Generate: Finished writing output to ' + ENV['cdns_scanner_input_file']
end
- def create_input_lines(out_file, secure: false)
- state = secure ? :secure : :insecure
- @store[state].keys.each do |nameserver|
- domains = @store[state][nameserver].join(' ')
+ def create_input_lines(out_file, state)
+ @input_store[state].keys.each do |nameserver|
+ domains = @input_store[state][nameserver].join(' ')
next unless domains.length.positive?
out_file.puts "#{nameserver} #{domains}"
diff --git a/app/mailers/csync_mailer.rb b/app/mailers/csync_mailer.rb
new file mode 100644
index 000000000..2779ff8a5
--- /dev/null
+++ b/app/mailers/csync_mailer.rb
@@ -0,0 +1,23 @@
+class CsyncMailer < ApplicationMailer
+ def dnssec_updated(domain:)
+ @domain = domain
+ emails = contact_emails(domain)
+
+ subject = default_i18n_subject(domain_name: domain.name)
+ mail(to: emails, subject: subject)
+ end
+
+ def dnssec_deleted(domain:)
+ @domain = domain
+ emails = contact_emails(domain)
+
+ subject = default_i18n_subject(domain_name: domain.name)
+ mail(to: emails, subject: subject)
+ end
+
+ private
+
+ def contact_emails(domain)
+ (domain.contacts.map(&:email) << domain.registrant.email).uniq
+ end
+end
diff --git a/app/models/actions/contact_create.rb b/app/models/actions/contact_create.rb
new file mode 100644
index 000000000..22fabc7b9
--- /dev/null
+++ b/app/models/actions/contact_create.rb
@@ -0,0 +1,81 @@
+module Actions
+ class ContactCreate
+ attr_reader :contact, :legal_document, :ident
+
+ def initialize(contact, legal_document, ident)
+ @contact = contact
+ @legal_document = legal_document
+ @ident = ident
+ end
+
+ def call
+ maybe_remove_address
+ maybe_attach_legal_doc
+ validate_ident
+ commit
+ end
+
+ def maybe_remove_address
+ return if Contact.address_processing?
+
+ contact.city = nil
+ contact.zip = nil
+ contact.street = nil
+ contact.state = nil
+ contact.country_code = nil
+ end
+
+ def validate_ident
+ validate_ident_integrity
+ validate_ident_birthday
+
+ identifier = ::Contact::Ident.new(code: ident[:ident], type: ident[:ident_type],
+ country_code: ident[:ident_country_code])
+
+ identifier.validate
+ contact.identifier = identifier
+ end
+
+ def validate_ident_integrity
+ return if ident.blank?
+
+ if ident[:ident_type].blank?
+ contact.add_epp_error('2003', nil, 'ident_type',
+ I18n.t('errors.messages.required_ident_attribute_missing'))
+ @error = true
+ elsif !%w[priv org birthday].include?(ident[:ident_type])
+ contact.add_epp_error('2003', nil, 'ident_type', 'Invalid ident type')
+ @error = true
+ end
+ end
+
+ def validate_ident_birthday
+ return if ident.blank?
+ return unless ident[:ident_type] != 'birthday' && ident[:ident_country_code].blank?
+
+ contact.add_epp_error('2003', nil, 'ident_country_code',
+ I18n.t('errors.messages.required_ident_attribute_missing'))
+ @error = true
+ end
+
+ def maybe_attach_legal_doc
+ return unless legal_document
+
+ doc = LegalDocument.create(
+ documentable_type: Contact,
+ document_type: legal_document[:type], body: legal_document[:body]
+ )
+
+ contact.legal_documents = [doc]
+ contact.legal_document_id = doc.id
+ end
+
+ def commit
+ contact.id = nil # new record
+ return false if @error
+
+ contact.generate_code
+ contact.save
+ end
+ end
+end
diff --git a/app/models/actions/contact_delete.rb b/app/models/actions/contact_delete.rb
new file mode 100644
index 000000000..60d3252c4
--- /dev/null
+++ b/app/models/actions/contact_delete.rb
@@ -0,0 +1,46 @@
+module Actions
+ class ContactDelete
+ attr_reader :contact
+ attr_reader :new_attributes
+ attr_reader :legal_document
+ attr_reader :ident
+ attr_reader :user
+
+ def initialize(contact, legal_document = nil)
+ @legal_document = legal_document
+ @contact = contact
+ end
+
+ def call
+ maybe_attach_legal_doc
+
+ if contact.linked?
+ contact.errors.add(:domains, :exist)
+ return
+ end
+
+ if contact.delete_prohibited?
+ contact.errors.add(:statuses, :delete_prohibited)
+ return
+ end
+
+ commit
+ end
+
+ def maybe_attach_legal_doc
+ return unless legal_document
+
+ document = contact.legal_documents.create(
+ document_type: legal_document[:type],
+ body: legal_document[:body]
+ )
+
+ contact.legal_document_id = document.id
+ contact.save
+ end
+
+ def commit
+ contact.destroy
+ end
+ end
+end
diff --git a/app/models/actions/contact_update.rb b/app/models/actions/contact_update.rb
new file mode 100644
index 000000000..7ca7b6b04
--- /dev/null
+++ b/app/models/actions/contact_update.rb
@@ -0,0 +1,109 @@
+module Actions
+ class ContactUpdate
+ attr_reader :contact
+ attr_reader :new_attributes
+ attr_reader :legal_document
+ attr_reader :ident
+ attr_reader :user
+
+ def initialize(contact, new_attributes, legal_document, ident, user)
+ @contact = contact
+ @new_attributes = new_attributes
+ @legal_document = legal_document
+ @ident = ident
+ @user = user
+ end
+
+ def call
+ maybe_remove_address
+ maybe_update_statuses
+ maybe_update_ident if ident.present?
+ maybe_attach_legal_doc
+ commit
+ end
+
+ def maybe_remove_address
+ return if Contact.address_processing?
+
+ new_attributes.delete(:city)
+ new_attributes.delete(:zip)
+ new_attributes.delete(:street)
+ new_attributes.delete(:state)
+ new_attributes.delete(:country_code)
+ end
+
+ def maybe_update_statuses
+ return unless Setting.client_status_editing_enabled
+
+ new_statuses =
+ contact.statuses - new_attributes[:statuses_to_remove] + new_attributes[:statuses_to_add]
+
+ new_attributes[:statuses] = new_statuses
+ end
+
+ def maybe_attach_legal_doc
+ return unless legal_document
+
+ document = contact.legal_documents.create(
+ document_type: legal_document[:type],
+ body: legal_document[:body]
+ )
+
+ contact.legal_document_id = document.id
+ end
+
+ def maybe_update_ident
+ unless ident.is_a?(Hash)
+ contact.add_epp_error('2308', nil, nil, I18n.t('epp.contacts.errors.valid_ident'))
+ @error = true
+ return
+ end
+
+ if contact.identifier.valid?
+ submitted_ident = ::Contact::Ident.new(code: ident[:ident],
+ type: ident[:ident_type],
+ country_code: ident[:ident_country_code])
+
+ if submitted_ident != contact.identifier
+ contact.add_epp_error('2308', nil, nil, I18n.t('epp.contacts.errors.valid_ident'))
+ @error = true
+ end
+ else
+ ident_update_attempt = ident[:ident] != contact.ident
+
+ if ident_update_attempt
+ contact.add_epp_error('2308', nil, nil, I18n.t('epp.contacts.errors.ident_update'))
+ @error = true
+ end
+
+ identifier = ::Contact::Ident.new(code: ident[:ident],
+ type: ident[:ident_type],
+ country_code: ident[:ident_country_code])
+
+ identifier.validate
+
+ contact.identifier = identifier
+ contact.ident_updated_at ||= Time.zone.now
+ end
+ end
+
+ def commit
+ return false if @error
+
+ contact.upid = user.registrar&.id
+ contact.up_date = Time.zone.now
+
+ contact.attributes = new_attributes
+
+ email_changed = contact.will_save_change_to_email?
+ old_email = contact.email_was
+ updated = contact.save
+
+ if updated && email_changed && contact.registrant?
+ ContactMailer.email_changed(contact: contact, old_email: old_email).deliver_now
+ end
+
+ updated
+ end
+ end
+end
diff --git a/app/models/actions/domain_transfer.rb b/app/models/actions/domain_transfer.rb
new file mode 100644
index 000000000..1ff9aafe9
--- /dev/null
+++ b/app/models/actions/domain_transfer.rb
@@ -0,0 +1,74 @@
+module Actions
+ class DomainTransfer
+ attr_reader :domain
+ attr_reader :transfer_code
+ attr_reader :legal_document
+ attr_reader :ident
+ attr_reader :user
+
+ def initialize(domain, transfer_code, user)
+ @domain = domain
+ @transfer_code = transfer_code
+ @user = user
+ end
+
+ def call
+ return unless domain_exists?
+ return unless valid_transfer_code?
+
+ run_validations
+
+ # return domain.pending_transfer if domain.pending_transfer
+ # attach_legal_document(::Deserializers::Xml::LegalDocument.new(frame).call)
+
+ return if domain.errors[:epp_errors].any?
+
+ commit
+ end
+
+ def domain_exists?
+ return true if domain.persisted?
+
+ domain.add_epp_error('2303', nil, nil, 'Object does not exist')
+
+ false
+ end
+
+ def run_validations
+ validate_registrar
+ validate_eligilibty
+ validate_not_discarded
+ end
+
+ def valid_transfer_code?
+ return true if transfer_code == domain.transfer_code
+
+ domain.add_epp_error('2202', nil, nil, 'Invalid authorization information')
+ false
+ end
+
+ def validate_registrar
+ return unless user == domain.registrar
+
+ domain.add_epp_error('2002', nil, nil,
+ I18n.t(:domain_already_belongs_to_the_querying_registrar))
+ end
+
+ def validate_eligilibty
+ return unless domain.non_transferable?
+
+ domain.add_epp_error('2304', nil, nil, 'Object status prohibits operation')
+ end
+
+ def validate_not_discarded
+ return unless domain.discarded?
+
+ domain.add_epp_error('2106', nil, nil, 'Object is not eligible for transfer')
+ end
+
+ def commit
+ bare_domain = Domain.find(domain.id)
+ ::DomainTransfer.request(bare_domain, user)
+ end
+ end
+end
diff --git a/app/models/concerns/csync_record/diggable.rb b/app/models/concerns/csync_record/diggable.rb
new file mode 100644
index 000000000..fe5d3c072
--- /dev/null
+++ b/app/models/concerns/csync_record/diggable.rb
@@ -0,0 +1,45 @@
+module CsyncRecord::Diggable
+ extend ActiveSupport::Concern
+
+ def valid_security_level?(post: false)
+ res = post ? valid_post_action? : valid_pre_action?
+
+ log_dnssec_entry(valid: res, post: post)
+ res
+ rescue Dnsruby::NXDomain
+ log.info("CsyncRecord: #{domain.name}: Could not resolve (NXDomain)")
+ false
+ end
+
+ def valid_pre_action?
+ case domain.dnssec_security_level
+ when Dnsruby::Message::SecurityLevel.SECURE
+ return true if %w[rollover deactivate].include?(action)
+ when Dnsruby::Message::SecurityLevel.INSECURE, Dnsruby::Message::SecurityLevel.BOGUS
+ return true if action == 'initialized'
+ end
+
+ false
+ end
+
+ def valid_post_action?
+ secure_msg = Dnsruby::Message::SecurityLevel.SECURE
+ security_level = domain.dnssec_security_level(stubber: dnskey)
+ return true if action == 'deactivate' && security_level != secure_msg
+ return true if %w[rollover initialized].include?(action) && security_level == secure_msg
+
+ false
+ end
+
+ def dnssec_validates?
+ return false unless dnskey.valid?
+ return true if valid_security_level? && valid_security_level?(post: true)
+
+ false
+ end
+
+ def log_dnssec_entry(valid:, post:)
+ log.info("#{domain.name}: #{post ? 'Post' : 'Pre'} DNSSEC validation " \
+ "#{valid ? 'PASSED' : 'FAILED'} for action '#{action}'")
+ end
+end
diff --git a/app/models/csync_record.rb b/app/models/csync_record.rb
new file mode 100644
index 000000000..b4893c7d1
--- /dev/null
+++ b/app/models/csync_record.rb
@@ -0,0 +1,132 @@
+# frozen_string_literal: true
+
+class CsyncRecord < ApplicationRecord
+ include CsyncRecord::Diggable
+ belongs_to :domain, optional: false
+ validates :domain, uniqueness: true
+ validates :cdnskey, :action, :last_scan, presence: true
+ validate :validate_unique_pub_key
+ validate :validate_csync_action
+ validate :validate_cdnskey_format
+ after_save :process_new_dnskey, if: proc { pushable? && !disable_requested? }
+ after_save :remove_dnskeys, if: proc { pushable? && disable_requested? }
+
+ SCAN_CYCLES = 3
+
+ def record_new_scan(result)
+ assign_scanner_data!(result)
+ prefix = "CsyncRecord: #{domain.name}:"
+
+ if save
+ log.info "#{prefix} Cycle done."
+ else
+ log.info "#{prefix}: not processing. Reason: #{errors.full_messages.join(' .')}"
+ CsyncRecord.where(domain: domain).delete_all
+ end
+ end
+
+ def assign_scanner_data!(result)
+ state = result[:type]
+ self.last_scan = Time.zone.now
+ self.times_scanned = (persisted? && cdnskey != result[:cdnskey] ? 1 : times_scanned + 1)
+ self.cdnskey = result[:cdnskey]
+ self.action = initializes_dnssec?(state) ? 'initialized' : determine_csync_intention(state)
+ end
+
+ def dnskey
+ key = Dnskey.new_from_csync(domain: domain, cdnskey: cdnskey)
+ log.info "DNSKEY not valid. #{key.errors.full_messages.join('. ')}." unless key.valid?
+
+ key
+ end
+
+ def destroy_all_but_last_one
+ domain.dnskeys.order(id: :desc).offset(1).destroy_all
+ end
+
+ def process_new_dnskey
+ return unless dnssec_validates?
+
+ if dnskey.save
+ destroy_all_but_last_one
+ finalize_and_notify
+ else
+ log.info "Failed to save DNSKEY. Errors: #{dnskey.errors.full_messages.join('. ')}"
+ end
+ end
+
+ def finalize_and_notify
+ CsyncMailer.dnssec_updated(domain: domain).deliver_now
+ notify_registrar_about_csync
+ CsyncRecord.where(domain: domain).destroy_all
+ log.info "CsyncRecord: #{domain.name}: DNSKEYs updated."
+ end
+
+ def pushable?
+ return true if domain.dnskeys.any? || times_scanned >= SCAN_CYCLES
+
+ false
+ end
+
+ def disable_requested?
+ ['0 3 0 AA==', '0 3 0 0'].include? cdnskey
+ end
+
+ def remove_dnskeys
+ log.info "CsyncJob: Removing DNSKEYs for domain '#{domain.name}'"
+ domain.dnskeys.destroy_all
+ CsyncMailer.dnssec_deleted(domain: domain).deliver_now
+ notify_registrar_about_csync
+
+ destroy
+ end
+
+ def notify_registrar_about_csync
+ domain.update_whois_record
+ domain.registrar.notifications.create!(text: I18n.t('notifications.texts.csync',
+ domain: domain.name, action: action))
+ end
+
+ def validate_unique_pub_key
+ return false unless domain
+ return true if disable_requested?
+ return true unless domain.dnskeys.where(public_key: dnskey.public_key).any?
+
+ errors.add(:public_key, 'already tied to this domain')
+ end
+
+ def self.by_domain_name(domain_name)
+ domain = Domain.find_by(name: domain_name)
+ log.info "CsyncRecord: '#{domain_name}' not in zone. Not initializing record." unless domain
+ CsyncRecord.find_or_initialize_by(domain: domain) if domain
+ end
+
+ def determine_csync_intention(scan_state)
+ return unless domain.dnskeys.any? && scan_state == 'secure'
+
+ disable_requested? ? 'deactivate' : 'rollover'
+ end
+
+ def initializes_dnssec?(scan_state)
+ true if domain.dnskeys.empty? && !disable_requested? && scan_state == 'insecure'
+ end
+
+ def log
+ @log ||= Rails.env.test? ? logger : Logger.new(STDOUT)
+ @log
+ end
+
+ def validate_csync_action
+ return true if %w[initialized rollover].include? action
+ return true if action == 'deactivate' && disable_requested?
+
+ errors.add(:action, :invalid)
+ end
+
+ def validate_cdnskey_format
+ return true if disable_requested?
+ return true if dnskey.valid?
+
+ errors.add(:cdnskey, :invalid)
+ end
+end
diff --git a/app/models/dnskey.rb b/app/models/dnskey.rb
index 6c6df6e44..58d4d9e31 100644
--- a/app/models/dnskey.rb
+++ b/app/models/dnskey.rb
@@ -28,7 +28,7 @@ class Dnskey < ApplicationRecord
PROTOCOLS = %w(3)
FLAGS = %w(0 256 257) # 256 = ZSK, 257 = KSK
DS_DIGEST_TYPE = [1,2]
-
+ RESOLVERS = ENV['dnssec_resolver_ips'].to_s.strip.split(', ').freeze
self.ignored_columns = %w[legacy_domain_id]
def epp_code_map
@@ -122,6 +122,26 @@ class Dnskey < ApplicationRecord
errors.add(:public_key, :invalid)
end
+ def self.new_from_csync(cdnskey:, domain:)
+ cdnskey ||= '' # avoid strip() issues for gibberish key
+
+ flags, proto, alg, pub = cdnskey.strip.split(' ')
+ Dnskey.new(domain: domain, flags: flags, protocol: proto, alg: alg, public_key: pub)
+ end
+
+ def ds_rr
+ # Break the DNSSEC trust chain as we are not able to fake RRSIG's
+ Dnsruby::Dnssec.clear_trust_anchors
+ Dnsruby::Dnssec.clear_trusted_keys
+
+ # Basically let's configure domain as root anchor. We can still verify
+ # RRSIG's / DNSKEY targeted by DS of this domain
+ generate_digest
+ generate_ds_key_tag
+ Dnsruby::RR.create("#{domain.name}. 3600 IN DS #{ds_key_tag} #{ds_alg} " \
+ "#{ds_digest_type} #{ds_digest}")
+ end
+
class << self
def int_to_hex(s)
s = s.to_s(16)
diff --git a/app/models/domain.rb b/app/models/domain.rb
index 5f32e02e2..3deaf594b 100644
--- a/app/models/domain.rb
+++ b/app/models/domain.rb
@@ -58,6 +58,7 @@ class Domain < ApplicationRecord
has_many :legal_documents, as: :documentable
has_many :registrant_verifications, dependent: :destroy
+ has_one :csync_record, dependent: :destroy
after_initialize do
self.pending_json = {} if pending_json.blank?
@@ -167,6 +168,35 @@ class Domain < ApplicationRecord
validate :validate_nameserver_ips
validate :statuses_uniqueness
+
+ def security_level_resolver
+ resolver = Dnsruby::Resolver.new(nameserver: Dnskey::RESOLVERS)
+ resolver.do_validation = true
+ resolver.do_caching = false
+ resolver.dnssec = true
+ resolver
+ end
+
+ def dnssec_security_level(stubber: nil)
+ Dnsruby::Dnssec.reset
+ resolver = security_level_resolver
+ Dnsruby::Recursor.clear_caches(resolver)
+ if Rails.env.staging?
+ clear_dnssec_trusted_anchors_and_keys
+ elsif stubber
+ Dnsruby::Dnssec.add_trust_anchor(stubber.ds_rr)
+ end
+ recursor = Dnsruby::Recursor.new(resolver)
+ recursor.dnssec = true
+ recursor.query(name, 'A', 'IN').security_level
+ end
+
+ def clear_dnssec_trusted_anchors_and_keys
+ Dnsruby::Dnssec.clear_trust_anchors
+ Dnsruby::Dnssec.clear_trusted_keys
+ Dnsruby::Dnssec.add_trust_anchor(Dnsruby::RR.create(ENV['trusted_dnskey']))
+ end
+
def statuses_uniqueness
return if statuses.uniq == statuses
errors.add(:statuses, :taken)
diff --git a/app/models/epp/contact.rb b/app/models/epp/contact.rb
index 4cd876d5f..6bd5507b9 100644
--- a/app/models/epp/contact.rb
+++ b/app/models/epp/contact.rb
@@ -44,6 +44,8 @@ class Epp::Contact < Contact
def check_availability(codes, reg:)
codes = [codes] if codes.is_a?(String)
+ codes = codes.map { |c| c.include?(':') ? c : "#{reg}:#{c}" }
+
res = []
codes.map { |c| c.include?(':') ? c : "#{reg}:#{c}" }.map { |c| c.strip.upcase }.each do |x|
c = find_by_epp_code(x)
diff --git a/app/models/nameserver.rb b/app/models/nameserver.rb
index 85e64ce41..fff757b95 100644
--- a/app/models/nameserver.rb
+++ b/app/models/nameserver.rb
@@ -58,8 +58,8 @@ class Nameserver < ApplicationRecord
end
def hostname=(hostname)
- self[:hostname] = SimpleIDN.to_unicode(hostname)
- self[:hostname_puny] = SimpleIDN.to_ascii(hostname)
+ self[:hostname] = SimpleIDN.to_unicode(hostname).gsub(/\.+$/, '')
+ self[:hostname_puny] = SimpleIDN.to_ascii(hostname).gsub(/\.+$/, '')
end
class << self
diff --git a/app/views/admin/domains/partials/_dnskeys.haml b/app/views/admin/domains/partials/_dnskeys.haml
deleted file mode 100644
index ddd95e952..000000000
--- a/app/views/admin/domains/partials/_dnskeys.haml
+++ /dev/null
@@ -1,18 +0,0 @@
-.panel.panel-default
- .panel-heading.clearfix
- = t(:dnskeys)
- .table-responsive
- %table.table.table-hover.table-bordered.table-condensed
- %thead
- %tr
- %th{class: 'col-xs-1'}= t(:flag)
- %th{class: 'col-xs-1'}= t(:protocol)
- %th{class: 'col-xs-1'}= t(:algorithm)
- %th{class: 'col-xs-9'}= t(:public_key)
- %tbody
- - @domain.dnskeys.each do |x|
- %tr
- %td= x.flags
- %td= x.protocol
- %td= x.alg
- %td= x.public_key
diff --git a/app/views/admin/domains/partials/_legal_documents.haml b/app/views/admin/domains/partials/_legal_documents.haml
deleted file mode 100644
index 900a9784a..000000000
--- a/app/views/admin/domains/partials/_legal_documents.haml
+++ /dev/null
@@ -1,14 +0,0 @@
-.panel.panel-default
- .panel-heading.clearfix
- = t(:legal_documents)
- .table-responsive
- %table.table.table-hover.table-bordered.table-condensed
- %thead
- %tr
- %th{class: 'col-xs-8'}= t(:created_at)
- %th{class: 'col-xs-4'}= t(:type)
- %tbody
- - legal_documents.each do |x|
- %tr
- %td= link_to(x.created_at, [:admin, x])
- %td= x.document_type
diff --git a/app/views/admin/domains/partials/_nameservers.haml b/app/views/admin/domains/partials/_nameservers.haml
deleted file mode 100644
index 0bc22732d..000000000
--- a/app/views/admin/domains/partials/_nameservers.haml
+++ /dev/null
@@ -1,16 +0,0 @@
-.panel.panel-default
- .panel-heading.clearfix
- = t(:nameservers)
- .table-responsive
- %table.table.table-hover.table-bordered.table-condensed
- %thead
- %tr
- %th{class: 'col-xs-4'}= t(:hostname)
- %th{class: 'col-xs-4'}= t(:ipv4)
- %th{class: 'col-xs-4'}= t(:ipv6)
- %tbody
- - @domain.nameservers.each do |x|
- %tr
- %td= x
- %td= x.ipv4
- %td= x.ipv6
diff --git a/app/views/admin/domains/partials/_statuses.haml b/app/views/admin/domains/partials/_statuses.haml
deleted file mode 100644
index ab8e55e6c..000000000
--- a/app/views/admin/domains/partials/_statuses.haml
+++ /dev/null
@@ -1,18 +0,0 @@
-#domain_statuses.panel.panel-default
- .panel-heading.clearfix
- = t(:statuses)
- .table-responsive
- %table.table.table-hover.table-bordered.table-condensed
- %thead
- %tr
- %th{class: 'col-xs-6'}= t(:status)
- %th{class: 'col-xs-6'}= t(:notes)
- %tbody
- - @domain.statuses.each do |status|
- %tr
- %td
- - if @domain.pending_json.present? && [DomainStatus::PENDING_UPDATE, DomainStatus::PENDING_DELETE].include?(status)
- = link_to status, admin_domain_domain_versions_path(@domain.id)
- - else
- = status
- %td= @domain.status_notes[status]
diff --git a/app/views/mailers/csync_mailer/dnssec_deleted.html.erb b/app/views/mailers/csync_mailer/dnssec_deleted.html.erb
new file mode 100644
index 000000000..5729c5e02
--- /dev/null
+++ b/app/views/mailers/csync_mailer/dnssec_deleted.html.erb
@@ -0,0 +1,14 @@
+Tere,
+
+Oleme eemaldanud Teie domeeni <%= @domain.name %> DNSSEC kirjed registrist.
+
+Domeeni <%= @domain.name %> DNSSEC on nüüd välja lülitatud.
+<%= render 'mailers/shared/signatures/signature.et.html' %>
+