diff --git a/README.md b/README.md index b967e0c..aefb712 100644 --- a/README.md +++ b/README.md @@ -146,15 +146,72 @@ Currently the different available records are } ``` -#### `TXT` +#### `CAA` ``` js { - data: 'text' || Buffer || [ Buffer || 'text' ] + flags: 128, // octet + tag: 'issue|issuewild|iodef', + value: 'ca.example.net' } ``` -When encoding, scalar values are converted to an array and strings are converted to UTF-8 encoded Buffers. When decoding, the return value will always be an array of Buffer. +#### `CNAME` + +``` js +{ + data: 'cname.to.another.record' +} +``` + +#### `DNAME` + +``` js +{ + data: 'dname.to.another.record' +} +``` + +#### `DNSKEY` + +``` js +{ + flags: 257, // 16 bits + algorithm: 1, // octet + key: buffer +} +``` + +#### `DS` + +``` js +{ + keyTag: 12345, + algorithm: 8, + digestType: 1, + digest: buffer +} +``` + +#### `HINFO` + +``` js +{ + data: { + cpu: 'cpu info', + os: 'os info' + } +} +``` + +#### `MX` + +``` js +{ + preference: 10, + exchange: 'mail.example.net' +} +``` #### `NS` @@ -164,6 +221,28 @@ When encoding, scalar values are converted to an array and strings are converted } ``` +#### `NSEC` + +``` js +{ + nextDomain: 'a.domain', + rrtypes: ['A', 'TXT', 'RRSIG'] +} +``` + +#### `NSEC3` + +``` js +{ + algorithm: 1, + flags: 0, + iterations: 2, + salt: buffer, + nextDomain: buffer, // Hashed per RFC5155 + rrtypes: ['A', 'TXT', 'RRSIG'] +} +``` + #### `NULL` ``` js @@ -172,6 +251,45 @@ When encoding, scalar values are converted to an array and strings are converted } ``` +#### `OPT` + +``` js +{ + type: 'OPT', + name: '.', + udpPayloadSize: 4096, + flags: packet.DNSSEC_OK, + options: [{ + code: 12, + data: Buffer.alloc(31) + }] +} +``` + +#### `PTR` + +``` js +{ + data: 'points.to.another.record' +} +``` + +#### `RRSIG` + +``` js +{ + typeCovered: 'A', + algorithm: 8, + labels: 1, + originalTTL: 3600, + expiration: timestamp, + inception: timestamp, + keyTag: 12345, + signersName: 'a.name', + signature: buffer +} +``` + #### `SOA` ``` js @@ -202,74 +320,15 @@ When encoding, scalar values are converted to an array and strings are converted } ``` -#### `HINFO` +#### `TXT` ``` js { - data: { - cpu: 'cpu info', - os: 'os info' - } + data: 'text' || Buffer || [ Buffer || 'text' ] } ``` -#### `PTR` - -``` js -{ - data: 'points.to.another.record' -} -``` - -#### `CNAME` - -``` js -{ - data: 'cname.to.another.record' -} -``` - -#### `DNAME` - -``` js -{ - data: 'dname.to.another.record' -} -``` - -#### `CAA` - -``` js -{ - flags: 128, // octet - tag: 'issue|issuewild|iodef', - value: 'ca.example.net' -} -``` - -#### `MX` - -``` js -{ - preference: 10, - exchange: 'mail.example.net' -} -``` - -#### `OPT` - -``` js -{ - type: 'OPT', - name: '.', - udpPayloadSize: 4096, - flags: packet.DNSSEC_OK, - options: [{ - code: 12, - data: Buffer.alloc(31) - }] -} -``` +When encoding, scalar values are converted to an array and strings are converted to UTF-8 encoded Buffers. When decoding, the return value will always be an array of Buffer. If you need another one, open an issue and we'll try to add it. diff --git a/index.js b/index.js index e0dac38..17185d5 100644 --- a/index.js +++ b/index.js @@ -743,6 +743,398 @@ ropt.encodingLength = function (options) { return 2 + encodingLengthList(options || [], roption) } +const rdnskey = exports.dnskey = {} + +rdnskey.PROTOCOL_DNSSEC = 3 +rdnskey.ZONE_KEY = 0x80 +rdnskey.SECURE_ENTRYPOINT = 0x8000 + +rdnskey.encode = function (key, buf, offset) { + if (!buf) buf = Buffer.allocUnsafe(rdnskey.encodingLength(key)) + if (!offset) offset = 0 + const oldOffset = offset + + const keydata = key.key + if (!Buffer.isBuffer(keydata)) { + throw new Error('Key must be a Buffer') + } + + offset += 2 // Leave space for length + buf.writeUInt16BE(key.flags, offset) + offset += 2 + buf.writeUInt8(rdnskey.PROTOCOL_DNSSEC, offset) + offset += 1 + buf.writeUInt8(key.algorithm, offset) + offset += 1 + keydata.copy(buf, offset, 0, keydata.length) + offset += keydata.length + + rdnskey.encode.bytes = offset - oldOffset + buf.writeUInt16BE(rdnskey.encode.bytes - 2, oldOffset) + return buf +} + +rdnskey.encode.bytes = 0 + +rdnskey.decode = function (buf, offset) { + if (!offset) offset = 0 + const oldOffset = offset + + var key = {} + var length = buf.readUInt16BE(offset) + offset += 2 + key.flags = buf.readUInt16BE(offset) + offset += 2 + if (buf.readUInt8(offset) !== rdnskey.PROTOCOL_DNSSEC) { + throw new Error('Protocol must be 3') + } + offset += 1 + key.algorithm = buf.readUInt8(offset) + offset += 1 + key.key = buf.slice(offset, oldOffset + length + 2) + offset += key.key.length + rdnskey.decode.bytes = offset - oldOffset + return key +} + +rdnskey.decode.bytes = 0 + +rdnskey.encodingLength = function (key) { + return 6 + Buffer.byteLength(key.key) +} + +const rrrsig = exports.rrsig = {} + +rrrsig.encode = function (sig, buf, offset) { + if (!buf) buf = Buffer.allocUnsafe(rrrsig.encodingLength(sig)) + if (!offset) offset = 0 + const oldOffset = offset + + const signature = sig.signature + if (!Buffer.isBuffer(signature)) { + throw new Error('Signature must be a Buffer') + } + + offset += 2 // Leave space for length + buf.writeUInt16BE(types.toType(sig.typeCovered), offset) + offset += 2 + buf.writeUInt8(sig.algorithm, offset) + offset += 1 + buf.writeUInt8(sig.labels, offset) + offset += 1 + buf.writeUInt32BE(sig.originalTTL, offset) + offset += 4 + buf.writeUInt32BE(sig.expiration, offset) + offset += 4 + buf.writeUInt32BE(sig.inception, offset) + offset += 4 + buf.writeUInt16BE(sig.keyTag, offset) + offset += 2 + name.encode(sig.signersName, buf, offset) + offset += name.encode.bytes + signature.copy(buf, offset, 0, signature.length) + offset += signature.length + + rrrsig.encode.bytes = offset - oldOffset + buf.writeUInt16BE(rrrsig.encode.bytes - 2, oldOffset) + return buf +} + +rrrsig.encode.bytes = 0 + +rrrsig.decode = function (buf, offset) { + if (!offset) offset = 0 + const oldOffset = offset + + var sig = {} + var length = buf.readUInt16BE(offset) + offset += 2 + sig.typeCovered = types.toString(buf.readUInt16BE(offset)) + offset += 2 + sig.algorithm = buf.readUInt8(offset) + offset += 1 + sig.labels = buf.readUInt8(offset) + offset += 1 + sig.originalTTL = buf.readUInt32BE(offset) + offset += 4 + sig.expiration = buf.readUInt32BE(offset) + offset += 4 + sig.inception = buf.readUInt32BE(offset) + offset += 4 + sig.keyTag = buf.readUInt16BE(offset) + offset += 2 + sig.signersName = name.decode(buf, offset) + offset += name.decode.bytes + sig.signature = buf.slice(offset, oldOffset + length + 2) + offset += sig.signature.length + rrrsig.decode.bytes = offset - oldOffset + return sig +} + +rrrsig.decode.bytes = 0 + +rrrsig.encodingLength = function (sig) { + return 20 + + name.encodingLength(sig.signersName) + + Buffer.byteLength(sig.signature) +} + +const typebitmap = {} + +typebitmap.encode = function (typelist, buf, offset) { + if (!buf) buf = Buffer.allocUnsafe(typebitmap.encodingLength(typelist)) + if (!offset) offset = 0 + const oldOffset = offset + + var typesByWindow = [] + for (var i = 0; i < typelist.length; i++) { + var typeid = types.toType(typelist[i]) + if (typesByWindow[typeid >> 8] === undefined) { + typesByWindow[typeid >> 8] = [] + } + typesByWindow[typeid >> 8][(typeid >> 3) & 0x1F] |= 1 << (7 - (typeid & 0x7)) + } + + for (i = 0; i < typesByWindow.length; i++) { + if (typesByWindow[i] !== undefined) { + var windowBuf = Buffer.from(typesByWindow[i]) + buf.writeUInt8(i, offset) + offset += 1 + buf.writeUInt8(windowBuf.length, offset) + offset += 1 + windowBuf.copy(buf, offset) + offset += windowBuf.length + } + } + + typebitmap.encode.bytes = offset - oldOffset + return buf +} + +typebitmap.encode.bytes = 0 + +typebitmap.decode = function (buf, offset, length) { + if (!offset) offset = 0 + const oldOffset = offset + + var typelist = [] + while (offset - oldOffset < length) { + var window = buf.readUInt8(offset) + offset += 1 + var windowLength = buf.readUInt8(offset) + offset += 1 + for (var i = 0; i < windowLength; i++) { + var b = buf.readUInt8(offset + i) + for (var j = 0; j < 8; j++) { + if (b & (1 << (7 - j))) { + var typeid = types.toString((window << 8) | (i << 3) | j) + typelist.push(typeid) + } + } + } + offset += windowLength + } + + typebitmap.decode.bytes = offset - oldOffset + return typelist +} + +typebitmap.decode.bytes = 0 + +typebitmap.encodingLength = function (typelist) { + var extents = [] + for (var i = 0; i < typelist.length; i++) { + var typeid = types.toType(typelist[i]) + extents[typeid >> 8] |= Math.max(extents[typeid >> 8] || 0, typeid & 0xFF) + } + + var len = 0 + for (i = 0; i < extents.length; i++) { + if (extents[i] !== undefined) { + len += 2 + Math.ceil((extents[i] + 1) / 8) + } + } + + return len +} + +const rnsec = exports.nsec = {} + +rnsec.encode = function (record, buf, offset) { + if (!buf) buf = Buffer.allocUnsafe(rnsec.encodingLength(record)) + if (!offset) offset = 0 + const oldOffset = offset + + offset += 2 // Leave space for length + name.encode(record.nextDomain, buf, offset) + offset += name.encode.bytes + typebitmap.encode(record.rrtypes, buf, offset) + offset += typebitmap.encode.bytes + + rnsec.encode.bytes = offset - oldOffset + buf.writeUInt16BE(rnsec.encode.bytes - 2, oldOffset) + return buf +} + +rnsec.encode.bytes = 0 + +rnsec.decode = function (buf, offset) { + if (!offset) offset = 0 + const oldOffset = offset + + var record = {} + var length = buf.readUInt16BE(offset) + offset += 2 + record.nextDomain = name.decode(buf, offset) + offset += name.decode.bytes + record.rrtypes = typebitmap.decode(buf, offset, length - (offset - oldOffset)) + offset += typebitmap.decode.bytes + + rnsec.decode.bytes = offset - oldOffset + return record +} + +rnsec.decode.bytes = 0 + +rnsec.encodingLength = function (record) { + return 2 + + name.encodingLength(record.nextDomain) + + typebitmap.encodingLength(record.rrtypes) +} + +const rnsec3 = exports.nsec3 = {} + +rnsec3.encode = function (record, buf, offset) { + if (!buf) buf = Buffer.allocUnsafe(rnsec3.encodingLength(record)) + if (!offset) offset = 0 + const oldOffset = offset + + const salt = record.salt + if (!Buffer.isBuffer(salt)) { + throw new Error('salt must be a Buffer') + } + + const nextDomain = record.nextDomain + if (!Buffer.isBuffer(nextDomain)) { + throw new Error('nextDomain must be a Buffer') + } + + offset += 2 // Leave space for length + buf.writeUInt8(record.algorithm, offset) + offset += 1 + buf.writeUInt8(record.flags, offset) + offset += 1 + buf.writeUInt16BE(record.iterations, offset) + offset += 2 + buf.writeUInt8(salt.length, offset) + offset += 1 + salt.copy(buf, offset, 0, salt.length) + offset += salt.length + buf.writeUInt8(nextDomain.length, offset) + offset += 1 + nextDomain.copy(buf, offset, 0, nextDomain.length) + offset += nextDomain.length + typebitmap.encode(record.rrtypes, buf, offset) + offset += typebitmap.encode.bytes + + rnsec3.encode.bytes = offset - oldOffset + buf.writeUInt16BE(rnsec3.encode.bytes - 2, oldOffset) + return buf +} + +rnsec3.encode.bytes = 0 + +rnsec3.decode = function (buf, offset) { + if (!offset) offset = 0 + const oldOffset = offset + + var record = {} + var length = buf.readUInt16BE(offset) + offset += 2 + record.algorithm = buf.readUInt8(offset) + offset += 1 + record.flags = buf.readUInt8(offset) + offset += 1 + record.iterations = buf.readUInt16BE(offset) + offset += 2 + const saltLength = buf.readUInt8(offset) + offset += 1 + record.salt = buf.slice(offset, offset + saltLength) + offset += saltLength + const hashLength = buf.readUInt8(offset) + offset += 1 + record.nextDomain = buf.slice(offset, offset + hashLength) + offset += hashLength + record.rrtypes = typebitmap.decode(buf, offset, length - (offset - oldOffset)) + offset += typebitmap.decode.bytes + + rnsec3.decode.bytes = offset - oldOffset + return record +} + +rnsec3.decode.bytes = 0 + +rnsec3.encodingLength = function (record) { + return 8 + + record.salt.length + + record.nextDomain.length + + typebitmap.encodingLength(record.rrtypes) +} + +const rds = exports.ds = {} + +rds.encode = function (digest, buf, offset) { + if (!buf) buf = Buffer.allocUnsafe(rds.encodingLength(digest)) + if (!offset) offset = 0 + const oldOffset = offset + + const digestdata = digest.digest + if (!Buffer.isBuffer(digestdata)) { + throw new Error('Digest must be a Buffer') + } + + offset += 2 // Leave space for length + buf.writeUInt16BE(digest.keyTag, offset) + offset += 2 + buf.writeUInt8(digest.algorithm, offset) + offset += 1 + buf.writeUInt8(digest.digestType, offset) + offset += 1 + digestdata.copy(buf, offset, 0, digestdata.length) + offset += digestdata.length + + rds.encode.bytes = offset - oldOffset + buf.writeUInt16BE(rds.encode.bytes - 2, oldOffset) + return buf +} + +rds.encode.bytes = 0 + +rds.decode = function (buf, offset) { + if (!offset) offset = 0 + const oldOffset = offset + + var digest = {} + var length = buf.readUInt16BE(offset) + offset += 2 + digest.keyTag = buf.readUInt16BE(offset) + offset += 2 + digest.algorithm = buf.readUInt8(offset) + offset += 1 + digest.digestType = buf.readUInt8(offset) + offset += 1 + digest.digest = buf.slice(offset, oldOffset + length + 2) + offset += digest.digest.length + rds.decode.bytes = offset - oldOffset + return digest +} + +rds.decode.bytes = 0 + +rds.encodingLength = function (digest) { + return 6 + Buffer.byteLength(digest.digest) +} + const renc = exports.record = function (type) { switch (type.toUpperCase()) { case 'A': return ra @@ -759,6 +1151,11 @@ const renc = exports.record = function (type) { case 'SOA': return rsoa case 'MX': return rmx case 'OPT': return ropt + case 'DNSKEY': return rdnskey + case 'RRSIG': return rrrsig + case 'NSEC': return rnsec + case 'NSEC3': return rnsec3 + case 'DS': return rds } return runknown } diff --git a/package.json b/package.json index 107c9ed..a2d67b8 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "test": "eslint --color *.js && tape test.js" }, "dependencies": { + "axios": "^0.18.0", "ip": "^1.1.5", "safe-buffer": "^5.1.1" }, diff --git a/test.js b/test.js index 31f2579..9c27d77 100644 --- a/test.js +++ b/test.js @@ -359,6 +359,81 @@ tape('opt', function (t) { t.end() }) +tape('dnskey', function (t) { + testEncoder(t, packet.dnskey, { + flags: packet.dnskey.SECURE_ENTRYPOINT | packet.dnskey.ZONE_KEY, + algorithm: 1, + key: Buffer.from([0, 1, 2, 3, 4, 5]) + }) + t.end() +}) + +tape('rrsig', function (t) { + const testRRSIG = { + typeCovered: 'A', + algorithm: 1, + labels: 2, + originalTTL: 3600, + expiration: 1234, + inception: 1233, + keyTag: 2345, + signersName: 'foo.com', + signature: Buffer.from([0, 1, 2, 3, 4, 5]) + } + testEncoder(t, packet.rrsig, testRRSIG) + + // Check the signature length is correct with extra junk at the end + const buf = Buffer.allocUnsafe(packet.rrsig.encodingLength(testRRSIG) + 4) + packet.rrsig.encode(testRRSIG, buf) + const val2 = packet.rrsig.decode(buf) + t.ok(compare(t, testRRSIG, val2)) + + t.end() +}) + +tape('nsec', function (t) { + testEncoder(t, packet.nsec, { + nextDomain: 'foo.com', + rrtypes: ['A', 'DNSKEY', 'CAA', 'DLV'] + }) + + // Test with the sample NSEC from https://tools.ietf.org/html/rfc4034#section-4.3 + var sampleNSEC = Buffer.from('003704686f7374076578616d706c6503636f6d00' + + '0006400100000003041b000000000000000000000000000000000000000000000' + + '000000020', 'hex') + var decoded = packet.nsec.decode(sampleNSEC) + t.ok(compare(t, decoded, { + nextDomain: 'host.example.com', + rrtypes: ['A', 'MX', 'RRSIG', 'NSEC', 'UNKNOWN_1234'] + })) + var reencoded = packet.nsec.encode(decoded) + t.same(sampleNSEC.length, reencoded.length) + t.same(sampleNSEC, reencoded) + t.end() +}) + +tape('nsec3', function (t) { + testEncoder(t, packet.nsec3, { + algorithm: 1, + flags: 0, + iterations: 257, + salt: new Buffer([42, 42, 42]), + nextDomain: new Buffer([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]), + rrtypes: ['A', 'DNSKEY', 'CAA', 'DLV'] + }) + t.end() +}) + +tape('ds', function (t) { + testEncoder(t, packet.ds, { + keyTag: 1234, + algorithm: 1, + digestType: 1, + digest: Buffer.from([0, 1, 2, 3, 4, 5]) + }) + t.end() +}) + tape('unpack', function (t) { const buf = Buffer.from([ 0x00, 0x79, diff --git a/types.js b/types.js index 358eb49..3cd78bd 100644 --- a/types.js +++ b/types.js @@ -98,5 +98,6 @@ exports.toType = function (name) { case 'ANY': return 255 case '*': return 255 } + if (name.toUpperCase().startsWith('UNKNOWN_')) return parseInt(name.slice(8)) return 0 }