Skip to content

Commit

Permalink
Implement SSHFP records (mafintosh#80)
Browse files Browse the repository at this point in the history
* Add SSHFP record support

Use offset since we may receive a full dns packet
Account for maximum hash lengths for each hash type
fix: don't use the output of `Buffer#copy()`, instead use `Buffer#byteLength`
fix: normalize fingerprint string
fix: the offset pointer starts out at RDLENGTH
fix: account for the `RDLENGTH` field in `rsshfp.decode()`

* tests: add test for the SSHFP record type
  • Loading branch information
wolfy1339 authored and martinheidegger committed Dec 2, 2022
1 parent 27ac35d commit d8f9403
Show file tree
Hide file tree
Showing 6 changed files with 125 additions and 8 deletions.
2 changes: 2 additions & 0 deletions buffer_utils.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ export function bytelength (input: string | Uint8Array): number
export function from (input: Uint8Array | Array | string): Uint8Array
export function write (arr: Uint8Array, str: string, start: number): number
export function toHex (buf: Uint8Array, start?: number, end?: number): string
export function hexLength (str: string): number
export function writeHex (buf: Uint8Array, str: string, start: number, end: number): Uint8Array
export function readUInt32BE (buf: Uint8Array, offset: number): number
export function readUInt16BE (buf: Uint8Array, offset: number): number
export function writeUInt32BE (buf: Uint8Array, value: number, offset: number): number
Expand Down
37 changes: 35 additions & 2 deletions buffer_utils.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,49 @@ export function write (arr, str, start) {
return utf8.encode.bytes
}

const hexNum = {}
const numHex = new Array(0xff)
for (let b0 = 0; b0 <= 0xf; b0 += 1) {
const b0L = b0.toString(16)
const b0U = b0L.toUpperCase()
for (let b1 = 0; b1 <= 0xf; b1 += 1) {
const b1L = b1.toString(16)
const b1U = b1L.toUpperCase()
const num = b0 << 4 | b1
const hex = `${b0L}${b1L}`
numHex[num] = hex
hexNum[hex] = num
hexNum[`${b0U}${b1L}`] = num
hexNum[`${b0L}${b1U}`] = num
hexNum[`${b0U}${b1U}`] = num
}
}

export function toHex (buf, start, end) {
let result = ''
for (let offset = start; offset < end;) {
const num = buf[offset++]
const str = num.toString(16)
result += (str.length === 1) ? '0' + str : str
result += numHex[num]
}
return result
}

export function hexLength (string) {
return string.length >>> 1
}

export function writeHex (buf, string, offset, end) {
let i = 0
while (offset < end) {
const hex = string.substr(i, 2)
const num = hexNum[hex]
if (num === undefined) return
buf[offset++] = num
i += 2
}
return buf
}

const P_24 = Math.pow(2, 24)
const P_16 = Math.pow(2, 16)
const P_8 = Math.pow(2, 8)
Expand Down
53 changes: 53 additions & 0 deletions index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1166,6 +1166,57 @@ const rds = codec({
}
})

const rsshfp = codec({
encode (record, buf, offset) {
if (!buf) buf = new Uint8Array(rsshfp.encodingLength(record))
if (!offset) offset = 0
const oldOffset = offset

offset += 2 // The function call starts with the offset pointer at the RDLENGTH field, not the RDATA one
buf[offset] = record.algorithm
offset += 1
buf[offset] = record.hash
offset += 1

const fingerprintLength = b.hexLength(record.fingerprint)
const expectedLength = getFingerprintLengthForHashType(record.hash)
if (fingerprintLength !== expectedLength) {
throw new Error(`Invalid length of fingerprint "${record.fingerprint}" for hashType=${record.hash}: ${fingerprintLength} != ${expectedLength}`)
}
b.writeHex(buf, record.fingerprint, offset, offset += fingerprintLength)
rsshfp.encode.bytes = offset - oldOffset
b.writeUInt16BE(buf, rsshfp.encode.bytes - 2, oldOffset)

return buf
},
decode (buf, offset) {
if (!offset) offset = 0
const oldOffset = offset

const record = {}
offset += 2 // Account for the RDLENGTH field
record.algorithm = buf[offset]
offset += 1
record.hash = buf[offset]
offset += 1

const fingerprintLength = getFingerprintLengthForHashType(record.hash)
record.fingerprint = b.toHex(buf, offset, offset + fingerprintLength)
offset += fingerprintLength
rsshfp.decode.bytes = offset - oldOffset
return record
},
encodingLength (record) {
return 4 + b.hexLength(record.fingerprint)
}
})
function getFingerprintLengthForHashType (hashType) {
if (hashType === 1) return 20
if (hashType === 2) return 32
throw new Error(`Invalid hashType=${hashType}, supported=1,2`)
}
rsshfp.getFingerprintLengthForHashType = getFingerprintLengthForHashType

