diff --git a/app/models/domain.rb b/app/models/domain.rb
index deafa86..0fd518f 100644
--- a/app/models/domain.rb
+++ b/app/models/domain.rb
@@ -1,110 +1,124 @@
 class Domain < ActiveRecord::Base
+  class NotAChild < StandardError; end
   self.inheritance_column = :nx
 
   # List all supported domain types.
   def self.domain_types
     [
       'NATIVE',
       'MASTER',
       'SLAVE',
     ]
   end
 
   # List domain types that can be created.
   def self.allowed_domain_types
     domain_types - WebDNS.settings[:prohibit_domain_types]
   end
 
   belongs_to :group
   has_many :records
   # BUG in bump_serial_trigger
   has_one :soa, -> { unscope(where: :type) }, class_name: SOA
 
   validates :group_id, presence: true
   validates :name, uniqueness: true, presence: true
   validates :type, presence: true, inclusion: { in: domain_types }
   validates :master, presence: true, ipv4: true, if: :slave?
 
   after_create :generate_soa
   after_create :generate_ns
 
   attr_writer :serial_strategy
 
   # Get the zone's serial strategy.
   #
   # Returns one of the supported serial strategies.
   def serial_strategy
     @serial_strategy ||= WebDNS.settings[:serial_strategy]
   end
 
   # Returns true if this a reverse zone.
   def reverse?
     name.end_with?('.in-addr.arpa') || name.end_with?('.ip6.arpa')
   end
 
   # Returns true if this a ENUM zone.
   def enum?
     name.end_with?('.e164.arpa')
   end
 
   # Returns true if this is a slave zone.
   def slave?
     type == 'SLAVE'
   end
 
   # Compute subnet for reverse records
   def subnet
     return if not reverse?
 
     if name.end_with?('.in-addr.arpa')
       subnet_v4
     elsif name.end_with?('.ip6.arpa')
       subnet_v6
     end
   end
 
+  def self.replace_ds(parent, child, records)
+    parent = find_by_name!(parent)
+    fail NotAChild if not child.end_with?(parent.name)
+
+    existing = parent.records.where(name: child, type: 'DS')
+    recs = records.map { |rec| DS.new(domain: parent, name: child, content: rec) }
+
+    ActiveRecord::Base.transaction do
+      existing.destroy_all
+      recs.map(&:save!)
+    end
+  end
+
   private
 
   def subnet_v4
     # get ip octets (remove .in-addr.arpa)
     octets = name.split('.')[0...-2].reverse
     return if octets.any? { |_| false }
 
     mask = 8 * octets.size
     octets += [0, 0, 0, 0]
 
     ip = IPAddr.new octets[0, 4].join('.')
 
     [ip, mask].join('/')
   end
 
   def subnet_v6
     nibbles = name.split('.')[0...-2].reverse
     return if nibbles.any? { |_| false }
 
     mask = 4 * nibbles.size
     nibbles += [0] * 32
 
     ip = IPAddr.new nibbles[0, 32].in_groups_of(4).map(&:join).join(':')
 
     [ip, mask].join('/')
   end
 
   # Hooks
 
   def generate_soa
     soa_record = SOA.new(domain: self)
 
     soa_record.save!
   end
 
   def generate_ns
     return if slave?
     return if WebDNS.settings[:default_ns].empty?
 
     WebDNS.settings[:default_ns].each { |ns|
       Record.find_or_create_by!(domain: self, type: 'NS', name: '', content: ns)
     }
   end
 
 end
diff --git a/test/models/domain_test.rb b/test/models/domain_test.rb
index 44852e2..6479a74 100644
--- a/test/models/domain_test.rb
+++ b/test/models/domain_test.rb
@@ -1,87 +1,116 @@
 require 'test_helper'
 
 class DomainTest < ActiveSupport::TestCase
   def setup
     @domain = build(:domain)
   end
 
   test 'automatic SOA creation' do
     @domain.save!
     @domain.reload
     assert_not_nil @domain.soa
   end
 
   test 'increment serial on new record' do
     @domain.save!
     soa = @domain.soa
 
     assert_serial_update soa do
       www = A.new(name: 'www', domain: @domain, content: '1.2.3.4')
       www.save!
     end
   end
 
   test 'increment serial on record update' do
     @domain.save!
     www = A.new(name: 'www', domain: @domain, content: '1.2.3.4')
     www.save!
     soa = @domain.soa.reload
 
     assert_serial_update soa do
       www.content = '1.2.3.5'
       www.save!
     end
   end
 
   test 'automatic NS creation' do
     @domain.save!
     @domain.reload
     assert_equal WebDNS.settings[:default_ns].sort,
                  @domain.records.where(type: 'NS').pluck(:content).sort
   end
 
   test 'increment serial on record destroy' do
     @domain.save!
     www = A.new(name: 'www', domain: @domain, content: '1.2.3.4')
     www.save!
     soa = @domain.soa.reload
 
     assert_serial_update soa do
       www.destroy!
     end
   end
 
   class SlaveDomainTest < ActiveSupport::TestCase
     def setup
       @domain = build(:slave)
     end
 
     test 'saves' do
       @domain.save
 
       assert_empty @domain.errors
     end
 
     test 'automatic SOA creation' do
       @domain.save!
       @domain.reload
       assert_not_nil @domain.soa
       assert_equal 1, @domain.soa.serial
     end
 
     test 'validates master' do
       @domain.master = 'not-an-ip'
       @domain.save
 
       assert_not_empty @domain.errors['master']
     end
 
     test 'no records are allowed for users' do
       @domain.save!
       rec = build(:a, domain_id: @domain.id)
 
       assert_not rec.valid?
       assert_not_empty rec.errors[:type]
     end
   end
+
+  class DsDomainTest < ActiveSupport::TestCase
+    def setup
+      @domain = create(:domain)
+      @ds = [
+        '31406 8 1 189968811e6eba862dd6c209f75623d8d9ed9142',
+        '31406 8 2 f78cf3344f72137235098ecbbd08947c2c9001c7f6a085a17f518b5d8f6b916d',
+      ]
+      @child = "dnssec.#{@domain.name}"
+      @extra = DS.create(domain: @domain, name: @child, content: 'other')
+    end
+
+    test 'add ds records' do
+      Domain.replace_ds(@domain.name, @child, @ds)
+      @extra.save! # Should be deleted
+
+      assert_equal @ds.size, DS.where(name: "dnssec.#{@domain.name}").count
+      @ds.each { |ds|
+        assert_equal 1, DS.where(name: "dnssec.#{@domain.name}", content: ds).count
+      }
+    end
+
+    test 'check if child is a valid subdomain' do
+      assert_raise Domain::NotAChild do
+        Domain.replace_ds(@domain.name, 'dnssec.example.net', @ds)
+      end
+    end
+
+  end
 end