pending-dns 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +8 -0
- package/CLAUDE.md +15 -3
- package/README.md +83 -4
- package/config/default.toml +38 -0
- package/config/test.toml +10 -0
- package/lib/api-server.js +185 -17
- package/lib/certs.js +2 -17
- package/lib/dns-handler.js +350 -25
- package/lib/dns-server.js +90 -25
- package/lib/dnssec-wire.js +321 -0
- package/lib/dnssec.js +461 -0
- package/lib/lock.js +37 -0
- package/lib/zone-store.js +86 -3
- package/package.json +5 -2
- package/release-please-config.json +1 -0
- package/test/api.test.js +93 -1
- package/test/dns-handler.test.js +32 -1
- package/test/dns-server.test.js +93 -0
- package/test/dnssec-handler.test.js +550 -0
- package/test/dnssec-wire.test.js +163 -0
- package/test/dnssec.test.js +213 -0
- package/test/helpers.js +3 -1
- package/test/zone-store.test.js +37 -1
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/* eslint-disable no-bitwise */
|
|
4
|
+
// Bit/byte manipulation is intrinsic to DNS wire encoding.
|
|
5
|
+
|
|
6
|
+
// Pure DNSSEC wire-format layer. No Redis, no config, no I/O - only `crypto`
|
|
7
|
+
// (for DS digests) and `ipaddr.js` (to turn an AAAA string into 16 octets).
|
|
8
|
+
// Everything here is deterministic and unit-testable in isolation.
|
|
9
|
+
//
|
|
10
|
+
// Invariant: DNSSEC signatures are computed over the canonical, UNCOMPRESSED,
|
|
11
|
+
// lowercased wire form produced here (RFC 4034 6.2), never over what dns2
|
|
12
|
+
// serializes. dns2 may apply name compression on the wire; validators
|
|
13
|
+
// decompress and re-canonicalize before verifying, so the two always agree.
|
|
14
|
+
|
|
15
|
+
const crypto = require('crypto');
|
|
16
|
+
const ipaddr = require('ipaddr.js');
|
|
17
|
+
|
|
18
|
+
// IANA RR type numbers used by this server. dns2's Packet.TYPE only knows a
|
|
19
|
+
// subset; these fill the gaps so the dns-handler normalize loop and the signer
|
|
20
|
+
// can address every type by number.
|
|
21
|
+
const TYPE = {
|
|
22
|
+
A: 1,
|
|
23
|
+
NS: 2,
|
|
24
|
+
CNAME: 5,
|
|
25
|
+
SOA: 6,
|
|
26
|
+
MX: 15,
|
|
27
|
+
TXT: 16,
|
|
28
|
+
AAAA: 28,
|
|
29
|
+
SRV: 33,
|
|
30
|
+
DS: 43,
|
|
31
|
+
RRSIG: 46,
|
|
32
|
+
NSEC: 47,
|
|
33
|
+
DNSKEY: 48,
|
|
34
|
+
TLSA: 52,
|
|
35
|
+
CDS: 59,
|
|
36
|
+
CDNSKEY: 60,
|
|
37
|
+
CAA: 257
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Types dns2 cannot encode natively (not in its Packet.TYPE). The dns-handler
|
|
41
|
+
// emits these by setting `{ type:<num>, data:<Buffer> }`, which routes through
|
|
42
|
+
// dns2's raw-RDATA fallback (RFC 3597).
|
|
43
|
+
const EXTRA_TYPES = {
|
|
44
|
+
TLSA: TYPE.TLSA,
|
|
45
|
+
RRSIG: TYPE.RRSIG,
|
|
46
|
+
NSEC: TYPE.NSEC,
|
|
47
|
+
DS: TYPE.DS,
|
|
48
|
+
CDS: TYPE.CDS,
|
|
49
|
+
CDNSKEY: TYPE.CDNSKEY
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const u8 = value => Buffer.from([value & 0xff]);
|
|
53
|
+
|
|
54
|
+
const u16 = value => {
|
|
55
|
+
const b = Buffer.alloc(2);
|
|
56
|
+
b.writeUInt16BE(value & 0xffff, 0);
|
|
57
|
+
return b;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const u32 = value => {
|
|
61
|
+
const b = Buffer.alloc(4);
|
|
62
|
+
b.writeUInt32BE(value >>> 0, 0);
|
|
63
|
+
return b;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const b64url = value => Buffer.from((value || '').toString(), 'base64url');
|
|
67
|
+
|
|
68
|
+
const MAX_LABEL = 63;
|
|
69
|
+
|
|
70
|
+
// Canonical, uncompressed, lowercased wire form of a domain name (RFC 4034 6.2).
|
|
71
|
+
// Input names are already punycode ASCII by the time they reach here.
|
|
72
|
+
const encodeName = name => {
|
|
73
|
+
name = (name || '').toString().replace(/\.+$/, '').toLowerCase();
|
|
74
|
+
if (!name) {
|
|
75
|
+
return Buffer.from([0]);
|
|
76
|
+
}
|
|
77
|
+
const parts = [];
|
|
78
|
+
for (const label of name.split('.')) {
|
|
79
|
+
if (!label.length) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const buf = Buffer.from(label, 'utf8');
|
|
83
|
+
if (buf.length > MAX_LABEL) {
|
|
84
|
+
throw new Error(`Name encode: label "${label}" exceeds ${MAX_LABEL} octets`);
|
|
85
|
+
}
|
|
86
|
+
parts.push(u8(buf.length), buf);
|
|
87
|
+
}
|
|
88
|
+
parts.push(Buffer.from([0]));
|
|
89
|
+
return Buffer.concat(parts);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// RFC 4034 3.1.3 label count: excludes the root and a leading "*" wildcard.
|
|
93
|
+
const nameLabelCount = name => {
|
|
94
|
+
name = (name || '').toString().replace(/\.+$/, '').toLowerCase();
|
|
95
|
+
if (!name) {
|
|
96
|
+
return 0;
|
|
97
|
+
}
|
|
98
|
+
let labels = name.split('.').filter(label => label.length);
|
|
99
|
+
if (labels[0] === '*') {
|
|
100
|
+
labels = labels.slice(1);
|
|
101
|
+
}
|
|
102
|
+
return labels.length;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// One DNS character-string list, matching dns2's TXT encoder byte-for-byte
|
|
106
|
+
// (length octet + bytes per chunk). `data` is an array of <=255 byte strings.
|
|
107
|
+
const encodeCharacterStrings = data => {
|
|
108
|
+
const chunks = (Array.isArray(data) ? data : [data]).map(chunk => (Buffer.isBuffer(chunk) ? chunk : Buffer.from((chunk || '').toString(), 'utf8')));
|
|
109
|
+
const parts = [];
|
|
110
|
+
for (const chunk of chunks) {
|
|
111
|
+
parts.push(u8(chunk.length), chunk);
|
|
112
|
+
}
|
|
113
|
+
return Buffer.concat(parts);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const encodeDNSKEYRdata = ({ flags, protocol, algorithm, pubkey }) =>
|
|
117
|
+
// DNSKEY public keys are presented in standard base64 (RFC 4034), not base64url.
|
|
118
|
+
Buffer.concat([u16(flags), u8(protocol), u8(algorithm), Buffer.isBuffer(pubkey) ? pubkey : Buffer.from((pubkey || '').toString(), 'base64')]);
|
|
119
|
+
|
|
120
|
+
// Canonical RDATA for a single resource record (RFC 4034 6.2). For the types
|
|
121
|
+
// dns2 encodes natively this MUST reproduce dns2's wire RDATA (modulo name
|
|
122
|
+
// compression/case, which validators normalize); for the raw types the handler
|
|
123
|
+
// has already built `rr.data` so we pass it straight through.
|
|
124
|
+
const canonicalRdata = (type, rr) => {
|
|
125
|
+
if (Buffer.isBuffer(rr.data)) {
|
|
126
|
+
// TLSA/DS/NSEC/RRSIG and any other pre-built raw RDATA.
|
|
127
|
+
return rr.data;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
switch (type) {
|
|
131
|
+
case TYPE.A:
|
|
132
|
+
return Buffer.from(rr.address.split('.').map(part => parseInt(part, 10) & 0xff));
|
|
133
|
+
|
|
134
|
+
case TYPE.AAAA:
|
|
135
|
+
return Buffer.from(ipaddr.parse(rr.address).toByteArray());
|
|
136
|
+
|
|
137
|
+
case TYPE.NS:
|
|
138
|
+
return encodeName(rr.ns);
|
|
139
|
+
|
|
140
|
+
case TYPE.CNAME:
|
|
141
|
+
return encodeName(rr.domain);
|
|
142
|
+
|
|
143
|
+
case TYPE.MX:
|
|
144
|
+
return Buffer.concat([u16(rr.priority), encodeName(rr.exchange)]);
|
|
145
|
+
|
|
146
|
+
case TYPE.TXT:
|
|
147
|
+
return encodeCharacterStrings(rr.data !== undefined ? rr.data : rr.value);
|
|
148
|
+
|
|
149
|
+
case TYPE.CAA: {
|
|
150
|
+
const tag = (rr.tag || '').toString();
|
|
151
|
+
const value = (rr.value || '').toString();
|
|
152
|
+
return Buffer.concat([u8(rr.flags || 0), u8(tag.length), Buffer.from(tag + value, 'utf8')]);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
case TYPE.SOA:
|
|
156
|
+
return Buffer.concat([
|
|
157
|
+
encodeName(rr.primary),
|
|
158
|
+
encodeName(rr.admin),
|
|
159
|
+
u32(rr.serial),
|
|
160
|
+
u32(rr.refresh),
|
|
161
|
+
u32(rr.retry),
|
|
162
|
+
u32(rr.expiration),
|
|
163
|
+
u32(rr.minimum)
|
|
164
|
+
]);
|
|
165
|
+
|
|
166
|
+
case TYPE.DNSKEY:
|
|
167
|
+
return encodeDNSKEYRdata({ flags: rr.flags, protocol: rr.protocol, algorithm: rr.algorithm, pubkey: rr.key });
|
|
168
|
+
|
|
169
|
+
default:
|
|
170
|
+
throw new Error(`canonicalRdata: unsupported type ${type}`);
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// Canonical wire form of a full RR for signing (RFC 4034 6.2): owner name,
|
|
175
|
+
// type, class, the RRSIG Original TTL, RDLENGTH and canonical RDATA.
|
|
176
|
+
const canonicalRR = (name, type, klass, ttl, rdata) => Buffer.concat([encodeName(name), u16(type), u16(klass), u32(ttl), u16(rdata.length), rdata]);
|
|
177
|
+
|
|
178
|
+
const compareCanonicalRdata = (a, b) => Buffer.compare(a, b);
|
|
179
|
+
|
|
180
|
+
const encodeTLSARdata = ({ usage, selector, matchingType, certificate }) => {
|
|
181
|
+
const hex = (certificate || '').toString();
|
|
182
|
+
// Buffer.from(hex, 'hex') silently drops a trailing nibble / invalid chars,
|
|
183
|
+
// and an empty string would serve a TLSA with no association data - both are
|
|
184
|
+
// corrupt DANE records. Fail loud instead; callers on the query path skip the
|
|
185
|
+
// record rather than dropping the whole response.
|
|
186
|
+
if (!hex.length || hex.length % 2 !== 0 || !/^[0-9a-fA-F]+$/.test(hex)) {
|
|
187
|
+
throw new Error('encodeTLSARdata: certificate must be non-empty even-length hex');
|
|
188
|
+
}
|
|
189
|
+
// u8() masks to a single byte, so reject out-of-range fields instead of
|
|
190
|
+
// wrapping them (usage 256 -> 0 would silently flip the cert-usage semantics).
|
|
191
|
+
for (const field of [usage, selector, matchingType]) {
|
|
192
|
+
if (!Number.isInteger(field) || field < 0 || field > 255) {
|
|
193
|
+
throw new Error('encodeTLSARdata: usage/selector/matchingType must be integers in 0-255');
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return Buffer.concat([u8(usage), u8(selector), u8(matchingType), Buffer.from(hex, 'hex')]);
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
// RFC 4034 Appendix B key tag, computed over the full DNSKEY RDATA.
|
|
200
|
+
const dnskeyKeyTag = rdata => {
|
|
201
|
+
let ac = 0;
|
|
202
|
+
for (let i = 0; i < rdata.length; i++) {
|
|
203
|
+
ac += i & 1 ? rdata[i] : rdata[i] << 8;
|
|
204
|
+
}
|
|
205
|
+
ac += (ac >> 16) & 0xffff;
|
|
206
|
+
return ac & 0xffff;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const DIGEST_ALG = { 1: 'sha1', 2: 'sha256', 4: 'sha384' };
|
|
210
|
+
|
|
211
|
+
// DS digest (RFC 4034 5.1.4): hash of canonical owner name || DNSKEY RDATA.
|
|
212
|
+
const dsDigest = (ownerName, dnskeyRdata, digestType) => {
|
|
213
|
+
const algo = DIGEST_ALG[digestType];
|
|
214
|
+
if (!algo) {
|
|
215
|
+
throw new Error(`dsDigest: unsupported digest type ${digestType}`);
|
|
216
|
+
}
|
|
217
|
+
return crypto
|
|
218
|
+
.createHash(algo)
|
|
219
|
+
.update(Buffer.concat([encodeName(ownerName), dnskeyRdata]))
|
|
220
|
+
.digest();
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
// RFC 4034 4.1.2 type bitmap: window-block list, each block is
|
|
224
|
+
// window(1) || bitmapLength(1) || bitmap. Empty windows are omitted.
|
|
225
|
+
const nsecTypeBitmap = typeNums => {
|
|
226
|
+
const windows = new Map();
|
|
227
|
+
for (const type of typeNums) {
|
|
228
|
+
const window = type >> 8;
|
|
229
|
+
const bit = type & 0xff;
|
|
230
|
+
if (!windows.has(window)) {
|
|
231
|
+
windows.set(window, []);
|
|
232
|
+
}
|
|
233
|
+
windows.get(window).push(bit);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const blocks = [];
|
|
237
|
+
for (const window of [...windows.keys()].sort((a, b) => a - b)) {
|
|
238
|
+
const bits = windows.get(window);
|
|
239
|
+
const maxBit = Math.max(...bits);
|
|
240
|
+
const bitmap = Buffer.alloc((maxBit >> 3) + 1);
|
|
241
|
+
for (const bit of bits) {
|
|
242
|
+
bitmap[bit >> 3] |= 0x80 >> (bit & 7);
|
|
243
|
+
}
|
|
244
|
+
blocks.push(u8(window), u8(bitmap.length), bitmap);
|
|
245
|
+
}
|
|
246
|
+
return Buffer.concat(blocks);
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const encodeNSECRdata = (nextName, typeNums) => Buffer.concat([encodeName(nextName), nsecTypeBitmap(typeNums)]);
|
|
250
|
+
|
|
251
|
+
// RRSIG RDATA up to and including the signer name - the bytes that, prepended
|
|
252
|
+
// to the canonical RRset, form the signing input (RFC 4034 3.1.8.1).
|
|
253
|
+
const encodeRRSIGSigningPreimage = ({ typeCovered, algorithm, labels, originalTtl, expiration, inception, keyTag, signerName }) =>
|
|
254
|
+
Buffer.concat([u16(typeCovered), u8(algorithm), u8(labels), u32(originalTtl), u32(expiration), u32(inception), u16(keyTag), encodeName(signerName)]);
|
|
255
|
+
|
|
256
|
+
const encodeRRSIGRdata = (fields, signature) => Buffer.concat([encodeRRSIGSigningPreimage(fields), signature]);
|
|
257
|
+
|
|
258
|
+
// Per-algorithm crypto. `sign`/`verify` operate on the canonical signing input
|
|
259
|
+
// (the validator-visible bytes), `pubkeyFromJwk` yields the DNSSEC public-key
|
|
260
|
+
// octets that go into DNSKEY RDATA. Key tags 8/13/15 cover the algorithms this
|
|
261
|
+
// server offers; the table is the single place to add more.
|
|
262
|
+
const ALGS = {
|
|
263
|
+
// RSASHA256
|
|
264
|
+
8: {
|
|
265
|
+
name: 'RSASHA256',
|
|
266
|
+
dsDigestType: 2,
|
|
267
|
+
generate: { type: 'rsa', options: { modulusLength: 2048, publicExponent: 65537 } },
|
|
268
|
+
sign: (tbs, key) => crypto.sign('sha256', tbs, { key, padding: crypto.constants.RSA_PKCS1_PADDING }),
|
|
269
|
+
verify: (tbs, key, sig) => crypto.verify('sha256', tbs, { key, padding: crypto.constants.RSA_PKCS1_PADDING }, sig),
|
|
270
|
+
// RFC 3110: exponent length prefix, exponent, modulus.
|
|
271
|
+
pubkeyFromJwk: jwk => {
|
|
272
|
+
const exp = b64url(jwk.e);
|
|
273
|
+
const mod = b64url(jwk.n);
|
|
274
|
+
const prefix = exp.length < 256 ? Buffer.from([exp.length]) : Buffer.concat([Buffer.from([0]), u16(exp.length)]);
|
|
275
|
+
return Buffer.concat([prefix, exp, mod]);
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
// ECDSAP256SHA256 - signature is raw r||s (IEEE P1363), 64 bytes (RFC 6605).
|
|
280
|
+
13: {
|
|
281
|
+
name: 'ECDSAP256SHA256',
|
|
282
|
+
dsDigestType: 2,
|
|
283
|
+
generate: { type: 'ec', options: { namedCurve: 'prime256v1' } },
|
|
284
|
+
sign: (tbs, key) => crypto.sign('sha256', tbs, { key, dsaEncoding: 'ieee-p1363' }),
|
|
285
|
+
verify: (tbs, key, sig) => crypto.verify('sha256', tbs, { key, dsaEncoding: 'ieee-p1363' }, sig),
|
|
286
|
+
// Uncompressed point x||y without the 0x04 prefix.
|
|
287
|
+
pubkeyFromJwk: jwk => Buffer.concat([b64url(jwk.x), b64url(jwk.y)])
|
|
288
|
+
},
|
|
289
|
+
|
|
290
|
+
// ED25519 - signature already raw 64 bytes (RFC 8080).
|
|
291
|
+
15: {
|
|
292
|
+
name: 'ED25519',
|
|
293
|
+
dsDigestType: 2,
|
|
294
|
+
generate: { type: 'ed25519', options: {} },
|
|
295
|
+
sign: (tbs, key) => crypto.sign(null, tbs, key),
|
|
296
|
+
verify: (tbs, key, sig) => crypto.verify(null, tbs, key, sig),
|
|
297
|
+
pubkeyFromJwk: jwk => b64url(jwk.x)
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
module.exports = {
|
|
302
|
+
TYPE,
|
|
303
|
+
EXTRA_TYPES,
|
|
304
|
+
DNSKEY_FLAGS_CSK: 257, // ZONE (bit 7) + SEP (bit 15)
|
|
305
|
+
DNSKEY_PROTOCOL: 3,
|
|
306
|
+
ALGS,
|
|
307
|
+
encodeName,
|
|
308
|
+
nameLabelCount,
|
|
309
|
+
canonicalRdata,
|
|
310
|
+
canonicalRR,
|
|
311
|
+
compareCanonicalRdata,
|
|
312
|
+
encodeCharacterStrings,
|
|
313
|
+
encodeTLSARdata,
|
|
314
|
+
encodeDNSKEYRdata,
|
|
315
|
+
dnskeyKeyTag,
|
|
316
|
+
dsDigest,
|
|
317
|
+
nsecTypeBitmap,
|
|
318
|
+
encodeNSECRdata,
|
|
319
|
+
encodeRRSIGSigningPreimage,
|
|
320
|
+
encodeRRSIGRdata
|
|
321
|
+
};
|