Skip to content

Commit 3ccbef4

Browse files
authored
Merge pull request #61 from CodeMakerInc/fix/secdns-rfc5910
Release 1.7.0: complete the secDNS (DNSSEC) extension per RFC 5910
2 parents 0e9af46 + ab13ad8 commit 3ccbef4

8 files changed

Lines changed: 579 additions & 24 deletions

File tree

‎EppLib.UnitTests/EppLib.UnitTests.csproj‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -301,8 +301,9 @@
301301
<None Include="TestData\iis-1.2.xsd">
302302
<SubType>Designer</SubType>
303303
</None>
304-
<None Include="TestData\secDNS-1.1.xsd">
304+
<Content Include="TestData\secDNS-1.1.xsd">
305305
<SubType>Designer</SubType>
306-
</None>
306+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
307+
</Content>
307308
</ItemGroup>
308309
</Project>

‎EppLib.UnitTests/SecDNSExtensionLocalTest.cs‎

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
using EppLib.Entities;
22
using EppLib.Extensions.SecDNS;
33
using Microsoft.VisualStudio.TestTools.UnitTesting;
4+
using System;
45
using System.Diagnostics;
56
using System.IO;
7+
using System.Xml;
68

79
namespace EppLib.Tests
810
{
@@ -83,5 +85,273 @@ public void SecDNSUpdateExtension()
8385

8486
Assert.AreEqual(expected, xml);
8587
}
88+
89+
private const string SecDnsNamespace = "urn:ietf:params:xml:ns:secDNS-1.1";
90+
91+
// Returns the secDNS extension element of a command, after validating it against secDNS-1.1.xsd.
92+
private static XmlElement SecDnsElement(XmlDocument commandXml)
93+
{
94+
var namespaces = new XmlNamespaceManager(commandXml.NameTable);
95+
namespaces.AddNamespace("secDNS", SecDnsNamespace);
96+
var element = (XmlElement)commandXml.SelectSingleNode("//secDNS:*[parent::*[local-name()='extension']]", namespaces);
97+
Assert.IsNotNull(element, "no secDNS extension element");
98+
99+
var doc = new XmlDocument();
100+
doc.AppendChild(doc.ImportNode(element, true));
101+
var schemaPath = Path.Combine(Path.GetDirectoryName(typeof(SecDNSExtensionLocalTest).Assembly.Location), "TestData", "secDNS-1.1.xsd");
102+
doc.Schemas.Add(SecDnsNamespace, schemaPath);
103+
doc.Validate((sender, e) => Assert.Fail("secDNS schema validation: " + e.Message));
104+
105+
return doc.DocumentElement;
106+
}
107+
108+
private static string Text(XmlNode node, string xpath)
109+
{
110+
var namespaces = new XmlNamespaceManager(node.OwnerDocument.NameTable);
111+
namespaces.AddNamespace("secDNS", SecDnsNamespace);
112+
var match = node.SelectSingleNode(xpath, namespaces);
113+
return match?.InnerText;
114+
}
115+
116+
private static int Count(XmlNode node, string xpath)
117+
{
118+
var namespaces = new XmlNamespaceManager(node.OwnerDocument.NameTable);
119+
namespaces.AddNamespace("secDNS", SecDnsNamespace);
120+
return node.SelectNodes(xpath, namespaces).Count;
121+
}
122+
123+
private static SecDNSKeyData RfcKey()
124+
{
125+
return new SecDNSKeyData { Flags = 257, Protocol = 3, Algorithm = SecDNSAlgorithm.RSAMD5, PublicKey = "AQPJ////4Q==" };
126+
}
127+
128+
/// <summary>
129+
/// RFC 5910 section 5.2.1, DS Data Interface with the optional key data (issue #31).
130+
/// </summary>
131+
[TestMethod]
132+
[TestCategory("SecDNSExtension")]
133+
public void SecDNSCreateDsDataWithKeyData()
134+
{
135+
var command = new DomainCreate("example.com", "jd1234");
136+
var extension = new SecDNSCreate { MaxSigLife = 604800 };
137+
extension.DsData.Add(new SecDNSData
138+
{
139+
KeyTag = 12345,
140+
Algorithm = SecDNSAlgorithm.DSA,
141+
Digest = "49FD46E6C4B45C55D4AC",
142+
KeyData = RfcKey()
143+
});
144+
command.Extensions.Add(extension);
145+
146+
var secDns = SecDnsElement(command.ToXml());
147+
148+
Assert.AreEqual("604800", Text(secDns, "secDNS:maxSigLife"));
149+
Assert.AreEqual("12345", Text(secDns, "secDNS:dsData/secDNS:keyTag"));
150+
Assert.AreEqual("3", Text(secDns, "secDNS:dsData/secDNS:alg"));
151+
Assert.AreEqual("1", Text(secDns, "secDNS:dsData/secDNS:digestType"));
152+
Assert.AreEqual("257", Text(secDns, "secDNS:dsData/secDNS:keyData/secDNS:flags"));
153+
Assert.AreEqual("3", Text(secDns, "secDNS:dsData/secDNS:keyData/secDNS:protocol"));
154+
Assert.AreEqual("1", Text(secDns, "secDNS:dsData/secDNS:keyData/secDNS:alg"));
155+
Assert.AreEqual("AQPJ////4Q==", Text(secDns, "secDNS:dsData/secDNS:keyData/secDNS:pubKey"));
156+
}
157+
158+
/// <summary>
159+
/// RFC 5910 section 5.2.1, Key Data Interface (issue #31).
160+
/// </summary>
161+
[TestMethod]
162+
[TestCategory("SecDNSExtension")]
163+
public void SecDNSCreateKeyData()
164+
{
165+
var command = new DomainCreate("example.com", "jd1234");
166+
var extension = new SecDNSCreate();
167+
extension.KeyData.Add(RfcKey());
168+
command.Extensions.Add(extension);
169+
170+
var secDns = SecDnsElement(command.ToXml());
171+
172+
Assert.AreEqual(0, Count(secDns, "secDNS:dsData"));
173+
Assert.AreEqual(1, Count(secDns, "secDNS:keyData"));
174+
Assert.AreEqual("AQPJ////4Q==", Text(secDns, "secDNS:keyData/secDNS:pubKey"));
175+
}
176+
177+
[TestMethod]
178+
[TestCategory("SecDNSExtension")]
179+
[ExpectedException(typeof(InvalidOperationException))]
180+
public void SecDNSCreateRejectsDsDataAndKeyDataTogether()
181+
{
182+
var command = new DomainCreate("example.com", "jd1234");
183+
var extension = new SecDNSCreate();
184+
extension.DsData.Add(new SecDNSData { KeyTag = 1, Algorithm = SecDNSAlgorithm.RSASHA256, Digest = "AB" });
185+
extension.KeyData.Add(RfcKey());
186+
command.Extensions.Add(extension);
187+
188+
command.ToXml();
189+
}
190+
191+
/// <summary>
192+
/// Key tags go up to 65535, and modern DS records use SHA-256 with algorithm 13.
193+
/// </summary>
194+
[TestMethod]
195+
[TestCategory("SecDNSExtension")]
196+
public void SecDNSCreateHighKeyTagAndSha256()
197+
{
198+
var command = new DomainCreate("example.com", "jd1234");
199+
var extension = new SecDNSCreate();
200+
extension.DsData.Add(new SecDNSData
201+
{
202+
KeyTag = 65535,
203+
Algorithm = SecDNSAlgorithm.ECDSAP256SHA256,
204+
DigestType = SecDNSDigestType.SHA256,
205+
Digest = "E2D3C916F6DEEAC73294E8268FB5885044A833FC5459588F4A9184CFC41A5766"
206+
});
207+
command.Extensions.Add(extension);
208+
209+
var secDns = SecDnsElement(command.ToXml());
210+
211+
Assert.AreEqual("65535", Text(secDns, "secDNS:dsData/secDNS:keyTag"));
212+
Assert.AreEqual("13", Text(secDns, "secDNS:dsData/secDNS:alg"));
213+
Assert.AreEqual("2", Text(secDns, "secDNS:dsData/secDNS:digestType"));
214+
}
215+
216+
/// <summary>
217+
/// RFC 5910 section 5.2.5: urgent update that replaces all DNSSEC data and changes maxSigLife.
218+
/// </summary>
219+
[TestMethod]
220+
[TestCategory("SecDNSExtension")]
221+
public void SecDNSUpdateRemoveAllAddChangeUrgent()
222+
{
223+
var command = new DomainUpdate("example.com");
224+
var extension = new SecDNSUpdate { RemoveAll = true, MaxSigLife = 605900, Urgent = true };
225+
extension.ToAdd.Add(new SecDNSData { KeyTag = 12346, Algorithm = SecDNSAlgorithm.DSA, Digest = "38EC35D5B3A34B44C39B" });
226+
command.Extensions.Add(extension);
227+
228+
var secDns = SecDnsElement(command.ToXml());
229+
230+
Assert.AreEqual("true", secDns.GetAttribute("urgent"));
231+
Assert.AreEqual("true", Text(secDns, "secDNS:rem/secDNS:all"));
232+
Assert.AreEqual("12346", Text(secDns, "secDNS:add/secDNS:dsData/secDNS:keyTag"));
233+
Assert.AreEqual("605900", Text(secDns, "secDNS:chg/secDNS:maxSigLife"));
234+
}
235+
236+
/// <summary>
237+
/// RFC 5910 section 5.2.5: add and remove DNSKEY data.
238+
/// </summary>
239+
[TestMethod]
240+
[TestCategory("SecDNSExtension")]
241+
public void SecDNSUpdateKeyData()
242+
{
243+
var command = new DomainUpdate("example.com");
244+
var extension = new SecDNSUpdate();
245+
extension.KeyDataToRemove.Add(RfcKey());
246+
extension.KeyDataToAdd.Add(new SecDNSKeyData { Flags = 257, Algorithm = SecDNSAlgorithm.ED25519, PublicKey = "l02Woi0iS8Aa25FQkUd9RMzZHJpBoRQwAQEX1SxZJA4=" });
247+
command.Extensions.Add(extension);
248+
249+
// Check the raw command: schema validation fills in the urgent="false" default.
250+
Assert.IsFalse(command.ToXml().OuterXml.Contains("urgent"));
251+
252+
var secDns = SecDnsElement(command.ToXml());
253+
254+
Assert.AreEqual("AQPJ////4Q==", Text(secDns, "secDNS:rem/secDNS:keyData/secDNS:pubKey"));
255+
Assert.AreEqual("15", Text(secDns, "secDNS:add/secDNS:keyData/secDNS:alg"));
256+
Assert.AreEqual(0, Count(secDns, "secDNS:chg"));
257+
}
258+
259+
/// <summary>
260+
/// RFC 5910 section 5.2.5: change only maxSigLife.
261+
/// </summary>
262+
[TestMethod]
263+
[TestCategory("SecDNSExtension")]
264+
public void SecDNSUpdateChangeMaxSigLifeOnly()
265+
{
266+
var command = new DomainUpdate("example.com");
267+
command.Extensions.Add(new SecDNSUpdate { MaxSigLife = 605900 });
268+
269+
var secDns = SecDnsElement(command.ToXml());
270+
271+
Assert.AreEqual(0, Count(secDns, "secDNS:rem"));
272+
Assert.AreEqual(0, Count(secDns, "secDNS:add"));
273+
Assert.AreEqual("605900", Text(secDns, "secDNS:chg/secDNS:maxSigLife"));
274+
}
275+
276+
[TestMethod]
277+
[TestCategory("SecDNSExtension")]
278+
[ExpectedException(typeof(InvalidOperationException))]
279+
public void SecDNSUpdateRejectsRemoveAllWithSpecificRemovals()
280+
{
281+
var command = new DomainUpdate("example.com");
282+
var extension = new SecDNSUpdate { RemoveAll = true };
283+
extension.ToRemove.Add(new SecDNSData { KeyTag = 1, Algorithm = SecDNSAlgorithm.RSASHA256, Digest = "AB" });
284+
command.Extensions.Add(extension);
285+
286+
command.ToXml();
287+
}
288+
289+
private const string InfoResponseTemplate =
290+
"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>" +
291+
"<epp xmlns=\"urn:ietf:params:xml:ns:epp-1.0\"><response><result code=\"1000\"><msg>Command completed successfully</msg></result>" +
292+
"<resData><domain:infData xmlns:domain=\"urn:ietf:params:xml:ns:domain-1.0\"><domain:name>example.com</domain:name><domain:roid>EXAMPLE1-REP</domain:roid></domain:infData></resData>" +
293+
"{0}<trID><clTRID>ABC-12345</clTRID><svTRID>54322-XYZ</svTRID></trID></response></epp>";
294+
295+
/// <summary>
296+
/// RFC 5910 section 5.1.2, info response with DS data and key data.
297+
/// </summary>
298+
[TestMethod]
299+
[TestCategory("SecDNSExtension")]
300+
public void SecDNSInfDataDsData()
301+
{
302+
var xml = string.Format(InfoResponseTemplate,
303+
"<extension><secDNS:infData xmlns:secDNS=\"urn:ietf:params:xml:ns:secDNS-1.1\"><secDNS:maxSigLife>604800</secDNS:maxSigLife>" +
304+
"<secDNS:dsData><secDNS:keyTag>12345</secDNS:keyTag><secDNS:alg>3</secDNS:alg><secDNS:digestType>1</secDNS:digestType><secDNS:digest>49FD46E6C4B45C55D4AC</secDNS:digest>" +
305+
"<secDNS:keyData><secDNS:flags>257</secDNS:flags><secDNS:protocol>3</secDNS:protocol><secDNS:alg>1</secDNS:alg><secDNS:pubKey>AQPJ////4Q==</secDNS:pubKey></secDNS:keyData></secDNS:dsData>" +
306+
"<secDNS:dsData><secDNS:keyTag>54321</secDNS:keyTag><secDNS:alg>13</secDNS:alg><secDNS:digestType>2</secDNS:digestType><secDNS:digest>E2D3C916F6DEEAC7</secDNS:digest></secDNS:dsData>" +
307+
"</secDNS:infData></extension>");
308+
309+
var info = SecDNSInfData.FromResponse(new DomainInfoResponse(xml));
310+
311+
Assert.IsNotNull(info);
312+
Assert.AreEqual(604800, info.MaxSigLife);
313+
Assert.AreEqual(2, info.DsData.Count);
314+
Assert.AreEqual(0, info.KeyData.Count);
315+
Assert.AreEqual(12345, info.DsData[0].KeyTag);
316+
Assert.AreEqual(SecDNSAlgorithm.DSA, info.DsData[0].Algorithm);
317+
Assert.AreEqual(SecDNSDigestType.SHA1, info.DsData[0].DigestType);
318+
Assert.AreEqual("49FD46E6C4B45C55D4AC", info.DsData[0].Digest);
319+
Assert.AreEqual(257, info.DsData[0].KeyData.Flags);
320+
Assert.AreEqual("AQPJ////4Q==", info.DsData[0].KeyData.PublicKey);
321+
Assert.AreEqual(54321, info.DsData[1].KeyTag);
322+
Assert.AreEqual(SecDNSAlgorithm.ECDSAP256SHA256, info.DsData[1].Algorithm);
323+
Assert.AreEqual(SecDNSDigestType.SHA256, info.DsData[1].DigestType);
324+
Assert.IsNull(info.DsData[1].KeyData);
325+
}
326+
327+
/// <summary>
328+
/// RFC 5910 section 5.1.2, info response with key data only.
329+
/// </summary>
330+
[TestMethod]
331+
[TestCategory("SecDNSExtension")]
332+
public void SecDNSInfDataKeyData()
333+
{
334+
var xml = string.Format(InfoResponseTemplate,
335+
"<extension><secDNS:infData xmlns:secDNS=\"urn:ietf:params:xml:ns:secDNS-1.1\">" +
336+
"<secDNS:keyData><secDNS:flags>257</secDNS:flags><secDNS:protocol>3</secDNS:protocol><secDNS:alg>1</secDNS:alg><secDNS:pubKey>AQPJ////4Q==</secDNS:pubKey></secDNS:keyData>" +
337+
"</secDNS:infData></extension>");
338+
339+
var info = SecDNSInfData.FromResponse(new DomainInfoResponse(xml));
340+
341+
Assert.IsNull(info.MaxSigLife);
342+
Assert.AreEqual(0, info.DsData.Count);
343+
Assert.AreEqual(1, info.KeyData.Count);
344+
Assert.AreEqual(3, info.KeyData[0].Protocol);
345+
Assert.AreEqual(SecDNSAlgorithm.RSAMD5, info.KeyData[0].Algorithm);
346+
}
347+
348+
[TestMethod]
349+
[TestCategory("SecDNSExtension")]
350+
public void SecDNSInfDataAbsent()
351+
{
352+
var info = SecDNSInfData.FromResponse(new DomainInfoResponse(string.Format(InfoResponseTemplate, "")));
353+
354+
Assert.IsNull(info);
355+
}
86356
}
87357
}

