diff --git a/lib/server/auth.js b/lib/server/auth.js index 4e0c8b4..3d6a4e1 100644 --- a/lib/server/auth.js +++ b/lib/server/auth.js @@ -23,6 +23,11 @@ class AuthServer extends DNSServer { this.initOptions(options); } + setZSKFromString(str) { + this.zone.setZSKFromString(str); + return this; + } + setOrigin(name) { this.zone.setOrigin(name); return this; diff --git a/lib/zone.js b/lib/zone.js index d3dedf3..d990cbb 100644 --- a/lib/zone.js +++ b/lib/zone.js @@ -14,6 +14,10 @@ const fs = require('bfile'); const constants = require('./constants'); const util = require('./util'); const wire = require('./wire'); +const dnssec = require('./dnssec'); +const {keyFlags} = dnssec; +const {ZONE} = keyFlags; + const { types, @@ -46,8 +50,10 @@ class Zone { this.origin = '.'; this.count = 0; this.names = new Map(); - this.wild = new RecordMap(); + this.wild = new RecordMap(this); this.nsec = new NameList(); + this.zskpriv = null; + this.zskkey = null; this.setOrigin(origin); } @@ -65,6 +71,12 @@ class Zone { return this; } + setZSKFromString(str) { + const [alg, zskpriv] = dnssec.decodePrivate(str); + this.zskpriv = zskpriv; + this.zskkey = dnssec.makeKey(this.origin, alg, zskpriv, ZONE); + } + setOrigin(origin) { if (origin == null) origin = '.'; @@ -95,7 +107,7 @@ class Zone { this.wild.insert(rr); } else { if (!this.names.has(rr.name)) - this.names.set(rr.name, new RecordMap()); + this.names.set(rr.name, new RecordMap(this)); const map = this.names.get(rr.name); @@ -356,11 +368,12 @@ class Zone { */ class RecordMap { - constructor() { + constructor(zone) { // type -> rrs this.rrs = new Map(); // type covered -> sigs this.sigs = new Map(); + this.zone = zone; } clear() { @@ -454,6 +467,12 @@ class RecordMap { an.push(convert(name, rr)); } + if (!sigs && this.zone.zskkey && this.zone.zskpriv) { + // Create dnssec sig on the fly (especially useful for wildcard) + const sig = dnssec.sign(this.zone.zskkey, this.zone.zskpriv, an); + an.push(sig); + } + return this; } } @@ -472,6 +491,12 @@ class RecordMap { for (const rr of sigs) an.push(convert(name, rr)); } + + if (!sigs && this.zone.zskkey && this.zone.zskpriv) { + // Create dnssec sig on the fly (especially useful for wildcard) + const sig = dnssec.sign(this.zone.zskkey, this.zone.zskpriv, an); + an.push(sig); + } } return this; diff --git a/test/zone-test.js b/test/zone-test.js index 53659d2..9e15b94 100644 --- a/test/zone-test.js +++ b/test/zone-test.js @@ -9,6 +9,9 @@ const fs = require('bfile'); const wire = require('../lib/wire'); const Zone = require('../lib/zone'); const {types, codes} = wire; +const dnssec = require('../lib/dnssec'); +const {RSASHA256} = dnssec.algs; +const {ZONE, KSK} = dnssec.keyFlags; const ROOT_ZONE = Path.resolve(__dirname, 'data', 'root.zone'); const COM_RESPONSE = Path.resolve(__dirname, 'data', 'com-response.zone'); @@ -153,7 +156,7 @@ describe('Zone', function() { if (an.type === types.A) { a = true; - assert (an.data.address = '10.20.30.40'); + assert (an.data.address === '10.20.30.40'); } } assert(cname); @@ -173,8 +176,8 @@ describe('Zone', function() { assert(msg.authority.length === 0); assert(msg.additional.length === 0); assert(msg.answer.length === 1); - assert(msg.answer[0].type = types.CNAME); - assert(msg.answer[0].data.target = 'idontexist.'); + assert(msg.answer[0].type === types.CNAME); + assert(msg.answer[0].data.target === 'idontexist.'); }); } }); @@ -222,7 +225,7 @@ describe('Zone', function() { if (an.type === types.A) { a = true; - assert (an.data.address = '10.20.30.40'); + assert (an.data.address === '10.20.30.40'); } } assert(cname); @@ -231,4 +234,100 @@ describe('Zone', function() { }); } }); + + describe('DNSSEC for wildcard', function() { + const zone = new Zone(); + const domain = 'thebnszone.'; + const subdomain = 'subdomain.' + domain; + + // TLD + zone.setOrigin(domain); + // Reset zone. + zone.clearRecords(); + // A record for TLD (Common in Handshake, not in DNS) + zone.fromString(`${domain} 21600 IN A 10.20.30.40`); + // wildcard for subdomains + zone.fromString(`*.${domain} 21600 IN A 50.60.70.80`); + // SOA + zone.fromString( + `${domain} 21600 IN SOA ns1.${domain} admin.${domain} ` + + '2020070500 86400 7200 604800 300' + ); + + zone.zskpriv = dnssec.createPrivate(RSASHA256, 2048); + zone.zskkey = dnssec.makeKey(domain, RSASHA256, zone.zskpriv, ZONE); + + let wrongsig = null; + + it('should serve signed A record from defined name', () => { + const msg = zone.resolve(domain, types.A); + assert(msg.code === codes.NOERROR); + assert(msg.aa); + assert(msg.authority.length === 0); + assert(msg.additional.length === 0); + assert(msg.answer.length === 2); + let rrsig = null; + let a = null; + for (const an of msg.answer) { + if (an.type === types.RRSIG) + rrsig = an; + + if (an.type === types.A) { + a = an; + assert (an.data.address === '10.20.30.40'); + } + } + assert(rrsig); + assert(a); + assert(dnssec.verify(rrsig, zone.zskkey, [a])); + wrongsig = rrsig; + }); + + it('should serve signed A record from wildcard', () => { + const msg = zone.resolve(subdomain, types.A); + assert(msg.code === codes.NOERROR); + assert(msg.aa); + assert(msg.authority.length === 0); + assert(msg.additional.length === 0); + assert(msg.answer.length === 2); + let rrsig = null; + let a = null; + for (const an of msg.answer) { + if (an.type === types.RRSIG) + rrsig = an; + + if (an.type === types.A) { + a = an; + assert (an.data.address === '50.60.70.80'); + } + } + assert(rrsig); + assert(a); + assert(dnssec.verify(rrsig, zone.zskkey, [a])); + }); + + it('should not verify with the wrong signature', () => { + const msg = zone.resolve(subdomain, types.A); + assert(msg.code === codes.NOERROR); + assert(msg.aa); + assert(msg.authority.length === 0); + assert(msg.additional.length === 0); + assert(msg.answer.length === 2); + let rrsig = null; + let a = null; + for (const an of msg.answer) { + if (an.type === types.RRSIG) + rrsig = an; + + if (an.type === types.A) { + a = an; + assert (an.data.address === '50.60.70.80'); + } + } + assert(rrsig); + assert(a); + // sanity check + assert(!dnssec.verify(wrongsig, zone.zskkey, [a])); + }); + }); });