Skip to content

Commit b9e9050

Browse files
authored
Add CERT resource record support (#203)
CERT resource record support (rfc4398)
1 parent f4dab14 commit b9e9050

File tree

9 files changed

+275
-1
lines changed

9 files changed

+275
-1
lines changed

src/DnsClient/DnsRecordFactory.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,10 @@ public DnsResourceRecord GetRecord(ResourceRecordInfo info)
135135
result = ResolveNaptrRecord(info);
136136
break;
137137

138+
case ResourceRecordType.CERT: // 37
139+
result = ResolveCertRecord(info);
140+
break;
141+
138142
case ResourceRecordType.OPT: // 41
139143
result = ResolveOptRecord(info);
140144
break;
@@ -265,6 +269,17 @@ private DnsResourceRecord ResolveNaptrRecord(ResourceRecordInfo info)
265269
return new NAPtrRecord(info, order, preference, flags, services, regexp, replacement);
266270
}
267271

272+
private DnsResourceRecord ResolveCertRecord(ResourceRecordInfo info)
273+
{
274+
var startIndex = _reader.Index;
275+
var certType = _reader.ReadUInt16NetworkOrder();
276+
var keyTag = _reader.ReadUInt16NetworkOrder();
277+
var algorithm = _reader.ReadByte();
278+
var publicKey = _reader.ReadBytesToEnd(startIndex, info.RawDataLength).ToArray();
279+
280+
return new CertRecord(info, certType, keyTag, algorithm, publicKey);
281+
}
282+
268283
private DnsResourceRecord ResolveOptRecord(ResourceRecordInfo info)
269284
{
270285
// Consume bytes in case the OPT record has any.
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Text;
4+
5+
namespace DnsClient.Protocol;
6+
7+
8+
/// <summary>A representation of CERT RDATA format.
9+
/// <remarks>
10+
/// RFC 4398.
11+
///
12+
/// Record format:
13+
/// <code>
14+
/// 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 3 3
15+
/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
16+
/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
17+
/// | type | key tag |
18+
/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
19+
/// | algorithm | /
20+
/// +---------------+ certificate or CRL /
21+
/// / /
22+
/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-|
23+
/// </code>
24+
/// </remarks>
25+
/// <see cref="https://datatracker.ietf.org/doc/html/rfc4398#section-2"/>
26+
/// </summary>
27+
public class CertRecord : DnsResourceRecord
28+
{
29+
/// <summary>
30+
/// Gets the <see cref="CertType"/> referred to by this record
31+
/// </summary>
32+
/// <value>The CERT RR type of this certificate</value>
33+
public CertificateType CertType { get; }
34+
35+
/// <summary>
36+
/// Gets the key tag value of the <see cref="DnsKeyRecord"/> referred to by this record.
37+
/// </summary>
38+
/// <seealso href="https://tools.ietf.org/html/rfc4034#appendix-B">Key Tag Calculation</seealso>
39+
public int KeyTag { get; }
40+
41+
/// <summary>
42+
/// Gets certificate algorithm (see RFC 4034, Appendix 1)
43+
/// </summary>
44+
public DnsSecurityAlgorithm Algorithm { get; }
45+
46+
/// <summary>
47+
/// Gets the raw certificate RDATA.
48+
/// </summary>
49+
/// <summary>
50+
/// Gets the public key material.
51+
/// The format depends on the <see cref="Algorithm"/> of the key being stored.
52+
/// </summary>
53+
public IReadOnlyList<byte> PublicKey { get; }
54+
55+
/// <summary>
56+
/// Gets the base64 string representation of the <see cref="PublicKey"/>.
57+
/// </summary>
58+
public string PublicKeyAsString { get; }
59+
60+
/// <summary>
61+
/// Initializes a new instance of the <see cref="DnsKeyRecord"/> class
62+
/// </summary>
63+
/// <param name="info"></param>
64+
/// <param name="flags"></param>
65+
/// <param name="protocol"></param>
66+
/// <param name="algorithm"></param>
67+
/// <param name="publicKey"></param>
68+
/// <exception cref="ArgumentNullException">If <paramref name="info"/> or <paramref name="publicKey"/> is null.</exception>
69+
public CertRecord(ResourceRecordInfo info, int certType, int keyTag, byte algorithm, byte[] publicKey)
70+
: base(info)
71+
{
72+
CertType = (CertificateType)certType;
73+
KeyTag = keyTag;
74+
Algorithm = (DnsSecurityAlgorithm)algorithm;
75+
PublicKey = publicKey ?? throw new ArgumentNullException(nameof(publicKey));
76+
PublicKeyAsString = Convert.ToBase64String(publicKey);
77+
}
78+
79+
80+
private protected override string RecordToString()
81+
{
82+
return string.Format("{0} {1} {2} {3}", CertType, KeyTag, Algorithm, PublicKeyAsString);
83+
}
84+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
namespace DnsClient.Protocol;
2+
3+
/// <summary>
4+
/// Certificate type values
5+
/// </summary>
6+
/// <remarks>
7+
/// <seealso href="https://tools.ietf.org/html/rfc4398#section-2.1">RFC 4398 section 2.1</seealso>
8+
/// </remarks>
9+
public enum CertificateType
10+
{
11+
/// <summary>
12+
/// Reserved certificate type.
13+
/// </summary>
14+
Reserved = 0,
15+
/// <summary>
16+
/// X.509 as per PKIX
17+
/// </summary>
18+
PKIX = 1,
19+
/// <summary>
20+
/// SPKI certificate
21+
/// </summary>
22+
SPKI,
23+
/// <summary>
24+
/// OpenPGP packet
25+
/// </summary>
26+
PGP,
27+
/// <summary>
28+
/// URL to an X.509 data object
29+
/// </summary>
30+
IPKIX,
31+
/// <summary>
32+
/// Url of an SPKI certificate
33+
/// </summary>
34+
ISPKI,
35+
/// <summary>
36+
/// fingerprint + URL of an OpenPGP packet
37+
/// </summary>
38+
IPGP,
39+
/// <summary>
40+
/// Attribute Certificate
41+
/// </summary>
42+
ACPKIX,
43+
/// <summary>
44+
/// The URL of an Attribute Certificate
45+
/// </summary>
46+
IACPKIK,
47+
/// <summary>
48+
/// URI private
49+
/// </summary>
50+
URI = 253,
51+
/// <summary>
52+
/// OID private
53+
/// </summary>
54+
OID = 254
55+
}

src/DnsClient/Protocol/ResourceRecordType.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,16 @@ public enum ResourceRecordType
168168
/// <seealso cref="NAPtrRecord"/>
169169
NAPTR = 35,
170170

171+
/// <summary>
172+
/// Cryptographic public keys are frequently published, and their
173+
/// authenticity is demonstrated by certificates. A CERT resource record
174+
/// (RR) is defined so that such certificates and related certificate
175+
/// revocation lists can be stored in the Domain Name System (DNS).
176+
/// </summary>
177+
/// <seealso href="https://tools.ietf.org/html/rfc4398">RFC 4398</seealso>
178+
/// <seealso cref="CertRecord"/>
179+
CERT = 37,
180+
171181
/// <summary>
172182
/// Option record.
173183
/// </summary>

src/DnsClient/QueryType.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,16 @@ public enum QueryType
165165
/// <seealso cref="NAPtrRecord"/>
166166
NAPTR = ResourceRecordType.NAPTR,
167167

168+
/// <summary>
169+
/// Cryptographic public keys are frequently published, and their
170+
/// authenticity is demonstrated by certificates. A CERT resource record
171+
/// (RR) is defined so that such certificates and related certificate
172+
/// revocation lists can be stored in the Domain Name System (DNS).
173+
/// </summary>
174+
/// <seealso href="https://tools.ietf.org/html/rfc4398">RFC 4398</seealso>
175+
/// <seealso cref="CertRecord"/>
176+
CERT = ResourceRecordType.CERT,
177+
168178
/// <summary>
169179
/// DS rfc4034
170180
/// </summary>

src/DnsClient/ResourceRecordCollectionExtensions.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,17 @@ public static IEnumerable<NAPtrRecord> NAPtrRecords(this IEnumerable<DnsResource
205205
return records.OfType<NAPtrRecord>();
206206
}
207207

208+
/// <summary>
209+
/// Filters the elements of an <see cref="IEnumerable{T}"/> to return <see cref="CertRecord"/>s only.
210+
/// </summary>
211+
/// <param name="records">The records.</param>
212+
/// <returns>The list of <see cref="CertRecord"/>.</returns>
213+
public static IEnumerable<CertRecord> CertRecords(this IEnumerable<DnsResourceRecord> records)
214+
{
215+
return records.OfType<CertRecord>();
216+
}
217+
218+
208219
/// <summary>
209220
/// Filters the elements of an <see cref="IEnumerable{T}"/> to return <see cref="UriRecord"/>s only.
210221
/// </summary>

test/DnsClient.Tests/DnsRecordFactoryTest.cs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@
22
using System.Collections.Generic;
33
using System.Linq;
44
using System.Net;
5+
using System.Security.Cryptography;
6+
using System.Security.Cryptography.X509Certificates;
57
using System.Text;
68
using DnsClient.Internal;
79
using DnsClient.Protocol;
810
using Xunit;
11+
using Xunit.Abstractions;
912

1013
namespace DnsClient.Tests
1114
{
@@ -301,6 +304,66 @@ public void DnsRecordFactory_NAPTRRecord()
301304
Assert.Equal("", result.RegularExpression);
302305
}
303306

307+
[Fact]
308+
public void DnsRecordFactory_CertRecord()
309+
{
310+
var expectedPublicKey = @"-----BEGIN CERTIFICATE-----
311+
MIIEMzCCAxugAwIBAgIBAzANBgkqhkiG9w0BAQsFADAmMSQwIgYDVQQDDBtkY2R0
312+
MzEuaGVhbHRoaXQuZ292X2NhX3Jvb3QwHhcNMjIwMjA0MTUzNzUxWhcNMzIwMjA1
313+
MDE0OTUxWjBBMS0wKwYJKoZIhvcNAQkBFh5kMUBkb21haW4xLmRjZHQzMS5oZWFs
314+
dGhpdC5nb3YxEDAOBgNVBAMMB0QxX3ZhbEEwggEiMA0GCSqGSIb3DQEBAQUAA4IB
315+
DwAwggEKAoIBAQDHPWJogAq6zCU1zU6ar4GAvRb6bjCTSzm19E98E3dCCG8ZSgWH
316+
yZh3w6M/btu7qMDStrpzMGD1H5TiqS/mEFNNcJP2r8C6T8RKV2xEqhsJlwOoguzJ
317+
4MyePoVYG84/gm5v03BCp91uoz4O1WFrppu439njipv8wUwsvf6ukidhAgP9mEoN
318+
w1sCB1U9zOtpPmbRczMrYyDBWqFaxiaDD9xYaYqal7Ph7adKohBDZA1P7H/Jkxdf
319+
uCwULVDn+bcHD3eW9NToeZ7gc0CV75kVnI/7WbJ6mfx72zOIzEm1AFed36yuEpal
320+
VjCzhJO4ZmmfJxfXr36UICKHQIM/xwSEXqJtAgMBAAGjggFPMIIBSzAfBgNVHSME
321+
GDAWgBSIM9vz74ArTwMFMk3q5ShNOYQhMjApBgNVHQ4EIgQg1WLu98WJoAtR1X7K
322+
ZiHWfIcONgrBBtzuLgNWkQklJugwCQYDVR0TBAIwADAOBgNVHQ8BAf8EBAMCBaAw
323+
KQYDVR0RBCIwIIEeZDFAZG9tYWluMS5kY2R0MzEuaGVhbHRoaXQuZ292MFUGA1Ud
324+
HwROMEwwSqBIoEaGRGh0dHA6Ly9wa2kuZGNkdDMxLmhlYWx0aGl0LmdvdjoxMDA4
325+
MC9kY2R0MzEuaGVhbHRoaXQuZ292X2NhX3Jvb3QuY3JsMGAGCCsGAQUFBwEBBFQw
326+
UjBQBggrBgEFBQcwAoZEaHR0cDovL3BraS5kY2R0MzEuaGVhbHRoaXQuZ292OjEw
327+
MDgwL2RjZHQzMS5oZWFsdGhpdC5nb3ZfY2Ffcm9vdC5jZXIwDQYJKoZIhvcNAQEL
328+
BQADggEBAGqMC2kEA6acNgmUueCbPuLj7uePRGaRk6x0rSEY6mTGoBXci+s9EXbx
329+
a7d/glNFNgQC9KP35esriqSfUn2bsDmtlTs+A79+ldMRH5SWvEmI5f7s9SitLIYR
330+
uRBLE693R7/1DjyUrEFxpdL16O8Y2kIKO9S8lrscNBOg7hW0RKYb4VBnlsNw3jk2
331+
rXyGcFZ63D8VsdgUJTh2BKhpiY37gd/+ILUcylpmC5Uf3yWM2wYRMS6IVACllv+U
332+
PoPWSE2fsrMpfCtDFeUL71gn8g6TYIctVHTn4OeuhHQ6Yt21rgQnlpDFVt0p9sGl
333+
H+L10KwE7wqqmkxwfib5kwgNyrlXtx0=
334+
-----END CERTIFICATE-----";
335+
336+
var expectedBytes = Encoding.UTF8.GetBytes(expectedPublicKey);
337+
var name = DnsString.Parse("example.com");
338+
using var memory = new PooledBytes(expectedBytes.Length);
339+
340+
var writer = new DnsDatagramWriter(new ArraySegment<byte>(memory.Buffer));
341+
writer.WriteInt16NetworkOrder((short)CertificateType.PKIX); // 2 bytes
342+
writer.WriteInt16NetworkOrder((short)27891); // 2 bytes
343+
writer.WriteByte((byte)DnsSecurityAlgorithm.RSASHA256); // 1 byte
344+
writer.WriteBytes(expectedBytes, expectedBytes.Length);
345+
346+
var factory = GetFactory(writer.Data);
347+
348+
var info = new ResourceRecordInfo(name, ResourceRecordType.CERT, QueryClass.IN, 0, writer.Data.Count);
349+
350+
var result = factory.GetRecord(info) as CertRecord;
351+
Assert.NotNull(result);
352+
Assert.Equal(27891, result.KeyTag);
353+
Assert.Equal(CertificateType.PKIX, result.CertType);
354+
Assert.Equal(DnsSecurityAlgorithm.RSASHA256, result.Algorithm);
355+
Assert.Equal(expectedBytes, result.PublicKey);
356+
357+
var cert = new X509Certificate2(Convert.FromBase64String(result.PublicKeyAsString));
358+
Assert.Equal("sha256RSA", cert.SignatureAlgorithm.FriendlyName);
359+
Assert.Equal("CN=D1_valA, [email protected]", cert.Subject);
360+
361+
var x509Extension = cert.Extensions["2.5.29.17"];
362+
Assert.NotNull(x509Extension);
363+
var asnData = new AsnEncodedData(x509Extension.Oid, x509Extension.RawData);
364+
Assert.Equal("RFC822 [email protected]", asnData.Format(false));
365+
}
366+
304367
[Fact]
305368
public void DnsRecordFactory_TXTRecordEmpty()
306369
{

test/DnsClient.Tests/DnsResponseParsingTest.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ public void DnsRecordFactory_McnetValidateSupport()
4545
ResourceRecordType.NSEC3PARAM,
4646
ResourceRecordType.SPF,
4747
ResourceRecordType.DNSKEY,
48-
ResourceRecordType.DS
48+
ResourceRecordType.DS,
49+
ResourceRecordType.CERT
4950
};
5051

5152
foreach (var t in types)

test/DnsClient.Tests/LookupTest.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
using System;
22
using System.Linq;
33
using System.Net;
4+
using System.Security.Cryptography;
5+
using System.Security.Cryptography.X509Certificates;
46
using System.Threading;
57
using System.Threading.Tasks;
68
using DnsClient.Protocol;
@@ -816,6 +818,29 @@ public async Task Lookup_Query_NaPtr()
816818
Assert.NotNull(host.HostName);
817819
}
818820

821+
[Fact]
822+
public async Task Lookup_Query_CERT()
823+
{
824+
var client = new LookupClient(NameServer.Cloudflare);
825+
var result = await client.QueryAsync("d1.domain1.dcdt31.healthit.gov", QueryType.CERT).ConfigureAwait(false);
826+
827+
Assert.NotEmpty(result.Answers.CertRecords());
828+
var certRecord = result.Answers.CertRecords().First();
829+
Assert.NotNull(certRecord);
830+
Assert.Equal("d1.domain1.dcdt31.healthit.gov.", certRecord.DomainName);
831+
Assert.Equal(CertificateType.PKIX, certRecord.CertType);
832+
833+
var cert = new X509Certificate2(Convert.FromBase64String(certRecord.PublicKeyAsString));
834+
Assert.Equal("sha256RSA", cert.SignatureAlgorithm.FriendlyName);
835+
Assert.Equal("CN=D1_valA, [email protected]", cert.Subject);
836+
837+
var x509Extension = cert.Extensions["2.5.29.17"];
838+
Assert.NotNull(x509Extension);
839+
var asnData = new AsnEncodedData(x509Extension.Oid, x509Extension.RawData);
840+
Assert.Equal("RFC822 [email protected]", asnData.Format(false));
841+
842+
}
843+
819844
[Fact]
820845
public async Task GetHostEntry_ExampleSub()
821846
{

0 commit comments

Comments
 (0)