Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions EppLib.UnitTests/EppLib.UnitTests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -301,8 +301,9 @@
<None Include="TestData\iis-1.2.xsd">
<SubType>Designer</SubType>
</None>
<None Include="TestData\secDNS-1.1.xsd">
<Content Include="TestData\secDNS-1.1.xsd">
<SubType>Designer</SubType>
</None>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>
270 changes: 270 additions & 0 deletions EppLib.UnitTests/SecDNSExtensionLocalTest.cs
Original file line number Diff line number Diff line change
@@ -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
{
Expand Down Expand Up @@ -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==" };
}

/// <summary>
/// RFC 5910 section 5.2.1, DS Data Interface with the optional key data (issue #31).
/// </summary>
[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"));
}

/// <summary>
/// RFC 5910 section 5.2.1, Key Data Interface (issue #31).
/// </summary>
[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();
}

/// <summary>
/// Key tags go up to 65535, and modern DS records use SHA-256 with algorithm 13.
/// </summary>
[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"));
}

/// <summary>
/// RFC 5910 section 5.2.5: urgent update that replaces all DNSSEC data and changes maxSigLife.
/// </summary>
[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"));
}

/// <summary>
/// RFC 5910 section 5.2.5: add and remove DNSKEY data.
/// </summary>
[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"));
}

/// <summary>
/// RFC 5910 section 5.2.5: change only maxSigLife.
/// </summary>
[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 =
"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>" +
"<epp xmlns=\"urn:ietf:params:xml:ns:epp-1.0\"><response><result code=\"1000\"><msg>Command completed successfully</msg></result>" +
"<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>" +
"{0}<trID><clTRID>ABC-12345</clTRID><svTRID>54322-XYZ</svTRID></trID></response></epp>";

/// <summary>
/// RFC 5910 section 5.1.2, info response with DS data and key data.
/// </summary>
[TestMethod]
[TestCategory("SecDNSExtension")]
public void SecDNSInfDataDsData()
{
var xml = string.Format(InfoResponseTemplate,
"<extension><secDNS:infData xmlns:secDNS=\"urn:ietf:params:xml:ns:secDNS-1.1\"><secDNS:maxSigLife>604800</secDNS:maxSigLife>" +
"<secDNS:dsData><secDNS:keyTag>12345</secDNS:keyTag><secDNS:alg>3</secDNS:alg><secDNS:digestType>1</secDNS:digestType><secDNS:digest>49FD46E6C4B45C55D4AC</secDNS:digest>" +
"<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>" +
"<secDNS:dsData><secDNS:keyTag>54321</secDNS:keyTag><secDNS:alg>13</secDNS:alg><secDNS:digestType>2</secDNS:digestType><secDNS:digest>E2D3C916F6DEEAC7</secDNS:digest></secDNS:dsData>" +
"</secDNS:infData></extension>");

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);
}

/// <summary>
/// RFC 5910 section 5.1.2, info response with key data only.
/// </summary>
[TestMethod]
[TestCategory("SecDNSExtension")]
public void SecDNSInfDataKeyData()
{
var xml = string.Format(InfoResponseTemplate,
"<extension><secDNS:infData xmlns:secDNS=\"urn:ietf:params:xml:ns:secDNS-1.1\">" +
"<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:infData></extension>");

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);
}
}
}
6 changes: 4 additions & 2 deletions EppLib/EppLib.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<TargetFrameworks>netstandard2.0;net10.0</TargetFrameworks>
<AssemblyName>EppLib</AssemblyName>
<PackageId>EppLib</PackageId>
<Version>1.6.0</Version>
<Version>1.7.0</Version>
<Authors>Code Maker Inc. and Contributors</Authors>
<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>
<PackageTags>epp;registrar;registry;domain;rfc5730</PackageTags>
Expand All @@ -13,7 +13,7 @@
<RepositoryUrl>https://github.com/CodeMakerInc/EppLib.NET.git</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageReadmeFile>README.md</PackageReadmeFile>
<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>
<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>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
Expand Down Expand Up @@ -174,6 +174,8 @@
<Compile Include="Extensions\Nominet\DomainUpdate\NominetDomainUpdateExtension.cs" />
<Compile Include="Extensions\SecDNS\SecDNSCreate.cs" />
<Compile Include="Extensions\SecDNS\SecDNSData.cs" />
<Compile Include="Extensions\SecDNS\SecDNSInfData.cs" />
<Compile Include="Extensions\SecDNS\SecDNSKeyData.cs" />
<Compile Include="Extensions\SecDNS\SecDNSUpdate.cs" />
<Compile Include="Extensions\Smallregistry\ContactCreate\SmallregistryContactCreate.cs" />
<Compile Include="Extensions\Smallregistry\ContactCreate\SmallregistryContactPMCreateExtension.cs" />
Expand Down
26 changes: 22 additions & 4 deletions EppLib/Extensions/SecDNS/SecDNSCreate.cs
Original file line number Diff line number Diff line change
@@ -1,29 +1,42 @@
using EppLib.Entities;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Xml;

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

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";
root.Attributes.Append(xsd);

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);
}

Expand All @@ -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;
}
}
Expand Down
Loading
Loading