Contents
DNS Encryption
Background
Unlike most DNS services ENUM requests contain the sort of information that the NSA and telcos were caught up in the previous couple of years. Of late we have implemented our own name server software so we felt compelled to extend this to encrypt DNS requests and replies. We can only assume the only reason that the NSA is the only government spy agency that has made the news is because they were the only ones to get caught, not because they are the only ones doing it, or if others aren't doing it now they most likely will be within the next decade or so.
Besides the obvious government spy efforts, even if you have nothing to hide from any government, at least at this point in time, that doesn't mean you don't want to hide or conceal your personal information from your neighbours, employers, employees, your business competitiors or whoever the list can really go on and is unique to our own situations and what it is we're doing that we don't want others to know we're doing. No matter what you are doing there is bound to be someone you don't want sticking their nose into your business. After all, if we weren't worried about everyone knowing everything occurring in our lives we wouldn't put curtains up in our houses.
Currently there is no internet draft nor RFC covering this subject as far as we are aware, but that will be the next step for us from here.
We'll probably get yelled at by the DNS purists because we hacked it together and cheated a little in the process, but again our intent wasn't to do anything more than a proof of concept to prove that it could be done.
We haven't designed the system to be ENUM specific and it should be usable for any DNS although it is possibly not the best way to do things and we want further discussions on this topic.
What about DNSSec?
DNSSec and DNS encryption share things in common, however DNSSec was only ever designed to authenticate that the DNS information you received hadn't been tampered with and it doesn't handle encryption at all but both technologies share many things in common and there was no reason DNSSec couldn't have included an encryption component as well.
Technical details
The idea is simple, take a normal DNS request, generate a 16 byte or 128 bit shared secret pre-pend the shared secret to the DNS request, and then encrypt the whole thing using a 3072 bit RSA key. To get this to work so it's backward compatible to some extent you would also need to pre-pend 0x00 0x00 0xff to the start of the encrypted request packet.
From there the name server simply responds in a similar manner, but using the shared secret and 128 bit AES encryption to encode the response.
Wider Application
This system wasn't designed to be specific to ENUM, or even our own servers and our thoughts on preventing information leak due to how DNS normally works. Normally when processing a DNS request, the client and any caching servers will always send the full host name to any servers listed in any NS records returned. Name servers responding will send a NS reply and the full request will be sent to another name server, or some other reply or a request failure will be sent to the requestor.
This behaviour could be securely worked around by only sending the minimal amount of information to each name server. This would require splitting the hostname into segments and requesting a NS record until you reach the full length of the hostname requested in which case you send the original request to last name server hit.
eg 5.5.3.8.5.5.5.0.0.8.1.e164.org dig -t ns org @A.ROOT-SERVERS.NET dig -t ns e164.org @TLD1.ULTRADNS.NET dig -t ns 1.e164.org @pa.e164.ws dig -t ns 8.1.e164.org @pa.e164.ws .... dig -t ns 5.3.8.5.5.5.0.0.8.1.e164.org @pa.e164.ws dig -t naptr 5.5.3.8.5.5.5.0.0.8.1.e164.org @pa.e164.ws
Obviously this would increase the number of packets sent to get a result for a DNS request, although ENUM requests would be worst case, a normal DNS lookup for e164.org wouldn't use any more DNS requests then normally. Although for www.e164.org there would only be one additional lookup needed compared to e164.org, although with reasonable TTLs and caching in a smart manner this could alleviate similar penalties with additional lookups.
TODO
Currently we are using the same method to store RSA public keys in name servers as DNSSec, although if this was to be used beyond our own systems or for DNS lookups in general, there would need to be some way to distinguish which servers support encryption. A possible solution would be to return additional information on any KEY/DNSKEY record lookups indicating encryption is supported, to differentiate from DNSSec.
It would be ideal to store the public key for the next name server in glue records, just like A or AAAA records are, otherwise information would be leaked. If the request for the public key is requested or sent directly from the server itself the request, or response could be intercepted and the response received by the requestor could be the key of an attacker running a man in the middle attack.
Even though DNSSec won't prevent information leaking, it was designed to prevent man in the middle attacks from altering the information returned to the requestor.
Proof of Concept
Currently the proof of concept does everything up until decoding the DNS response.
#!/usr/bin/php -q
<?
$nl = "\n";
$pubkey = '-----BEGIN PUBLIC KEY-----'.$nl.
'MIICAjANBgkqhkiG9w0BAQEFAAOCAe8AMIIB6gKCAeEAorGbkc81xGWePkinYXbLM'.$nl.
'f/rDwepU/etd2ycfwsToxwnt6iHf7hO9ycjOmYpYG4uMG0PmYISaBiwWd55qpUUgI'.$nl.
'nhwVFMGX8KOe6c3nreQ9iKQWYu47A2QVk81qX3k5UvjY+Ab63Mg63GtA/DQ7ScI+S'.$nl.
'MJuPCkVlXaUy82xeR7zs1eGYI20/mcNoB/nM5OcSpkSJ0dPXFSTmC9DKLd0emUZX9'.$nl.
'PdTfz7iKzjWYSg5DOlJwnggeErzDeTHVPoHOa+jT5JkVHzX5wSR2oyN65/s5rswCf'.$nl.
'gjqi8lyO+zCSZiu9ZdtvFeLHcmDAkKh610iTdH8WeHt4HSp61yppQ/wnVqrguDms1'.$nl.
'ER5lNt70pB+qIZStyB4DXjlXK4PkEAWTwJRV8E0Ix7hbcJ9IuwVxUUbTqyCWRFVed'.$nl.
'dSVoMN9yI6eHKoGzFXRojncBRK0Rtq9F1yEKnP7lz1QB3XUdZ28ipW1Flg2zsWsKL'.$nl.
'PbKoyn5A0Q8TJRiXDpEkBhwwWaoaxDR1xy8UDOIWt/9Rf0sgjfjmN649gXqAnaysr'.$nl.
'WcpPO130W4u5SOyRZnQhGk2Jw+4hEub9c1/Fcf3la1BaMlMIzIJqfUwoTMRlakBFE'.$nl.
'faMmgFGT57Kfng+c7gb5ecipKVrMRXbjNdAgMBAAE='.$nl.
'-----END PUBLIC KEY-----'.$nl;
function bit_value($value, $bit)
{
if((ord($value) & (1 << (8 - $bit))) > 0)
return(1);
return(0);
}
function bit_values($value, $start, $num = false)
{
$size = strlen($value);
if($num === false)
$num = $size - $start;
$stop = $num;
$result = 0;
for($x = $start; $x<$size, $x<($start + $num); $x++)
$result |= bit_value($value, $x) << $num;
return $result>0?$result-1:0;
}
function byte_values($value, $start = false, $num = false)
{
$stop = strlen($value);
if($num === false)
$num = $stop - $start;
$ret = 0;
for($x = $start; $num > ($x - $start); $x++)
$result |= (ord($value{$x}) << 8 * (($num - ($x - $start))-1));
return $result;
}
function dns_parse($dnsreply, $verbose = false)
{
$queries = $answers = $auths = $additional = array();
$flags = '';
$edns0 = 0;
if($verbose == true)
{
$str = '';
for($i = 0; $i < strlen($dnsreply); $i++)
$str .= sprintf('%02x', ord($dnsreply{$i})).' ';
$str = wordwrap($str, 48);
echo $str."\n\n";
}
$qr = bit_value($dnsreply{2}, 2, 1);
if($qr) $flags .= ' qr';
$op = bit_values($dnsreply{2}, 2, 4);
$opcode = 'FAILED';
if($op == 0)
$opcode = 'OK';
$aa = bit_value($dnsreply{2}, 6);
$tc = bit_value($dnsreply{2}, 7);
$rd = bit_value($dnsreply{2}, 8);
if($qr) $flags .= ' rd';
$ra = bit_value($dnsreply{3}, 1);
if($qr) $flags .= ' ra';
$z = bit_value($dnsreply{3}, 2);
$aa2 = bit_value($dnsreply{3}, 3);
$rc = bit_values($dnsreply{3}, 5, 4);
$querys = byte_values($dnsreply, 4, 2);
$ans = byte_values($dnsreply, 6, 2);
$aths = byte_values($dnsreply, 8, 2);
$adds = byte_values($dnsreply, 10, 2);
$offset = 12;
for($i = 0; $i < $querys; $i++)
{
while(($nextlen = byte_values($dnsreply, $offset++, 1)) > 0)
{
if(isset($queries[$i]['hostname']))
$queries[$i]['hostname'] .= '.';
$queries[$i]['hostname'] .= substr($dnsreply, $offset, $nextlen);
$offset += $nextlen;
}
$queries[$i]['type'] = byte_values($dnsreply, $offset, 2);
$offset += 2;
$queries[$i]['class'] = byte_values($dnsreply, $offset, 2);
$offset += 2;
}
for($i = 0; $i < $ans; $i++)
{
if(byte_values($dnsreply, $offset, 2) == 49164)
{
$answers[$i]['hostname'] = $queries[0]['hostname'];
$offset += 2;
} else {
while(($nextlen = byte_values($dnsreply, $offset++, 1)) > 0)
{
if(isset($answers[$i]['hostname']))
$answers[$i]['hostname'] .= '.';
$answers[$i]['hostname'] .= substr($dnsreply,
$offset, $nextlen);
$offset += $nextlen;
}
}
$answers[$i]['type'] = byte_values($dnsreply, $offset, 2);
$offset += 2;
$answers[$i]['class'] = byte_values($dnsreply, $offset, 2);
$offset += 2;
$answers[$i]['ttl'] = byte_values($dnsreply, $offset, 4);
$offset += 4;
$datalength = byte_values($dnsreply, $offset, 2);
$offset += 2;
$off2 = 0;
$record = substr($dnsreply, $offset, $datalength);
$offset += $datalength;
if($answers[$i]['type'] == 1)
{
for($j = 0; $j <= 3; $j++)
{
if($j > 0)
$answers[$i]['data'] .= '.';
$answers[$i]['data'] .= ord($record{$j});
}
} elseif($answers[$i]['type'] == 2) {
while(($nextlen = byte_values($record, $off2++, 1)) > 0)
{
if(isset($answers[$i]['data']))
$answers[$i]['data'] .= '.';
$answers[$i]['data'] .= substr($record, $off2, $nextlen);
$off2 += $nextlen;
if(byte_values($record, $off2, 2) == 49164)
{
$answers[$i]['data'] .= '.'.
$queries[0]['hostname'];
$off2 += 2;
if($off2 >= ($datalength - 1))
break;
}
}
$answers[$i]['data'] .= '.';
$answers[$i]['data'] = trim($answers[$i]['data']);
} elseif($answers[$i]['type'] == 6) {
while(($nextlen = byte_values($record, $off2++, 1)) > 0)
{
if(isset($answers[$i]['data']))
$answers[$i]['data'] .= '.';
$answers[$i]['data'] .= substr($record, $off2, $nextlen);
$off2 += $nextlen;
if(byte_values($record, $off2, 2) == 49164)
{
$answers[$i]['data'] .= '.'.
$queries[0]['hostname'];
$off2 += 2;
if($off2 >= ($datalength - 1))
break;
}
}
$answers[$i]['data'] .= '. ';
$MB = '';
while(($nextlen = byte_values($record, $off2++, 1)) > 0)
{
if(isset($MB{0}))
$MB .= '.';
$MB .= substr($record, $off2, $nextlen);
$off2 += $nextlen;
if(byte_values($record, $off2, 2) == 49164)
{
$answers[$i]['data'] .= '.'.
$queries[0]['hostname'];
$off2 += 2;
if($off2 >= ($datalength - 1))
break;
}
}
$answers[$i]['data'] .= $MB.'. ';
$answers[$i]['data'] .= byte_values($record, $off2, 4).' ';
$off2 += 4;
$answers[$i]['data'] .= byte_values($record, $off2, 4).' ';
$off2 += 4;
$answers[$i]['data'] .= byte_values($record, $off2, 4).' ';
$off2 += 4;
$answers[$i]['data'] .= byte_values($record, $off2, 4).' ';
$off2 += 4;
$answers[$i]['data'] .= byte_values($record, $off2, 4);
$off2 += 4;
$answers[$i]['data'] = trim($answers[$i]['data']);
} elseif($answers[$i]['type'] == 15) {
$pref = byte_values($record, $off2, 2);
$off2 += 2;
while(($nextlen = byte_values($record, $off2++, 1)) > 0)
{
if(isset($answers[$i]['data']))
$answers[$i]['data'] .= '.';
$answers[$i]['data'] .= substr($record, $off2, $nextlen);
$off2 += $nextlen;
if(byte_values($record, $off2, 2) == 49164)
{
$answers[$i]['data'] .= '.'.
$queries[0]['hostname'];
$off2 += 2;
if($off2 >= ($datalength - 1))
break;
}
}
$answers[$i]['data'] = $pref.' '.trim($answers[$i]['data']).'.';
} elseif($answers[$i]['type'] == 16) {
while(($nextlen = byte_values($record, $off2++, 1)) > 0)
{
$answers[$i]['data'] .= '"'.substr($record,
$off2, $nextlen).'" ';
$off2 += $nextlen;
}
$answers[$i]['data'] = trim($answers[$i]['data']);
} elseif($answers[$i]['type'] == 25) {
$off2 += 2;
$proto = byte_values($record, $off2++, 1);
$alg = byte_values($record, $off2++, 1);
$answers[$i]['data'] = '??? '.$proto.' '.$alg.' '.
base64_encode(substr($record, $off2));
} elseif($answers[$i]['type'] == 28) {
for($j = 0; $j <= 15; $j += 2)
{
if($j > 0)
$answers[$i]['data'] .= ':';
$answers[$i]['data'] .= sprintf('%04x',
byte_values($record, $j, 2));
}
$answers[$i]['data'] = inet_ntop(inet_pton($answers[$i]['data']));
} elseif($answers[$i]['type'] == 35) {
$answers[$i]['data'] = byte_values($record, $off2, 2).' ';
$off2 += 2;
$answers[$i]['data'] .= byte_values($record, $off2, 2).' ';
$off2 += 2;
$flagslength = byte_values($record, $off2++, 1);
$answers[$i]['data'] .= '"'.substr($record,
$off2, $flagslength).'" ';
$off2 += $flagslength;
$srvlength = byte_values($record, $off2++, 1);
$answers[$i]['data'] .= '"'.substr($record,
$off2, $srvlength).'" ';
$off2 += $srvlength;
$regexlen = byte_values($record, $off2++, 1);
if($regexlen > 0)
{
$answers[$i]['data'] .= '"'.substr($record,
$off2, $regexlen).'" ';
$off2 += $regexlen;
}
$replacelen = byte_values($record, $off2++, 1);
if($replacelen > 0)
{
$answers[$i]['data'] .= '"'.substr($record,
$off2, $regexlen).'" ';
$off2 += $regexlen;
}
$answers[$i]['data'] = trim($answers[$i]['data']);
}
}
for($i = 0; $i < $aths; $i++)
{
}
for($i = 0; $i < $adds; $i++)
{
if(byte_values($dnsreply, $offset + 1, 2) == 41)
{
$edns0 = byte_values($dnsreply, $offset + 3, 2);
$offset += 11;
continue;
}
}
return(array($queries, $answers, $auths, $additional, $flags, $edns0));
}
dns_get_record('e164.org', DNS_NS, $NS, $additonal);
foreach($NS as $key => $host)
$servers[] = $host['target'];
$types = array(1 => 'A', 2 => 'NS', 6 => 'SOA', 15 => 'MX', 16 => 'TXT',
25 => 'KEY', 28 => 'AAAA', 29 => 'LOC', 33 => 'SRV',
35 => 'NAPTR', 255 => 'ANY');
$type = 1;
$hostnames = array();
foreach($argv as $key => $val)
{
if($key == 0)
continue;
$cmdline .= ' '.$val;
if($val{0} == '@')
{
if(!isset($server))
$server = substr($val, 1);
continue;
} else {
$val = strtoupper($val);
if(($key = array_search($val, $types)) !== FALSE)
$type = intval($key);
else
$hostnames[] = $val;
continue;
}
}
if(count($hostnames) <= 0)
die("No hostnames or domains given to lookup on the command line.\n");
$socket4 = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
socket_bind($socket4, '0.0.0.0', 0);
$socket6 = socket_create(AF_INET6, SOCK_DGRAM, SOL_UDP);
socket_bind($socket6, '::', 0);
$rnd = fopen("/dev/urandom", "r");
$urnd = fgets($rnd, 16);
fclose($rnd);
$td = mcrypt_module_open('rijndael-128', '', 'cbc', '');
$key = substr(hash('sha1', $urnd, 1), 0, mcrypt_enc_get_key_size($td));
if($server != '')
$servers = array($server);
$tmp6 = $tmp4 = array();
foreach($servers as $server)
{
$tmp6 = array_merge($tmp6, dns_get_record($server, DNS_AAAA, $NS2, $add2));
$tmp4 = array_merge($tmp4, dns_get_record($server, DNS_A, $NS2, $add2));
}
$IPs = array_merge($tmp6, $tmp4);
foreach($hostnames as $hostname)
{
$bytes = $compbytes = 0;
foreach($IPs as $IP)
{
if(!isset($IP['ip']) && isset($IP['ipv6']))
{
$IP['ip'] = $IP['ipv6'];
$socket = $socket6;
} else {
$socket = $socket4;
}
$server = $IP['host'];
$server_address = $IP['ip'];
$qryID = rand(0,65535);
$qry = chr($qryID >> 8).chr($qryID % 256).chr(1).chr(0).chr(0).
chr(1).chr(0).chr(0).chr(0).chr(0).chr(0).chr(1);
$bits = explode('.', $hostname);
foreach($bits as $bit)
$qry .= chr(strlen($bit)).$bit;
$qry .= chr(0).chr(0).chr($type).chr(0).chr(1).chr(0).chr(0).
chr(41).chr(16).chr(0).chr(0).chr(0).chr(0).
chr(0).chr(0).chr(0);
openssl_public_encrypt($key.$qry, $encrypted, $pubkey);
$strlen = strlen($encrypted) + 3;
$encrypted = chr($strlen >> 8).chr($strlen % 256).chr(255).
$encrypted;
$time_start = microtime(true);
if(($len = socket_sendto($socket, $encrypted,
strlen($encrypted), 0, $IP['ip'], 53)) <= 0)
continue;
$dnsreply = $read = '';
$strlen = 0;
do
{
$streams = array($socket);
if(@socket_select($streams, $write = NULL,
$except = NULL, 2) <= 0)
break;
socket_set_option($socket, SOL_SOCKET, SO_RCVTIMEO,
array("sec" => 0, "usec" => 1));
while(($flag = @socket_recv($socket, $buf, 4096, 0)) > 0)
{
$time_end = microtime(true);
$read .= $buf;
if($strlen == 0)
$strlen = (ord($buf{0}) << 8) + ord($buf{1});
if($strlen > 0 && strlen($read) >= $strlen)
break(2);
}
} while(1);
if($strlen == strlen($read))
{
$time = intval(($time_end - $time_start) * 1000);
$cipher_text = substr($read, 3);
$iv_size = mcrypt_enc_get_iv_size($td);
$iv = substr($cipher_text, 0, $iv_size);
$cipher_text = substr($cipher_text, $iv_size);
$plain = false;
if(mcrypt_generic_init($td, $key, $iv) != -1)
$plain = mdecrypt_generic($td, $cipher_text);
mcrypt_generic_deinit($td);
mcrypt_module_close($td);
$compbytes = strlen($plain);
if(($dnsreply = @gzinflate($plain)) == false)
$dnsreply = $plain;
break;
}
}
$bytes = strlen($dnsreply);
if(!isset($dnsreply{0}))
die('No valid DNS reply was received.'.$nl);
list($queries, $answers, $auths,
$additional, $flags, $edns0) = dns_parse($dnsreply);
$replyID = byte_values($dnsreply, 0, 2);
$output = '; <<>> PHP-DiG 0.0.1a <<>>'.$cmdline.$nl.
';; global options: printcmd'.$nl.
';; Got answer:'.$nl.
';; ->>HEADER<<- opcode: '.$opcode.', status: '.
$status.', id: '.$replyID.$nl.
';; flags: '.$flags.'; QUERY: '.count($queries).
', ANSWER: '.count($answers).
', AUTHORITY: '.count($auths).
', ADDITIONAL: '.count($additional).$nl;
if($replyID != $qryID)
$output .= ';; Mismatched query IDs, '.$qryID.' != '.$replyID.$nl;
$output .= $nl;
if($edns0 > 0)
$output .= ';; OPT PSEUDOSECTION:'.$nl.
'; EDNS: version: 0, flags:; udp: '.$edns0.$nl;
$output .= ';; QUESTION SECTION:'.$nl;
foreach($queries as $key => $query)
{
$type = $query['type'];
$output .= ';'.$query['hostname'].'. '.$query['ttl']."\t".
'IN '.$types[$type].$nl;
}
$output .= $nl.';; ANSWER SECTION:'.$nl;
foreach($answers as $key => $answer)
{
$type = $answer['type'];
$output .= $answer['hostname'].'. '.$answer['ttl']."\t".
'IN '.$types[$type]."\t".$answer['data'].$nl;
}
if(count($auths) > 0)
{
$output .= $nl.';; AUTHORITY SECTION:'.$nl;
foreach($auths as $key => $auth)
{
$type = $auth['type'];
$output .= $auth['hostname'].'. '.$auth['ttl']."\t".'IN '.
$types[$type]."\t".$auth['data'].$nl;
}
}
if(count($additional) > 0)
{
$output .= $nl.';; ADDITIONAL SECTION:'.$nl;
foreach($additional as $key => $add)
{
$type = $add['type'];
$output .= $add['hostname'].'. '.$add['ttl']."\t".'IN '.
$types[$type]."\t".$add['data'].$nl;
}
}
$output .= $nl.';; Query time: '.$time.' msec'.$nl.
';; SERVER: '.$server.'#53('.$server_address.')'.$nl.
';; WHEN: '.date('D M j G:i:s Y').$nl.
';; MSG SIZE rcvd: '.$bytes;
if($compbytes > 0 && $compbytes != $bytes)
$output .= ', compbytes: '.$compbytes;
$output .= $nl.$nl;
echo $output;
}
More information
The FastAGI scripts are a work in progress, there is a number of issues and we'd welcome help with spit and polishing.