/*
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 System;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using TechnitiumLibrary.Net.Dns.Dnssec;
using TechnitiumLibrary.Net.Dns.EDnsOptions;
using TechnitiumLibrary.Net.Dns.ResourceRecords;
namespace DnsServerCore.Dns.Dnssec
{
//DNSSEC Key Rollover Timing Considerations
//https://datatracker.ietf.org/doc/html/rfc7583
public enum DnssecPrivateKeyType : byte
{
Unknown = 0,
KeySigningKey = 1,
ZoneSigningKey = 2
}
public enum DnssecPrivateKeyState : byte
{
Unknown = 0,
///
/// Although keys may be created immediately prior to first
/// use, some implementations may find it convenient to
/// create a pool of keys in one operation and draw from it
/// as required. (Note: such a pre-generated pool must be
/// secured against surreptitious use.) In the timelines
/// below, before the first event, the keys are considered to
/// be created but not yet used: they are said to be in the
/// "Generated" state.
///
Generated = 1,
///
/// A key enters the published state when either it or its associated data
/// first appears in the appropriate zone.
///
Published = 2,
///
/// The DNSKEY or its associated data have been published for long enough
/// to guarantee that copies of the key(s) it is replacing (or associated
/// data related to that key) have expired from caches.
///
Ready = 3,
///
/// The data is starting to be used for validation. In the
/// case of a ZSK, it means that the key is now being used to
/// sign RRsets and that both it and the created RRSIGs
/// appear in the zone. In the case of a KSK, it means that
/// it is possible to use it to validate a DNSKEY RRset as
/// both the DNSKEY and DS records are present in their
/// respective zones. Note that when this state is entered,
/// it may not be possible for validating resolvers to use
/// the data for validation in all cases: the zone signing
/// may not have finished or the data might not have reached
/// the resolver because of propagation delays and/or caching
/// issues. If this is the case, the resolver will have to
/// rely on the predecessor data instead.
///
Active = 4,
///
/// The data has ceased to be used for validation. In the
/// case of a ZSK, it means that the key is no longer used to
/// sign RRsets. In the case of a KSK, it means that the
/// successor DNSKEY and DS records are in place. In both
/// cases, the key (and its associated data) can be removed
/// as soon as it is safe to do so, i.e., when all validating
/// resolvers are able to use the new key and associated data
/// to validate the zone.However, until this happens, the
/// current key and associated data must remain in their
/// respective zones.
///
Retired = 5,
///
/// The key and its associated data are present in their
/// respective zones, but there is no longer information
/// anywhere that requires their presence for use in
/// validation. Hence, they can be removed at any time.
///
Dead = 6,
///
/// Both the DNSKEY and its associated data have been removed
/// from their respective zones.
///
Removed = 7,
///
/// The DNSKEY is published for a period with the "revoke"
/// bit set as a way of notifying validating resolvers that
/// have configured it as a trust anchor, as used in
/// [RFC5011], that it is about to be removed from the zone.
/// This state is used when [RFC5011] considerations are in
/// effect (see Section 3.3.4).
///
Revoked = 8
}
public abstract class DnssecPrivateKey
{
#region variables
readonly DnssecAlgorithm _algorithm;
readonly DnssecPrivateKeyType _keyType;
DnssecPrivateKeyState _state;
DateTime _stateChangedOn;
bool _isRetiring;
ushort _rolloverDays;
DnsDNSKEYRecordData _dnsKey;
#endregion
#region constructor
protected DnssecPrivateKey(DnssecAlgorithm algorithm, DnssecPrivateKeyType keyType)
{
_algorithm = algorithm;
_keyType = keyType;
_state = DnssecPrivateKeyState.Generated;
_stateChangedOn = DateTime.UtcNow;
}
protected DnssecPrivateKey(DnssecAlgorithm algorithm, BinaryReader bR)
{
_algorithm = algorithm;
_keyType = (DnssecPrivateKeyType)bR.ReadByte();
_state = (DnssecPrivateKeyState)bR.ReadByte();
_stateChangedOn = DateTime.UnixEpoch.AddSeconds(bR.ReadInt64());
_isRetiring = bR.ReadBoolean();
_rolloverDays = bR.ReadUInt16();
ReadPrivateKeyFrom(bR);
}
#endregion
#region static
public static DnssecPrivateKey Create(DnssecAlgorithm algorithm, DnssecPrivateKeyType keyType, int keySize = -1)
{
switch (algorithm)
{
case DnssecAlgorithm.RSAMD5:
case DnssecAlgorithm.RSASHA1:
case DnssecAlgorithm.RSASHA1_NSEC3_SHA1:
case DnssecAlgorithm.RSASHA256:
case DnssecAlgorithm.RSASHA512:
if ((keySize < 1024) || (keySize > 4096))
throw new ArgumentOutOfRangeException(nameof(keySize), "Valid RSA key size range is between 1024-4096 bits.");
using (RSA rsa = RSA.Create(keySize))
{
return new DnssecRsaPrivateKey(algorithm, keyType, keySize, rsa.ExportParameters(true));
}
case DnssecAlgorithm.ECDSAP256SHA256:
using (ECDsa ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256))
{
return new DnssecEcdsaPrivateKey(algorithm, keyType, ecdsa.ExportParameters(true));
}
case DnssecAlgorithm.ECDSAP384SHA384:
using (ECDsa ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP384))
{
return new DnssecEcdsaPrivateKey(algorithm, keyType, ecdsa.ExportParameters(true));
}
default:
throw new NotSupportedException("DNSSEC algorithm is not supported: " + algorithm.ToString());
}
}
public static DnssecPrivateKey ReadFrom(BinaryReader bR)
{
if (Encoding.ASCII.GetString(bR.ReadBytes(2)) != "DK")
throw new InvalidDataException("DNSSEC private key format is invalid.");
int version = bR.ReadByte();
switch (version)
{
case 1:
DnssecAlgorithm algorithm = (DnssecAlgorithm)bR.ReadByte();
switch (algorithm)
{
case DnssecAlgorithm.RSAMD5:
case DnssecAlgorithm.RSASHA1:
case DnssecAlgorithm.RSASHA1_NSEC3_SHA1:
case DnssecAlgorithm.RSASHA256:
case DnssecAlgorithm.RSASHA512:
return new DnssecRsaPrivateKey(algorithm, bR);
case DnssecAlgorithm.ECDSAP256SHA256:
case DnssecAlgorithm.ECDSAP384SHA384:
return new DnssecEcdsaPrivateKey(algorithm, bR);
default:
throw new NotSupportedException("DNSSEC algorithm is not supported: " + algorithm.ToString());
}
default:
throw new InvalidDataException("DNSSEC private key version not supported: " + version);
}
}
#endregion
#region protected
protected void InitDnsKey(DnssecPublicKey publicKey)
{
DnsDnsKeyFlag flags = DnsDnsKeyFlag.ZoneKey;
if (KeyType == DnssecPrivateKeyType.KeySigningKey)
flags |= DnsDnsKeyFlag.SecureEntryPoint;
if (_state == DnssecPrivateKeyState.Revoked)
flags |= DnsDnsKeyFlag.Revoke;
_dnsKey = new DnsDNSKEYRecordData(flags, 3, _algorithm, publicKey);
}
protected abstract byte[] SignHash(byte[] hash);
protected abstract void ReadPrivateKeyFrom(BinaryReader bR);
protected abstract void WritePrivateKeyTo(BinaryWriter bW);
#endregion
#region internal
internal DnsResourceRecord SignRRSet(string signersName, IReadOnlyList records, uint signatureInceptionOffset, uint signatureValidityPeriod)
{
DnsResourceRecord firstRecord = records[0];
DnsRRSIGRecordData unsignedRRSigRecord = new DnsRRSIGRecordData(firstRecord.Type, _algorithm, DnsRRSIGRecordData.GetLabelCount(firstRecord.Name), firstRecord.OriginalTtlValue, Convert.ToUInt32((DateTime.UtcNow.AddSeconds(signatureValidityPeriod) - DateTime.UnixEpoch).TotalSeconds % uint.MaxValue), Convert.ToUInt32((DateTime.UtcNow.AddSeconds(-signatureInceptionOffset) - DateTime.UnixEpoch).TotalSeconds % uint.MaxValue), DnsKey.ComputedKeyTag, signersName, null);
if (!DnsRRSIGRecordData.TryGetRRSetHash(unsignedRRSigRecord, records, out byte[] hash, out EDnsExtendedDnsErrorCode extendedDnsErrorCode))
throw new DnsServerException("Failed to sign record set: " + extendedDnsErrorCode.ToString());
byte[] signature = SignHash(hash);
DnsRRSIGRecordData signedRRSigRecord = new DnsRRSIGRecordData(unsignedRRSigRecord.TypeCovered, unsignedRRSigRecord.Algorithm, unsignedRRSigRecord.Labels, unsignedRRSigRecord.OriginalTtl, unsignedRRSigRecord.SignatureExpiration, unsignedRRSigRecord.SignatureInception, unsignedRRSigRecord.KeyTag, unsignedRRSigRecord.SignersName, signature);
return new DnsResourceRecord(firstRecord.Name, DnsResourceRecordType.RRSIG, firstRecord.Class, firstRecord.OriginalTtlValue, signedRRSigRecord);
}
internal void SetState(DnssecPrivateKeyState state)
{
if (_state >= state)
throw new InvalidOperationException();
_state = state;
_stateChangedOn = DateTime.UtcNow;
if (_state == DnssecPrivateKeyState.Revoked)
InitDnsKey(_dnsKey.PublicKey);
}
internal void SetToRetire()
{
_isRetiring = true;
}
internal bool IsRolloverNeeded()
{
return (_rolloverDays > 0) && (DateTime.UtcNow > _stateChangedOn.AddDays(_rolloverDays));
}
internal void WriteTo(BinaryWriter bW)
{
bW.Write(Encoding.ASCII.GetBytes("DK")); //format
bW.Write((byte)1); //version
bW.Write((byte)_algorithm);
bW.Write((byte)_keyType);
bW.Write((byte)_state);
bW.Write(Convert.ToInt64((_stateChangedOn - DateTime.UnixEpoch).TotalSeconds));
bW.Write(_isRetiring);
bW.Write(_rolloverDays);
WritePrivateKeyTo(bW);
}
#endregion
#region properties
public DnssecAlgorithm Algorithm
{ get { return _algorithm; } }
public DnssecPrivateKeyType KeyType
{ get { return _keyType; } }
public DnssecPrivateKeyState State
{ get { return _state; } }
public DateTime StateChangedOn
{ get { return _stateChangedOn; } }
public bool IsRetiring
{ get { return _isRetiring; } }
public ushort RolloverDays
{
get { return _rolloverDays; }
set
{
if (_keyType == DnssecPrivateKeyType.ZoneSigningKey)
{
if (value > 365)
throw new ArgumentOutOfRangeException(nameof(RolloverDays), "Zone Signing Key (ZSK) automatic rollover days valid range is 0-365.");
switch (_state)
{
case DnssecPrivateKeyState.Generated:
case DnssecPrivateKeyState.Published:
case DnssecPrivateKeyState.Ready:
case DnssecPrivateKeyState.Active:
if (_isRetiring)
throw new InvalidOperationException("Zone Signing Key (ZSK) automatic rollover cannot be set since it is set to retire.");
break;
default:
throw new InvalidOperationException("Zone Signing Key (ZSK) automatic rollover cannot be set due to invalid key state.");
}
}
else
{
if (value != 0)
throw new NotSupportedException("Automatic rollover is not supported for Key Signing Keys (KSK).");
}
_rolloverDays = value;
}
}
public DnsDNSKEYRecordData DnsKey
{ get { return _dnsKey; } }
public ushort KeyTag
{ get { return _dnsKey.ComputedKeyTag; } }
#endregion
}
}