Added 'DNSSEC' implementation

This commit is contained in:
2025-08-04 11:05:11 +02:00
parent 497fe5cbd8
commit 5697b8f921
10 changed files with 628 additions and 10 deletions

View File

@@ -63,7 +63,7 @@ default-deploy:
tags:
- docker
- lnx
- 64bit
- server
rules:
- if: $CI_COMMIT_TAG == null
script:
@@ -122,7 +122,7 @@ core-deploy:
tags:
- docker
- lnx
- 64bit
- server
rules:
- if: $CI_COMMIT_TAG =~ /^v[0-9.]+/
script:
@@ -182,7 +182,7 @@ extensions-deploy:
tags:
- docker
- lnx
- 64bit
- server
rules:
- if: $CI_COMMIT_TAG =~ /^[a-z]+\/v[0-9.]+/
script:

View File

@@ -0,0 +1,59 @@
using System.Threading;
using System.Threading.Tasks;
using AMWD.Net.Api.Cloudflare.Dns.Internals;
namespace AMWD.Net.Api.Cloudflare.Dns
{
/// <summary>
/// Extensions for <see href="https://developers.cloudflare.com/api/resources/dns/subresources/dnssec/">DNS DNSSEC records</see>.
/// </summary>
public static class DnsDnssecExtensions
{
/// <summary>
/// Delete DNSSEC.
/// </summary>
/// <param name="client">The <see cref="ICloudflareClient"/> instance.</param>
/// <param name="zoneId">The zone identifier.</param>
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
public static Task<CloudflareResponse<string>> DeleteDnssecRecords(this ICloudflareClient client, string zoneId, CancellationToken cancellationToken = default)
{
zoneId.ValidateCloudflareId();
return client.DeleteAsync<string>($"/zones/{zoneId}/dnssec", null, cancellationToken);
}
/// <summary>
/// Enable or disable DNSSEC.
/// </summary>
/// <param name="client">The <see cref="ICloudflareClient"/> instance.</param>
/// <param name="request">The request.</param>
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
public static Task<CloudflareResponse<DNSSEC>> EditDnssecStatus(this ICloudflareClient client, EditDnssecStatusRequest request, CancellationToken cancellationToken = default)
{
request.ZoneId.ValidateCloudflareId();
var req = new InternalEditDnssecStatusRequest
{
DnssecMultiSigner = request.DnssecMultiSigner,
DnssecPresigned = request.DnssecPresigned,
DnssecUseNsec3 = request.DnssecUseNsec3,
Status = request.Status
};
return client.PatchAsync<DNSSEC, InternalEditDnssecStatusRequest>($"/zones/{request.ZoneId}/dnssec", req, cancellationToken);
}
/// <summary>
/// Details about DNSSEC status and configuration.
/// </summary>
/// <param name="client">The <see cref="ICloudflareClient"/> instance.</param>
/// <param name="zoneId">The zone identifier.</param>
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
public static Task<CloudflareResponse<DNSSEC>> DnssecDetails(this ICloudflareClient client, string zoneId, CancellationToken cancellationToken = default)
{
zoneId.ValidateCloudflareId();
return client.GetAsync<DNSSEC>($"/zones/{zoneId}/dnssec", null, cancellationToken);
}
}
}

View File

