UNPKG

hsd

Version:
949 lines (752 loc) 18 kB
/*! * resource.js - hns records for hsd * Copyright (c) 2017-2018, Christopher Jeffrey (MIT License). * https://github.com/handshake-org/hsd */ 'use strict'; const assert = require('bsert'); const {encoding, wire, util} = require('bns'); const base32 = require('bcrypto/lib/encoding/base32'); const {IP} = require('binet'); const bio = require('bufio'); const key = require('./key'); const nsec = require('./nsec'); const {Struct} = bio; const { DUMMY, DEFAULT_TTL, TYPE_MAP_EMPTY, TYPE_MAP_NS, TYPE_MAP_TXT, hsTypes } = require('./common'); const { sizeName, writeNameBW, readNameBR, sizeString, writeStringBW, readStringBR, isName, readIP, writeIP } = encoding; const { Message, Record, ARecord, AAAARecord, NSRecord, TXTRecord, DSRecord, types } = wire; /** * Resource * @extends {Struct} */ class Resource extends Struct { constructor() { super(); this.ttl = DEFAULT_TTL; this.records = []; } hasType(type) { assert((type & 0xff) === type); for (const record of this.records) { if (record.type === type) return true; } return false; } hasNS() { for (const {type} of this.records) { if (type < hsTypes.NS || type > hsTypes.SYNTH6) continue; return true; } return false; } hasDS() { return this.hasType(hsTypes.DS); } encode() { const bw = bio.write(512); this.write(bw, new Map()); return bw.slice(); } getSize(map) { let size = 1; for (const rr of this.records) size += 1 + rr.getSize(map); return size; } write(bw, map) { bw.writeU8(0); for (const rr of this.records) { bw.writeU8(rr.type); rr.write(bw, map); } return this; } read(br) { const version = br.readU8(); if (version !== 0) throw new Error(`Unknown serialization version: ${version}.`); while (br.left()) { const RD = typeToClass(br.readU8()); // Break at unknown records. if (!RD) break; this.records.push(RD.read(br)); } return this; } toNS(name) { const authority = []; const set = new Set(); for (const record of this.records) { switch (record.type) { case hsTypes.NS: case hsTypes.GLUE4: case hsTypes.GLUE6: case hsTypes.SYNTH4: case hsTypes.SYNTH6: break; default: continue; } const rr = record.toDNS(name, this.ttl); if (set.has(rr.data.ns)) continue; set.add(rr.data.ns); authority.push(rr); } return authority; } toGlue(name) { const additional = []; for (const record of this.records) { switch (record.type) { case hsTypes.GLUE4: case hsTypes.GLUE6: if (!util.isSubdomain(name, record.ns)) continue; break; case hsTypes.SYNTH4: case hsTypes.SYNTH6: break; default: continue; } additional.push(record.toGlue(record.ns, this.ttl)); } return additional; } toDS(name) { const answer = []; for (const record of this.records) { if (record.type !== hsTypes.DS) continue; answer.push(record.toDNS(name, this.ttl)); } return answer; } toTXT(name) { const answer = []; for (const record of this.records) { if (record.type !== hsTypes.TXT) continue; answer.push(record.toDNS(name, this.ttl)); } return answer; } toZone(name, sign = false) { const zone = []; const set = new Set(); for (const record of this.records) { const rr = record.toDNS(name, this.ttl); if (rr.type === types.NS) { if (set.has(rr.data.ns)) continue; set.add(rr.data.ns); } zone.push(rr); } if (sign) { const set = new Set(); for (const rr of zone) set.add(rr.type); const types = [...set].sort(); for (const type of types) key.signZSK(zone, type); } // Add the glue last. for (const record of this.records) { switch (record.type) { case hsTypes.GLUE4: case hsTypes.GLUE6: case hsTypes.SYNTH4: case hsTypes.SYNTH6: { if (!util.isSubdomain(name, record.ns)) continue; zone.push(record.toGlue(record.ns, this.ttl)); break; } } } return zone; } toReferral(name, type, isTLD) { const res = new Message(); // no DS referrals for TLDs const badReferral = isTLD && type === types.DS; if (this.hasNS() && !badReferral) { res.authority = this.toNS(name).concat(this.toDS(name)); res.additional = this.toGlue(name); if (this.hasDS()) { key.signZSK(res.authority, types.DS); } else { // unsigned zone proof res.authority.push(this.toNSEC(name)); key.signZSK(res.authority, types.NSEC); } } else { // Needs SOA. res.aa = true; // negative answer proof res.authority.push(this.toNSEC(name)); key.signZSK(res.authority, types.NSEC); } return res; } toNSEC(name) { let typeMap = TYPE_MAP_EMPTY; if (this.hasNS()) typeMap = TYPE_MAP_NS; else if (this.hasType(hsTypes.TXT)) typeMap = TYPE_MAP_TXT; return nsec.create(name, nsec.nextName(name), typeMap); } toDNS(name, type) { assert(util.isFQDN(name)); assert((type >>> 0) === type); const labels = util.split(name); // Referral. if (labels.length > 1) { const tld = util.from(name, labels, -1); return this.toReferral(tld, type, false); } // Potentially an answer. const res = new Message(); // TLDs are authoritative over their own NS & TXT records. // The NS records in the root zone are just "hints" // and therefore are not signed by the root ZSK. // The only records root is authoritative over is DS. switch (type) { case types.TXT: if (!this.hasNS()) { res.aa = true; res.answer = this.toTXT(name); key.signZSK(res.answer, types.TXT); } break; case types.DS: res.aa = true; res.answer = this.toDS(name); key.signZSK(res.answer, types.DS); break; } // Nope, we may need a referral if (res.answer.length === 0 && res.authority.length === 0) { return this.toReferral(name, type, true); } return res; } getJSON(name) { const json = { records: [] }; for (const record of this.records) json.records.push(record.getJSON()); return json; } fromJSON(json) { assert(json && typeof json === 'object', 'Invalid json.'); assert(Array.isArray(json.records), 'Invalid records.'); for (const item of json.records) { assert(item && typeof item === 'object', 'Invalid record.'); const RD = stringToClass(item.type); if (!RD) throw new Error(`Unknown type: ${item.type}.`); this.records.push(RD.fromJSON(item)); } return this; } } /** * DS * @extends {Struct} */ class DS extends Struct { constructor() { super(); this.keyTag = 0; this.algorithm = 0; this.digestType = 0; this.digest = DUMMY; } get type() { return hsTypes.DS; } getSize() { return 5 + this.digest.length; } write(bw) { bw.writeU16BE(this.keyTag); bw.writeU8(this.algorithm); bw.writeU8(this.digestType); bw.writeU8(this.digest.length); bw.writeBytes(this.digest); return this; } read(br) { this.keyTag = br.readU16BE(); this.algorithm = br.readU8(); this.digestType = br.readU8(); this.digest = br.readBytes(br.readU8()); return this; } toDNS(name = '.', ttl = DEFAULT_TTL) { assert(util.isFQDN(name)); assert((ttl >>> 0) === ttl); const rr = new Record(); const rd = new DSRecord(); rr.name = name; rr.type = types.DS; rr.ttl = ttl; rr.data = rd; rd.keyTag = this.keyTag; rd.algorithm = this.algorithm; rd.digestType = this.digestType; rd.digest = this.digest; return rr; } getJSON() { return { type: 'DS', keyTag: this.keyTag, algorithm: this.algorithm, digestType: this.digestType, digest: this.digest.toString('hex') }; } fromJSON(json) { assert(json && typeof json === 'object', 'Invalid DS record.'); assert(json.type === 'DS', 'Invalid DS record. Type must be "DS".'); assert((json.keyTag & 0xffff) === json.keyTag, 'Invalid DS record. KeyTag must be a uint16.'); assert((json.algorithm & 0xff) === json.algorithm, 'Invalid DS record. Algorithm must be a uint8.'); assert((json.digestType & 0xff) === json.digestType, 'Invalid DS record. DigestType must be a uint8.'); assert(typeof json.digest === 'string', 'Invalid DS record. Digest must be a String.'); assert((json.digest.length >>> 1) <= 255, 'Invalid DS record. Digest is too large.'); this.keyTag = json.keyTag; this.algorithm = json.algorithm; this.digestType = json.digestType; this.digest = util.parseHex(json.digest); return this; } } /** * NS * @extends {Struct} */ class NS extends Struct { constructor() { super(); this.ns = '.'; } get type() { return hsTypes.NS; } getSize(map) { return sizeName(this.ns, map); } write(bw, map) { writeNameBW(bw, this.ns, map); return this; } read(br) { this.ns = readNameBR(br); return this; } toDNS(name = '.', ttl = DEFAULT_TTL) { return createNS(name, ttl, this.ns); } getJSON() { return { type: 'NS', ns: this.ns }; } fromJSON(json) { assert(json && typeof json === 'object', 'Invalid NS record.'); assert(json.type === 'NS', 'Invalid NS record. Type must be "NS".'); assert(isName(json.ns), 'Invalid NS record. ns must be a valid name.'); this.ns = json.ns; return this; } } /** * GLUE4 * @extends {Struct} */ class GLUE4 extends Struct { constructor() { super(); this.ns = '.'; this.address = '0.0.0.0'; } get type() { return hsTypes.GLUE4; } getSize(map) { return sizeName(this.ns, map) + 4; } write(bw, map) { writeNameBW(bw, this.ns, map); writeIP(bw, this.address, 4); return this; } read(br) { this.ns = readNameBR(br); this.address = readIP(br, 4); return this; } toDNS(name = '.', ttl = DEFAULT_TTL) { return createNS(name, ttl, this.ns); } toGlue(name = '.', ttl = DEFAULT_TTL) { return createA(name, ttl, this.address); } getJSON() { return { type: 'GLUE4', ns: this.ns, address: this.address }; } fromJSON(json) { assert(json && typeof json === 'object', 'Invalid GLUE4 record.'); assert(json.type === 'GLUE4', 'Invalid GLUE4 record. Type must be "GLUE4".'); assert(isName(json.ns), 'Invalid GLUE4 record. ns must be a valid name.'); assert(IP.isIPv4String(json.address), 'Invalid GLUE4 record. Address must be a valid IPv4 address.'); this.ns = json.ns; this.address = IP.normalize(json.address); return this; } } /** * GLUE6 * @extends {Struct} */ class GLUE6 extends Struct { constructor() { super(); this.ns = '.'; this.address = '::'; } get type() { return hsTypes.GLUE6; } getSize(map) { return sizeName(this.ns, map) + 16; } write(bw, map) { writeNameBW(bw, this.ns, map); writeIP(bw, this.address, 16); return this; } read(br) { this.ns = readNameBR(br); this.address = readIP(br, 16); return this; } toDNS(name = '.', ttl = DEFAULT_TTL) { return createNS(name, ttl, this.ns); } toGlue(name = '.', ttl = DEFAULT_TTL) { return createAAAA(name, ttl, this.address); } getJSON() { return { type: 'GLUE6', ns: this.ns, address: this.address }; } fromJSON(json) { assert(json && typeof json === 'object', 'Invalid GLUE6 record.'); assert(json.type === 'GLUE6', 'Invalid GLUE6 record. Type must be "GLUE6".'); assert(isName(json.ns), 'Invalid GLUE6 record. ns must be a valid name.'); assert(IP.isIPv6String(json.address), 'Invalid GLUE6 record. Address must be a valid IPv6 address.'); this.ns = json.ns; this.address = IP.normalize(json.address); return this; } } /** * SYNTH4 * @extends {Struct} */ class SYNTH4 extends Struct { constructor() { super(); this.address = '0.0.0.0'; } get type() { return hsTypes.SYNTH4; } get ns() { const ip = IP.toBuffer(this.address).slice(12); return `_${base32.encodeHex(ip)}._synth.`; } getSize() { return 4; } write(bw) { writeIP(bw, this.address, 4); return this; } read(br) { this.address = readIP(br, 4); return this; } toDNS(name = '.', ttl = DEFAULT_TTL) { return createNS(name, ttl, this.ns); } toGlue(name = '.', ttl = DEFAULT_TTL) { return createA(name, ttl, this.address); } getJSON() { return { type: 'SYNTH4', address: this.address }; } fromJSON(json) { assert(json && typeof json === 'object', 'Invalid SYNTH4 record.'); assert(json.type === 'SYNTH4', 'Invalid SYNTH4 record. Type must be "SYNTH4".'); assert(IP.isIPv4String(json.address), 'Invalid SYNTH4 record. Address must be a valid IPv4 address.'); this.address = IP.normalize(json.address); return this; } } /** * SYNTH6 * @extends {Struct} */ class SYNTH6 extends Struct { constructor() { super(); this.address = '::'; } get type() { return hsTypes.SYNTH6; } get ns() { const ip = IP.toBuffer(this.address); return `_${base32.encodeHex(ip)}._synth.`; } getSize() { return 16; } write(bw) { writeIP(bw, this.address, 16); return this; } read(br) { this.address = readIP(br, 16); return this; } toDNS(name = '.', ttl = DEFAULT_TTL) { return createNS(name, ttl, this.ns); } toGlue(name = '.', ttl = DEFAULT_TTL) { return createAAAA(name, ttl, this.address); } getJSON() { return { type: 'SYNTH6', address: this.address }; } fromJSON(json) { assert(json && typeof json === 'object', 'Invalid SYNTH6 record.'); assert(json.type === 'SYNTH6', 'Invalid SYNTH6 record. Type must be "SYNTH6".'); assert(IP.isIPv6String(json.address), 'Invalid SYNTH6 record. Address must be a valid IPv6 address.'); this.address = IP.normalize(json.address); return this; } } /** * TXT * @extends {Struct} */ class TXT extends Struct { constructor() { super(); this.txt = []; } get type() { return hsTypes.TXT; } getSize() { let size = 1; for (const txt of this.txt) size += sizeString(txt); return size; } write(bw) { bw.writeU8(this.txt.length); for (const txt of this.txt) writeStringBW(bw, txt); return this; } read(br) { const count = br.readU8(); for (let i = 0; i < count; i++) this.txt.push(readStringBR(br)); return this; } toDNS(name = '.', ttl = DEFAULT_TTL) { assert(util.isFQDN(name)); assert((ttl >>> 0) === ttl); const rr = new Record(); const rd = new TXTRecord(); rr.name = name; rr.type = types.TXT; rr.ttl = ttl; rr.data = rd; rd.txt.push(...this.txt); return rr; } getJSON() { return { type: 'TXT', txt: this.txt }; } fromJSON(json) { assert(json && typeof json === 'object', 'Invalid TXT record.'); assert(json.type === 'TXT', 'Invalid TXT record. Type must be "TXT".'); assert(Array.isArray(json.txt), 'Invalid TXT record. txt must be an Array.'); for (const txt of json.txt) { assert(typeof txt === 'string', 'Invalid TXT record. Entries in txt Array must be type String.'); assert(txt.length <= 255, 'Invalid TXT record. Entries in txt Array must be <= 255 in length.'); this.txt.push(txt); } return this; } } /* * Helpers */ function typeToClass(type) { assert((type & 0xff) === type); switch (type) { case hsTypes.DS: return DS; case hsTypes.NS: return NS; case hsTypes.GLUE4: return GLUE4; case hsTypes.GLUE6: return GLUE6; case hsTypes.SYNTH4: return SYNTH4; case hsTypes.SYNTH6: return SYNTH6; case hsTypes.TXT: return TXT; default: return null; } } function stringToClass(type) { assert(typeof type === 'string'); if (!Object.prototype.hasOwnProperty.call(hsTypes, type)) return null; return typeToClass(hsTypes[type]); } function createNS(name, ttl, ns) { assert(util.isFQDN(name)); assert((ttl >>> 0) === ttl); assert(util.isFQDN(ns)); const rr = new Record(); const rd = new NSRecord(); rr.name = name; rr.ttl = ttl; rr.type = types.NS; rr.data = rd; rd.ns = ns; return rr; } function createA(name, ttl, address) { assert(util.isFQDN(name)); assert((ttl >>> 0) === ttl); assert(IP.isIPv4String(address)); const rr = new Record(); const rd = new ARecord(); rr.name = name; rr.ttl = ttl; rr.type = types.A; rr.data = rd; rd.address = address; return rr; } function createAAAA(name, ttl, address) { assert(util.isFQDN(name)); assert((ttl >>> 0) === ttl); assert(IP.isIPv6String(address)); const rr = new Record(); const rd = new AAAARecord(); rr.name = name; rr.ttl = ttl; rr.type = types.AAAA; rr.data = rd; rd.address = address; return rr; } /* * Expose */ exports.Resource = Resource; exports.DS = DS; exports.NS = NS; exports.GLUE4 = GLUE4; exports.GLUE6 = GLUE6; exports.SYNTH4 = SYNTH4; exports.SYNTH6 = SYNTH6; exports.TXT = TXT;