/* 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 } }