@@ -0,0 +1,33 @@
namespace AMWD.Net.Api.Cloudflare.Dns.Internals
{
internal class InternalEditDnssecStatusRequest
{
[JsonProperty("dnssec_multi_signer")]
public bool? DnssecMultiSigner { get; set; }
/// <summary>
/// If <see langword="true"/>, allows Cloudflare to transfer in a DNSSEC-signed zone including signatures from an external provider, without requiring Cloudflare to sign any records on the fly.
/// </summary>
/// <remarks>
/// Note that this feature has some limitations. See <see href="https://developers.cloudflare.com/dns/zone-setups/zone-transfers/cloudflare-as-secondary/setup/#dnssec">Cloudflare as Secondary</see> for details.
/// </remarks>
[JsonProperty("dnssec_presigned")]
public bool? DnssecPresigned { get; set; }
/// <summary>
/// If <see langword="true"/>, enables the use of NSEC3 together with DNSSEC on the zone.
/// </summary>
/// <remarks>
/// Combined with setting <see cref="DnssecPresigned"/> to <see langword="true"/>, this enables the use of NSEC3 records when transferring in from an external provider.
/// If <see cref="DnssecPresigned"/> is instead set to <see langword="false"/> (default), NSEC3 records will be generated and signed at request time.
/// </remarks>
[JsonProperty("dnssec_use_nsec3")]
public bool? DnssecUseNsec3 { get; set; }
/// <summary>
/// Status of DNSSEC, based on user-desired state and presence of necessary records.
/// </summary>
[JsonProperty("status")]
public DnssecEditStatus? Status { get; set; }
}
}

View File

@@ -0,0 +1,148 @@
using System.Runtime.Serialization;
using Newtonsoft.Json.Converters;
namespace AMWD.Net.Api.Cloudflare.Dns
{
/// <summary>
/// Represents DNS Security Extensions (DNSSEC) information from Cloudflare.
/// </summary>
public class DNSSEC
{
/// <summary>
/// Algorithm key code.
/// </summary>
[JsonProperty("algorithm")]
public string? Algorithm { get; set; }
/// <summary>
/// Digest hash.
/// </summary>
[JsonProperty("digest")]
public string? Digest { get; set; }
/// <summary>
/// Type of digest algorithm.
/// </summary>
[JsonProperty("digest_algorithm")]
public string? DigestAlgorithm { get; set; }
/// <summary>
/// Coded type for digest algorithm.
/// </summary>
[JsonProperty("digest_type")]
public string? DigestType { get; set; }
/// <summary>
/// If <see langword="true"/>, multi-signer DNSSEC is enabled on the zone, allowing multiple providers to serve a DNSSEC-signed zone at the same time.
/// </summary>
/// <remarks>
/// This is required for DNSKEY records (except those automatically generated by Cloudflare) to be added to the zone.
/// <br/>
/// See <see href="https://developers.cloudflare.com/dns/dnssec/multi-signer-dnssec/">Multi-signer DNSSEC</see> for details.
/// </remarks>
[JsonProperty("dnssec_multi_signer")]
public bool? DnssecMultiSigner { get; set; }
/// <summary>
/// If <see langword="true"/>, allows Cloudflare to transfer in a DNSSEC-signed zone including signatures from an external provider, without requiring Cloudflare to sign any records on the fly.
/// </summary>
/// <remarks>
/// Note that this feature has some limitations.
/// See <see href="https://developers.cloudflare.com/dns/zone-setups/zone-transfers/cloudflare-as-secondary/setup/#dnssec">Cloudflare as Secondary</see> for details.
/// </remarks>
[JsonProperty("dnssec_presigned")]
public bool? DnssecPresigned { get; set; }
/// <summary>
/// If <see langword="true"/>, enables the use of NSEC3 together with DNSSEC on the zone.
/// </summary>
/// <remarks>
/// Combined with setting <see cref="DnssecPresigned"/> to <see langword="true"/>, this enables the use of NSEC3 records when transferring in from an external provider.
/// If <see cref="DnssecPresigned"/> is instead set to <see langword="false"/> (default), NSEC3 records will be generated and signed at request time.
/// <br/>
/// See <see href="https://developers.cloudflare.com/dns/dnssec/enable-nsec3/">DNSSEC with NSEC3</see> for details.
/// </remarks>
[JsonProperty("dnssec_use_nsec3")]
public bool? DnssecUseNsec3 { get; set; }
/// <summary>
/// Full DS record.
/// </summary>
[JsonProperty("ds")]
public string? Ds { get; set; }
/// <summary>
/// Flag for DNSSEC record.
/// </summary>
[JsonProperty("flags")]
public int? Flags { get; set; }
/// <summary>
/// Code for key tag.
/// </summary>
[JsonProperty("key_tag")]
public int? KeyTag { get; set; }
/// <summary>
/// Algorithm key type.
/// </summary>
[JsonProperty("key_type")]
public string? KeyType { get; set; }
/// <summary>
/// When DNSSEC was last modified.
/// </summary>
[JsonProperty("modified_on")]
public DateTime? ModifiedOn { get; set; }
/// <summary>
/// Public key for DS record.
/// </summary>
[JsonProperty("public_key")]
public string? PublicKey { get; set; }
/// <summary>
/// Status of DNSSEC, based on user-desired state and presence of necessary records.
/// </summary>
[JsonProperty("status")]
public DNSSECStatus? Status { get; set; }
}
/// <summary>
/// Status of DNSSEC, based on user-desired state and presence of necessary records.
/// <see href="https://github.com/cloudflare/cloudflare-typescript/blob/v4.4.1/src/resources/dns/dnssec.ts#L153">Source</see>
/// </summary>
[JsonConverter(typeof(StringEnumConverter))]
public enum DNSSECStatus
{
/// <summary>
/// Active.
/// </summary>
[EnumMember(Value = "active")]
Active = 1,
/// <summary>
/// Pending.
/// </summary>
[EnumMember(Value = "pending")]
Pending = 2,
/// <summary>
/// Disabled.
/// </summary>
[EnumMember(Value = "disabled")]
Disabled = 3,
/// <summary>
/// Pending disabled.
/// </summary>
[EnumMember(Value = "pending-disabled")]
PendingDisabled = 4,
/// <summary>
/// Error.
/// </summary>
[EnumMember(Value = "error")]
Error = 5
}
}

