|
1 | 1 | using EppLib.Entities; |
2 | 2 | using EppLib.Extensions.SecDNS; |
3 | 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; |
| 4 | +using System; |
4 | 5 | using System.Diagnostics; |
5 | 6 | using System.IO; |
| 7 | +using System.Xml; |
6 | 8 |
|
7 | 9 | namespace EppLib.Tests |
8 | 10 | { |
@@ -83,5 +85,273 @@ public void SecDNSUpdateExtension() |
83 | 85 |
|
84 | 86 | Assert.AreEqual(expected, xml); |
85 | 87 | } |
| 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 | + } |
86 | 356 | } |
87 | 357 | } |
0 commit comments