function renc (type) {
switch (type.toUpperCase()) {
case 'A': return ra
Expand All @@ -1187,6 +1238,7 @@ function renc (type) {
case 'RP': return rrp
case 'NSEC': return rnsec
case 'NSEC3': return rnsec3
case 'SSHFP': return rsshfp
case 'DS': return rds
}
return runknown
Expand Down Expand Up @@ -1337,6 +1389,7 @@ export {
rnsec as nsec,
rnsec3 as nsec3,
rds as ds,
rsshfp as sshfp,
renc as enc
}

Expand Down
28 changes: 22 additions & 6 deletions test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as rcodes from './rcodes.mjs'
import * as opcodes from './opcodes.mjs'
import * as optioncodes from './optioncodes.mjs'
import { decode as toUtf8 } from 'utf8-codec'
import { write, toHex, bytelength } from './buffer_utils.mjs'
import { write, toHex, bytelength, writeHex, hexLength } from './buffer_utils.mjs'

tape('unknown', function (t) {
testEncoder(t, packet.unknown, Buffer.from('hello world'))
Expand Down Expand Up @@ -107,6 +107,20 @@ tape('soa', function (t) {
t.end()
})

tape('sshfp', function (t) {
testEncoder(t, packet.sshfp, {
algorithm: 1,
hash: 1,
fingerprint: 'a108c9f834354d5b37af988141c9294822f5bc00'
})
testEncoder(t, packet.sshfp, {
algorithm: 1,
hash: 2,
fingerprint: 'a108c9f834354d5b37af988141c9294822f5bc00afa0dfafa2dfa1dfafa5dfa1'
})
t.end()
})

tape('a', function (t) {
testEncoder(t, packet.a, '127.0.0.1')
t.end()
Expand Down Expand Up @@ -686,8 +700,8 @@ tape('single query -> response encoding', function (t) {
})

test('buffer utf8', function (sub) {
[
'', // empty
for (const [index, fixture] of [
// '', // empty
'basic: hi',
'japanese: 日本語',
'mixed: 日本語 hi',
Expand All @@ -701,18 +715,20 @@ test('buffer utf8', function (sub) {
'odd 3 byte: \xa158',
'max 3 byte: \xffff',
`4 byte: ${String.fromCodePoint(100000)}`
].forEach((fixture, index) => {
].entries()) {
sub.test(`fixture #${index}`, t => {
const check = Buffer.from(fixture)
const checkHex = check.toString('hex')
const len = check.length
t.equals(bytelength(fixture), len, `fixture ${fixture} length`)
const buf = new Uint8Array(len)
t.equals(write(buf, fixture, 0), len, 'write.num')
t.equals(toHex(buf, 0, len), check.toString('hex'), `write: ${fixture}`)
t.equals(toHex(buf, 0, len), checkHex, `write: ${fixture}`)
t.equals(check.compare(writeHex(buf, checkHex, 0, hexLength(checkHex))), 0)
t.equals(toUtf8(check, 0, check.length), check.toString(), `toUtf8: ${fixture}`)
t.end()
})
})
}

sub.test('surrogate pairs', function (t) {
[
Expand Down
12 changes: 12 additions & 0 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,14 @@ export interface DigestData {
algorithm: number;
digestType: number;
}
export type SSHFPFingerPrintLength = 20 | 32;
export type SSHFPHash = 1 | 2;
export type SSHFPFingerPrintLengthFor <T extends SSHFPHash> = T extends 1 ? 20 : 32;
export interface SSHFP {
algorithm: number;
hash: SSHFPHash;
fingerprint: string;
}

export interface NSecData {
nextDomain: Uint8Array;
Expand Down Expand Up @@ -215,6 +223,9 @@ export type DNSKeyCodec = Codec<DNSKeyData> & {
SECURE_ENTRYPOINT: 0x8000
};
export type DSCodec = Codec<DigestData>;
export type SSHFPCodec = Codec<SSHFP> & {
getFingerprintLengthForHashType(hashType: SSHFPHash): SSHFPFingerPrintLength;
};
export type HInfoCodec = Codec<HInfoData>;
export type AAAACodec = Codec<string>;
export type UnknownCodec = Codec<Uint8Array>;
Expand Down Expand Up @@ -244,6 +255,7 @@ export const cname: PtrCodec;
export const dname: PtrCodec;
export const dnskey: DNSKeyCodec;
export const ds: DSCodec;
export const sshfp: SSHFPCodec;
export const hinfo: HInfoCodec;
export const aaaa: AAAACodec;
export const answer: Codec<Answer>;
Expand Down
1 change: 1 addition & 0 deletions types/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const codec: Array<Codec<any>> = [
dnsPacket.mx,
dnsPacket.name,
dnsPacket.ns,
dnsPacket.sshfp,
dnsPacket.nsec,
dnsPacket.nsec3,
dnsPacket.null,
Expand Down

0 comments on commit d8f9403

Please sign in to comment.