View File

@@ -13,6 +13,13 @@ This package contains the feature set of the _DNS_ section of the Cloudflare API
### [DNS]
### [DNSSEC]
- [Delete DNSSEC Records](https://developers.cloudflare.com/api/resources/dns/subresources/dnssec/methods/delete/)
- [Edit DNSSEC Status](https://developers.cloudflare.com/api/resources/dns/subresources/dnssec/methods/edit/)
- [DNSSEC Details](https://developers.cloudflare.com/api/resources/dns/subresources/dnssec/methods/get/)
#### [Records]
- [Batch DNS Records](https://developers.cloudflare.com/api/resources/dns/subresources/records/methods/batch/)
@@ -46,6 +53,7 @@ This package contains the feature set of the _DNS_ section of the Cloudflare API
- [Show DNS Settings](https://developers.cloudflare.com/api/resources/dns/subresources/settings/subresources/zone/methods/get/)
---
Published under MIT License (see [choose a license])
@@ -57,8 +65,8 @@ Published under MIT License (see [choose a license])
[Account Custom Nameservers]: https://developers.cloudflare.com/api/resources/custom_nameservers/
[DNS]: https://developers.cloudflare.com/api/resources/dns/
[Records]: https://developers.cloudflare.com/api/resources/dns/subresources/records/
[Settings]: https://developers.cloudflare.com/api/resources/dns/subresources/settings/
[Account]: https://developers.cloudflare.com/api/resources/dns/subresources/settings/subresources/account/
[Zone]: https://developers.cloudflare.com/api/resources/dns/subresources/settings/subresources/zone/
[DNSSEC]: https://developers.cloudflare.com/api/resources/dns/subresources/dnssec/
[Records]: https://developers.cloudflare.com/api/resources/dns/subresources/records/
[Settings]: https://developers.cloudflare.com/api/resources/dns/subresources/settings/
[Account]: https://developers.cloudflare.com/api/resources/dns/subresources/settings/subresources/account/
[Zone]: https://developers.cloudflare.com/api/resources/dns/subresources/settings/subresources/zone/

View File

@@ -0,0 +1,76 @@
using System.Runtime.Serialization;
using Newtonsoft.Json.Converters;
namespace AMWD.Net.Api.Cloudflare.Dns
{
/// <summary>
/// Represents a request to edit the DNSSEC (Domain Name System Security Extensions) status for a domain.
/// </summary>
public class EditDnssecStatusRequest
{
/// <summary>
/// Initializes a new instance of the <see cref="EditDnssecStatusRequest"/> class.
/// </summary>
/// <param name="zoneId">The zone identifier.</param>
public EditDnssecStatusRequest(string zoneId)
{
ZoneId = zoneId;
}
/// <summary>
/// The zone identifier.
/// </summary>
public string ZoneId { get; set; }
/// <summary>
/// If <see langword="true"/>, multi-signer DNSSEC is enabled on the zone, allowing multiple providers to serve a DNSSEC-signed zone at the same time.
/// </summary>
/// <remarks>
/// This is required for DNSKEY records (except those automatically generated by Cloudflare) to be added to the zone.
/// <br/>
/// See <see href="https://developers.cloudflare.com/dns/dnssec/multi-signer-dnssec/">Multi-signer DNSSEC</see> for details.
/// </remarks>
public bool? DnssecMultiSigner { get; set; }
/// <summary>
/// If <see langword="true"/>, allows Cloudflare to transfer in a DNSSEC-signed zone including signatures from an external provider, without requiring Cloudflare to sign any records on the fly.
/// </summary>
/// <remarks>
/// Note that this feature has some limitations. See <see href="https://developers.cloudflare.com/dns/zone-setups/zone-transfers/cloudflare-as-secondary/setup/#dnssec">Cloudflare as Secondary</see> for details.
/// </remarks>
public bool? DnssecPresigned { get; set; }
/// <summary>
/// If <see langword="true"/>, enables the use of NSEC3 together with DNSSEC on the zone.
/// </summary>
/// <remarks>
/// Combined with setting <see cref="DnssecPresigned"/> to <see langword="true"/>, this enables the use of NSEC3 records when transferring in from an external provider.
/// If <see cref="DnssecPresigned"/> is instead set to <see langword="false"/> (default), NSEC3 records will be generated and signed at request time.
/// </remarks>
public bool? DnssecUseNsec3 { get; set; }
/// <summary>
/// Status of DNSSEC, based on user-desired state and presence of necessary records.
/// </summary>
public DnssecEditStatus? Status { get; set; }
}
/// <summary>
/// Status of DNSSEC, based on user-desired state and presence of necessary records.
/// </summary>
[JsonConverter(typeof(StringEnumConverter))]
public enum DnssecEditStatus
{
/// <summary>
/// DNSSEC is enabled.
/// </summary>
[EnumMember(Value = "active")]
Active = 1,
/// <summary>
/// DNSSEC is disabled.
/// </summary>
[EnumMember(Value = "disabled")]
Disabled = 3,
}
}

View File

@@ -13,8 +13,8 @@
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="MSTest.TestAdapter" Version="3.9.3" />
<PackageReference Include="MSTest.TestFramework" Version="3.9.3" />
<PackageReference Include="MSTest.TestAdapter" Version="3.10.0" />
<PackageReference Include="MSTest.TestFramework" Version="3.10.0" />
</ItemGroup>
<ItemGroup>

View File

@@ -0,0 +1,82 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AMWD.Net.Api.Cloudflare;
using AMWD.Net.Api.Cloudflare.Dns;
using Moq;
namespace Cloudflare.Dns.Tests.DnsDnssecExtensions
{
[TestClass]
public class DeleteDnssecRecordsTest
{
private const string ZoneId = "023e105f4ecef8ad9ca31a8372d0c353";
private Mock<ICloudflareClient> _clientMock;
private CloudflareResponse<string> _response;
private List<(string RequestPath, IQueryParameterFilter? QueryFilter)> _callbacks;
[TestInitialize]
public void Initialize()
{
_callbacks = [];
_response = new CloudflareResponse<string>
{
Success = true,
Messages =
[
new ResponseInfo(1000, "Message 1")
],
Errors =
[
new ResponseInfo(1000, "Error 1")
],
Result = "023e105f4ecef8ad9ca31a8372d0c353"
};
}
[TestMethod]
public async Task ShouldDeleteDnssecRecords()
{
// Arrange
var client = GetClient();
// Act
var response = await client.DeleteDnssecRecords(ZoneId);
// Assert
Assert.IsNotNull(response);
Assert.IsTrue(response.Success);
Assert.AreEqual(_response.Result, response.Result);
Assert.AreEqual(1, _callbacks.Count);
var callback = _callbacks.First();
Assert.AreEqual($"/zones/{ZoneId}/dnssec", callback.RequestPath);
Assert.IsNull(callback.QueryFilter);
_clientMock.Verify(m => m.DeleteAsync<string>(
$"/zones/{ZoneId}/dnssec",
null,
It.IsAny<CancellationToken>()), Times.Once);
_clientMock.VerifyNoOtherCalls();
}
private ICloudflareClient GetClient()
{
_clientMock = new Mock<ICloudflareClient>();
_clientMock
.Setup(m => m.DeleteAsync<string>(
It.IsAny<string>(),
It.IsAny<IQueryParameterFilter?>(),
It.IsAny<CancellationToken>()))
.Callback<string, IQueryParameterFilter?, CancellationToken>((requestPath, queryFilter, _) => _callbacks.Add((requestPath, queryFilter)))
.ReturnsAsync(() => _response);
return _clientMock.Object;
}
}
}

View File

@@ -0,0 +1,98 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AMWD.Net.Api.Cloudflare;
using AMWD.Net.Api.Cloudflare.Dns;
using Moq;
namespace Cloudflare.Dns.Tests.DnsDnssecExtensions
{
[TestClass]
public class DnssecDetailsTest
{
private const string ZoneId = "023e105f4ecef8ad9ca31a8372d0c353";
private Mock<ICloudflareClient> _clientMock;
private CloudflareResponse<DNSSEC> _response;
private List<(string RequestPath, IQueryParameterFilter QueryFilter)> _callbacks;
[TestInitialize]
public void Initialize()
{
_callbacks = [];
_response = new CloudflareResponse<DNSSEC>
{
Success = true,
Messages =
[
new ResponseInfo(1000, "Message 1")
],
Errors =
[
new ResponseInfo(1000, "Error 1")
],
Result = new DNSSEC
{
Algorithm = "ECDSAP256SHA256",
Digest = "1234567890ABCDEF",
DigestAlgorithm = "SHA256",
DigestType = "2",
DnssecMultiSigner = true,
DnssecPresigned = false,
DnssecUseNsec3 = true,
Ds = "12345 13 2 1234567890ABCDEF",
Flags = 257,
KeyTag = 12345,
KeyType = "ECDSAP256SHA256",
ModifiedOn = DateTime.Parse("2025-08-02 10:20:30"),
PublicKey = "ABCDEF1234567890",
Status = DNSSECStatus.Active
}
};
}
[TestMethod]
public async Task ShouldGetDnssecDetails()
{
// Arrange
var client = GetClient();
// Act
var response = await client.DnssecDetails(ZoneId);
// Assert
Assert.IsNotNull(response);
Assert.IsTrue(response.Success);
Assert.AreEqual(_response.Result, response.Result);
Assert.AreEqual(1, _callbacks.Count);
var callback = _callbacks.First();
Assert.AreEqual($"/zones/{ZoneId}/dnssec", callback.RequestPath);
Assert.IsNull(callback.QueryFilter);
_clientMock.Verify(m => m.GetAsync<DNSSEC>(
$"/zones/{ZoneId}/dnssec",
null,
It.IsAny<CancellationToken>()), Times.Once);
_clientMock.VerifyNoOtherCalls();
}
private ICloudflareClient GetClient()
{
_clientMock = new Mock<ICloudflareClient>();
_clientMock
.Setup(m => m.GetAsync<DNSSEC>(
It.IsAny<string>(),
It.IsAny<IQueryParameterFilter>(),
It.IsAny<CancellationToken>()))
.Callback<string, IQueryParameterFilter, CancellationToken>((requestPath, queryFilter, _) => _callbacks.Add((requestPath, queryFilter)))
.ReturnsAsync(() => _response);
return _clientMock.Object;
}
}
}

View File

@@ -0,0 +1,114 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AMWD.Net.Api.Cloudflare;
using AMWD.Net.Api.Cloudflare.Dns;
using AMWD.Net.Api.Cloudflare.Dns.Internals;
using Moq;
namespace Cloudflare.Dns.Tests.DnsDnssecExtensions
{
[TestClass]
public class EditDnssecStatusTest
{
private const string ZoneId = "023e105f4ecef8ad9ca31a8372d0c353";
private Mock<ICloudflareClient> _clientMock;
private CloudflareResponse<DNSSEC> _response;
private List<(string RequestPath, InternalEditDnssecStatusRequest Request)> _callbacks;
private EditDnssecStatusRequest _request;
[TestInitialize]
public void Initialize()
{
_callbacks = [];
_response = new CloudflareResponse<DNSSEC>
{
Success = true,
Messages =
[
new ResponseInfo(1000, "Message 1")
],
Errors =
[
new ResponseInfo(1000, "Error 1")
],
Result = new DNSSEC
{
Algorithm = "ECDSAP256SHA256",
Digest = "1234567890ABCDEF",
DigestAlgorithm = "SHA256",
DigestType = "2",
DnssecMultiSigner = true,
DnssecPresigned = false,
DnssecUseNsec3 = true,
Ds = "12345 13 2 1234567890ABCDEF",
Flags = 257,
KeyTag = 12345,
KeyType = "ECDSAP256SHA256",
ModifiedOn = DateTime.UtcNow,
PublicKey = "ABCDEF1234567890",
Status = DNSSECStatus.Active
}
};
_request = new EditDnssecStatusRequest(ZoneId)
{
DnssecMultiSigner = true,
DnssecPresigned = false,
DnssecUseNsec3 = true,
Status = DnssecEditStatus.Active
};
}
[TestMethod]
public async Task ShouldEditDnssecStatus()
{
// Arrange
var client = GetClient();
// Act
var response = await client.EditDnssecStatus(_request);
// Assert
Assert.IsNotNull(response);
Assert.IsTrue(response.Success);
Assert.AreEqual(_response.Result, response.Result);
Assert.AreEqual(1, _callbacks.Count);
var callback = _callbacks.First();
Assert.AreEqual($"/zones/{ZoneId}/dnssec", callback.RequestPath);
Assert.IsNotNull(callback.Request);
Assert.AreEqual(_request.DnssecMultiSigner, callback.Request.DnssecMultiSigner);
Assert.AreEqual(_request.DnssecPresigned, callback.Request.DnssecPresigned);
Assert.AreEqual(_request.DnssecUseNsec3, callback.Request.DnssecUseNsec3);
Assert.AreEqual(_request.Status, callback.Request.Status);
_clientMock.Verify(m => m.PatchAsync<DNSSEC, InternalEditDnssecStatusRequest>(
$"/zones/{ZoneId}/dnssec",
It.IsAny<InternalEditDnssecStatusRequest>(),
It.IsAny<CancellationToken>()), Times.Once);
_clientMock.VerifyNoOtherCalls();
}
private ICloudflareClient GetClient()
{
_clientMock = new Mock<ICloudflareClient>();
_clientMock
.Setup(m => m.PatchAsync<DNSSEC, InternalEditDnssecStatusRequest>(
It.IsAny<string>(),
It.IsAny<InternalEditDnssecStatusRequest>(),
It.IsAny<CancellationToken>()))
.Callback<string, InternalEditDnssecStatusRequest, CancellationToken>((requestPath, request, _) => _callbacks.Add((requestPath, request)))
.ReturnsAsync(() => _response);
return _clientMock.Object;
}
}
}