/*
Technitium DNS Server
Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
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.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
using DnsServerCore.Dns.Dnssec;
using DnsServerCore.Dns.ResourceRecords;
using DnsServerCore.Dns.ZoneManagers;
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using TechnitiumLibrary.Net.Dns;
using TechnitiumLibrary.Net.Dns.ResourceRecords;
namespace DnsServerCore.Dns.Zones
{
public enum AuthZoneDnssecStatus : byte
{
Unsigned = 0,
SignedWithNSEC = 1,
SignedWithNSEC3 = 2,
}
//DNSSEC Operational Practices, Version 2
//https://datatracker.ietf.org/doc/html/rfc6781
//DNSSEC Key Rollover Timing Considerations
//https://datatracker.ietf.org/doc/html/rfc7583
class PrimaryZone : ApexZone
{
#region variables
readonly DnsServer _dnsServer;
readonly bool _internal;
Dictionary _dnssecPrivateKeys;
const uint DNSSEC_SIGNATURE_INCEPTION_OFFSET = 60 * 60;
Timer _dnssecTimer;
const int DNSSEC_TIMER_INITIAL_INTERVAL = 30000;
const int DNSSEC_TIMER_PERIODIC_INTERVAL = 900000;
DateTime _lastSignatureRefreshCheckedOn;
readonly object _dnssecUpdateLock = new object();
#endregion
#region constructor
public PrimaryZone(DnsServer dnsServer, AuthZoneInfo zoneInfo)
: base(zoneInfo)
{
_dnsServer = dnsServer;
IReadOnlyCollection dnssecPrivateKeys = zoneInfo.DnssecPrivateKeys;
if (dnssecPrivateKeys is not null)
{
_dnssecPrivateKeys = new Dictionary(dnssecPrivateKeys.Count);
foreach (DnssecPrivateKey dnssecPrivateKey in dnssecPrivateKeys)
_dnssecPrivateKeys.Add(dnssecPrivateKey.KeyTag, dnssecPrivateKey);
}
InitNotify(_dnsServer);
}
public PrimaryZone(DnsServer dnsServer, string name, string primaryNameServer, bool @internal)
: base(name)
{
_dnsServer = dnsServer;
_internal = @internal;
if (_internal)
{
_zoneTransfer = AuthZoneTransfer.Deny;
_notify = AuthZoneNotify.None;
_update = AuthZoneUpdate.Deny;
}
else
{
_zoneTransfer = AuthZoneTransfer.AllowOnlyZoneNameServers;
_notify = AuthZoneNotify.ZoneNameServers;
_update = AuthZoneUpdate.Deny;
InitNotify(_dnsServer);
}
DnsSOARecordData soa = new DnsSOARecordData(primaryNameServer, _name.Length == 0 ? "hostadmin@localhost" : "hostadmin@" + _name, 1, 900, 300, 604800, 900);
_entries[DnsResourceRecordType.SOA] = new DnsResourceRecord[] { new DnsResourceRecord(_name, DnsResourceRecordType.SOA, DnsClass.IN, soa.Minimum, soa) };
_entries[DnsResourceRecordType.NS] = new DnsResourceRecord[] { new DnsResourceRecord(_name, DnsResourceRecordType.NS, DnsClass.IN, 3600, new DnsNSRecordData(soa.PrimaryNameServer)) };
}
internal PrimaryZone(DnsServer dnsServer, string name, DnsSOARecordData soa, DnsNSRecordData ns)
: base(name)
{
_dnsServer = dnsServer;
_internal = true;
_zoneTransfer = AuthZoneTransfer.Deny;
_notify = AuthZoneNotify.None;
_update = AuthZoneUpdate.Deny;
_entries[DnsResourceRecordType.SOA] = new DnsResourceRecord[] { new DnsResourceRecord(_name, DnsResourceRecordType.SOA, DnsClass.IN, soa.Minimum, soa) };
_entries[DnsResourceRecordType.NS] = new DnsResourceRecord[] { new DnsResourceRecord(_name, DnsResourceRecordType.NS, DnsClass.IN, 3600, ns) };
}
#endregion
#region IDisposable
bool _disposed;
protected override void Dispose(bool disposing)
{
try
{
if (_disposed)
return;
if (disposing)
{
Timer dnssecTimer = _dnssecTimer;
if (dnssecTimer is not null)
{
lock (dnssecTimer)
{
dnssecTimer.Dispose();
_dnssecTimer = null;
}
}
}
_disposed = true;
}
finally
{
base.Dispose(disposing);
}
}
#endregion
#region DNSSEC
internal override void UpdateDnssecStatus()
{
base.UpdateDnssecStatus();
if (_dnssecStatus != AuthZoneDnssecStatus.Unsigned)
_dnssecTimer = new Timer(DnssecTimerCallback, null, DNSSEC_TIMER_INITIAL_INTERVAL, Timeout.Infinite);
}
private async void DnssecTimerCallback(object state)
{
try
{
List kskToReady = null;
List kskToActivate = null;
List kskToRetire = null;
List kskToRevoke = null;
List zskToActivate = null;
List zskToRetire = null;
List zskToRollover = null;
List keysToUnpublish = null;
bool saveZone = false;
lock (_dnssecPrivateKeys)
{
foreach (KeyValuePair privateKeyEntry in _dnssecPrivateKeys)
{
DnssecPrivateKey privateKey = privateKeyEntry.Value;
if (privateKey.KeyType == DnssecPrivateKeyType.KeySigningKey)
{
//KSK
switch (privateKey.State)
{
case DnssecPrivateKeyState.Published:
if (DateTime.UtcNow > GetDnsKeyStateReadyOn(privateKey))
{
//long enough time for old RRsets to expire from caches
if (kskToReady is null)
kskToReady = new List();
kskToReady.Add(privateKey);
}
break;
case DnssecPrivateKeyState.Ready:
if (privateKey.IsRetiring)
{
if (kskToRetire is null)
kskToRetire = new List();
kskToRetire.Add(privateKey);
}
else
{
if (kskToActivate is null)
kskToActivate = new List();
kskToActivate.Add(privateKey);
}
break;
case DnssecPrivateKeyState.Active:
if (privateKey.IsRetiring)
{
if (kskToRetire is null)
kskToRetire = new List();
kskToRetire.Add(privateKey);
}
break;
case DnssecPrivateKeyState.Retired:
//KSK needs to be revoked for RFC5011 consideration
if (kskToRevoke is null)
kskToRevoke = new List();
kskToRevoke.Add(privateKey);
break;
case DnssecPrivateKeyState.Revoked:
//rfc7583#section-3.3.4
//modifiedQueryInterval = MAX(1hr, MIN(15 days, TTLkey / 2))
uint modifiedQueryInterval = Math.Max(3600u, Math.Min(15 * 24 * 60 * 60, GetDnsKeyTtl() / 2));
if (DateTime.UtcNow > privateKey.StateChangedOn.AddSeconds(modifiedQueryInterval))
{
//key has been revoked for sufficient time
if (keysToUnpublish is null)
keysToUnpublish = new List();
keysToUnpublish.Add(privateKey);
}
break;
}
}
else
{
//ZSK
switch (privateKey.State)
{
case DnssecPrivateKeyState.Published:
if (DateTime.UtcNow > privateKey.StateChangedOn.AddSeconds(GetDnsKeyTtl() + GetPropagationDelay()))
{
//long enough time old RRset to expire from caches
privateKey.SetState(DnssecPrivateKeyState.Ready);
if (zskToActivate is null)
zskToActivate = new List();
zskToActivate.Add(privateKey);
}
break;
case DnssecPrivateKeyState.Ready:
if (zskToActivate is null)
zskToActivate = new List();
zskToActivate.Add(privateKey);
break;
case DnssecPrivateKeyState.Active:
if (privateKey.IsRetiring)
{
if (zskToRetire is null)
zskToRetire = new List();
zskToRetire.Add(privateKey);
}
else
{
if (privateKey.IsRolloverNeeded())
{
if (zskToRollover is null)
zskToRollover = new List();
zskToRollover.Add(privateKey);
}
}
break;
case DnssecPrivateKeyState.Retired:
if (DateTime.UtcNow > privateKey.StateChangedOn.AddSeconds(GetMaxRRSigTtl() + GetPropagationDelay()))
{
//key has been retired for sufficient time
if (keysToUnpublish is null)
keysToUnpublish = new List();
keysToUnpublish.Add(privateKey);
}
break;
}
}
}
}
#region KSK actions
if (kskToReady is not null)
{
string dnsKeyTags = null;
foreach (DnssecPrivateKey kskPrivateKey in kskToReady)
{
kskPrivateKey.SetState(DnssecPrivateKeyState.Ready);
if (kskToActivate is null)
kskToActivate = new List();
kskToActivate.Add(kskPrivateKey);
if (dnsKeyTags is null)
dnsKeyTags = kskPrivateKey.KeyTag.ToString();
else
dnsKeyTags += ", " + kskPrivateKey.KeyTag.ToString();
}
saveZone = true;
LogManager log = _dnsServer.LogManager;
if (log is not null)
log.Write("The KSK DNSKEYs (" + dnsKeyTags + ") from the primary zone are ready for changing the DS records at the parent zone: " + _name);
}
if (kskToActivate is not null)
{
try
{
IReadOnlyList kskPrivateKeys = await GetDSPublishedPrivateKeys(kskToActivate);
if (kskPrivateKeys.Count > 0)
{
string dnsKeyTags = null;
foreach (DnssecPrivateKey kskPrivateKey in kskPrivateKeys)
{
kskPrivateKey.SetState(DnssecPrivateKeyState.Active);
if (dnsKeyTags is null)
dnsKeyTags = kskPrivateKey.KeyTag.ToString();
else
dnsKeyTags += ", " + kskPrivateKey.KeyTag.ToString();
}
saveZone = true;
LogManager log = _dnsServer.LogManager;
if (log is not null)
log.Write("The KSK DNSKEYs (" + dnsKeyTags + ") from the primary zone were activated successfully: " + _name);
}
}
catch (Exception ex)
{
LogManager log = _dnsServer.LogManager;
if (log is not null)
log.Write(ex);
}
}
if (kskToRetire is not null)
saveZone = RetireKskDnsKeys(kskToRetire, false);
if (kskToRevoke is not null)
{
uint dsTtl = await GetDSTtl();
uint parentSidePropagationDelay = await GetParentSidePropagationDelayAsync();
List revokeKskPrivateKeys = null;
foreach (DnssecPrivateKey privateKey in kskToRevoke)
{
if (DateTime.UtcNow > privateKey.StateChangedOn.AddSeconds(dsTtl + parentSidePropagationDelay))
{
if (revokeKskPrivateKeys is null)
revokeKskPrivateKeys = new List();
revokeKskPrivateKeys.Add(privateKey);
}
}
if (revokeKskPrivateKeys is not null)
{
RevokeKskDnsKeys(revokeKskPrivateKeys);
saveZone = true;
}
}
#endregion
#region ZSK actions
if (zskToActivate is not null)
{
ActivateZskDnsKeys(zskToActivate);
saveZone = true;
}
if (zskToRetire is not null)
saveZone = RetireZskDnsKeys(zskToRetire, false);
if (zskToRollover is not null)
{
foreach (DnssecPrivateKey zskPrivateKey in zskToRollover)
RolloverDnsKey(zskPrivateKey.KeyTag);
saveZone = true;
}
#endregion
if (keysToUnpublish is not null)
{
UnpublishDnsKeys(keysToUnpublish);
saveZone = true;
}
//re-signing task
uint reSignPeriod = GetSignatureValidityPeriod() / 10; //the period when signature refresh check is done
if (DateTime.UtcNow > _lastSignatureRefreshCheckedOn.AddSeconds(reSignPeriod))
{
if (TryRefreshAllSignatures())
saveZone = true;
_lastSignatureRefreshCheckedOn = DateTime.UtcNow;
}
if (saveZone)
_dnsServer.AuthZoneManager.SaveZoneFile(_name);
}
catch (Exception ex)
{
LogManager log = _dnsServer.LogManager;
if (log is not null)
log.Write(ex);
}
finally
{
Timer dnssecTimer = _dnssecTimer;
if (dnssecTimer is not null)
{
lock (dnssecTimer)
{
dnssecTimer.Change(DNSSEC_TIMER_PERIODIC_INTERVAL, Timeout.Infinite);
}
}
}
}
public void SignZoneWithRsaNSec(string hashAlgorithm, int kskKeySize, int zskKeySize, uint dnsKeyTtl, ushort zskRolloverDays)
{
SignZoneWithRsa(hashAlgorithm, kskKeySize, zskKeySize, false, 0, 0, dnsKeyTtl, zskRolloverDays);
}
public void SignZoneWithRsaNSec3(string hashAlgorithm, int kskKeySize, int zskKeySize, ushort iterations, byte saltLength, uint dnsKeyTtl, ushort zskRolloverDays)
{
SignZoneWithRsa(hashAlgorithm, kskKeySize, zskKeySize, true, iterations, saltLength, dnsKeyTtl, zskRolloverDays);
}
private void SignZoneWithRsa(string hashAlgorithm, int kskKeySize, int zskKeySize, bool useNSec3, ushort iterations, byte saltLength, uint dnsKeyTtl, ushort zskRolloverDays)
{
if (_dnssecStatus != AuthZoneDnssecStatus.Unsigned)
throw new DnsServerException("Cannot sign zone: the zone is already signed.");
if (iterations > 50)
throw new ArgumentOutOfRangeException(nameof(iterations), "NSEC3 iterations valid range is 0-50");
if (saltLength > 32)
throw new ArgumentOutOfRangeException(nameof(saltLength), "NSEC3 salt length valid range is 0-32");
//generate private keys
DnssecAlgorithm algorithm;
switch (hashAlgorithm.ToUpper())
{
case "MD5":
algorithm = DnssecAlgorithm.RSAMD5;
break;
case "SHA1":
algorithm = DnssecAlgorithm.RSASHA1;
break;
case "SHA256":
algorithm = DnssecAlgorithm.RSASHA256;
break;
case "SHA512":
algorithm = DnssecAlgorithm.RSASHA512;
break;
default:
throw new NotSupportedException("Hash algorithm is not supported: " + hashAlgorithm);
}
DnssecPrivateKey kskPrivateKey = DnssecPrivateKey.Create(algorithm, DnssecPrivateKeyType.KeySigningKey, kskKeySize);
DnssecPrivateKey zskPrivateKey = DnssecPrivateKey.Create(algorithm, DnssecPrivateKeyType.ZoneSigningKey, zskKeySize);
zskPrivateKey.RolloverDays = zskRolloverDays;
_dnssecPrivateKeys = new Dictionary(4);
_dnssecPrivateKeys.Add(kskPrivateKey.KeyTag, kskPrivateKey);
_dnssecPrivateKeys.Add(zskPrivateKey.KeyTag, zskPrivateKey);
//sign zone
SignZone(useNSec3, iterations, saltLength, dnsKeyTtl);
}
public void SignZoneWithEcdsaNSec(string curve, uint dnsKeyTtl, ushort zskRolloverDays)
{
SignZoneWithEcdsa(curve, false, 0, 0, dnsKeyTtl, zskRolloverDays);
}
public void SignZoneWithEcdsaNSec3(string curve, ushort iterations, byte saltLength, uint dnsKeyTtl, ushort zskRolloverDays)
{
SignZoneWithEcdsa(curve, true, iterations, saltLength, dnsKeyTtl, zskRolloverDays);
}
private void SignZoneWithEcdsa(string curve, bool useNSec3, ushort iterations, byte saltLength, uint dnsKeyTtl, ushort zskRolloverDays)
{
if (_dnssecStatus != AuthZoneDnssecStatus.Unsigned)
throw new DnsServerException("Cannot sign zone: the zone is already signed.");
if (iterations > 50)
throw new ArgumentOutOfRangeException(nameof(iterations), "NSEC3 iterations valid range is 0-50");
if (saltLength > 32)
throw new ArgumentOutOfRangeException(nameof(saltLength), "NSEC3 salt length valid range is 0-32");
//generate private keys
DnssecAlgorithm algorithm;
switch (curve.ToUpper())
{
case "P256":
algorithm = DnssecAlgorithm.ECDSAP256SHA256;
break;
case "P384":
algorithm = DnssecAlgorithm.ECDSAP384SHA384;
break;
default:
throw new NotSupportedException("ECDSA curve is not supported: " + curve);
}
DnssecPrivateKey kskPrivateKey = DnssecPrivateKey.Create(algorithm, DnssecPrivateKeyType.KeySigningKey);
DnssecPrivateKey zskPrivateKey = DnssecPrivateKey.Create(algorithm, DnssecPrivateKeyType.ZoneSigningKey);
zskPrivateKey.RolloverDays = zskRolloverDays;
_dnssecPrivateKeys = new Dictionary(4);
_dnssecPrivateKeys.Add(kskPrivateKey.KeyTag, kskPrivateKey);
_dnssecPrivateKeys.Add(zskPrivateKey.KeyTag, zskPrivateKey);
//sign zone
SignZone(useNSec3, iterations, saltLength, dnsKeyTtl);
}
private void SignZone(bool useNSec3, ushort iterations, byte saltLength, uint dnsKeyTtl)
{
List addedRecords = new List();
List deletedRecords = new List();
try
{
//update private key state
foreach (KeyValuePair privateKeyEntry in _dnssecPrivateKeys)
{
DnssecPrivateKey privateKey = privateKeyEntry.Value;
switch (privateKey.KeyType)
{
case DnssecPrivateKeyType.KeySigningKey:
privateKey.SetState(DnssecPrivateKeyState.Published);
break;
case DnssecPrivateKeyType.ZoneSigningKey:
privateKey.SetState(DnssecPrivateKeyState.Ready);
break;
}
}
//add DNSKEYs
List dnsKeyRecords = new List(_dnssecPrivateKeys.Count);
foreach (KeyValuePair privateKey in _dnssecPrivateKeys)
dnsKeyRecords.Add(new DnsResourceRecord(_name, DnsResourceRecordType.DNSKEY, DnsClass.IN, dnsKeyTtl, privateKey.Value.DnsKey));
if (!TrySetRecords(DnsResourceRecordType.DNSKEY, dnsKeyRecords, out IReadOnlyList deletedDnsKeyRecords))
throw new InvalidOperationException("Failed to add DNSKEY.");
addedRecords.AddRange(dnsKeyRecords);
deletedRecords.AddRange(deletedDnsKeyRecords);
//sign all RRSets
IReadOnlyList zones = _dnsServer.AuthZoneManager.GetZoneWithSubDomainZones(_name);
foreach (AuthZone zone in zones)
{
IReadOnlyList newRRSigRecords = zone.SignAllRRSets();
if (newRRSigRecords.Count > 0)
{
zone.AddOrUpdateRRSigRecords(newRRSigRecords, out IReadOnlyList deletedRRSigRecords);
addedRecords.AddRange(newRRSigRecords);
deletedRecords.AddRange(deletedRRSigRecords);
}
}
if (useNSec3)
{
EnableNSec3(zones, iterations, saltLength);
_dnssecStatus = AuthZoneDnssecStatus.SignedWithNSEC3;
}
else
{
EnableNSec(zones);
_dnssecStatus = AuthZoneDnssecStatus.SignedWithNSEC;
}
//update private key state
foreach (KeyValuePair privateKeyEntry in _dnssecPrivateKeys)
{
DnssecPrivateKey privateKey = privateKeyEntry.Value;
switch (privateKey.KeyType)
{
case DnssecPrivateKeyType.ZoneSigningKey:
privateKey.SetState(DnssecPrivateKeyState.Active);
break;
}
}
_dnssecTimer = new Timer(DnssecTimerCallback, null, DNSSEC_TIMER_INITIAL_INTERVAL, Timeout.Infinite);
CommitAndIncrementSerial(deletedRecords, addedRecords);
TriggerNotify();
}
catch
{
_dnssecStatus = AuthZoneDnssecStatus.Unsigned;
_dnssecPrivateKeys = null;
foreach (DnsResourceRecord addedRecord in addedRecords)
TryDeleteRecord(addedRecord.Type, addedRecord.RDATA, out _);
foreach (DnsResourceRecord deletedRecord in deletedRecords)
AddRecord(deletedRecord, out _, out _);
throw;
}
}
public void UnsignZone()
{
if (_dnssecStatus == AuthZoneDnssecStatus.Unsigned)
throw new DnsServerException("Cannot unsign zone: the is zone not signed.");
List deletedRecords = new List();
IReadOnlyList zones = _dnsServer.AuthZoneManager.GetZoneWithSubDomainZones(_name);
foreach (AuthZone zone in zones)
{
deletedRecords.AddRange(zone.RemoveAllDnssecRecords());
if (zone is SubDomainZone subDomainZone)
{
if (zone.IsEmpty)
_dnsServer.AuthZoneManager.RemoveSubDomainZone(zone.Name); //remove empty sub zone
else
subDomainZone.AutoUpdateState();
}
}
Timer dnssecTimer = _dnssecTimer;
if (dnssecTimer is not null)
{
lock (dnssecTimer)
{
dnssecTimer.Dispose();
_dnssecTimer = null;
}
}
_dnssecPrivateKeys = null;
_dnssecStatus = AuthZoneDnssecStatus.Unsigned;
CommitAndIncrementSerial(deletedRecords);
TriggerNotify();
}
public void ConvertToNSec()
{
if (_dnssecStatus != AuthZoneDnssecStatus.SignedWithNSEC3)
throw new DnsServerException("Cannot convert to NSEC: the zone must be signed with NSEC3 for conversion.");
lock (_dnssecUpdateLock)
{
IReadOnlyList zones = _dnsServer.AuthZoneManager.GetZoneWithSubDomainZones(_name);
DisableNSec3(zones);
//since zones were removed when disabling NSEC3; get updated non empty zones list
List nonEmptyZones = new List(zones.Count);
foreach (AuthZone zone in zones)
{
if (!zone.IsEmpty)
nonEmptyZones.Add(zone);
}
EnableNSec(nonEmptyZones);
_dnssecStatus = AuthZoneDnssecStatus.SignedWithNSEC;
}
TriggerNotify();
}
public void ConvertToNSec3(ushort iterations, byte saltLength)
{
if (_dnssecStatus != AuthZoneDnssecStatus.SignedWithNSEC)
throw new DnsServerException("Cannot convert to NSEC3: the zone must be signed with NSEC for conversion.");
if (iterations > 50)
throw new ArgumentOutOfRangeException(nameof(iterations), "NSEC3 iterations valid range is 0-50");
if (saltLength > 32)
throw new ArgumentOutOfRangeException(nameof(saltLength), "NSEC3 salt length valid range is 0-32");
lock (_dnssecUpdateLock)
{
IReadOnlyList zones = _dnsServer.AuthZoneManager.GetZoneWithSubDomainZones(_name);
DisableNSec(zones);
EnableNSec3(zones, iterations, saltLength);
_dnssecStatus = AuthZoneDnssecStatus.SignedWithNSEC3;
}
TriggerNotify();
}
public void UpdateNSec3Parameters(ushort iterations, byte saltLength)
{
if (_dnssecStatus != AuthZoneDnssecStatus.SignedWithNSEC3)
throw new DnsServerException("Cannot update NSEC3 parameters: the zone must be signed with NSEC3 first.");
if (iterations > 50)
throw new ArgumentOutOfRangeException(nameof(iterations), "NSEC3 iterations valid range is 0-50");
if (saltLength > 32)
throw new ArgumentOutOfRangeException(nameof(saltLength), "NSEC3 salt length valid range is 0-32");
lock (_dnssecUpdateLock)
{
IReadOnlyList zones = _dnsServer.AuthZoneManager.GetZoneWithSubDomainZones(_name);
DisableNSec3(zones);
//since zones were removed when disabling NSEC3; get updated non empty zones list
List nonEmptyZones = new List(zones.Count);
foreach (AuthZone zone in zones)
{
if (!zone.IsEmpty)
nonEmptyZones.Add(zone);
}
EnableNSec3(nonEmptyZones, iterations, saltLength);
}
TriggerNotify();
}
private void RefreshNSec()
{
lock (_dnssecUpdateLock)
{
IReadOnlyList zones = _dnsServer.AuthZoneManager.GetZoneWithSubDomainZones(_name);
EnableNSec(zones);
}
}
private void RefreshNSec3()
{
lock (_dnssecUpdateLock)
{
IReadOnlyList zones = _dnsServer.AuthZoneManager.GetZoneWithSubDomainZones(_name);
//get non NSEC3 zones
List nonNSec3Zones = new List(zones.Count);
foreach (AuthZone zone in zones)
{
if (zone.HasOnlyNSec3Records())
continue;
nonNSec3Zones.Add(zone);
}
IReadOnlyList nsec3ParamRecords = GetRecords(DnsResourceRecordType.NSEC3PARAM);
DnsNSEC3PARAMRecordData nsec3Param = nsec3ParamRecords[0].RDATA as DnsNSEC3PARAMRecordData;
EnableNSec3(nonNSec3Zones, nsec3Param.Iterations, nsec3Param.Salt);
}
}
private void EnableNSec(IReadOnlyList zones)
{
List addedRecords = new List();
List deletedRecords = new List();
uint ttl = GetZoneSoaMinimum();
for (int i = 0; i < zones.Count; i++)
{
AuthZone zone = zones[i];
AuthZone nextZone;
if (i < zones.Count - 1)
nextZone = zones[i + 1];
else
nextZone = zones[0];
IReadOnlyList newNSecRecords = zone.GetUpdatedNSecRRSet(nextZone.Name, ttl);
if (newNSecRecords.Count > 0)
{
if (!zone.TrySetRecords(DnsResourceRecordType.NSEC, newNSecRecords, out IReadOnlyList deletedNSecRecords))
throw new DnsServerException("Failed to set DNSSEC records. Please try again.");
addedRecords.AddRange(newNSecRecords);
deletedRecords.AddRange(deletedNSecRecords);
IReadOnlyList newRRSigRecords = SignRRSet(newNSecRecords);
if (newRRSigRecords.Count > 0)
{
zone.AddOrUpdateRRSigRecords(newRRSigRecords, out IReadOnlyList deletedRRSigRecords);
addedRecords.AddRange(newRRSigRecords);
deletedRecords.AddRange(deletedRRSigRecords);
}
}
}
CommitAndIncrementSerial(deletedRecords, addedRecords);
}
private void DisableNSec(IReadOnlyList zones)
{
List deletedRecords = new List();
foreach (AuthZone zone in zones)
deletedRecords.AddRange(zone.RemoveNSecRecordsWithRRSig());
CommitAndIncrementSerial(deletedRecords);
}
private void EnableNSec3(IReadOnlyList zones, ushort iterations, byte saltLength)
{
byte[] salt;
if (saltLength > 0)
{
salt = new byte[saltLength];
using RandomNumberGenerator rng = RandomNumberGenerator.Create();
rng.GetBytes(salt);
}
else
{
salt = Array.Empty();
}
EnableNSec3(zones, iterations, salt);
}
private void EnableNSec3(IReadOnlyList zones, ushort iterations, byte[] salt)
{
List addedRecords = new List();
List deletedRecords = new List();
List partialNSec3Records = new List(zones.Count);
int apexLabelCount = DnsRRSIGRecordData.GetLabelCount(_name);
uint ttl = GetZoneSoaMinimum();
//list all partial NSEC3 records
foreach (AuthZone zone in zones)
{
partialNSec3Records.Add(zone.GetPartialNSec3Record(_name, ttl, iterations, salt));
int zoneLabelCount = DnsRRSIGRecordData.GetLabelCount(zone.Name);
if ((zoneLabelCount - apexLabelCount) > 1)
{
//empty non-terminal (ENT) may exists
string currentOwnerName = zone.Name;
while (true)
{
currentOwnerName = AuthZoneManager.GetParentZone(currentOwnerName);
if (currentOwnerName.Equals(_name, StringComparison.OrdinalIgnoreCase))
break;
//add partial NSEC3 record for ENT
AuthZone entZone = new PrimarySubDomainZone(null, currentOwnerName); //dummy empty non-terminal (ENT) sub domain object
partialNSec3Records.Add(entZone.GetPartialNSec3Record(_name, ttl, iterations, salt));
}
}
}
//sort partial NSEC3 records
partialNSec3Records.Sort(delegate (DnsResourceRecord rr1, DnsResourceRecord rr2)
{
return string.CompareOrdinal(rr1.Name, rr2.Name);
});
//deduplicate partial NSEC3 records and insert next hashed owner name to complete them
List uniqueNSec3Records = new List(partialNSec3Records.Count);
for (int i = 0; i < partialNSec3Records.Count; i++)
{
DnsResourceRecord partialNSec3Record = partialNSec3Records[i];
DnsResourceRecord nextPartialNSec3Record;
if (i < partialNSec3Records.Count - 1)
{
nextPartialNSec3Record = partialNSec3Records[i + 1];
//check for duplicates
if (partialNSec3Record.Name.Equals(nextPartialNSec3Record.Name, StringComparison.OrdinalIgnoreCase))
{
//found duplicate; merge current nsec3 into next nsec3
DnsNSEC3RecordData nsec3 = partialNSec3Record.RDATA as DnsNSEC3RecordData;
DnsNSEC3RecordData nextNSec3 = nextPartialNSec3Record.RDATA as DnsNSEC3RecordData;
List uniqueTypes = new List(nsec3.Types.Count + nextNSec3.Types.Count);
uniqueTypes.AddRange(nsec3.Types);
foreach (DnsResourceRecordType type in nextNSec3.Types)
{
if (!uniqueTypes.Contains(type))
uniqueTypes.Add(type);
}
uniqueTypes.Sort();
//update the next nsec3 record and continue
DnsNSEC3RecordData mergedPartialNSec3 = new DnsNSEC3RecordData(DnssecNSEC3HashAlgorithm.SHA1, DnssecNSEC3Flags.None, iterations, salt, Array.Empty(), uniqueTypes);
partialNSec3Records[i + 1] = new DnsResourceRecord(partialNSec3Record.Name, DnsResourceRecordType.NSEC3, DnsClass.IN, ttl, mergedPartialNSec3);
continue;
}
}
else
{
//for last NSEC3, next NSEC3 is the first in list
nextPartialNSec3Record = partialNSec3Records[0];
}
//add NSEC3 record with next hashed owner name
{
DnsNSEC3RecordData partialNSec3 = partialNSec3Record.RDATA as DnsNSEC3RecordData;
byte[] nextHashedOwnerName = DnsNSEC3RecordData.GetHashedOwnerNameFrom(nextPartialNSec3Record.Name);
DnsNSEC3RecordData updatedNSec3 = new DnsNSEC3RecordData(DnssecNSEC3HashAlgorithm.SHA1, DnssecNSEC3Flags.None, iterations, salt, nextHashedOwnerName, partialNSec3.Types);
uniqueNSec3Records.Add(new DnsResourceRecord(partialNSec3Record.Name, DnsResourceRecordType.NSEC3, DnsClass.IN, ttl, updatedNSec3));
}
}
//insert and sign NSEC3 records
foreach (DnsResourceRecord uniqueNSec3Record in uniqueNSec3Records)
{
AuthZone zone = _dnsServer.AuthZoneManager.GetOrAddSubDomainZone(_name, uniqueNSec3Record.Name);
DnsResourceRecord[] newNSec3Records = new DnsResourceRecord[] { uniqueNSec3Record };
if (!zone.TrySetRecords(DnsResourceRecordType.NSEC3, newNSec3Records, out IReadOnlyList deletedNSec3Records))
throw new InvalidOperationException();
addedRecords.AddRange(newNSec3Records);
deletedRecords.AddRange(deletedNSec3Records);
IReadOnlyList newRRSigRecords = SignRRSet(newNSec3Records);
if (newRRSigRecords.Count > 0)
{
zone.AddOrUpdateRRSigRecords(newRRSigRecords, out IReadOnlyList deletedRRSigRecords);
addedRecords.AddRange(newRRSigRecords);
deletedRecords.AddRange(deletedRRSigRecords);
}
}
//insert and sign NSEC3PARAM record
{
DnsNSEC3PARAMRecordData newNSec3Param = new DnsNSEC3PARAMRecordData(DnssecNSEC3HashAlgorithm.SHA1, DnssecNSEC3Flags.None, iterations, salt);
DnsResourceRecord[] newNSec3ParamRecords = new DnsResourceRecord[] { new DnsResourceRecord(_name, DnsResourceRecordType.NSEC3PARAM, DnsClass.IN, ttl, newNSec3Param) };
if (!TrySetRecords(DnsResourceRecordType.NSEC3PARAM, newNSec3ParamRecords, out IReadOnlyList deletedNSec3ParamRecords))
throw new InvalidOperationException();
addedRecords.AddRange(newNSec3ParamRecords);
deletedRecords.AddRange(deletedNSec3ParamRecords);
IReadOnlyList newRRSigRecords = SignRRSet(newNSec3ParamRecords);
if (newRRSigRecords.Count > 0)
{
AddOrUpdateRRSigRecords(newRRSigRecords, out IReadOnlyList deletedRRSigRecords);
addedRecords.AddRange(newRRSigRecords);
deletedRecords.AddRange(deletedRRSigRecords);
}
}
CommitAndIncrementSerial(deletedRecords, addedRecords);
}
private void DisableNSec3(IReadOnlyList zones)
{
List deletedRecords = new List();
foreach (AuthZone zone in zones)
{
deletedRecords.AddRange(zone.RemoveNSec3RecordsWithRRSig());
if (zone is SubDomainZone subDomainZone)
{
if (zone.IsEmpty)
_dnsServer.AuthZoneManager.RemoveSubDomainZone(zone.Name); //remove empty sub zone
else
subDomainZone.AutoUpdateState();
}
}
CommitAndIncrementSerial(deletedRecords);
}
public void GenerateAndAddRsaKey(DnssecPrivateKeyType keyType, string hashAlgorithm, int keySize, ushort rolloverDays)
{
if (_dnssecStatus == AuthZoneDnssecStatus.Unsigned)
throw new DnsServerException("The zone must be signed.");
DnssecAlgorithm algorithm;
switch (hashAlgorithm.ToUpper())
{
case "MD5":
algorithm = DnssecAlgorithm.RSAMD5;
break;
case "SHA1":
algorithm = DnssecAlgorithm.RSASHA1;
break;
case "SHA256":
algorithm = DnssecAlgorithm.RSASHA256;
break;
case "SHA512":
algorithm = DnssecAlgorithm.RSASHA512;
break;
default:
throw new NotSupportedException("Hash algorithm is not supported: " + hashAlgorithm);
}
GenerateAndAddRsaKey(keyType, algorithm, keySize, rolloverDays);
}
private void GenerateAndAddRsaKey(DnssecPrivateKeyType keyType, DnssecAlgorithm algorithm, int keySize, ushort rolloverDays)
{
int i = 0;
while (i++ < 5)
{
DnssecPrivateKey privateKey = DnssecPrivateKey.Create(algorithm, keyType, keySize);
privateKey.RolloverDays = rolloverDays;
lock (_dnssecPrivateKeys)
{
if (_dnssecPrivateKeys.TryAdd(privateKey.KeyTag, privateKey))
return;
}
}
throw new DnsServerException("Failed to add private key: key tag collision.");
}
public void GenerateAndAddEcdsaKey(DnssecPrivateKeyType keyType, string curve, ushort rolloverDays)
{
if (_dnssecStatus == AuthZoneDnssecStatus.Unsigned)
throw new DnsServerException("The zone must be signed.");
DnssecAlgorithm algorithm;
switch (curve.ToUpper())
{
case "P256":
algorithm = DnssecAlgorithm.ECDSAP256SHA256;
break;
case "P384":
algorithm = DnssecAlgorithm.ECDSAP384SHA384;
break;
default:
throw new NotSupportedException("ECDSA curve is not supported: " + curve);
}
GenerateAndAddEcdsaKey(keyType, algorithm, rolloverDays);
}
private void GenerateAndAddEcdsaKey(DnssecPrivateKeyType keyType, DnssecAlgorithm algorithm, ushort rolloverDays)
{
int i = 0;
while (i++ < 5)
{
DnssecPrivateKey privateKey = DnssecPrivateKey.Create(algorithm, keyType);
privateKey.RolloverDays = rolloverDays;
lock (_dnssecPrivateKeys)
{
if (_dnssecPrivateKeys.TryAdd(privateKey.KeyTag, privateKey))
return;
}
}
throw new DnsServerException("Failed to add private key: key tag collision.");
}
public void UpdatePrivateKey(ushort keyTag, ushort rolloverDays)
{
lock (_dnssecPrivateKeys)
{
if (!_dnssecPrivateKeys.TryGetValue(keyTag, out DnssecPrivateKey privateKey))
throw new DnsServerException("Cannot update private key: no such private key was found.");
privateKey.RolloverDays = rolloverDays;
}
}
public void DeletePrivateKey(ushort keyTag)
{
if (_dnssecStatus == AuthZoneDnssecStatus.Unsigned)
throw new DnsServerException("The zone must be signed.");
lock (_dnssecPrivateKeys)
{
if (!_dnssecPrivateKeys.TryGetValue(keyTag, out DnssecPrivateKey privateKey))
throw new DnsServerException("Cannot delete private key: no such private key was found.");
if (privateKey.State != DnssecPrivateKeyState.Generated)
throw new DnsServerException("Cannot delete private key: only keys with Generated state can be deleted.");
_dnssecPrivateKeys.Remove(keyTag);
}
}
public void PublishAllGeneratedKeys()
{
if (_dnssecStatus == AuthZoneDnssecStatus.Unsigned)
throw new DnsServerException("The zone must be signed.");
List generatedPrivateKeys = new List();
List newDnsKeyRecords = new List();
uint dnsKeyTtl = GetDnsKeyTtl();
lock (_dnssecPrivateKeys)
{
foreach (KeyValuePair privateKeyEntry in _dnssecPrivateKeys)
{
DnssecPrivateKey privateKey = privateKeyEntry.Value;
if (privateKey.State == DnssecPrivateKeyState.Generated)
{
generatedPrivateKeys.Add(privateKey);
newDnsKeyRecords.Add(new DnsResourceRecord(_name, DnsResourceRecordType.DNSKEY, DnsClass.IN, dnsKeyTtl, privateKey.DnsKey));
}
}
}
if (generatedPrivateKeys.Count == 0)
throw new DnsServerException("Cannot publish DNSKEY: no generated private keys were found.");
IReadOnlyList dnsKeyRecords = _entries.AddOrUpdate(DnsResourceRecordType.DNSKEY, delegate (DnsResourceRecordType key)
{
return newDnsKeyRecords;
},
delegate (DnsResourceRecordType key, IReadOnlyList existingRecords)
{
foreach (DnsResourceRecord existingRecord in existingRecords)
{
foreach (DnsResourceRecord newDnsKeyRecord in newDnsKeyRecords)
{
if (existingRecord.Equals(newDnsKeyRecord))
throw new DnsServerException("Cannot publish DNSKEY: the key is already published.");
}
}
List dnsKeyRecords = new List(existingRecords.Count + newDnsKeyRecords.Count);
dnsKeyRecords.AddRange(existingRecords);
dnsKeyRecords.AddRange(newDnsKeyRecords);
return dnsKeyRecords;
});
//update private key state before signing
foreach (DnssecPrivateKey privateKey in generatedPrivateKeys)
privateKey.SetState(DnssecPrivateKeyState.Published);
List addedRecords = new List();
List deletedRecords = new List();
addedRecords.AddRange(newDnsKeyRecords);
IReadOnlyList newRRSigRecords = SignRRSet(dnsKeyRecords);
if (newRRSigRecords.Count > 0)
{
AddOrUpdateRRSigRecords(newRRSigRecords, out IReadOnlyList deletedRRSigRecords);
addedRecords.AddRange(newRRSigRecords);
deletedRecords.AddRange(deletedRRSigRecords);
}
CommitAndIncrementSerial(deletedRecords, addedRecords);
TriggerNotify();
}
private void ActivateZskDnsKeys(IReadOnlyList zskPrivateKeys)
{
List addedRecords = new List();
List deletedRecords = new List();
//re-sign all records with new private keys
IReadOnlyList zones = _dnsServer.AuthZoneManager.GetZoneWithSubDomainZones(_name);
foreach (AuthZone zone in zones)
{
IReadOnlyList newRRSigRecords = zone.SignAllRRSets();
if (newRRSigRecords.Count > 0)
{
zone.AddOrUpdateRRSigRecords(newRRSigRecords, out IReadOnlyList deletedRRSigRecords);
addedRecords.AddRange(newRRSigRecords);
deletedRecords.AddRange(deletedRRSigRecords);
}
}
CommitAndIncrementSerial(deletedRecords, addedRecords);
TriggerNotify();
//update private key state
string dnsKeyTags = null;
foreach (DnssecPrivateKey privateKey in zskPrivateKeys)
{
privateKey.SetState(DnssecPrivateKeyState.Active);
if (dnsKeyTags is null)
dnsKeyTags = privateKey.KeyTag.ToString();
else
dnsKeyTags += ", " + privateKey.KeyTag.ToString();
}
LogManager log = _dnsServer.LogManager;
if (log is not null)
log.Write("The ZSK DNSKEYs (" + dnsKeyTags + ") from the primary zone were activated successfully: " + _name);
}
public void RolloverDnsKey(ushort keyTag)
{
if (_dnssecStatus == AuthZoneDnssecStatus.Unsigned)
throw new DnsServerException("The zone must be signed.");
DnssecPrivateKey privateKey;
lock (_dnssecPrivateKeys)
{
if (!_dnssecPrivateKeys.TryGetValue(keyTag, out privateKey))
throw new DnsServerException("Cannot rollover private key: no such private key was found.");
}
switch (privateKey.State)
{
case DnssecPrivateKeyState.Ready:
case DnssecPrivateKeyState.Active:
if (privateKey.IsRetiring)
throw new DnsServerException("Cannot rollover private key: the private key is already set to retire.");
break;
default:
throw new DnsServerException("Cannot rollover private key: the private key state must be Ready or Active to be able to rollover.");
}
switch (privateKey.Algorithm)
{
case DnssecAlgorithm.RSAMD5:
case DnssecAlgorithm.RSASHA1:
case DnssecAlgorithm.RSASHA1_NSEC3_SHA1:
case DnssecAlgorithm.RSASHA256:
case DnssecAlgorithm.RSASHA512:
GenerateAndAddRsaKey(privateKey.KeyType, privateKey.Algorithm, (privateKey as DnssecRsaPrivateKey).KeySize, privateKey.RolloverDays);
break;
case DnssecAlgorithm.ECDSAP256SHA256:
case DnssecAlgorithm.ECDSAP384SHA384:
GenerateAndAddEcdsaKey(privateKey.KeyType, privateKey.Algorithm, privateKey.RolloverDays);
break;
default:
throw new NotSupportedException("DNSSEC algorithm is not supported: " + privateKey.Algorithm.ToString());
}
PublishAllGeneratedKeys();
privateKey.SetToRetire();
}
public void RetireDnsKey(ushort keyTag)
{
if (_dnssecStatus == AuthZoneDnssecStatus.Unsigned)
throw new DnsServerException("The zone must be signed.");
DnssecPrivateKey privateKeyToRetire;
lock (_dnssecPrivateKeys)
{
if (!_dnssecPrivateKeys.TryGetValue(keyTag, out privateKeyToRetire))
throw new DnsServerException("Cannot retire private key: no such private key was found.");
}
switch (privateKeyToRetire.KeyType)
{
case DnssecPrivateKeyType.KeySigningKey:
switch (privateKeyToRetire.State)
{
case DnssecPrivateKeyState.Ready:
case DnssecPrivateKeyState.Active:
if (!RetireKskDnsKeys(new DnssecPrivateKey[] { privateKeyToRetire }, true))
throw new DnsServerException("Cannot retire private key: no successor key was found to safely retire the key.");
break;
default:
throw new DnsServerException("Cannot retire private key: the KSK private key state must be Ready or Active to be able to retire.");
}
break;
case DnssecPrivateKeyType.ZoneSigningKey:
switch (privateKeyToRetire.State)
{
case DnssecPrivateKeyState.Active:
if (!RetireZskDnsKeys(new DnssecPrivateKey[] { privateKeyToRetire }, true))
throw new DnsServerException("Cannot retire private key: no successor key was found to safely retire the key.");
break;
default:
throw new DnsServerException("Cannot retire private key: the ZSK private key state must be Active to be able to retire.");
}
break;
default:
throw new InvalidOperationException();
}
}
private bool RetireKskDnsKeys(IReadOnlyList kskPrivateKeys, bool ignoreAlgorithm)
{
string dnsKeyTags = null;
foreach (DnssecPrivateKey kskPrivateKey in kskPrivateKeys)
{
bool isSafeToRetire = false;
lock (_dnssecPrivateKeys)
{
foreach (KeyValuePair privateKeyEntry in _dnssecPrivateKeys)
{
DnssecPrivateKey privateKey = privateKeyEntry.Value;
if ((privateKey.KeyType == DnssecPrivateKeyType.KeySigningKey) && (privateKey.KeyTag != kskPrivateKey.KeyTag) && !privateKey.IsRetiring)
{
if (ignoreAlgorithm)
{
//manual retire case
if (privateKey.Algorithm != kskPrivateKey.Algorithm)
{
//check if the sucessor ksk has a matching zsk
bool foundMatchingZsk = false;
foreach (KeyValuePair zskPrivateKeyEntry in _dnssecPrivateKeys)
{
DnssecPrivateKey zskPrivateKey = zskPrivateKeyEntry.Value;
if ((zskPrivateKey.KeyType == DnssecPrivateKeyType.ZoneSigningKey) && (zskPrivateKey.Algorithm == privateKey.Algorithm) && (zskPrivateKey.State == DnssecPrivateKeyState.Active) && !zskPrivateKey.IsRetiring)
{
foundMatchingZsk = true;
break;
}
}
if (!foundMatchingZsk)
continue;
}
}
else
{
//rollover case
if (privateKey.Algorithm != kskPrivateKey.Algorithm)
continue;
}
if (privateKey.State == DnssecPrivateKeyState.Active)
{
isSafeToRetire = true;
break;
}
if ((privateKey.State == DnssecPrivateKeyState.Ready) && (kskPrivateKey.State == DnssecPrivateKeyState.Ready))
{
isSafeToRetire = true;
break;
}
}
}
}
if (isSafeToRetire)
{
kskPrivateKey.SetState(DnssecPrivateKeyState.Retired);
if (dnsKeyTags is null)
dnsKeyTags = kskPrivateKey.KeyTag.ToString();
else
dnsKeyTags += ", " + kskPrivateKey.KeyTag.ToString();
}
}
if (dnsKeyTags is not null)
{
LogManager log = _dnsServer.LogManager;
if (log is not null)
log.Write("The KSK DNSKEYs (" + dnsKeyTags + ") from the primary zone were retired successfully: " + _name);
return true;
}
return false;
}
private bool RetireZskDnsKeys(IReadOnlyList zskPrivateKeys, bool ignoreAlgorithm)
{
string dnsKeyTags = null;
List zskToDeactivate = null;
foreach (DnssecPrivateKey zskPrivateKey in zskPrivateKeys)
{
bool isSafeToRetire = false;
lock (_dnssecPrivateKeys)
{
foreach (KeyValuePair privateKeyEntry in _dnssecPrivateKeys)
{
DnssecPrivateKey privateKey = privateKeyEntry.Value;
if ((privateKey.KeyType == DnssecPrivateKeyType.ZoneSigningKey) && (privateKey.KeyTag != zskPrivateKey.KeyTag) && (privateKey.State == DnssecPrivateKeyState.Active) && !privateKey.IsRetiring)
{
if (ignoreAlgorithm)
{
//manual retire case
if (privateKey.Algorithm != zskPrivateKey.Algorithm)
{
//check if the sucessor zsk has a matching ksk
bool foundMatchingKsk = false;
foreach (KeyValuePair kskPrivateKeyEntry in _dnssecPrivateKeys)
{
DnssecPrivateKey kskPrivateKey = kskPrivateKeyEntry.Value;
if ((kskPrivateKey.KeyType == DnssecPrivateKeyType.KeySigningKey) && (kskPrivateKey.Algorithm == privateKey.Algorithm) && ((kskPrivateKey.State == DnssecPrivateKeyState.Ready) || (kskPrivateKey.State == DnssecPrivateKeyState.Active)) && !kskPrivateKey.IsRetiring)
{
foundMatchingKsk = true;
break;
}
}
if (!foundMatchingKsk)
continue;
}
}
else
{
//rollover case
if (privateKey.Algorithm != zskPrivateKey.Algorithm)
continue;
}
isSafeToRetire = true;
break;
}
}
}
if (isSafeToRetire)
{
zskPrivateKey.SetState(DnssecPrivateKeyState.Retired);
if (zskToDeactivate is null)
zskToDeactivate = new List();
zskToDeactivate.Add(zskPrivateKey);
if (dnsKeyTags is null)
dnsKeyTags = zskPrivateKey.KeyTag.ToString();
else
dnsKeyTags += ", " + zskPrivateKey.KeyTag.ToString();
}
}
if (zskToDeactivate is not null)
DeactivateZskDnsKeys(zskToDeactivate);
if (dnsKeyTags is not null)
{
LogManager log = _dnsServer.LogManager;
if (log is not null)
log.Write("The ZSK DNSKEYs (" + dnsKeyTags + ") from the primary zone were retired successfully: " + _name);
return true;
}
return false;
}
private void DeactivateZskDnsKeys(IReadOnlyList zskPrivateKeys)
{
//remove all RRSIGs for the DNSKEYs
List deletedRecords = new List();
IReadOnlyList zones = _dnsServer.AuthZoneManager.GetZoneWithSubDomainZones(_name);
foreach (AuthZone zone in zones)
{
IReadOnlyList rrsigRecords = zone.GetRecords(DnsResourceRecordType.RRSIG);
List rrsigsToRemove = new List();
foreach (DnsResourceRecord rrsigRecord in rrsigRecords)
{
DnsRRSIGRecordData rrsig = rrsigRecord.RDATA as DnsRRSIGRecordData;
foreach (DnssecPrivateKey privateKey in zskPrivateKeys)
{
if (rrsig.KeyTag == privateKey.KeyTag)
{
rrsigsToRemove.Add(rrsigRecord);
break;
}
}
}
if (zone.TryDeleteRecords(DnsResourceRecordType.RRSIG, rrsigsToRemove, out IReadOnlyList deletedRRSigRecords))
deletedRecords.AddRange(deletedRRSigRecords);
}
CommitAndIncrementSerial(deletedRecords);
TriggerNotify();
string dnsKeyTags = null;
foreach (DnssecPrivateKey privateKey in zskPrivateKeys)
{
if (dnsKeyTags is null)
dnsKeyTags = privateKey.KeyTag.ToString();
else
dnsKeyTags += ", " + privateKey.KeyTag.ToString();
}
LogManager log = _dnsServer.LogManager;
if (log is not null)
log.Write("The ZSK DNSKEYs (" + dnsKeyTags + ") from the primary zone were deactivated successfully: " + _name);
}
private void RevokeKskDnsKeys(IReadOnlyList kskPrivateKeys)
{
if (!_entries.TryGetValue(DnsResourceRecordType.DNSKEY, out IReadOnlyList existingDnsKeyRecords))
throw new InvalidOperationException();
List addedRecords = new List();
List deletedRecords = new List();
List dnsKeyRecords = new List();
foreach (DnsResourceRecord existingDnsKeyRecord in existingDnsKeyRecords)
{
bool found = false;
foreach (DnssecPrivateKey privateKey in kskPrivateKeys)
{
if (existingDnsKeyRecord.RDATA.Equals(privateKey.DnsKey))
{
found = true;
break;
}
}
if (!found)
dnsKeyRecords.Add(existingDnsKeyRecord);
}
uint dnsKeyTtl = existingDnsKeyRecords[0].OriginalTtlValue;
List keyTagsToRemove = new List(kskPrivateKeys.Count);
foreach (DnssecPrivateKey privateKey in kskPrivateKeys)
{
keyTagsToRemove.Add(privateKey.KeyTag);
privateKey.SetState(DnssecPrivateKeyState.Revoked);
DnsResourceRecord revokedDnsKeyRecord = new DnsResourceRecord(_name, DnsResourceRecordType.DNSKEY, DnsClass.IN, dnsKeyTtl, privateKey.DnsKey);
dnsKeyRecords.Add(revokedDnsKeyRecord);
}
if (!TrySetRecords(DnsResourceRecordType.DNSKEY, dnsKeyRecords, out IReadOnlyList deletedDnsKeyRecords))
throw new InvalidOperationException();
addedRecords.AddRange(dnsKeyRecords);
deletedRecords.AddRange(deletedDnsKeyRecords);
IReadOnlyList newRRSigRecords = SignRRSet(dnsKeyRecords);
if (newRRSigRecords.Count > 0)
{
AddOrUpdateRRSigRecords(newRRSigRecords, out IReadOnlyList deletedRRSigRecords);
addedRecords.AddRange(newRRSigRecords);
deletedRecords.AddRange(deletedRRSigRecords);
}
//remove RRSIG for removed keys
{
IReadOnlyList rrsigRecords = GetRecords(DnsResourceRecordType.RRSIG);
List rrsigsToRemove = new List();
foreach (DnsResourceRecord rrsigRecord in rrsigRecords)
{
DnsRRSIGRecordData rrsig = rrsigRecord.RDATA as DnsRRSIGRecordData;
if (rrsig.TypeCovered != DnsResourceRecordType.DNSKEY)
continue;
foreach (ushort keyTagToRemove in keyTagsToRemove)
{
if (rrsig.KeyTag == keyTagToRemove)
{
rrsigsToRemove.Add(rrsigRecord);
break;
}
}
}
if (TryDeleteRecords(DnsResourceRecordType.RRSIG, rrsigsToRemove, out IReadOnlyList deletedRRSigRecords))
deletedRecords.AddRange(deletedRRSigRecords);
}
CommitAndIncrementSerial(deletedRecords, addedRecords);
TriggerNotify();
//update revoked private keys
string dnsKeyTags = null;
lock (_dnssecPrivateKeys)
{
//remove old entry
foreach (ushort keyTag in keyTagsToRemove)
{
if (_dnssecPrivateKeys.Remove(keyTag))
{
if (dnsKeyTags is null)
dnsKeyTags = keyTag.ToString();
else
dnsKeyTags += ", " + keyTag.ToString();
}
}
//add new entry
foreach (DnssecPrivateKey privateKey in kskPrivateKeys)
_dnssecPrivateKeys.Add(privateKey.KeyTag, privateKey);
}
LogManager log = _dnsServer.LogManager;
if (log is not null)
log.Write("The KSK DNSKEYs (" + dnsKeyTags + ") from the primary zone were revoked successfully: " + _name);
}
private void UnpublishDnsKeys(IReadOnlyList deadPrivateKeys)
{
if (!_entries.TryGetValue(DnsResourceRecordType.DNSKEY, out IReadOnlyList existingDnsKeyRecords))
throw new InvalidOperationException();
List addedRecords = new List();
List deletedRecords = new List();
List dnsKeyRecords = new List();
foreach (DnsResourceRecord existingDnsKeyRecord in existingDnsKeyRecords)
{
bool found = false;
foreach (DnssecPrivateKey privateKey in deadPrivateKeys)
{
if (existingDnsKeyRecord.RDATA.Equals(privateKey.DnsKey))
{
found = true;
break;
}
}
if (!found)
dnsKeyRecords.Add(existingDnsKeyRecord);
}
if (dnsKeyRecords.Count < 2)
throw new InvalidOperationException();
if (!TrySetRecords(DnsResourceRecordType.DNSKEY, dnsKeyRecords, out IReadOnlyList deletedDnsKeyRecords))
throw new InvalidOperationException();
addedRecords.AddRange(dnsKeyRecords);
deletedRecords.AddRange(deletedDnsKeyRecords);
IReadOnlyList newRRSigRecords = SignRRSet(dnsKeyRecords);
if (newRRSigRecords.Count > 0)
{
AddOrUpdateRRSigRecords(newRRSigRecords, out IReadOnlyList deletedRRSigRecords);
addedRecords.AddRange(newRRSigRecords);
deletedRecords.AddRange(deletedRRSigRecords);
}
//remove RRSig for revoked keys
{
IReadOnlyList rrsigRecords = GetRecords(DnsResourceRecordType.RRSIG);
List rrsigsToRemove = new List();
foreach (DnsResourceRecord rrsigRecord in rrsigRecords)
{
DnsRRSIGRecordData rrsig = rrsigRecord.RDATA as DnsRRSIGRecordData;
if (rrsig.TypeCovered != DnsResourceRecordType.DNSKEY)
continue;
foreach (DnssecPrivateKey privateKey in deadPrivateKeys)
{
if (rrsig.KeyTag == privateKey.KeyTag)
{
rrsigsToRemove.Add(rrsigRecord);
break;
}
}
}
if (TryDeleteRecords(DnsResourceRecordType.RRSIG, rrsigsToRemove, out IReadOnlyList deletedRRSigRecords))
deletedRecords.AddRange(deletedRRSigRecords);
}
CommitAndIncrementSerial(deletedRecords, addedRecords);
TriggerNotify();
//remove private keys permanently
string dnsKeyTags = null;
lock (_dnssecPrivateKeys)
{
foreach (DnssecPrivateKey privateKey in deadPrivateKeys)
{
if (_dnssecPrivateKeys.Remove(privateKey.KeyTag))
{
if (dnsKeyTags is null)
dnsKeyTags = privateKey.KeyTag.ToString();
else
dnsKeyTags += ", " + privateKey.KeyTag.ToString();
}
}
}
LogManager log = _dnsServer.LogManager;
if (log is not null)
log.Write("The DNSKEYs (" + dnsKeyTags + ") from the primary zone were unpublished successfully: " + _name);
}
private async Task> GetDSPublishedPrivateKeys(IReadOnlyList privateKeys)
{
if (_name.Length == 0)
return privateKeys; //zone is root
//delete any existing DS entries from cache to allow resolving latest ones
_dnsServer.CacheZoneManager.DeleteZone(_name);
IReadOnlyList dsRecords = DnsClient.ParseResponseDS(await _dnsServer.DirectQueryAsync(new DnsQuestionRecord(_name, DnsResourceRecordType.DS, DnsClass.IN)));
List activePrivateKeys = new List(dsRecords.Count);
foreach (DnsDSRecordData dsRecord in dsRecords)
{
foreach (DnssecPrivateKey privateKey in privateKeys)
{
if ((dsRecord.KeyTag == privateKey.DnsKey.ComputedKeyTag) && (dsRecord.Algorithm == privateKey.DnsKey.Algorithm) && privateKey.DnsKey.IsDnsKeyValid(_name, dsRecord))
{
activePrivateKeys.Add(privateKey);
break;
}
}
}
return activePrivateKeys;
}
private bool TryRefreshAllSignatures()
{
List addedRecords = new List();
List deletedRecords = new List();
IReadOnlyList zones = _dnsServer.AuthZoneManager.GetZoneWithSubDomainZones(_name);
foreach (AuthZone zone in zones)
{
IReadOnlyList newRRSigRecords = zone.RefreshSignatures();
if (newRRSigRecords.Count > 0)
{
zone.AddOrUpdateRRSigRecords(newRRSigRecords, out IReadOnlyList deletedRRSigRecords);
addedRecords.AddRange(newRRSigRecords);
deletedRecords.AddRange(deletedRRSigRecords);
}
}
if ((deletedRecords.Count > 0) || (addedRecords.Count > 0))
{
CommitAndIncrementSerial(deletedRecords, addedRecords);
TriggerNotify();
return true;
}
return false;
}
internal override IReadOnlyList SignRRSet(IReadOnlyList records)
{
DnsResourceRecordType rrsetType = records[0].Type;
List rrsigRecords = new List();
uint signatureValidityPeriod = GetSignatureValidityPeriod();
switch (rrsetType)
{
case DnsResourceRecordType.DNSKEY:
lock (_dnssecPrivateKeys)
{
foreach (KeyValuePair privateKeyEntry in _dnssecPrivateKeys)
{
DnssecPrivateKey privateKey = privateKeyEntry.Value;
if (privateKey.KeyType != DnssecPrivateKeyType.KeySigningKey)
continue;
switch (privateKey.State)
{
case DnssecPrivateKeyState.Published:
case DnssecPrivateKeyState.Ready:
case DnssecPrivateKeyState.Active:
case DnssecPrivateKeyState.Revoked:
rrsigRecords.Add(privateKey.SignRRSet(_name, records, DNSSEC_SIGNATURE_INCEPTION_OFFSET, signatureValidityPeriod));
break;
}
}
}
break;
case DnsResourceRecordType.RRSIG:
throw new InvalidOperationException();
case DnsResourceRecordType.ANAME:
case DnsResourceRecordType.APP:
throw new DnsServerException("Cannot sign RRSet: The record type [" + rrsetType.ToString() + "] is not supported by DNSSEC signed primary zones.");
default:
if ((rrsetType == DnsResourceRecordType.NS) && (records[0].Name.Length > _name.Length))
return Array.Empty(); //referrer NS records are not signed
lock (_dnssecPrivateKeys)
{
foreach (KeyValuePair privateKeyEntry in _dnssecPrivateKeys)
{
DnssecPrivateKey privateKey = privateKeyEntry.Value;
if (privateKey.KeyType != DnssecPrivateKeyType.ZoneSigningKey)
continue;
switch (privateKey.State)
{
case DnssecPrivateKeyState.Ready:
case DnssecPrivateKeyState.Active:
rrsigRecords.Add(privateKey.SignRRSet(_name, records, DNSSEC_SIGNATURE_INCEPTION_OFFSET, signatureValidityPeriod));
break;
}
}
}
break;
}
if (rrsigRecords.Count == 0)
throw new InvalidOperationException("Cannot sign RRSet: no private key was available.");
return rrsigRecords;
}
internal void UpdateDnssecRecordsFor(AuthZone zone, DnsResourceRecordType type)
{
//lock to sync this call to prevent inconsistent NSEC/NSEC3 updates
lock (_dnssecUpdateLock)
{
IReadOnlyList records = zone.GetRecords(type);
if (records.Count > 0)
{
//rrset added or updated
//sign rrset
IReadOnlyList newRRSigRecords = SignRRSet(records);
if (newRRSigRecords.Count > 0)
{
zone.AddOrUpdateRRSigRecords(newRRSigRecords, out IReadOnlyList deletedRRSigRecords);
CommitAndIncrementSerial(deletedRRSigRecords, newRRSigRecords);
}
}
else
{
//rrset deleted
//delete rrsig
IReadOnlyList existingRRSigRecords = zone.GetRecords(DnsResourceRecordType.RRSIG);
if (existingRRSigRecords.Count > 0)
{
List recordsToDelete = new List();
foreach (DnsResourceRecord existingRRSigRecord in existingRRSigRecords)
{
DnsRRSIGRecordData rrsig = existingRRSigRecord.RDATA as DnsRRSIGRecordData;
if (rrsig.TypeCovered == type)
recordsToDelete.Add(existingRRSigRecord);
}
if (zone.TryDeleteRecords(DnsResourceRecordType.RRSIG, recordsToDelete, out IReadOnlyList deletedRRSigRecords))
CommitAndIncrementSerial(deletedRRSigRecords);
}
}
if (_dnssecStatus == AuthZoneDnssecStatus.SignedWithNSEC)
{
UpdateNSecRRSetFor(zone);
}
else
{
UpdateNSec3RRSetFor(zone);
int apexLabelCount = DnsRRSIGRecordData.GetLabelCount(_name);
int zoneLabelCount = DnsRRSIGRecordData.GetLabelCount(zone.Name);
if ((zoneLabelCount - apexLabelCount) > 1)
{
//empty non-terminal (ENT) may exists
string currentOwnerName = zone.Name;
while (true)
{
currentOwnerName = AuthZoneManager.GetParentZone(currentOwnerName);
if (currentOwnerName.Equals(_name, StringComparison.OrdinalIgnoreCase))
break;
//update NSEC3 rrset for current owner name
AuthZone entZone = _dnsServer.AuthZoneManager.GetAuthZone(_name, currentOwnerName);
if (entZone is null)
entZone = new PrimarySubDomainZone(null, currentOwnerName); //dummy empty non-terminal (ENT) sub domain object
UpdateNSec3RRSetFor(entZone);
}
}
}
}
}
private void UpdateNSecRRSetFor(AuthZone zone)
{
uint ttl = GetZoneSoaMinimum();
IReadOnlyList newNSecRecords = GetUpdatedNSecRRSetFor(zone, ttl);
if (newNSecRecords.Count > 0)
{
DnsResourceRecord newNSecRecord = newNSecRecords[0];
DnsNSECRecordData newNSec = newNSecRecord.RDATA as DnsNSECRecordData;
if (newNSec.Types.Count == 2)
{
//only NSEC and RRSIG exists so remove NSEC
IReadOnlyList deletedNSecRecords = zone.RemoveNSecRecordsWithRRSig();
if (deletedNSecRecords.Count > 0)
CommitAndIncrementSerial(deletedNSecRecords);
//relink previous nsec
RelinkPreviousNSecRRSetFor(newNSecRecord, ttl, true);
}
else
{
List addedRecords = new List();
List deletedRecords = new List();
if (!zone.TrySetRecords(DnsResourceRecordType.NSEC, newNSecRecords, out IReadOnlyList deletedNSecRecords))
throw new DnsServerException("Failed to set DNSSEC records. Please try again.");
addedRecords.AddRange(newNSecRecords);
deletedRecords.AddRange(deletedNSecRecords);
IReadOnlyList newRRSigRecords = SignRRSet(newNSecRecords);
if (newRRSigRecords.Count > 0)
{
zone.AddOrUpdateRRSigRecords(newRRSigRecords, out IReadOnlyList deletedRRSigRecords);
addedRecords.AddRange(newRRSigRecords);
deletedRecords.AddRange(deletedRRSigRecords);
}
CommitAndIncrementSerial(deletedRecords, addedRecords);
if (deletedNSecRecords.Count == 0)
{
//new NSEC created since no old NSEC was removed
//relink previous nsec
RelinkPreviousNSecRRSetFor(newNSecRecord, ttl, false);
}
}
}
}
private void UpdateNSec3RRSetFor(AuthZone zone)
{
uint ttl = GetZoneSoaMinimum();
bool noSubDomainExistsForEmptyZone = (zone.IsEmpty || zone.HasOnlyNSec3Records()) && !_dnsServer.AuthZoneManager.SubDomainExists(_name, zone.Name);
IReadOnlyList newNSec3Records = GetUpdatedNSec3RRSetFor(zone, ttl, noSubDomainExistsForEmptyZone);
if (newNSec3Records.Count > 0)
{
DnsResourceRecord newNSec3Record = newNSec3Records[0];
AuthZone nsec3Zone = _dnsServer.AuthZoneManager.GetOrAddSubDomainZone(_name, newNSec3Record.Name);
if (nsec3Zone is null)
throw new InvalidOperationException();
if (noSubDomainExistsForEmptyZone)
{
//no records exists in real zone and no sub domain exists, so remove NSEC3
IReadOnlyList deletedNSec3Records = nsec3Zone.RemoveNSec3RecordsWithRRSig();
if (deletedNSec3Records.Count > 0)
CommitAndIncrementSerial(deletedNSec3Records);
//remove nsec3 sub domain zone if empty since it wont get removed otherwise
if (nsec3Zone is SubDomainZone nsec3SubDomainZone)
{
if (nsec3Zone.IsEmpty)
_dnsServer.AuthZoneManager.RemoveSubDomainZone(nsec3Zone.Name); //remove empty sub zone
else
nsec3SubDomainZone.AutoUpdateState();
}
//remove the real zone if empty so that any of the ENT that exists can also be removed later
if (zone is SubDomainZone subDomainZone)
{
if (zone.IsEmpty)
_dnsServer.AuthZoneManager.RemoveSubDomainZone(zone.Name); //remove empty sub zone
else
subDomainZone.AutoUpdateState();
}
//relink previous nsec3
RelinkPreviousNSec3RRSet(newNSec3Record, ttl, true);
}
else
{
List addedRecords = new List();
List deletedRecords = new List();
if (!nsec3Zone.TrySetRecords(DnsResourceRecordType.NSEC3, newNSec3Records, out IReadOnlyList deletedNSec3Records))
throw new DnsServerException("Failed to set DNSSEC records. Please try again.");
addedRecords.AddRange(newNSec3Records);
deletedRecords.AddRange(deletedNSec3Records);
IReadOnlyList newRRSigRecords = SignRRSet(newNSec3Records);
if (newRRSigRecords.Count > 0)
{
nsec3Zone.AddOrUpdateRRSigRecords(newRRSigRecords, out IReadOnlyList deletedRRSigRecords);
addedRecords.AddRange(newRRSigRecords);
deletedRecords.AddRange(deletedRRSigRecords);
}
CommitAndIncrementSerial(deletedRecords, addedRecords);
if (deletedNSec3Records.Count == 0)
{
//new NSEC3 created since no old NSEC3 was removed
//relink previous nsec
RelinkPreviousNSec3RRSet(newNSec3Record, ttl, false);
}
}
}
}
private IReadOnlyList GetUpdatedNSecRRSetFor(AuthZone zone, uint ttl)
{
AuthZone nextZone = _dnsServer.AuthZoneManager.FindNextSubDomainZone(_name, zone.Name);
if (nextZone is null)
nextZone = this;
return zone.GetUpdatedNSecRRSet(nextZone.Name, ttl);
}
private IReadOnlyList GetUpdatedNSec3RRSetFor(AuthZone zone, uint ttl, bool forceGetNewRRSet)
{
if (!_entries.TryGetValue(DnsResourceRecordType.NSEC3PARAM, out IReadOnlyList nsec3ParamRecords))
throw new InvalidOperationException();
DnsResourceRecord nsec3ParamRecord = nsec3ParamRecords[0];
DnsNSEC3PARAMRecordData nsec3Param = nsec3ParamRecord.RDATA as DnsNSEC3PARAMRecordData;
string hashedOwnerName = nsec3Param.ComputeHashedOwnerNameBase32HexString(zone.Name) + (_name.Length > 0 ? "." + _name : "");
byte[] nextHashedOwnerName = null;
//find next hashed owner name
string currentOwnerName = hashedOwnerName;
while (true)
{
AuthZone nextZone = _dnsServer.AuthZoneManager.FindNextSubDomainZone(_name, currentOwnerName);
if (nextZone is null)
break;
IReadOnlyList nextNSec3Records = nextZone.GetRecords(DnsResourceRecordType.NSEC3);
if (nextNSec3Records.Count > 0)
{
nextHashedOwnerName = DnsNSEC3RecordData.GetHashedOwnerNameFrom(nextNSec3Records[0].Name);
break;
}
currentOwnerName = nextZone.Name;
}
if (nextHashedOwnerName is null)
{
//didnt find next NSEC3 record since current must be last; find the first NSEC3 record
DnsResourceRecord previousNSec3Record = null;
while (true)
{
AuthZone previousZone = _dnsServer.AuthZoneManager.FindPreviousSubDomainZone(_name, currentOwnerName);
if (previousZone is null)
break;
IReadOnlyList previousNSec3Records = previousZone.GetRecords(DnsResourceRecordType.NSEC3);
if (previousNSec3Records.Count > 0)
previousNSec3Record = previousNSec3Records[0];
currentOwnerName = previousZone.Name;
}
if (previousNSec3Record is not null)
nextHashedOwnerName = DnsNSEC3RecordData.GetHashedOwnerNameFrom(previousNSec3Record.Name);
}
if (nextHashedOwnerName is null)
nextHashedOwnerName = DnsNSEC3RecordData.GetHashedOwnerNameFrom(hashedOwnerName); //only 1 NSEC3 record in zone
IReadOnlyList newNSec3Records = zone.CreateNSec3RRSet(hashedOwnerName, nextHashedOwnerName, ttl, nsec3Param.Iterations, nsec3Param.Salt);
if (forceGetNewRRSet)
return newNSec3Records;
AuthZone nsec3Zone = _dnsServer.AuthZoneManager.GetAuthZone(_name, hashedOwnerName);
if (nsec3Zone is null)
return newNSec3Records;
return nsec3Zone.GetUpdatedNSec3RRSet(newNSec3Records);
}
private void RelinkPreviousNSecRRSetFor(DnsResourceRecord currentNSecRecord, uint ttl, bool wasRemoved)
{
AuthZone previousNsecZone = _dnsServer.AuthZoneManager.FindPreviousSubDomainZone(_name, currentNSecRecord.Name);
if (previousNsecZone is null)
return; //current zone is apex
IReadOnlyList newPreviousNSecRecords;
if (wasRemoved)
newPreviousNSecRecords = previousNsecZone.GetUpdatedNSecRRSet((currentNSecRecord.RDATA as DnsNSECRecordData).NextDomainName, ttl);
else
newPreviousNSecRecords = previousNsecZone.GetUpdatedNSecRRSet(currentNSecRecord.Name, ttl);
if (newPreviousNSecRecords.Count > 0)
{
if (!previousNsecZone.TrySetRecords(DnsResourceRecordType.NSEC, newPreviousNSecRecords, out IReadOnlyList deletedNSecRecords))
throw new DnsServerException("Failed to set DNSSEC records. Please try again.");
List addedRecords = new List();
List deletedRecords = new List();
addedRecords.AddRange(newPreviousNSecRecords);
deletedRecords.AddRange(deletedNSecRecords);
IReadOnlyList newRRSigRecords = SignRRSet(newPreviousNSecRecords);
if (newRRSigRecords.Count > 0)
{
previousNsecZone.AddOrUpdateRRSigRecords(newRRSigRecords, out IReadOnlyList deletedRRSigRecords);
addedRecords.AddRange(newRRSigRecords);
deletedRecords.AddRange(deletedRRSigRecords);
}
CommitAndIncrementSerial(deletedRecords, addedRecords);
}
}
private void RelinkPreviousNSec3RRSet(DnsResourceRecord currentNSec3Record, uint ttl, bool wasRemoved)
{
DnsNSEC3RecordData currentNSec3 = currentNSec3Record.RDATA as DnsNSEC3RecordData;
//find the previous NSEC3 and update it
DnsResourceRecord previousNSec3Record = null;
AuthZone previousNSec3Zone;
string currentOwnerName = currentNSec3Record.Name;
while (true)
{
previousNSec3Zone = _dnsServer.AuthZoneManager.FindPreviousSubDomainZone(_name, currentOwnerName);
if (previousNSec3Zone is null)
break;
IReadOnlyList previousNSec3Records = previousNSec3Zone.GetRecords(DnsResourceRecordType.NSEC3);
if (previousNSec3Records.Count > 0)
{
previousNSec3Record = previousNSec3Records[0];
break;
}
currentOwnerName = previousNSec3Zone.Name;
}
if (previousNSec3Record is null)
{
//didnt find previous NSEC3; find the last NSEC3 to update
if (wasRemoved)
currentOwnerName = currentNSec3.NextHashedOwnerName + (_name.Length > 0 ? "." + _name : "");
else
currentOwnerName = currentNSec3Record.Name;
while (true)
{
AuthZone nextNSec3Zone = _dnsServer.AuthZoneManager.GetAuthZone(_name, currentOwnerName);
if (nextNSec3Zone is null)
break;
IReadOnlyList nextNSec3Records = nextNSec3Zone.GetRecords(DnsResourceRecordType.NSEC3);
if (nextNSec3Records.Count > 0)
{
previousNSec3Record = nextNSec3Records[0];
previousNSec3Zone = nextNSec3Zone;
string nextHashedOwnerNameString = (previousNSec3Record.RDATA as DnsNSEC3RecordData).NextHashedOwnerName + (_name.Length > 0 ? "." + _name : "");
if (DnsNSECRecordData.CanonicalComparison(previousNSec3Record.Name, nextHashedOwnerNameString) >= 0)
break; //found last NSEC3
//jump to next hashed owner
currentOwnerName = nextHashedOwnerNameString;
}
else
{
currentOwnerName = nextNSec3Zone.Name;
}
}
}
if (previousNSec3Record is null)
throw new InvalidOperationException();
DnsNSEC3RecordData previousNSec3 = previousNSec3Record.RDATA as DnsNSEC3RecordData;
DnsNSEC3RecordData newPreviousNSec3;
if (wasRemoved)
newPreviousNSec3 = new DnsNSEC3RecordData(DnssecNSEC3HashAlgorithm.SHA1, DnssecNSEC3Flags.None, previousNSec3.Iterations, previousNSec3.Salt, currentNSec3.NextHashedOwnerNameValue, previousNSec3.Types);
else
newPreviousNSec3 = new DnsNSEC3RecordData(DnssecNSEC3HashAlgorithm.SHA1, DnssecNSEC3Flags.None, previousNSec3.Iterations, previousNSec3.Salt, DnsNSEC3RecordData.GetHashedOwnerNameFrom(currentNSec3Record.Name), previousNSec3.Types);
DnsResourceRecord[] newPreviousNSec3Records = new DnsResourceRecord[] { new DnsResourceRecord(previousNSec3Record.Name, DnsResourceRecordType.NSEC3, DnsClass.IN, ttl, newPreviousNSec3) };
if (!previousNSec3Zone.TrySetRecords(DnsResourceRecordType.NSEC3, newPreviousNSec3Records, out IReadOnlyList deletedNSec3Records))
throw new DnsServerException("Failed to set DNSSEC records. Please try again.");
List addedRecords = new List();
List deletedRecords = new List();
addedRecords.AddRange(newPreviousNSec3Records);
deletedRecords.AddRange(deletedNSec3Records);
IReadOnlyList newRRSigRecords = SignRRSet(newPreviousNSec3Records);
if (newRRSigRecords.Count > 0)
{
previousNSec3Zone.AddOrUpdateRRSigRecords(newRRSigRecords, out IReadOnlyList deletedRRSigRecords);
addedRecords.AddRange(newRRSigRecords);
deletedRecords.AddRange(deletedRRSigRecords);
}
CommitAndIncrementSerial(deletedRecords, addedRecords);
}
private uint GetSignatureValidityPeriod()
{
//SOA EXPIRE + 3 days
return (_entries[DnsResourceRecordType.SOA][0].RDATA as DnsSOARecordData).Expire + (3 * 24 * 60 * 60);
}
internal DateTime GetDnsKeyStateReadyBy(DnssecPrivateKey privateKey)
{
return GetDnsKeyStateReadyOn(privateKey).AddMilliseconds(DNSSEC_TIMER_PERIODIC_INTERVAL);
}
private DateTime GetDnsKeyStateReadyOn(DnssecPrivateKey privateKey)
{
bool foundOldKsk = false;
lock (_dnssecPrivateKeys)
{
foreach (KeyValuePair dnssecPrivateKey in _dnssecPrivateKeys)
{
DnssecPrivateKey kskPrivateKey = dnssecPrivateKey.Value;
if (kskPrivateKey.KeyType == DnssecPrivateKeyType.KeySigningKey)
{
if ((kskPrivateKey.State == DnssecPrivateKeyState.Ready) || (kskPrivateKey.State == DnssecPrivateKeyState.Active))
{
foundOldKsk = true;
break;
}
}
}
}
if (foundOldKsk)
return privateKey.StateChangedOn.AddSeconds(GetDnsKeyTtl() + GetPropagationDelay());
else
return privateKey.StateChangedOn.AddSeconds(GetMaxRecordTtl() + GetPropagationDelay()); //newly signed zone case
}
private uint GetPropagationDelay()
{
//the max time required to sync zone changes to secondaries if NOTIFY fails to trigger a zone transfer
DnsSOARecordData soa = _entries[DnsResourceRecordType.SOA][0].RDATA as DnsSOARecordData;
return soa.Refresh + soa.Retry;
}
private async Task GetParentSidePropagationDelayAsync()
{
uint parentSidePropagationDelay = 24 * 60 * 60;
try
{
string parent = AuthZoneManager.GetParentZone(_name);
if (parent is null)
parent = "";
DnsDatagram soaResponse = await _dnsServer.DirectQueryAsync(new DnsQuestionRecord(parent, DnsResourceRecordType.SOA, DnsClass.IN), 10000);
if (soaResponse.RCODE == DnsResponseCode.NoError)
{
IReadOnlyList records;
if (soaResponse.Answer.Count > 0)
records = soaResponse.Answer;
else if (soaResponse.Authority.Count > 0)
records = soaResponse.Authority;
else
records = null;
if (records is not null)
{
foreach (DnsResourceRecord record in records)
{
if (record.Type == DnsResourceRecordType.SOA)
{
DnsSOARecordData parentSoa = record.RDATA as DnsSOARecordData;
parentSidePropagationDelay = parentSoa.Refresh + parentSoa.Retry;
break;
}
}
}
}
}
catch (Exception ex)
{
LogManager log = _dnsServer.LogManager;
if (log is not null)
log.Write(ex);
}
return parentSidePropagationDelay;
}
private uint GetMaxRecordTtl()
{
uint maxTtl = 0;
foreach (KeyValuePair> entry in _entries)
{
if (entry.Key == DnsResourceRecordType.RRSIG)
continue;
IReadOnlyList rrset = entry.Value;
//find min TTL
uint rrsetTtl = 0;
foreach (DnsResourceRecord rr in rrset)
{
if ((rrsetTtl == 0) || (rrsetTtl > rr.OriginalTtlValue))
rrsetTtl = rr.OriginalTtlValue;
}
if (rrsetTtl > maxTtl)
maxTtl = rrsetTtl;
}
return maxTtl;
}
private uint GetMaxRRSigTtl()
{
uint maxTtl = 0;
if (!_entries.TryGetValue(DnsResourceRecordType.RRSIG, out IReadOnlyList rrsigRecords))
throw new InvalidOperationException();
foreach (DnsResourceRecord rr in rrsigRecords)
{
if (rr.OriginalTtlValue > maxTtl)
maxTtl = rr.OriginalTtlValue;
}
return maxTtl;
}
private uint GetZoneSoaMinimum()
{
return (_entries[DnsResourceRecordType.SOA][0].RDATA as DnsSOARecordData).Minimum;
}
internal uint GetZoneSoaExpire()
{
return (_entries[DnsResourceRecordType.SOA][0].RDATA as DnsSOARecordData).Expire;
}
private async Task GetDSTtl()
{
uint dsTtl = 24 * 60 * 60;
try
{
DnsDatagram dsResponse = await _dnsServer.DirectQueryAsync(new DnsQuestionRecord(_name, DnsResourceRecordType.DS, DnsClass.IN), 10000);
if (dsResponse.RCODE == DnsResponseCode.NoError)
{
if (dsResponse.Answer.Count > 0)
{
//find min TTL
dsTtl = 0;
foreach (DnsResourceRecord answer in dsResponse.Answer)
{
if (answer.Type == DnsResourceRecordType.DS)
{
if ((dsTtl == 0) || (dsTtl > answer.OriginalTtlValue))
dsTtl = answer.OriginalTtlValue;
}
}
}
else
{
dsTtl = 0; //no DS was found
}
}
}
catch (Exception ex)
{
LogManager log = _dnsServer.LogManager;
if (log is not null)
log.Write(ex);
}
return dsTtl;
}
public uint GetDnsKeyTtl()
{
if (_entries.TryGetValue(DnsResourceRecordType.DNSKEY, out IReadOnlyList dnsKeyRecords))
return dnsKeyRecords[0].OriginalTtlValue;
return 24 * 60 * 60;
}
public void UpdateDnsKeyTtl(uint dnsKeyTtl)
{
if (_dnssecStatus == AuthZoneDnssecStatus.Unsigned)
throw new DnsServerException("The zone must be signed.");
lock (_dnssecPrivateKeys)
{
foreach (KeyValuePair privateKeyEntry in _dnssecPrivateKeys)
{
switch (privateKeyEntry.Value.State)
{
case DnssecPrivateKeyState.Ready:
case DnssecPrivateKeyState.Active:
break;
default:
throw new DnsServerException("Cannot update DNSKEY TTL value: one or more private keys have state other than Ready or Active.");
}
}
}
if (!_entries.TryGetValue(DnsResourceRecordType.DNSKEY, out IReadOnlyList dnsKeyRecords))
throw new InvalidOperationException();
DnsResourceRecord[] newDnsKeyRecords = new DnsResourceRecord[dnsKeyRecords.Count];
for (int i = 0; i < dnsKeyRecords.Count; i++)
{
DnsResourceRecord dnsKeyRecord = dnsKeyRecords[i];
newDnsKeyRecords[i] = new DnsResourceRecord(dnsKeyRecord.Name, DnsResourceRecordType.DNSKEY, DnsClass.IN, dnsKeyTtl, dnsKeyRecord.RDATA);
}
List addedRecords = new List();
List deletedRecords = new List();
if (!TrySetRecords(DnsResourceRecordType.DNSKEY, newDnsKeyRecords, out IReadOnlyList deletedDnsKeyRecords))
throw new DnsServerException("Failed to update DNSKEY TTL. Please try again.");
addedRecords.AddRange(newDnsKeyRecords);
deletedRecords.AddRange(deletedDnsKeyRecords);
IReadOnlyList newRRSigRecords = SignRRSet(newDnsKeyRecords);
if (newRRSigRecords.Count > 0)
{
AddOrUpdateRRSigRecords(newRRSigRecords, out IReadOnlyList deletedRRSigRecords);
addedRecords.AddRange(newRRSigRecords);
deletedRecords.AddRange(deletedRRSigRecords);
}
CommitAndIncrementSerial(deletedRecords, addedRecords);
TriggerNotify();
}
#endregion
#region versioning
internal void CommitAndIncrementSerial(IReadOnlyList deletedRecords = null, IReadOnlyList addedRecords = null)
{
if (_internal)
return;
lock (_zoneHistory)
{
DnsResourceRecord oldSoaRecord = _entries[DnsResourceRecordType.SOA][0];
DnsResourceRecord newSoaRecord;
{
DnsSOARecordData oldSoa = oldSoaRecord.RDATA as DnsSOARecordData;
if ((addedRecords is not null) && (addedRecords.Count == 1) && (addedRecords[0].Type == DnsResourceRecordType.SOA))
{
DnsResourceRecord addSoaRecord = addedRecords[0];
DnsSOARecordData addSoa = addSoaRecord.RDATA as DnsSOARecordData;
uint serial = oldSoa.Serial;
if (addSoa.Serial > serial)
serial = addSoa.Serial;
else if (serial < uint.MaxValue)
serial++;
else
serial = 1;
newSoaRecord = new DnsResourceRecord(_name, DnsResourceRecordType.SOA, DnsClass.IN, addSoaRecord.TTL, new DnsSOARecordData(addSoa.PrimaryNameServer, addSoa.ResponsiblePerson, serial, addSoa.Refresh, addSoa.Retry, addSoa.Expire, addSoa.Minimum)) { Tag = addSoaRecord.Tag };
addedRecords = null;
}
else
{
uint serial = oldSoa.Serial;
if (serial < uint.MaxValue)
serial++;
else
serial = 1;
newSoaRecord = new DnsResourceRecord(_name, DnsResourceRecordType.SOA, DnsClass.IN, oldSoaRecord.TTL, new DnsSOARecordData(oldSoa.PrimaryNameServer, oldSoa.ResponsiblePerson, serial, oldSoa.Refresh, oldSoa.Retry, oldSoa.Expire, oldSoa.Minimum)) { Tag = oldSoaRecord.Tag };
}
}
DnsResourceRecord[] newSoaRecords = new DnsResourceRecord[] { newSoaRecord };
//update SOA
_entries[DnsResourceRecordType.SOA] = newSoaRecords;
IReadOnlyList newRRSigRecords = null;
IReadOnlyList deletedRRSigRecords = null;
if (_dnssecStatus != AuthZoneDnssecStatus.Unsigned)
{
//sign SOA and update RRSig
newRRSigRecords = SignRRSet(newSoaRecords);
AddOrUpdateRRSigRecords(newRRSigRecords, out deletedRRSigRecords);
}
//remove RR info from old SOA to allow creating new RR info for it during SetDeletedOn()
oldSoaRecord.Tag = null;
//start commit
oldSoaRecord.GetAuthRecordInfo().DeletedOn = DateTime.UtcNow;
//write removed
_zoneHistory.Add(oldSoaRecord);
if (deletedRecords is not null)
{
foreach (DnsResourceRecord deletedRecord in deletedRecords)
{
if (deletedRecord.GetAuthRecordInfo().Disabled)
continue;
_zoneHistory.Add(deletedRecord);
if (deletedRecord.Type == DnsResourceRecordType.NS)
{
IReadOnlyList glueRecords = deletedRecord.GetAuthRecordInfo().GlueRecords;
if (glueRecords is not null)
_zoneHistory.AddRange(glueRecords);
}
}
}
if (deletedRRSigRecords is not null)
_zoneHistory.AddRange(deletedRRSigRecords);
//write added
_zoneHistory.Add(newSoaRecord);
if (addedRecords is not null)
{
foreach (DnsResourceRecord addedRecord in addedRecords)
{
if (addedRecord.GetAuthRecordInfo().Disabled)
continue;
_zoneHistory.Add(addedRecord);
if (addedRecord.Type == DnsResourceRecordType.NS)
{
IReadOnlyList glueRecords = addedRecord.GetAuthRecordInfo().GlueRecords;
if (glueRecords is not null)
_zoneHistory.AddRange(glueRecords);
}
}
}
if (newRRSigRecords is not null)
_zoneHistory.AddRange(newRRSigRecords);
//end commit
CleanupHistory(_zoneHistory);
}
}
#endregion
#region public
public override void SetRecords(DnsResourceRecordType type, IReadOnlyList records)
{
if (_dnssecStatus != AuthZoneDnssecStatus.Unsigned)
{
switch (type)
{
case DnsResourceRecordType.ANAME:
case DnsResourceRecordType.APP:
throw new DnsServerException("The record type is not supported by DNSSEC signed primary zones.");
default:
foreach (DnsResourceRecord record in records)
{
if (record.GetAuthRecordInfo().Disabled)
throw new DnsServerException("Cannot set records: disabling records in a signed zones is not supported.");
}
break;
}
}
switch (type)
{
case DnsResourceRecordType.CNAME:
case DnsResourceRecordType.DS:
throw new InvalidOperationException("Cannot set " + type.ToString() + " record at zone apex.");
case DnsResourceRecordType.SOA:
if ((records.Count != 1) || !records[0].Name.Equals(_name, StringComparison.OrdinalIgnoreCase))
throw new InvalidOperationException("Invalid SOA record.");
DnsResourceRecord newSoaRecord = records[0];
DnsSOARecordData newSoa = newSoaRecord.RDATA as DnsSOARecordData;
if (newSoaRecord.OriginalTtlValue > newSoa.Expire)
throw new DnsServerException("Failed to set records: TTL cannot be greater than SOA EXPIRE.");
if (newSoa.Retry > newSoa.Refresh)
throw new DnsServerException("Failed to set records: SOA RETRY cannot be greater than SOA REFRESH.");
if (newSoa.Refresh > newSoa.Expire)
throw new DnsServerException("Failed to set records: SOA REFRESH cannot be greater than SOA EXPIRE.");
//remove any record info except comments
string comments = newSoaRecord.GetAuthRecordInfo().Comments;
newSoaRecord.Tag = null; //remove old record info
newSoaRecord.GetAuthRecordInfo().Comments = comments;
uint oldSoaMinimum = GetZoneSoaMinimum();
//setting new SOA
CommitAndIncrementSerial(null, records);
if (oldSoaMinimum != newSoa.Minimum)
{
switch (_dnssecStatus)
{
case AuthZoneDnssecStatus.SignedWithNSEC:
RefreshNSec();
break;
case AuthZoneDnssecStatus.SignedWithNSEC3:
RefreshNSec3();
break;
}
}
TriggerNotify();
break;
case DnsResourceRecordType.DNSKEY:
case DnsResourceRecordType.RRSIG:
case DnsResourceRecordType.NSEC:
case DnsResourceRecordType.NSEC3PARAM:
case DnsResourceRecordType.NSEC3:
throw new InvalidOperationException("Cannot set DNSSEC records.");
case DnsResourceRecordType.FWD:
throw new DnsServerException("The record type is not supported by primary zones.");
default:
if (records[0].OriginalTtlValue > GetZoneSoaExpire())
throw new DnsServerException("Failed to set records: TTL cannot be greater than SOA EXPIRE.");
if (!TrySetRecords(type, records, out IReadOnlyList