diff --git a/EppLib.UnitTests/EppLib.UnitTests.csproj b/EppLib.UnitTests/EppLib.UnitTests.csproj
index 6acabf0..f88cfbf 100644
--- a/EppLib.UnitTests/EppLib.UnitTests.csproj
+++ b/EppLib.UnitTests/EppLib.UnitTests.csproj
@@ -301,8 +301,9 @@
Designer
-
+
Designer
-
+ PreserveNewest
+
\ No newline at end of file
diff --git a/EppLib.UnitTests/SecDNSExtensionLocalTest.cs b/EppLib.UnitTests/SecDNSExtensionLocalTest.cs
index ee51048..3983621 100644
--- a/EppLib.UnitTests/SecDNSExtensionLocalTest.cs
+++ b/EppLib.UnitTests/SecDNSExtensionLocalTest.cs
@@ -1,8 +1,10 @@
using EppLib.Entities;
using EppLib.Extensions.SecDNS;
using Microsoft.VisualStudio.TestTools.UnitTesting;
+using System;
using System.Diagnostics;
using System.IO;
+using System.Xml;
namespace EppLib.Tests
{
@@ -83,5 +85,273 @@ public void SecDNSUpdateExtension()
Assert.AreEqual(expected, xml);
}
+
+ private const string SecDnsNamespace = "urn:ietf:params:xml:ns:secDNS-1.1";
+
+ // Returns the secDNS extension element of a command, after validating it against secDNS-1.1.xsd.
+ private static XmlElement SecDnsElement(XmlDocument commandXml)
+ {
+ var namespaces = new XmlNamespaceManager(commandXml.NameTable);
+ namespaces.AddNamespace("secDNS", SecDnsNamespace);
+ var element = (XmlElement)commandXml.SelectSingleNode("//secDNS:*[parent::*[local-name()='extension']]", namespaces);
+ Assert.IsNotNull(element, "no secDNS extension element");
+
+ var doc = new XmlDocument();
+ doc.AppendChild(doc.ImportNode(element, true));
+ var schemaPath = Path.Combine(Path.GetDirectoryName(typeof(SecDNSExtensionLocalTest).Assembly.Location), "TestData", "secDNS-1.1.xsd");
+ doc.Schemas.Add(SecDnsNamespace, schemaPath);
+ doc.Validate((sender, e) => Assert.Fail("secDNS schema validation: " + e.Message));
+
+ return doc.DocumentElement;
+ }
+
+ private static string Text(XmlNode node, string xpath)
+ {
+ var namespaces = new XmlNamespaceManager(node.OwnerDocument.NameTable);
+ namespaces.AddNamespace("secDNS", SecDnsNamespace);
+ var match = node.SelectSingleNode(xpath, namespaces);
+ return match?.InnerText;
+ }
+
+ private static int Count(XmlNode node, string xpath)
+ {
+ var namespaces = new XmlNamespaceManager(node.OwnerDocument.NameTable);
+ namespaces.AddNamespace("secDNS", SecDnsNamespace);
+ return node.SelectNodes(xpath, namespaces).Count;
+ }
+
+ private static SecDNSKeyData RfcKey()
+ {
+ return new SecDNSKeyData { Flags = 257, Protocol = 3, Algorithm = SecDNSAlgorithm.RSAMD5, PublicKey = "AQPJ////4Q==" };
+ }
+
+ ///
+ /// RFC 5910 section 5.2.1, DS Data Interface with the optional key data (issue #31).
+ ///
+ [TestMethod]
+ [TestCategory("SecDNSExtension")]
+ public void SecDNSCreateDsDataWithKeyData()
+ {
+ var command = new DomainCreate("example.com", "jd1234");
+ var extension = new SecDNSCreate { MaxSigLife = 604800 };
+ extension.DsData.Add(new SecDNSData
+ {
+ KeyTag = 12345,
+ Algorithm = SecDNSAlgorithm.DSA,
+ Digest = "49FD46E6C4B45C55D4AC",
+ KeyData = RfcKey()
+ });
+ command.Extensions.Add(extension);
+
+ var secDns = SecDnsElement(command.ToXml());
+
+ Assert.AreEqual("604800", Text(secDns, "secDNS:maxSigLife"));
+ Assert.AreEqual("12345", Text(secDns, "secDNS:dsData/secDNS:keyTag"));
+ Assert.AreEqual("3", Text(secDns, "secDNS:dsData/secDNS:alg"));
+ Assert.AreEqual("1", Text(secDns, "secDNS:dsData/secDNS:digestType"));
+ Assert.AreEqual("257", Text(secDns, "secDNS:dsData/secDNS:keyData/secDNS:flags"));
+ Assert.AreEqual("3", Text(secDns, "secDNS:dsData/secDNS:keyData/secDNS:protocol"));
+ Assert.AreEqual("1", Text(secDns, "secDNS:dsData/secDNS:keyData/secDNS:alg"));
+ Assert.AreEqual("AQPJ////4Q==", Text(secDns, "secDNS:dsData/secDNS:keyData/secDNS:pubKey"));
+ }
+
+ ///
+ /// RFC 5910 section 5.2.1, Key Data Interface (issue #31).
+ ///
+ [TestMethod]
+ [TestCategory("SecDNSExtension")]
+ public void SecDNSCreateKeyData()
+ {
+ var command = new DomainCreate("example.com", "jd1234");
+ var extension = new SecDNSCreate();
+ extension.KeyData.Add(RfcKey());
+ command.Extensions.Add(extension);
+
+ var secDns = SecDnsElement(command.ToXml());
+
+ Assert.AreEqual(0, Count(secDns, "secDNS:dsData"));
+ Assert.AreEqual(1, Count(secDns, "secDNS:keyData"));
+ Assert.AreEqual("AQPJ////4Q==", Text(secDns, "secDNS:keyData/secDNS:pubKey"));
+ }
+
+ [TestMethod]
+ [TestCategory("SecDNSExtension")]
+ [ExpectedException(typeof(InvalidOperationException))]
+ public void SecDNSCreateRejectsDsDataAndKeyDataTogether()
+ {
+ var command = new DomainCreate("example.com", "jd1234");
+ var extension = new SecDNSCreate();
+ extension.DsData.Add(new SecDNSData { KeyTag = 1, Algorithm = SecDNSAlgorithm.RSASHA256, Digest = "AB" });
+ extension.KeyData.Add(RfcKey());
+ command.Extensions.Add(extension);
+
+ command.ToXml();
+ }
+
+ ///
+ /// Key tags go up to 65535, and modern DS records use SHA-256 with algorithm 13.
+ ///
+ [TestMethod]
+ [TestCategory("SecDNSExtension")]
+ public void SecDNSCreateHighKeyTagAndSha256()
+ {
+ var command = new DomainCreate("example.com", "jd1234");
+ var extension = new SecDNSCreate();
+ extension.DsData.Add(new SecDNSData
+ {
+ KeyTag = 65535,
+ Algorithm = SecDNSAlgorithm.ECDSAP256SHA256,
+ DigestType = SecDNSDigestType.SHA256,
+ Digest = "E2D3C916F6DEEAC73294E8268FB5885044A833FC5459588F4A9184CFC41A5766"
+ });
+ command.Extensions.Add(extension);
+
+ var secDns = SecDnsElement(command.ToXml());
+
+ Assert.AreEqual("65535", Text(secDns, "secDNS:dsData/secDNS:keyTag"));
+ Assert.AreEqual("13", Text(secDns, "secDNS:dsData/secDNS:alg"));
+ Assert.AreEqual("2", Text(secDns, "secDNS:dsData/secDNS:digestType"));
+ }
+
+ ///
+ /// RFC 5910 section 5.2.5: urgent update that replaces all DNSSEC data and changes maxSigLife.
+ ///
+ [TestMethod]
+ [TestCategory("SecDNSExtension")]
+ public void SecDNSUpdateRemoveAllAddChangeUrgent()
+ {
+ var command = new DomainUpdate("example.com");
+ var extension = new SecDNSUpdate { RemoveAll = true, MaxSigLife = 605900, Urgent = true };
+ extension.ToAdd.Add(new SecDNSData { KeyTag = 12346, Algorithm = SecDNSAlgorithm.DSA, Digest = "38EC35D5B3A34B44C39B" });
+ command.Extensions.Add(extension);
+
+ var secDns = SecDnsElement(command.ToXml());
+
+ Assert.AreEqual("true", secDns.GetAttribute("urgent"));
+ Assert.AreEqual("true", Text(secDns, "secDNS:rem/secDNS:all"));
+ Assert.AreEqual("12346", Text(secDns, "secDNS:add/secDNS:dsData/secDNS:keyTag"));
+ Assert.AreEqual("605900", Text(secDns, "secDNS:chg/secDNS:maxSigLife"));
+ }
+
+ ///
+ /// RFC 5910 section 5.2.5: add and remove DNSKEY data.
+ ///
+ [TestMethod]
+ [TestCategory("SecDNSExtension")]
+ public void SecDNSUpdateKeyData()
+ {
+ var command = new DomainUpdate("example.com");
+ var extension = new SecDNSUpdate();
+ extension.KeyDataToRemove.Add(RfcKey());
+ extension.KeyDataToAdd.Add(new SecDNSKeyData { Flags = 257, Algorithm = SecDNSAlgorithm.ED25519, PublicKey = "l02Woi0iS8Aa25FQkUd9RMzZHJpBoRQwAQEX1SxZJA4=" });
+ command.Extensions.Add(extension);
+
+ // Check the raw command: schema validation fills in the urgent="false" default.
+ Assert.IsFalse(command.ToXml().OuterXml.Contains("urgent"));
+
+ var secDns = SecDnsElement(command.ToXml());
+
+ Assert.AreEqual("AQPJ////4Q==", Text(secDns, "secDNS:rem/secDNS:keyData/secDNS:pubKey"));
+ Assert.AreEqual("15", Text(secDns, "secDNS:add/secDNS:keyData/secDNS:alg"));
+ Assert.AreEqual(0, Count(secDns, "secDNS:chg"));
+ }
+
+ ///
+ /// RFC 5910 section 5.2.5: change only maxSigLife.
+ ///
+ [TestMethod]
+ [TestCategory("SecDNSExtension")]
+ public void SecDNSUpdateChangeMaxSigLifeOnly()
+ {
+ var command = new DomainUpdate("example.com");
+ command.Extensions.Add(new SecDNSUpdate { MaxSigLife = 605900 });
+
+ var secDns = SecDnsElement(command.ToXml());
+
+ Assert.AreEqual(0, Count(secDns, "secDNS:rem"));
+ Assert.AreEqual(0, Count(secDns, "secDNS:add"));
+ Assert.AreEqual("605900", Text(secDns, "secDNS:chg/secDNS:maxSigLife"));
+ }
+
+ [TestMethod]
+ [TestCategory("SecDNSExtension")]
+ [ExpectedException(typeof(InvalidOperationException))]
+ public void SecDNSUpdateRejectsRemoveAllWithSpecificRemovals()
+ {
+ var command = new DomainUpdate("example.com");
+ var extension = new SecDNSUpdate { RemoveAll = true };
+ extension.ToRemove.Add(new SecDNSData { KeyTag = 1, Algorithm = SecDNSAlgorithm.RSASHA256, Digest = "AB" });
+ command.Extensions.Add(extension);
+
+ command.ToXml();
+ }
+
+ private const string InfoResponseTemplate =
+ "" +
+ "Command completed successfully" +
+ "example.comEXAMPLE1-REP" +
+ "{0}ABC-1234554322-XYZ";
+
+ ///
+ /// RFC 5910 section 5.1.2, info response with DS data and key data.
+ ///
+ [TestMethod]
+ [TestCategory("SecDNSExtension")]
+ public void SecDNSInfDataDsData()
+ {
+ var xml = string.Format(InfoResponseTemplate,
+ "604800" +
+ "123453149FD46E6C4B45C55D4AC" +
+ "25731AQPJ////4Q==" +
+ "54321132E2D3C916F6DEEAC7" +
+ "");
+
+ var info = SecDNSInfData.FromResponse(new DomainInfoResponse(xml));
+
+ Assert.IsNotNull(info);
+ Assert.AreEqual(604800, info.MaxSigLife);
+ Assert.AreEqual(2, info.DsData.Count);
+ Assert.AreEqual(0, info.KeyData.Count);
+ Assert.AreEqual(12345, info.DsData[0].KeyTag);
+ Assert.AreEqual(SecDNSAlgorithm.DSA, info.DsData[0].Algorithm);
+ Assert.AreEqual(SecDNSDigestType.SHA1, info.DsData[0].DigestType);
+ Assert.AreEqual("49FD46E6C4B45C55D4AC", info.DsData[0].Digest);
+ Assert.AreEqual(257, info.DsData[0].KeyData.Flags);
+ Assert.AreEqual("AQPJ////4Q==", info.DsData[0].KeyData.PublicKey);
+ Assert.AreEqual(54321, info.DsData[1].KeyTag);
+ Assert.AreEqual(SecDNSAlgorithm.ECDSAP256SHA256, info.DsData[1].Algorithm);
+ Assert.AreEqual(SecDNSDigestType.SHA256, info.DsData[1].DigestType);
+ Assert.IsNull(info.DsData[1].KeyData);
+ }
+
+ ///
+ /// RFC 5910 section 5.1.2, info response with key data only.
+ ///
+ [TestMethod]
+ [TestCategory("SecDNSExtension")]
+ public void SecDNSInfDataKeyData()
+ {
+ var xml = string.Format(InfoResponseTemplate,
+ "" +
+ "25731AQPJ////4Q==" +
+ "");
+
+ var info = SecDNSInfData.FromResponse(new DomainInfoResponse(xml));
+
+ Assert.IsNull(info.MaxSigLife);
+ Assert.AreEqual(0, info.DsData.Count);
+ Assert.AreEqual(1, info.KeyData.Count);
+ Assert.AreEqual(3, info.KeyData[0].Protocol);
+ Assert.AreEqual(SecDNSAlgorithm.RSAMD5, info.KeyData[0].Algorithm);
+ }
+
+ [TestMethod]
+ [TestCategory("SecDNSExtension")]
+ public void SecDNSInfDataAbsent()
+ {
+ var info = SecDNSInfData.FromResponse(new DomainInfoResponse(string.Format(InfoResponseTemplate, "")));
+
+ Assert.IsNull(info);
+ }
}
}
diff --git a/EppLib/EppLib.csproj b/EppLib/EppLib.csproj
index 186de71..3de0418 100644
--- a/EppLib/EppLib.csproj
+++ b/EppLib/EppLib.csproj
@@ -4,7 +4,7 @@
netstandard2.0;net10.0
EppLib
EppLib
- 1.6.0
+ 1.7.0
Code Maker Inc. and Contributors
EppLib.NET provides a library that makes easy for registrars to interact with registries implementing the Extensible Provisioning Protocol (EPP). Includes extensions for CIRA (.CA), Nominet (.UK), IIS (.SE), Verisign Namestore, LaunchPhase and SecDNS.
epp;registrar;registry;domain;rfc5730
@@ -13,7 +13,7 @@
https://github.com/CodeMakerInc/EppLib.NET.git
git
README.md
- Breaking: parsed EPP dates (DomainRenewResponse.ExDate and the Nominet dates) are now returned in UTC (DateTimeKind.Utc) as RFC 5730-5733 require, instead of being converted to the machine's local time. Values without a zone designator are taken as UTC. Fixes DomainRenew sending the wrong curExpDate east of UTC when given a full date-time, and a culture-dependent Smallregistry birthDate.
+ Completes the secDNS (DNSSEC, RFC 5910) extension (#31): keyData on create and update and inside dsData, remove-all, maxSigLife changes, the urgent flag, and SecDNSInfData for reading DNSSEC data from domain info responses. SecDNSData.KeyTag is now an int so key tags above 32767 work, DigestType can be set (for example SHA256) instead of always being SHA-1, and newer algorithms such as RSASHA256, ECDSAP256SHA256 and ED25519 are named in SecDNSAlgorithm.
true
false
false
@@ -174,6 +174,8 @@
+
+
diff --git a/EppLib/Extensions/SecDNS/SecDNSCreate.cs b/EppLib/Extensions/SecDNS/SecDNSCreate.cs
index 69949e1..947e9d6 100644
--- a/EppLib/Extensions/SecDNS/SecDNSCreate.cs
+++ b/EppLib/Extensions/SecDNS/SecDNSCreate.cs
@@ -1,20 +1,33 @@
using EppLib.Entities;
+using System;
using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
using System.Xml;
namespace EppLib.Extensions.SecDNS
{
+ ///
+ /// The secDNS create extension (RFC 5910 section 5.2.1). Add either DS records to
+ /// or DNSKEY records to , not both; which one depends on the registry.
+ ///
public class SecDNSCreate : EppExtension
{
public int? MaxSigLife { get; set; }
public IList DsData { get; } = new List();
+ public IList KeyData { get; } = new List();
protected override string Namespace { get; set; }
public override XmlNode ToXml(XmlDocument doc)
{
- var root = doc.CreateElement("secDNS:create", "urn:ietf:params:xml:ns:secDNS-1.1");
- root.SetAttribute("xmlns:secDNS", "urn:ietf:params:xml:ns:secDNS-1.1");
+ if (DsData.Any() && KeyData.Any())
+ {
+ throw new InvalidOperationException("secDNS create takes either DsData or KeyData, not both (RFC 5910 section 4).");
+ }
+
+ var root = doc.CreateElement("secDNS:create", SecDNSKeyData.NamespaceUri);
+ root.SetAttribute("xmlns:secDNS", SecDNSKeyData.NamespaceUri);
var xsd = doc.CreateAttribute("xsi", "schemaLocation", "http://www.w3.org/2001/XMLSchema-instance");
xsd.Value = "urn:ietf:params:xml:ns:secDNS-1.1 secDNS-1.1.xsd";
@@ -22,8 +35,8 @@ public override XmlNode ToXml(XmlDocument doc)
if (MaxSigLife.HasValue)
{
- var maxSigLifeNode = doc.CreateElement("secDNS:maxSigLife", "urn:ietf:params:xml:ns:secDNS-1.1");
- maxSigLifeNode.InnerText = MaxSigLife.Value.ToString();
+ var maxSigLifeNode = doc.CreateElement("secDNS:maxSigLife", SecDNSKeyData.NamespaceUri);
+ maxSigLifeNode.InnerText = MaxSigLife.Value.ToString(CultureInfo.InvariantCulture);
root.AppendChild(maxSigLifeNode);
}
@@ -32,6 +45,11 @@ public override XmlNode ToXml(XmlDocument doc)
root.AppendChild(data.ToXml(doc));
}
+ foreach (var key in KeyData)
+ {
+ root.AppendChild(key.ToXml(doc));
+ }
+
return root;
}
}
diff --git a/EppLib/Extensions/SecDNS/SecDNSData.cs b/EppLib/Extensions/SecDNS/SecDNSData.cs
index 3ed4ad6..a9d71df 100644
--- a/EppLib/Extensions/SecDNS/SecDNSData.cs
+++ b/EppLib/Extensions/SecDNS/SecDNSData.cs
@@ -1,7 +1,11 @@
-using System.Xml;
+using System.Globalization;
+using System.Xml;
namespace EppLib.Extensions.SecDNS
{
+ ///
+ /// DNSSEC algorithm numbers (IANA "DNS Security Algorithm Numbers"). Values not listed can be cast from int.
+ ///
public enum SecDNSAlgorithm
{
RSAMD5 = 1,
@@ -9,38 +13,97 @@ public enum SecDNSAlgorithm
DSA = 3,
ECC = 4,
RSASHA1 = 5,
+ DSANSEC3SHA1 = 6,
+ RSASHA1NSEC3SHA1 = 7,
+ RSASHA256 = 8,
+ RSASHA512 = 10,
+ ECCGOST = 12,
+ ECDSAP256SHA256 = 13,
+ ECDSAP384SHA384 = 14,
+ ED25519 = 15,
+ ED448 = 16,
INDIRECT = 252,
PRIVATEDNS = 253,
PRIVATEOID = 254
}
+ ///
+ /// DS record digest types (IANA "Delegation Signer (DS) Resource Record (RR) Type Digest Algorithms").
+ ///
+ public enum SecDNSDigestType
+ {
+ SHA1 = 1,
+ SHA256 = 2,
+ GOSTR341194 = 3,
+ SHA384 = 4
+ }
+
+ ///
+ /// A DS record, the secDNS dsData element of RFC 5910.
+ ///
public class SecDNSData
{
- public short KeyTag { get; set; }
+ ///
+ /// The key tag, 0 to 65535.
+ ///
+ public int KeyTag { get; set; }
public SecDNSAlgorithm Algorithm { get; set; }
+
+ ///
+ /// Defaults to SHA1 for compatibility with earlier versions; most registries expect SHA256 today.
+ ///
+ public SecDNSDigestType DigestType { get; set; } = SecDNSDigestType.SHA1;
+
+ ///
+ /// The digest, as hexadecimal.
+ ///
public string Digest { get; set; }
+ ///
+ /// The DNSKEY the DS record was made from. Optional; some registries require it.
+ ///
+ public SecDNSKeyData KeyData { get; set; }
+
public XmlNode ToXml(XmlDocument doc)
{
- var dataNode = doc.CreateElement("secDNS:dsData", "urn:ietf:params:xml:ns:secDNS-1.1");
+ var dataNode = doc.CreateElement("secDNS:dsData", SecDNSKeyData.NamespaceUri);
- var keyTagNode = doc.CreateElement("secDNS:keyTag", "urn:ietf:params:xml:ns:secDNS-1.1");
- keyTagNode.InnerText = KeyTag.ToString();
+ var keyTagNode = doc.CreateElement("secDNS:keyTag", SecDNSKeyData.NamespaceUri);
+ keyTagNode.InnerText = KeyTag.ToString(CultureInfo.InvariantCulture);
dataNode.AppendChild(keyTagNode);
- var algNode = doc.CreateElement("secDNS:alg", "urn:ietf:params:xml:ns:secDNS-1.1");
- algNode.InnerText = ((int)Algorithm).ToString();
+ var algNode = doc.CreateElement("secDNS:alg", SecDNSKeyData.NamespaceUri);
+ algNode.InnerText = ((int)Algorithm).ToString(CultureInfo.InvariantCulture);
dataNode.AppendChild(algNode);
- var digestTypeNode = doc.CreateElement("secDNS:digestType", "urn:ietf:params:xml:ns:secDNS-1.1");
- digestTypeNode.InnerText = "1";
+ var digestTypeNode = doc.CreateElement("secDNS:digestType", SecDNSKeyData.NamespaceUri);
+ digestTypeNode.InnerText = ((int)DigestType).ToString(CultureInfo.InvariantCulture);
dataNode.AppendChild(digestTypeNode);
- var digestNode = doc.CreateElement("secDNS:digest", "urn:ietf:params:xml:ns:secDNS-1.1");
+ var digestNode = doc.CreateElement("secDNS:digest", SecDNSKeyData.NamespaceUri);
digestNode.InnerText = Digest;
dataNode.AppendChild(digestNode);
+ if (KeyData != null)
+ {
+ dataNode.AppendChild(KeyData.ToXml(doc));
+ }
+
return dataNode;
}
+
+ internal static SecDNSData FromXml(XmlNode node, XmlNamespaceManager namespaces)
+ {
+ var keyDataNode = node.SelectSingleNode("secDNS:keyData", namespaces);
+
+ return new SecDNSData
+ {
+ KeyTag = int.Parse(node.SelectSingleNode("secDNS:keyTag", namespaces).InnerText, CultureInfo.InvariantCulture),
+ Algorithm = (SecDNSAlgorithm)int.Parse(node.SelectSingleNode("secDNS:alg", namespaces).InnerText, CultureInfo.InvariantCulture),
+ DigestType = (SecDNSDigestType)int.Parse(node.SelectSingleNode("secDNS:digestType", namespaces).InnerText, CultureInfo.InvariantCulture),
+ Digest = node.SelectSingleNode("secDNS:digest", namespaces).InnerText,
+ KeyData = keyDataNode != null ? SecDNSKeyData.FromXml(keyDataNode, namespaces) : null
+ };
+ }
}
}
diff --git a/EppLib/Extensions/SecDNS/SecDNSInfData.cs b/EppLib/Extensions/SecDNS/SecDNSInfData.cs
new file mode 100644
index 0000000..33f28af
--- /dev/null
+++ b/EppLib/Extensions/SecDNS/SecDNSInfData.cs
@@ -0,0 +1,57 @@
+using EppLib.Entities;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Xml;
+
+namespace EppLib.Extensions.SecDNS
+{
+ ///
+ /// The DNSSEC data returned with a domain info response (RFC 5910 section 5.1.2).
+ ///
+ public class SecDNSInfData
+ {
+ public int? MaxSigLife { get; set; }
+ public IList DsData { get; } = new List();
+ public IList KeyData { get; } = new List();
+
+ ///
+ /// Reads the secDNS infData from a domain info response, including registry-specific info responses.
+ /// Returns null when the response has no DNSSEC data.
+ ///
+ public static SecDNSInfData FromResponse(EppResponse response)
+ {
+ var doc = new XmlDocument();
+ doc.LoadXml(response.Xml);
+
+ var namespaces = new XmlNamespaceManager(doc.NameTable);
+ namespaces.AddNamespace("ns", "urn:ietf:params:xml:ns:epp-1.0");
+ namespaces.AddNamespace("secDNS", SecDNSKeyData.NamespaceUri);
+
+ var infData = doc.SelectSingleNode("/ns:epp/ns:response/ns:extension/secDNS:infData", namespaces);
+ if (infData == null)
+ {
+ return null;
+ }
+
+ var result = new SecDNSInfData();
+
+ var maxSigLifeNode = infData.SelectSingleNode("secDNS:maxSigLife", namespaces);
+ if (maxSigLifeNode != null)
+ {
+ result.MaxSigLife = int.Parse(maxSigLifeNode.InnerText, CultureInfo.InvariantCulture);
+ }
+
+ foreach (XmlNode node in infData.SelectNodes("secDNS:dsData", namespaces))
+ {
+ result.DsData.Add(SecDNSData.FromXml(node, namespaces));
+ }
+
+ foreach (XmlNode node in infData.SelectNodes("secDNS:keyData", namespaces))
+ {
+ result.KeyData.Add(SecDNSKeyData.FromXml(node, namespaces));
+ }
+
+ return result;
+ }
+ }
+}
diff --git a/EppLib/Extensions/SecDNS/SecDNSKeyData.cs b/EppLib/Extensions/SecDNS/SecDNSKeyData.cs
new file mode 100644
index 0000000..a05f9ac
--- /dev/null
+++ b/EppLib/Extensions/SecDNS/SecDNSKeyData.cs
@@ -0,0 +1,65 @@
+using System.Globalization;
+using System.Xml;
+
+namespace EppLib.Extensions.SecDNS
+{
+ ///
+ /// A DNSKEY record, the secDNS keyData element of RFC 5910. Used on its own by registries that
+ /// compute the DS record themselves, or inside .
+ ///
+ public class SecDNSKeyData
+ {
+ internal const string NamespaceUri = "urn:ietf:params:xml:ns:secDNS-1.1";
+
+ ///
+ /// DNSKEY flags: 257 for a key signing key, 256 for a zone signing key.
+ ///
+ public int Flags { get; set; }
+
+ ///
+ /// DNSKEY protocol; always 3 (RFC 4034).
+ ///
+ public int Protocol { get; set; } = 3;
+
+ public SecDNSAlgorithm Algorithm { get; set; }
+
+ ///
+ /// The public key, base64 encoded.
+ ///
+ public string PublicKey { get; set; }
+
+ public XmlNode ToXml(XmlDocument doc)
+ {
+ var keyDataNode = doc.CreateElement("secDNS:keyData", NamespaceUri);
+
+ var flagsNode = doc.CreateElement("secDNS:flags", NamespaceUri);
+ flagsNode.InnerText = Flags.ToString(CultureInfo.InvariantCulture);
+ keyDataNode.AppendChild(flagsNode);
+
+ var protocolNode = doc.CreateElement("secDNS:protocol", NamespaceUri);
+ protocolNode.InnerText = Protocol.ToString(CultureInfo.InvariantCulture);
+ keyDataNode.AppendChild(protocolNode);
+
+ var algNode = doc.CreateElement("secDNS:alg", NamespaceUri);
+ algNode.InnerText = ((int)Algorithm).ToString(CultureInfo.InvariantCulture);
+ keyDataNode.AppendChild(algNode);
+
+ var pubKeyNode = doc.CreateElement("secDNS:pubKey", NamespaceUri);
+ pubKeyNode.InnerText = PublicKey;
+ keyDataNode.AppendChild(pubKeyNode);
+
+ return keyDataNode;
+ }
+
+ internal static SecDNSKeyData FromXml(XmlNode node, XmlNamespaceManager namespaces)
+ {
+ return new SecDNSKeyData
+ {
+ Flags = int.Parse(node.SelectSingleNode("secDNS:flags", namespaces).InnerText, CultureInfo.InvariantCulture),
+ Protocol = int.Parse(node.SelectSingleNode("secDNS:protocol", namespaces).InnerText, CultureInfo.InvariantCulture),
+ Algorithm = (SecDNSAlgorithm)int.Parse(node.SelectSingleNode("secDNS:alg", namespaces).InnerText, CultureInfo.InvariantCulture),
+ PublicKey = node.SelectSingleNode("secDNS:pubKey", namespaces).InnerText
+ };
+ }
+ }
+}
diff --git a/EppLib/Extensions/SecDNS/SecDNSUpdate.cs b/EppLib/Extensions/SecDNS/SecDNSUpdate.cs
index 6a85304..d1e7cea 100644
--- a/EppLib/Extensions/SecDNS/SecDNSUpdate.cs
+++ b/EppLib/Extensions/SecDNS/SecDNSUpdate.cs
@@ -1,50 +1,129 @@
using EppLib.Entities;
+using System;
using System.Collections.Generic;
+using System.Globalization;
using System.Linq;
using System.Xml;
namespace EppLib.Extensions.SecDNS
{
+ ///
+ /// The secDNS update extension (RFC 5910 section 5.2.5). Removals are processed before additions,
+ /// so with new records replaces the domain's DNSSEC data.
+ ///
public class SecDNSUpdate : EppExtension
{
+ ///
+ /// DS records to remove.
+ ///
public IList ToRemove { get; } = new List();
+
+ ///
+ /// DS records to add.
+ ///
public IList ToAdd { get; } = new List();
+ ///
+ /// DNSKEY records to remove.
+ ///
+ public IList KeyDataToRemove { get; } = new List();
+
+ ///
+ /// DNSKEY records to add.
+ ///
+ public IList KeyDataToAdd { get; } = new List();
+
+ ///
+ /// Removes all DS and DNSKEY data from the domain. Cannot be combined with or .
+ ///
+ public bool RemoveAll { get; set; }
+
+ ///
+ /// Changes the maximum signature lifetime, in seconds.
+ ///
+ public int? MaxSigLife { get; set; }
+
+ ///
+ /// Asks the registry to process the update with high priority. Registries may reject it if unsupported.
+ ///
+ public bool Urgent { get; set; }
+
protected override string Namespace { get; set; }
public override XmlNode ToXml(XmlDocument doc)
{
- var root = doc.CreateElement("secDNS:update", "urn:ietf:params:xml:ns:secDNS-1.1");
- root.SetAttribute("xmlns:secDNS", "urn:ietf:params:xml:ns:secDNS-1.1");
+ if (new[] { RemoveAll, ToRemove.Any(), KeyDataToRemove.Any() }.Count(x => x) > 1)
+ {
+ throw new InvalidOperationException("secDNS rem takes one of RemoveAll, ToRemove or KeyDataToRemove (RFC 5910 section 5.2.5).");
+ }
+
+ if (ToAdd.Any() && KeyDataToAdd.Any())
+ {
+ throw new InvalidOperationException("secDNS add takes either ToAdd or KeyDataToAdd, not both (RFC 5910 section 5.2.5).");
+ }
+
+ var root = doc.CreateElement("secDNS:update", SecDNSKeyData.NamespaceUri);
+ root.SetAttribute("xmlns:secDNS", SecDNSKeyData.NamespaceUri);
+
+ if (Urgent)
+ {
+ root.SetAttribute("urgent", "true");
+ }
var xsd = doc.CreateAttribute("xsi", "schemaLocation", "http://www.w3.org/2001/XMLSchema-instance");
xsd.Value = "urn:ietf:params:xml:ns:secDNS-1.1 secDNS-1.1.xsd";
root.Attributes.Append(xsd);
- if (ToRemove.Any())
+ if (RemoveAll || ToRemove.Any() || KeyDataToRemove.Any())
{
- var removeNode = doc.CreateElement("secDNS:rem", "urn:ietf:params:xml:ns:secDNS-1.1");
+ var removeNode = doc.CreateElement("secDNS:rem", SecDNSKeyData.NamespaceUri);
+
+ if (RemoveAll)
+ {
+ var allNode = doc.CreateElement("secDNS:all", SecDNSKeyData.NamespaceUri);
+ allNode.InnerText = "true";
+ removeNode.AppendChild(allNode);
+ }
foreach (var data in ToRemove)
{
removeNode.AppendChild(data.ToXml(doc));
}
+ foreach (var key in KeyDataToRemove)
+ {
+ removeNode.AppendChild(key.ToXml(doc));
+ }
+
root.AppendChild(removeNode);
}
- if (ToAdd.Any())
+ if (ToAdd.Any() || KeyDataToAdd.Any())
{
- var addNode = doc.CreateElement("secDNS:add", "urn:ietf:params:xml:ns:secDNS-1.1");
+ var addNode = doc.CreateElement("secDNS:add", SecDNSKeyData.NamespaceUri);
foreach (var data in ToAdd)
{
addNode.AppendChild(data.ToXml(doc));
}
+ foreach (var key in KeyDataToAdd)
+ {
+ addNode.AppendChild(key.ToXml(doc));
+ }
+
root.AppendChild(addNode);
}
+ if (MaxSigLife.HasValue)
+ {
+ var chgNode = doc.CreateElement("secDNS:chg", SecDNSKeyData.NamespaceUri);
+ var maxSigLifeNode = doc.CreateElement("secDNS:maxSigLife", SecDNSKeyData.NamespaceUri);
+ maxSigLifeNode.InnerText = MaxSigLife.Value.ToString(CultureInfo.InvariantCulture);
+ chgNode.AppendChild(maxSigLifeNode);
+ root.AppendChild(chgNode);
+ }
+
return root;
}
}