Skip to content

Commit efe1e91

Browse files
committed
feat: added CERT and TLSA support
1 parent 56e65f1 commit efe1e91

File tree

3 files changed

+295
-4
lines changed

3 files changed

+295
-4
lines changed

README.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@
5151
* [`tangerine.resolveSoa(hostname[, options, abortController]))`](#tangerineresolvesoahostname-options-abortcontroller)
5252
* [`tangerine.resolveSrv(hostname[, options, abortController]))`](#tangerineresolvesrvhostname-options-abortcontroller)
5353
* [`tangerine.resolveTxt(hostname[, options, abortController]))`](#tangerineresolvetxthostname-options-abortcontroller)
54+
* [`tangerine.resolveCert(hostname, [, options, abortController]))`](#tangerineresolvecerthostname--options-abortcontroller)
55+
* [`tangerine.resolveTlsa(hostname, [, options, abortController]))`](#tangerineresolvetlsahostname--options-abortcontroller)
5456
* [`tangerine.reverse(ip[, abortController, purgeCache])`](#tangerinereverseip-abortcontroller-purgecache)
5557
* [`tangerine.setDefaultResultOrder(order)`](#tangerinesetdefaultresultorderorder)
5658
* [`tangerine.setServers(servers)`](#tangerinesetserversservers)
@@ -156,6 +158,7 @@ Thanks to the authors of [dohdec](https://github.com/hildjj/dohdec), [dns-packet
156158
* `resolveNs``queryNs`
157159
* `resolveNs``queryNs`
158160
* `resolveTxt``queryTxt`
161+
* `resolveTsla``queryTsla`
159162
* `resolveSrv``querySrv`
160163
* `resolvePtr``queryPtr`
161164
* `resolveNaptr``queryNaptr`
@@ -230,6 +233,7 @@ tangerine.resolve('forwardemail.net').then(console.log);
230233
* If set to `true`, then the result will be re-queried and re-cached – see [Cache](#cache) documentation for more insight.
231234
* Instances of `new Tangerine()` are instances of `dns.promises.Resolver` via `class Tangerine extends dns.promises.Resolver { ... }` (namely for compatibility with projects such as [cacheable-lookup](https://github.com/szmarczak/cacheable-lookup)).
232235
* See the complete list of [Options](#options) below.
236+
* Any `rrtype` from the list at <https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-4> is supported (unlike the native Node.js DNS module which only supports a limited set).
233237

234238
### `tangerine.cancel()`
235239

@@ -269,6 +273,58 @@ Tangerine supports a new `ecsSubnet` property in the `options` Object argument.
269273

270274
### `tangerine.resolveTxt(hostname[, options, abortController]))`
271275

276+
### `tangerine.resolveCert(hostname, [, options, abortController]))`
277+
278+
This function returns a Promise that resolves with an Array with parsed values from results:
279+
280+
```js
281+
[
282+
{
283+
algorithm: 0,
284+
certificate: 'MIIEoTCCA4mgAwIBAgICAacwDQYJKoZIhvcNAQELBQAwgY0xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJNRDEOMAwGA1UEBwwFQm95ZHMxEzARBgNVBAoMCkRyYWplciBMTEMxIjAgBgNVBAMMGWludGVybWVkaWF0ZS5oZWFsdGhpdC5nb3YxKDAmBgkqhkiG9w0BCQEWGWludGVybWVkaWF0ZS5oZWFsdGhpdC5nb3YwHhcNMTgwOTI1MTgyNDIzWhcNMjgwOTIyMTgyNDIzWjB7MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTUQxDjAMBgNVBAcMBUJveWRzMRMwEQYDVQQKDApEcmFqZXIgTExDMRkwFwYDVQQDDBBldHQuaGVhbHRoaXQuZ292MR8wHQYJKoZIhvcNAQkBFhBldHQuaGVhbHRoaXQuZ292MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxaA2MIuaqpvP2Id85KIhUVA6zlj+CgZh/3prgJ1q4leP3T5F1tSSgrQ/WYTFglEwN7FJx4yJ324NaKncaMPDBIg3IUgC3Q5nrPUbIJAUgM5+67pXnGgt6s9bQelEsTdbyA/JlLC7Hsv184mqo0yrueC9NJEea4/yTV51G9S4jLjnKhr0XUTw0Fb/PFNL9ZwaEdFgQfUaE1maleazKGDyLLuEGvpXsRNs1Ju/kdHkOUVLf741Cq8qLlqOKN2v5jQkUdFUKHbYIF5KXt4ToV9mvxTaz6Mps1UbS+a73Xr+VqmBqmEQnXA5DZ7ucikzv9DLokDwtmPzhdqye2msgDpw0QIDAQABo4IBGjCCARYwCQYDVR0TBAIwADAbBgNVHREEFDASghBldHQuaGVhbHRoaXQuZ292MB0GA1UdDgQWBBQ6E22jc99mm+WraUj93IvQcw6JHDAfBgNVHSMEGDAWgBRfW20fzencvG+Attm1rcvQV+3rOTALBgNVHQ8EBAMCBaAwSQYDVR0fBEIwQDA+oDygOoY4aHR0cDovL2NhLmRpcmVjdGNhLm9yZy9jcmwvaW50ZXJtZWRpYXRlLmhlYWx0aGl0Lmdvdi5jcmwwVAYIKwYBBQUHAQEESDBGMEQGCCsGAQUFBzAChjhodHRwOi8vY2EuZGlyZWN0Y2Eub3JnL2FpYS9pbnRlcm1lZGlhdGUuaGVhbHRoaXQuZ292LmRlcjANBgkqhkiG9w0BAQsFAAOCAQEAhCASLubdxWp+XzXO4a8zMgWOMpjft+ilIy2ROVKOKslbB7lKx0NR7chrTPxCmK+YTL2ttLaTpOniw/vTGrZgeFPyXzJCNtpnx8fFipPE18OAlKMc2nyy7RfUscf28UAEmFo2cEJfpsZjyynkBsTnQ5rQVNgM7TbXXfboxwWwhg4HnWIcmlTs2YM1a9v+idK6LSfX9y/Nvhf9pl0DQflc9ym4z/XCq87erCce+11kxH1+36N6rRqeiHVBYnoYIGMH690r4cgE8cW5B4eK7kaD3iCbmpChO0gZSa5Lex49WLXeFfM+ukd9y3AB00KMZcsUV5bCgwShH053ZQa+FMON8w==',
285+
certificate_type: 'PKIX',
286+
key_tag: 0,
287+
name: 'ett.healthit.gov',
288+
ttl: 19045,
289+
},
290+
]
291+
```
292+
293+
This mirrors output from <https://github.com/rthalley/dnspython>.
294+
295+
### `tangerine.resolveTlsa(hostname, [, options, abortController]))`
296+
297+
This method was added for DANE and TSLA support. See this [excellent article](https://www.mailhardener.com/kb/dane), [index.js](https://github.com/forwardemail/tangerine/blob/main/index.js), and <https://github.com/nodejs/node/issues/39569> for more insight.
298+
299+
This function returns a Promise that resolves with an Array with parsed values from results:
300+
301+
```js
302+
[
303+
{
304+
cert: Buffer @Uint8Array [
305+
e1ae9c3d e848ece1 ba72e0d9 91ae4d0d 9ec547c6 bad1ddda b9d6beb0 a7e0e0d8
306+
],
307+
mtype: 1,
308+
name: 'proloprod.mail._dane.internet.nl',
309+
selector: 1,
310+
ttl: 622,
311+
usage: 2,
312+
},
313+
{
314+
cert: Buffer @Uint8Array [
315+
d6fea64d 4e68caea b7cbb2e0 f905d7f3 ca3308b1 2fd88c5b 469f08ad 7e05c7c7
316+
],
317+
mtype: 1,
318+
name: 'proloprod.mail._dane.internet.nl',
319+
selector: 1,
320+
ttl: 622,
321+
usage: 3,
322+
},
323+
]
324+
```
325+
326+
This mirrors output from <https://github.com/rthalley/dnspython>.
327+
272328
### `tangerine.reverse(ip[, abortController, purgeCache])`
273329
274330
### `tangerine.setDefaultResultOrder(order)`

index.js

Lines changed: 174 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,19 @@ class Tangerine extends dns.promises.Resolver {
3838
return Number.isSafeInteger(port) && port >= 0 && port <= 65535;
3939
}
4040

41+
static CTYPE_BY_VALUE = {
42+
1: 'PKIX',
43+
2: 'SPKI',
44+
3: 'PGP',
45+
4: 'IPKIX',
46+
5: 'ISPKI',
47+
6: 'IPGP',
48+
7: 'ACPKIX',
49+
8: 'IACPKIX',
50+
253: 'URI',
51+
254: 'OID'
52+
};
53+
4154
static getAddrConfigTypes() {
4255
const networkInterfaces = os.networkInterfaces();
4356
let hasIPv4 = false;
@@ -138,7 +151,7 @@ class Tangerine extends dns.promises.Resolver {
138151
dns.TIMEOUT
139152
]);
140153

141-
static TYPES = new Set([
154+
static DNS_TYPES = new Set([
142155
'A',
143156
'AAAA',
144157
'CAA',
@@ -152,6 +165,99 @@ class Tangerine extends dns.promises.Resolver {
152165
'TXT'
153166
]);
154167

168+
// <https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-4>
169+
static TYPES = new Set([
170+
'A',
171+
'A6',
172+
'AAAA',
173+
'AFSDB',
174+
'AMTRELAY',
175+
'APL',
176+
'ATMA',
177+
'AVC',
178+
'AXFR',
179+
'CAA',
180+
'CDNSKEY',
181+
'CDS',
182+
'CERT',
183+
'CNAME',
184+
'CSYNC',
185+
'DHCID',
186+
'DLV',
187+
'DNAME',
188+
'DNSKEY',
189+
'DOA',
190+
'DS',
191+
'EID',
192+
'EUI48',
193+
'EUI64',
194+
'GID',
195+
'GPOS',
196+
'HINFO',
197+
'HIP',
198+
'HTTPS',
199+
'IPSECKEY',
200+
'ISDN',
201+
'IXFR',
202+
'KEY',
203+
'KX',
204+
'L32',
205+
'L64',
206+
'LOC',
207+
'LP',
208+
'MAILA',
209+
'MAILB',
210+
'MB',
211+
'MD',
212+
'MF',
213+
'MG',
214+
'MINFO',
215+
'MR',
216+
'MX',
217+
'NAPTR',
218+
'NID',
219+
'NIMLOC',
220+
'NINFO',
221+
'NS',
222+
'NSAP',
223+
'NSAP-PTR',
224+
'NSEC',
225+
'NSEC3',
226+
'NSEC3PARAM',
227+
'NULL',
228+
'NXT',
229+
'OPENPGPKEY',
230+
'OPT',
231+
'PTR',
232+
'PX',
233+
'RKEY',
234+
'RP',
235+
'RRSIG',
236+
'RT',
237+
'Reserved',
238+
'SIG',
239+
'SINK',
240+
'SMIMEA',
241+
'SOA',
242+
'SPF',
243+
'SRV',
244+
'SSHFP',
245+
'SVCB',
246+
'TA',
247+
'TALINK',
248+
'TKEY',
249+
'TLSA',
250+
'TSIG',
251+
'TXT',
252+
'UID',
253+
'UINFO',
254+
'UNSPEC',
255+
'URI',
256+
'WKS',
257+
'X25',
258+
'ZONEMD'
259+
]);
260+
155261
static ANY_TYPES = [
156262
'A',
157263
'AAAA',
@@ -805,6 +911,15 @@ class Tangerine extends dns.promises.Resolver {
805911
return this.resolve(name, 'TXT', options, abortController);
806912
}
807913

914+
resolveCert(name, options, abortController) {
915+
return this.resolve(name, 'CERT', options, abortController);
916+
}
917+
918+
// NOTE: parse this properly according to spec (see below default case)
919+
resolveTlsa(name, options, abortController) {
920+
return this.resolve(name, 'TLSA', options, abortController);
921+
}
922+
808923
// 1:1 mapping with node's official dns.promises API
809924
// (this means it's a drop-in replacement for `dns`)
810925
// <https://github.com/nodejs/node/blob/9bbde3d7baef584f14569ef79f116e9d288c7aaa/lib/internal/dns/utils.js#L87-L95>
@@ -1386,7 +1501,7 @@ class Tangerine extends dns.promises.Resolver {
13861501
// this supports both redis-based key/value/ttl and simple key/value implementations
13871502
result.expires = Date.now() + Math.round(result.ttl * 1000);
13881503
const args = [key, result, ...this.options.setCacheArgs(key, result)];
1389-
debug('setting cache', [key, result, ...args]);
1504+
debug('setting cache', { args });
13901505
await this.options.cache.set(...args);
13911506
}
13921507

@@ -1572,8 +1687,64 @@ class Tangerine extends dns.promises.Resolver {
15721687
});
15731688
}
15741689

1690+
case 'CERT': {
1691+
// CERT records `tangerine.resolveCert`
1692+
// <https://github.com/jpnarkinsky/tangerine/commit/5f70954875aa93ef4acf076172d7540298b0a16b>
1693+
// <https://www.rfc-editor.org/rfc/rfc4398.html>
1694+
return result.answers.map((answer) => {
1695+
if (!Buffer.isBuffer(answer.data))
1696+
throw new Error('Buffer was not available');
1697+
1698+
try {
1699+
// <https://github.com/rthalley/dnspython/blob/98b12e9e43847dac615bb690355d2fabaff969d2/dns/rdtypes/ANY/CERT.py#L69>
1700+
const obj = {
1701+
name: answer.name,
1702+
ttl: answer.ttl,
1703+
certificate_type: answer.data.subarray(0, 2).readUInt16BE(),
1704+
key_tag: answer.data.subarray(2, 4).readUInt16BE(),
1705+
algorithm: answer.data.subarray(4, 5).readUInt8(),
1706+
certificate: answer.data.subarray(5).toString('base64')
1707+
};
1708+
obj.certificate_type = this.constructor.CTYPE_BY_VALUE[
1709+
obj.certificate_type
1710+
]
1711+
? this.constructor.CTYPE_BY_VALUE[obj.certificate_type]
1712+
: obj.certificate_type.toString();
1713+
return obj;
1714+
} catch (err) {
1715+
console.error(err);
1716+
throw err;
1717+
}
1718+
});
1719+
}
1720+
1721+
case 'TLSA': {
1722+
// if it returns answers with `type: TLSA` then recursively lookup
1723+
// 3 1 1 D6FEA64D4E68CAEAB7CBB2E0F905D7F3CA3308B12FD88C5B469F08AD 7E05C7C7
1724+
return result.answers.map((answer) => {
1725+
if (!Buffer.isBuffer(answer.data))
1726+
throw new Error('Buffer was not available');
1727+
1728+
// <https://www.mailhardener.com/kb/dane>
1729+
return {
1730+
name: answer.name,
1731+
ttl: answer.ttl,
1732+
// <https://github.com/rthalley/dnspython/blob/98b12e9e43847dac615bb690355d2fabaff969d2/dns/rdtypes/tlsabase.py#L35>
1733+
usage: answer.data.subarray(0, 1).readUInt8(),
1734+
selector: answer.data.subarray(1, 2).readUInt8(),
1735+
mtype: answer.data.subarray(2, 3).readUInt8(),
1736+
cert: answer.data.subarray(3)
1737+
};
1738+
});
1739+
}
1740+
15751741
default: {
1576-
throw new Error(`Unknown type of ${rrtype}`);
1742+
this.options.logger.error(
1743+
new Error(
1744+
`Submit a PR at <https://github.com/forwardemail/tangerine> with proper parsing for ${rrtype} records. You can reference <https://github.com/rthalley/dnspython/tree/master/dns/rdtypes/ANY> for inspiration.`
1745+
)
1746+
);
1747+
return result.answers;
15771748
}
15781749
}
15791750
}

test/test.js

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const dns = require('node:dns');
2+
const { Buffer } = require('node:buffer');
23
const { isIP, isIPv4, isIPv6 } = require('node:net');
34

45
const Redis = require('ioredis-mock');
@@ -290,7 +291,7 @@ for (const host of [
290291
t.deepEqual(r1, r2);
291292
});
292293

293-
for (const type of Tangerine.TYPES) {
294+
for (const type of Tangerine.DNS_TYPES) {
294295
test(`resolve("${host}", "${type}")`, async (t) => {
295296
const tangerine = new Tangerine();
296297
const resolver = new Resolver();
@@ -763,3 +764,66 @@ test('supports decoding of cached Buffers', async (t) => {
763764
['hello world!']
764765
]);
765766
});
767+
768+
// <https://github.com/jpnarkinsky/tangerine/commit/5f70954875aa93ef4acf076172d7540298b0a16b#diff-a561630bb56b82342bc66697aee2ad96efddcbc9d150665abd6fb7ecb7c0ab2f>
769+
test('resolveCert', async (t) => {
770+
const tangerine = new Tangerine();
771+
772+
let r1;
773+
try {
774+
r1 = await tangerine.resolveCert('ett.healthit.gov');
775+
} catch (err) {
776+
r1 = err;
777+
}
778+
779+
// Since the node resolver has no support for resolving CERT
780+
// records, the standard approach won't work here. So, we lookup
781+
// a well known address that DOES have a CERT record, then check
782+
// that the resorts are sensible, since that's the best we can do.
783+
t.assert(r1.length > 0, "Couldn't resolve CERT record for ett.healthit.gov!");
784+
785+
t.log(r1);
786+
787+
for (const d of r1) {
788+
t.assert(typeof d === 'object', 'must be an object');
789+
t.assert(typeof d.name === 'string', 'name missing');
790+
t.assert(typeof d.ttl === 'number', 'ttl missing');
791+
t.assert(
792+
typeof d.certificate_type === 'string',
793+
'certificate_type missing'
794+
);
795+
t.assert(typeof d.key_tag === 'number', 'key_tag missing');
796+
t.assert(typeof d.algorithm === 'number', 'algorithm missing');
797+
t.assert(typeof d.certificate === 'string', 'certificate missing');
798+
}
799+
});
800+
801+
// similar edge case as resolveCert above, but for resolveTlsa
802+
// <https://github.com/internetstandards/toolbox-wiki/blob/main/DANE-for-SMTP-how-to.md>
803+
test('resolveTlsa', async (t) => {
804+
const tangerine = new Tangerine();
805+
806+
let r1;
807+
try {
808+
r1 = await tangerine.resolveTlsa('_25._tcp.internet.nl');
809+
} catch (err) {
810+
r1 = err;
811+
}
812+
813+
t.assert(
814+
r1.length > 0,
815+
"Couldn't resolve TLSA record for _25._tcp.internet.nl!"
816+
);
817+
818+
t.log(r1);
819+
820+
for (const d of r1) {
821+
t.assert(typeof d === 'object', 'must be an object');
822+
t.assert(typeof d.name === 'string', 'name missing');
823+
t.assert(typeof d.ttl === 'number', 'ttl missing');
824+
t.assert(typeof d.usage === 'number', 'usage missing');
825+
t.assert(typeof d.selector === 'number', 'selector missing');
826+
t.assert(typeof d.mtype === 'number', 'mtype missing');
827+
t.assert(Buffer.isBuffer(d.cert), 'cert must be buffer');
828+
}
829+
});

0 commit comments

Comments
 (0)