‎EppLib/EppLib.csproj‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<TargetFrameworks>netstandard2.0;net10.0</TargetFrameworks>
55
<AssemblyName>EppLib</AssemblyName>
66
<PackageId>EppLib</PackageId>
7-
<Version>1.6.0</Version>
7+
<Version>1.7.0</Version>
88
<Authors>Code Maker Inc. and Contributors</Authors>
99
<Description>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.</Description>
1010
<PackageTags>epp;registrar;registry;domain;rfc5730</PackageTags>
@@ -13,7 +13,7 @@
1313
<RepositoryUrl>https://github.com/CodeMakerInc/EppLib.NET.git</RepositoryUrl>
1414
<RepositoryType>git</RepositoryType>
1515
<PackageReadmeFile>README.md</PackageReadmeFile>
16-
<PackageReleaseNotes>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.</PackageReleaseNotes>
16+
<PackageReleaseNotes>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.</PackageReleaseNotes>
1717
<GenerateDocumentationFile>true</GenerateDocumentationFile>
1818
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
1919
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
@@ -174,6 +174,8 @@
174174
<Compile Include="Extensions\Nominet\DomainUpdate\NominetDomainUpdateExtension.cs" />
175175
<Compile Include="Extensions\SecDNS\SecDNSCreate.cs" />
176176
<Compile Include="Extensions\SecDNS\SecDNSData.cs" />
177+
<Compile Include="Extensions\SecDNS\SecDNSInfData.cs" />
178+
<Compile Include="Extensions\SecDNS\SecDNSKeyData.cs" />
177179
<Compile Include="Extensions\SecDNS\SecDNSUpdate.cs" />
178180
<Compile Include="Extensions\Smallregistry\ContactCreate\SmallregistryContactCreate.cs" />
179181
<Compile Include="Extensions\Smallregistry\ContactCreate\SmallregistryContactPMCreateExtension.cs" />

