itns-sidechain/test/ns-test.js
2021-12-22 11:58:05 -07:00

759 lines
22 KiB
JavaScript

'use strict';
const assert = require('bsert');
const IP = require('binet');
const base32 = require('bcrypto/lib/encoding/base32');
const {wire, util, encoding, StubResolver} = require('bns');
const {RootServer} = require('../lib/dns/server');
const {Resource} = require('../lib/dns/resource');
const NameState = require('../lib/covenants/namestate');
const rules = require('../lib/covenants/rules');
const nameData = require('./data/ns-names.json');
const icannData = require('./data/ns-icann.json');
const {
TYPE_MAP_ROOT,
TYPE_MAP_EMPTY,
TYPE_MAP_NS,
TYPE_MAP_TXT,
TYPE_MAP_A,
TYPE_MAP_AAAA
} = require('../lib/dns/common');
describe('RootServer', function() {
const ns = new RootServer({
port: 25349 // regtest
});
before(async () => {
await ns.open();
});
after(async () => {
await ns.close();
});
it('should resolve . NS as SYNTH4', async () => {
// Default
assert.strictEqual(ns.publicHost, '127.0.0.1');
// Sanity check
assert.strictEqual(
base32.encodeHex(IP.toBuffer('127.0.0.1').slice(12)),
'fs00008'
);
const name = '.';
const type = wire.types.NS;
const req = {
question: [{name, type}]
};
const res = await ns.resolve(req);
assert(res.answer.length);
const an = res.answer[0];
assert.strictEqual(an.name, name);
assert.strictEqual(an.type, type);
assert.strictEqual(an.data.ns, '_fs00008._synth.');
assert(res.additional.length);
const ad = res.additional[0];
assert.strictEqual(ad.name, '_fs00008._synth.');
assert.strictEqual(ad.type, wire.types.A);
assert.strictEqual(ad.data.address, '127.0.0.1');
});
it('should resolve a SYNTH4', async () => {
const name = '_fs0000g._synth.';
const type = wire.types.A;
const req = {
question: [{name, type}]
};
const res = await ns.resolve(req);
const answer = res.answer;
const rec = answer[0];
assert.strictEqual(rec.name, name);
assert.strictEqual(rec.type, wire.types.A);
assert.strictEqual(rec.data.address, '127.0.0.2');
});
it('should resolve a SYNTH6', async () => {
const name = '_00000000000000000000000008._synth.';
const type = wire.types.AAAA;
const req = {
question: [{name, type}]
};
const res = await ns.resolve(req);
const answer = res.answer;
const rec = answer[0];
assert.strictEqual(rec.name, name);
assert.strictEqual(rec.type, wire.types.AAAA);
assert.strictEqual(rec.data.address, '::2');
});
it('should not cache synth record', async () => {
// Start fresh
const cache = ns.cache.cache;
cache.reset();
assert.strictEqual(cache.size, 0);
// Query a record the RootResolver knows even without a database
let name = '.';
let type = wire.types.NS;
let req = {
question: [{name, type}]
};
await ns.resolve(req);
// Added to cache
assert.strictEqual(cache.size, 1);
// Query a SYNTH6 record
name = '_00000000000000000000000008._synth.';
type = wire.types.AAAA;
req = {
question: [{name, type}]
};
let res = await ns.resolve(req);
let answer = res.answer;
let rec = answer[0];
assert.strictEqual(rec.name, name);
assert.strictEqual(rec.type, wire.types.AAAA);
assert.strictEqual(rec.data.address, '::2');
// Nothing was added to the cache
assert.strictEqual(cache.size, 1);
// Handshake RootServer cache is keyed exclusively by the TLD when the
// name has more than one label (normally indicating a referral).
// If the cache doesn't handle the pseudo-TLD `_synth.` correctly,
// This SYNTH4 request would return the result of the SYNTH6
// record from the last request.
name = '_fs0000g._synth.';
type = wire.types.A;
req = {
question: [{name, type}]
};
res = await ns.resolve(req);
answer = res.answer;
rec = answer[0];
assert.strictEqual(rec.name, name);
assert.strictEqual(rec.type, wire.types.A);
assert.strictEqual(rec.data.address, '127.0.0.2');
// Nothing was added to the cache
assert.strictEqual(cache.size, 1);
});
});
describe('RootServer Blacklist', function() {
const ns = new RootServer({
port: 25349, // regtest
lookup: (hash) => {
// Normally an Urkel Tree goes here.
// Blacklisted names should never get this far.
if (hash.equals(rules.hashName('bit')))
throw new Error('Blacklisted name!');
// For this test all other names have the same record
const namestate = new NameState();
namestate.data = Resource.fromJSON({
records: [
{
type: 'NS',
ns: 'ns1.handshake.'
}
]
}).encode();
return namestate.encode();
}
});
before(async () => {
await ns.open();
});
after(async () => {
await ns.close();
});
it('should look up non-blacklisted name', async () => {
const name = 'icecream.';
const req = {
question: [{
name,
type: wire.types.NS
}]
};
const res = await ns.resolve(req);
const authority = res.authority;
const rec = authority[0];
assert.strictEqual(rec.name, name);
assert.strictEqual(rec.type, wire.types.NS);
assert.strictEqual(rec.data.ns, 'ns1.handshake.');
});
it('should not look up blacklisted name', async () => {
const name = 'bit.';
const req = {
question: [{
name,
type: wire.types.NS
}]
};
const res = await ns.resolve(req);
assert.strictEqual(res.code, wire.codes.NXDOMAIN);
assert.strictEqual(res.answer.length, 0);
});
});
describe('RootServer Plugins', function() {
const ns = new RootServer({
port: 25349, // regtest
lookup: (hash) => {
// Normally an Urkel Tree goes here.
// Blacklisted names should never get this far.
if (hash.equals(rules.hashName('bit')))
throw new Error('Blacklisted name!');
// For this test all other names have the same record
const namestate = new NameState();
namestate.data = Resource.fromJSON({
records: [
{
type: 'NS',
ns: 'ns1.handshake.'
}
]
}).encode();
return namestate.encode();
}
});
before(async () => {
// Plugin inserts middleware before server is opened
ns.middle = (tld, req) => {
const [qs] = req.question;
const name = qs.name.toLowerCase();
const type = qs.type;
if (tld === 'bit.') {
// This plugin runs an imaginary Namecoin full node.
// It looks up records and returns an authoritative answer.
// This makes it look like the complete record including
// the subdomain is in the HNS root zone.
const res = new wire.Message();
res.aa = true;
// This plugin only returns A records,
// and all Namecoin names have the same IP address.
if (type !== wire.types.A)
return null;
const rr = new wire.Record();
const rd = new wire.ARecord();
rr.name = name;
rr.type = wire.types.A;
rr.ttl = 518400;
rr.data = rd;
rd.address = '4.8.15.16';
res.answer.push(rr);
ns.signRRSet(res.answer, wire.types.A);
return res;
}
// Plugin doesn't care about this name
return null;
};
await ns.open();
});
after(async () => {
await ns.close();
});
it('should hijack lookup for blacklisted name', async () => {
const name = 'decentralize.bit.';
const req = {
question: [{
name,
type: wire.types.A
}]
};
const res = await ns.resolve(req);
assert.strictEqual(res.authority.length, 0);
assert.strictEqual(res.answer.length, 2);
const rec = res.answer[0];
assert.strictEqual(rec.name, name);
assert.strictEqual(rec.type, wire.types.A);
assert.strictEqual(rec.data.address, '4.8.15.16');
const sig = res.answer[1];
assert.strictEqual(sig.name, name);
assert.strictEqual(sig.type, wire.types.RRSIG);
});
});
describe('RootServer DNSSEC', function () {
const ns = new RootServer({
port: 25349, // regtest
lookup: (hash) => {
assert(hash instanceof Buffer);
const key = hash.toString('hex');
const data = nameData[key];
if (!data)
return null;
const namestate = new NameState();
namestate.data = Resource.fromJSON(data).encode();
return namestate.encode();
}
});
ns.icann = {
lookup: (name) => {
name = util.fqdn(name);
const data = icannData[name];
if (data)
return wire.Message.fromJSON(data);
const res = new wire.Message();
res.code = wire.codes.NXDOMAIN;
res.ad = true;
return res;
}
};
before(async () => {
await ns.open();
});
after(async () => {
await ns.close();
});
const resolve = (qname, qtype) => {
const req = {
question: [
new wire.Question(qname, qtype)
]
};
return ns.resolve(req);
};
it('should confirm NSEC bitmap constants', () => {
const tests = [
[TYPE_MAP_ROOT, [wire.types.NS,
wire.types.SOA,
wire.types.RRSIG,
wire.types.NSEC,
wire.types.DNSKEY]],
[TYPE_MAP_EMPTY, [wire.types.RRSIG,
wire.types.NSEC]],
[TYPE_MAP_NS, [wire.types.NS,
wire.types.RRSIG,
wire.types.NSEC]],
[TYPE_MAP_TXT, [wire.types.TXT,
wire.types.RRSIG,
wire.types.NSEC]],
[TYPE_MAP_A, [wire.types.A,
wire.types.RRSIG,
wire.types.NSEC]],
[TYPE_MAP_AAAA, [wire.types.AAAA,
wire.types.RRSIG,
wire.types.NSEC]]
];
for (const [actual, types] of tests) {
const record = new wire.NSECRecord();
record.setTypes(types);
assert.bufferEqual(actual, record.typeBitmap);
}
});
it('should refuse invalid names', async () => {
const qname = 'example\\000';
const res = await resolve(qname, wire.types.NS);
assert.strictEqual(res.code, wire.codes.REFUSED);
});
it('should prove NXDOMAIN', async () => {
const qnames = [
'icecream.',
'this-domain-name-has-sixty-three-octets-taking-max-label-length.'
];
for(const qname of qnames) {
const res = await resolve(qname, wire.types.NS);
assert(res.aa);
assert.strictEqual(res.code, wire.codes.NXDOMAIN);
assert.strictEqual(res.answer.length, 0);
assert.strictEqual(res.additional.length, 0);
assert(util.hasType(res.authority, wire.types.SOA));
const nameProof = findCoveringNSEC(res.authority, qname);
assert(nameProof.found);
}
});
it('should create minimally covering NSEC records', async () => {
const qname = 'icecream.';
const res = await resolve(qname, wire.types.NS);
assert(res.aa);
assert.strictEqual(res.code, wire.codes.NXDOMAIN);
assert.strictEqual(res.answer.length, 0);
assert.strictEqual(res.additional.length, 0);
const authority = res.authority;
assert(util.hasType(authority, wire.types.SOA));
const nameProof = findCoveringNSEC(authority, qname);
assert(nameProof.found);
// no other names must be covered by the 'icecream.' proof
// names like 'icecreal\255a.' are covered but these octets
// are not allowed in the root zone
// these are close names to 'icecream.'
const closeNames = ['icecreal.', 'icecrean.',
'icecreal0.', 'icecrea-a.', 'icecreama.'];
for (const name of closeNames) {
const covering = findCoveringNSEC(authority, name);
assert(!covering.found);
}
const wildcard = nameProof.wildcardNSEC;
// test it minimally covers wildcard
// characters like ",#,$,%,&,',),(,* are covered
// but not allowed in the root zone
assert(!cover('0.', wildcard.name, wildcard.data.nextDomain));
assert(!cover('a-m.', wildcard.name, wildcard.data.nextDomain));
assert(!cover('z.', wildcard.name, wildcard.data.nextDomain));
});
it('should prove non-existence of a type in the zone apex', async () => {
const qname = '.';
const res = await resolve(qname, wire.types.A);
assert(res.aa);
assert.strictEqual(res.code, wire.codes.NOERROR);
assert.strictEqual(res.answer.length, 0);
assert.strictEqual(res.additional.length, 0);
assert(util.hasType(res.authority, wire.types.SOA));
// check NSEC proof
const set = util.extractSet(res.authority, qname, wire.types.NSEC);
assert.strictEqual(set.length, 1);
const proof = set[0];
assert.strictEqual(proof.data.typeBitmap, TYPE_MAP_ROOT);
assert(cover('.', proof.name, proof.data.nextDomain));
assert(!cover('0.', proof.name, proof.data.nextDomain));
assert(!cover('aa.', proof.name, proof.data.nextDomain));
});
// Some tests must check both Handshake
// and ICANN names
//
// In the test data:
// 'proofofconcept.' and 'com.' are signed zones
// 'nb.' and 'cf.' are unsigned (no DS record)
// 'schematic.' only contains a TXT record.
// 'empty-name' has no records.
it('should be authoritative over DS for names in the root zone', async () => {
// example:
// ;; QUESTION
// ;proofofconcept. IN DS
//
// 1. Answer must have AA bit.
// 2. DS record must be in the answers section
const qnames = ['proofofconcept.', 'com.'];
for(const qname of qnames) {
const res = await resolve(qname, wire.types.DS);
assert(res.aa);
assert.strictEqual(res.code, wire.codes.NOERROR);
assert.strictEqual(res.answer.length, 2);
assert.strictEqual(res.authority.length, 0);
assert.strictEqual(res.additional.length, 0);
assert(util.hasType(res.answer, wire.types.DS));
assert(util.hasType(res.answer, wire.types.RRSIG));
}
});
it('should add DS to referral answers', async () => {
// ;; QUESTION
// ;proofofconcept. IN A
//
// 1. no AA bit
// 2. DS record in authority section
const queries = [
{qname: 'proofofconcept.', qtype: wire.types.A},
{qname: 'proofofconcept.', qtype: wire.types.NS},
{qname: 'example.proofofconcept.', qtype: wire.types.AAAA},
{qname: 'com.', qtype: wire.types.A},
{qname: 'com.', qtype: wire.types.NS},
{qname: 'example.com.', qtype: wire.types.AAAA},
// delegated sub-tree DS must NOT be authoritative
{qname: 'example.proofofconcept.', qtype: wire.types.DS},
{qname: 'example.com.', qtype: wire.types.DS}
];
for (const q of queries) {
const res = await resolve(q.qname, q.qtype);
assert(!res.aa);
assert.strictEqual(res.code, wire.codes.NOERROR);
assert.strictEqual(res.answer.length, 0);
assert(util.hasType(res.authority, wire.types.DS));
assert(util.hasType(res.authority, wire.types.NS));
assert(!util.hasType(res.authority, wire.types.SOA));
}
});
it('should add insecure delegation proof to DS lookups', async () => {
// example:
// ;; QUESTION
// ;nb. IN DS
//
// 1. Answer must have the AA bit
// 2. NSEC in authority section
const qnames = ['nb.', 'cf.'];
for(const qname of qnames) {
const res = await resolve(qname, wire.types.DS);
assert(res.aa);
assert.strictEqual(res.code, wire.codes.NOERROR);
assert.strictEqual(res.answer.length, 0);
assert.strictEqual(res.additional.length, 0);
assert(util.hasType(res.authority, wire.types.SOA));
const set = util.extractSet(res.authority, qname, wire.types.NSEC);
assert.strictEqual(set.length, 1);
const proof = set[0];
// NSEC must be exact match
assert.strictEqual(qname, proof.name);
assert.strictEqual(proof.data.typeBitmap, TYPE_MAP_NS);
}
});
it('should add insecure delegation proof to referral answers', async () => {
// example:
// ;; QUESTION
// ;nb. IN A
//
// 1. Answer must be referral (no AA bit)
// 2. DS record in authority section
const queries = [
{name: 'nb.', type: wire.types.NS},
{name: 'nb.', type: wire.types.AAAA},
{name: 'dot.nb.', type: wire.types.A},
{name: 'cf.', type: wire.types.NS},
{name: 'cf.', type: wire.types.AAAA},
{name: 'dot.cf.', type: wire.types.A}
];
for (const q of queries) {
const res = await resolve(q.name, q.type);
assert(!res.aa);
assert.strictEqual(res.code, wire.codes.NOERROR);
assert.strictEqual(res.answer.length, 0);
assert(!util.hasType(res.authority, wire.types.SOA));
// must have NSEC
const labels = util.split(q.name);
let tld = util.label(q.name, labels, -1);
tld = util.fqdn(tld);
const set = util.extractSet(res.authority, tld, wire.types.NSEC);
assert.strictEqual(set.length, 1);
assert.strictEqual(set[0].data.typeBitmap, TYPE_MAP_NS);
}
});
it('should prove non-existence of a type for non-delegated names', async () => {
// ;; QUESTION
// ;schematic. IN A
//
// the names 'schematic' and 'empty-name'
// don't have NS records so we are authoritative
// over all records (root zone only supports TXT)
// we must add a proof showing which
// types exist to prove non-existence
// of the requested type
const queries = [
{name: 'schematic.', type: wire.types.A, bitmap: TYPE_MAP_TXT},
{name: 'empty-name.', type: wire.types.TXT, bitmap: TYPE_MAP_EMPTY}
];
for (const query of queries) {
const res = await resolve(query.name, query.type);
assert(res.aa);
assert.strictEqual(res.code, wire.codes.NOERROR);
assert.strictEqual(res.answer.length, 0);
assert.strictEqual(res.additional.length, 0);
assert(util.hasType(res.authority, wire.types.SOA));
const set = util.extractSet(res.authority, query.name, wire.types.NSEC);
assert.strictEqual(set.length, 1);
const proof = set[0];
assert.strictEqual(proof.data.typeBitmap, query.bitmap);
}
});
// Synth records:
it('should prove _synth as an empty non-terminal', async () => {
const queries = [
{name: '_synth.', type: wire.types.NS},
{name: '_synth.', type: wire.types.DS},
{name: '_synth.', type: wire.types.TXT}
];
for (const query of queries) {
const res = await resolve(query.name, query.type);
assert(res.aa);
assert.strictEqual(res.code, wire.codes.NOERROR);
assert.strictEqual(res.answer.length, 0);
assert(util.hasType(res.authority, wire.types.SOA));
assert(util.hasType(res.authority, wire.types.NSEC));
const set = util.extractSet(res.authority, query.name, wire.types.NSEC);
assert.strictEqual(set.length, 1);
assert.strictEqual(set[0].data.typeBitmap, TYPE_MAP_EMPTY);
}
});
it('should prove non-existence of a type for _synth records', async () => {
const queries = [
{name: '_040g208._synth.', type: wire.types.AAAA, bitmap: TYPE_MAP_A},
{name: '_040g208._synth.', type: wire.types.TXT, bitmap: TYPE_MAP_A},
{name: '_4o34e027000000000000000h24._synth.', type: wire.types.A, bitmap: TYPE_MAP_AAAA}
];
for (const query of queries) {
const res = await resolve(query.name, query.type);
assert(res.aa);
assert.strictEqual(res.code, wire.codes.NOERROR);
assert.strictEqual(res.answer.length, 0);
assert.strictEqual(res.additional.length, 0);
assert(util.hasType(res.authority, wire.types.SOA));
const set = util.extractSet(res.authority, query.name, wire.types.NSEC);
assert.strictEqual(set.length, 1);
const proof = set[0];
assert.strictEqual(proof.data.typeBitmap, query.bitmap);
}
});
});
describe('RootServer SIG0', function() {
let ns;
afterEach(async () => {
await ns.close();
});
it('should answer with SIG0', async () => {
ns = new RootServer({
port: 25349
});
const stub = new StubResolver();
stub.setServers(['127.0.0.1:25349']);
// Use a synth name for this so no Urkel Tree or ICANN DNS is required
const name = '_fs0000g._synth.';
await ns.open();
const res = await stub.lookup(name);
assert(res.sig0);
const json = res.getJSON();
assert.strictEqual(json.sig0.algName, 'PRIVATEDNS');
});
it('should not answer with SIG0', async () => {
ns = new RootServer({
port: 25349,
noSig0: true
});
const stub = new StubResolver();
stub.setServers(['127.0.0.1:25349']);
// Use a synth name for this so no Urkel Tree or ICANN DNS is required
const name = '_fs0000g._synth.';
await ns.open();
const res = await stub.lookup(name);
assert(!res.sig0);
});
});
/*
* Helpers
*/
// compare two labels lexicographically
function compare(a, b) {
// 63 octets + 2 octets for length and zero suffix.
const buf1 = Buffer.alloc(65);
const buf2 = Buffer.alloc(65);
// convert to wire format
const [off1, lc1] = encoding.writeName(buf1, a, 0, null, false);
const [off2, lc2] = encoding.writeName(buf2, b, 0, null, false);
assert(lc1 === 1 && lc1 === lc2);
const name1 = buf1.slice(1, off1 - 1);
const name2 = buf2.slice(1, off2 - 1);
return name1.compare(name2);
}
// tests whether sname is between owner and next.
function cover(sname, owner, next) {
return compare(owner, sname) <= 0 && compare(next, sname) === 1;
}
// finds a covering NSEC record for sname in section
// and an NSEC record for the wildcard.
function findCoveringNSEC(section, sname) {
const result = {
found: false,
nameNSEC: null,
wildcardNSEC: null
};
for (const rr of section) {
if (rr.type !== wire.types.NSEC)
continue;
if (cover(sname, rr.name, rr.data.nextDomain)) {
result.nameNSEC = rr;
continue;
}
if (cover('*.', rr.name, rr.data.nextDomain))
result.wildcardNSEC = rr;
}
result.found = result.nameNSEC !== null && result.wildcardNSEC !== null;
return result;
}