‎EppLib/Extensions/SecDNS/SecDNSCreate.cs‎

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,42 @@
11
using EppLib.Entities;
2+
using System;
23
using System.Collections.Generic;
4+
using System.Globalization;
5+
using System.Linq;
36
using System.Xml;
47

58
namespace EppLib.Extensions.SecDNS
69
{
10+
/// <summary>
11+
/// The secDNS create extension (RFC 5910 section 5.2.1). Add either DS records to <see cref="DsData"/>
12+
/// or DNSKEY records to <see cref="KeyData"/>, not both; which one depends on the registry.
13+
/// </summary>
714
public class SecDNSCreate : EppExtension
815
{
916
public int? MaxSigLife { get; set; }
1017
public IList<SecDNSData> DsData { get; } = new List<SecDNSData>();
18+
public IList<SecDNSKeyData> KeyData { get; } = new List<SecDNSKeyData>();
1119

1220
protected override string Namespace { get; set; }
1321

1422
public override XmlNode ToXml(XmlDocument doc)
1523
{
16-
var root = doc.CreateElement("secDNS:create", "urn:ietf:params:xml:ns:secDNS-1.1");
17-
root.SetAttribute("xmlns:secDNS", "urn:ietf:params:xml:ns:secDNS-1.1");
24+
if (DsData.Any() && KeyData.Any())
25+
{
26+
throw new InvalidOperationException("secDNS create takes either DsData or KeyData, not both (RFC 5910 section 4).");
27+
}
28+
29+
var root = doc.CreateElement("secDNS:create", SecDNSKeyData.NamespaceUri);
30+
root.SetAttribute("xmlns:secDNS", SecDNSKeyData.NamespaceUri);
1831

1932
var xsd = doc.CreateAttribute("xsi", "schemaLocation", "http://www.w3.org/2001/XMLSchema-instance");
2033
xsd.Value = "urn:ietf:params:xml:ns:secDNS-1.1 secDNS-1.1.xsd";
2134
root.Attributes.Append(xsd);
2235

2336
if (MaxSigLife.HasValue)
2437
{
25-
var maxSigLifeNode = doc.CreateElement("secDNS:maxSigLife", "urn:ietf:params:xml:ns:secDNS-1.1");
26-
maxSigLifeNode.InnerText = MaxSigLife.Value.ToString();
38+
var maxSigLifeNode = doc.CreateElement("secDNS:maxSigLife", SecDNSKeyData.NamespaceUri);
39+
maxSigLifeNode.InnerText = MaxSigLife.Value.ToString(CultureInfo.InvariantCulture);
2740
root.AppendChild(maxSigLifeNode);
2841
}
2942

@@ -32,6 +45,11 @@ public override XmlNode ToXml(XmlDocument doc)
3245
root.AppendChild(data.ToXml(doc));
3346
}
3447

48+
foreach (var key in KeyData)
49+
{
50+
root.AppendChild(key.ToXml(doc));
51+
}
52+
3553
return root;
3654
}
3755
}

0 commit comments

Comments
 (0)