#!/usr/bin/perl -w # (C) 2012 Peter Conrad # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License version 3 as # published by the Free Software Foundation. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . use strict; use Fcntl; use Digest::SHA qw(sha1 sha1_hex); use Crypt::OpenSSL::RSA; use MIME::Base64; $main::ttl = 432000; %main::keys = (); %main::names = (); @main::zones = (); %main::locs = (); $main::catchAllLoc = undef; while ($#ARGV >= 0) { if ($ARGV[0] eq '-h' || $ARGV[0] eq '--help') { &usage(); # usage does not return } elsif ($ARGV[0] eq '-t') { if ($#ARGV < 1) { &usage(); } $main::ttl = $ARGV[1]; shift @ARGV; } elsif ($ARGV[0] eq '-g') { if ($#ARGV != 5) { &usage(); } &genkey($ARGV[1], $ARGV[2], $ARGV[3], $ARGV[4], $ARGV[5]); # genkey does not return exit(0); } else { # keyfile &addKey($ARGV[0]); } shift @ARGV; } my $now = time(); while ($_ = ) { s/\s+$//s; if (/^:[^:]*:(43|46|48|50|51|6528[12]):/) { next; } if (/^$/ || /^-/ || /^#[^KDP]/) { print "$_\n"; next; } if (/^\./) { print "# Was: $_\n"; while (! /^\.([^:]*):([0-9.]*):([^:]+):(\d*):(\d*):([^:]*)$/) { $_ .= ':'; } my ($dom,$ip,$ns,$ttl,$ts,$lo) = /^\.([^:]*):([0-9.]*):([^:]+):(\d*):(\d*):([^:]*)$/; # &fqdn:ip:x:ttl:timestamp:lo print "\&$dom:$ip:$ns:$ttl:$ts:$lo\n"; $dom =~ tr/A-Z/a-z/; $ns =~ tr/A-Z/a-z/; if ($ns !~ /\./) { $ns .= ".ns.$dom"; } my $rec = &getOrCreateRRs($dom); $rec->addNS($ns, $ttl, $ts, $lo); &addA($ns, $ip, $ttl || 259200, $ts, $lo); if (exists($rec->{byType}->{6})) { next; } # don't create more than one SOA per zone # Zfqdn:mname:rname:ser:ref:ret:exp:min:ttl:timestamp:lo $_ = "Z$dom:$ns:hostmaster.${dom}:::::$ttl:$ts:$lo\n"; } if (/^Z/) { my @fields = split /:/; if ($#fields < 3 || $fields[3] !~ /^00/) { $fields[3] = $now; $_ = join(":", @fields); } } print "$_\n"; if (/^%(\w\w?):?/) { $main::locs{$1} = $'; if ($' eq "") { $main::catchAllLoc = $1; } } elsif (/^#[KDP]/) { if (/^#K([^:]*):(\d+):(\d*):(\d+):([^:]*):(\d*):(\d*):([^:]*)$/) { # C<#Kname:flags:proto:algorithm:key:ttl:timestamp:lo> my ($dom,$flags,$proto,$alg,$key,$ttl,$ts,$lo) = ($1, $2, $3, $4, $5, $6, $7, $8); $dom =~ tr/A-Z/a-z/; my $pubkey = decode_base64($key); my $rdata = &htons($flags).chr($proto).chr($alg).$pubkey; &makeGenericRecord($dom, 48, $rdata, $ttl, $ts, $lo); my $rrs = &getOrCreateRRs($dom); $rrs->addKey($flags, $alg, $pubkey); $rrs->addRecord(48, $rdata, $ttl, $ts, $lo); } elsif (/^#D([^:]*):(\d+):(\d+):(\d+):([0-9a-fA-F]*):(\d*):(\d*):([^:]*)$/) { # C<#Dname:tag:algorithm:digest:fingerprint:ttl:timestamp:lo> my ($dom,$tag,$alg,$dig,$fp,$ttl,$ts,$lo) = ($1, $2, $3, $4, $5, $6, $7, $8); $dom =~ tr/A-Z/a-z/; my $rdata = &htons($tag).chr($alg).chr($dig).pack("H*", $fp); &makeGenericRecord($dom, 43, $rdata, $ttl, $ts, $lo); &getOrCreateRRs($dom)->addRecord(43, $rdata, $ttl, $ts, $lo); } elsif (/^#P([^:]*):(\d+):(\d+):(\d+):(\d*):([0-9a-fA-F]*):(\d*):(\d*):([^:]*)$/) { # C<#Pname:algorithm:flags:iter:len:salt:ttl:timestamp:lo> my ($dom,$alg,$flag,$iter,$len,$salt,$ttl,$ts,$lo) = ($1, $2, $3, $4, $5, $6, $7, $8, $9); $dom =~ tr/A-Z/a-z/; if ($salt eq "") { if ($len eq "") { $len = 4; } open(RANDOM, "addNS3P($alg, $flag, $iter, $salt, $ttl, $ts, $lo); $rrs->addRecord(51, $rdata, $ttl, $ts, $lo); } else { print STDERR "Warning: ignored incomplete pseudo record '$_'!\n"; } next; } my $type = substr($_, 0, 1); if ($type !~ /^[.\&=+\@'^CZ:36]/) { print STDERR "Warning: ignored unknown record type '$type'\n"; next; } my @stuff = split(/:/, substr($_, 1)); if ($#stuff < 0) { print STDERR "Warning: ignored empty record '$_'\n"; next; } my $dom = shift @stuff; $dom =~ tr/A-Z/a-z/; my $rec = &getOrCreateRRs($dom); if ($type eq '&') { # &fqdn:ip:x:ttl:timestamp:lo my $ns = $stuff[1]; if ($ns !~ /\./) { $ns .= ".ns.$dom"; } $rec->addNS($ns, $stuff[2], $stuff[3], $stuff[4]); &addA($ns, $stuff[0], $stuff[2] || 259200, $stuff[3], $stuff[4]); } elsif ($type eq '=') { # =fqdn:ip:ttl:timestamp:lo $rec->addA($stuff[0], $stuff[1], $stuff[2], $stuff[3]); &addPTR($stuff[0], $dom, $stuff[1], $stuff[2], $stuff[3]); } elsif ($type eq '+') { # +fqdn:ip:ttl:timestamp:lo $rec->addA($stuff[0], $stuff[1], $stuff[2], $stuff[3]); } elsif ($type eq '6') { # =fqdn:ip6:ttl:timestamp:lo $rec->addAAAA($stuff[0], $stuff[1], $stuff[2], $stuff[3]); &addPTR($stuff[0], $dom, $stuff[1], $stuff[2], $stuff[3]); } elsif ($type eq '3') { # +fqdn:ip6:ttl:timestamp:lo $rec->addAAAA($stuff[0], $stuff[1], $stuff[2], $stuff[3]); } elsif ($type eq '@') { # @fqdn:ip:x:dist:ttl:timestamp:lo my $mx = $stuff[1]; if ($mx !~ /\./) { $mx .= ".mx.$dom"; } $rec->addMX($mx, $stuff[2], $stuff[3], $stuff[4], $stuff[5]); &addA($mx, $stuff[0], $stuff[3], $stuff[4], $stuff[5]); } elsif ($type eq "'") { # 'fqdn:s:ttl:timestamp:lo $rec->addTXT(&parseData($stuff[0]), $stuff[1], $stuff[2], $stuff[3]); } elsif ($type eq '^') { # ^fqdn:p:ttl:timestamp:lo $rec->addPTR($stuff[0], $stuff[1], $stuff[2], $stuff[3]); } elsif ($type eq 'C') { # Cfqdn:p:ttl:timestamp:lo $rec->addCNAME($stuff[0], $stuff[1], $stuff[2], $stuff[3]); } elsif ($type eq 'Z') { # Zfqdn:mname:rname:ser:ref:ret:exp:min:ttl:timestamp:lo $rec->addSOA($stuff[0], $stuff[1], $stuff[2], $stuff[3], $stuff[4], $stuff[5], $stuff[6], $stuff[7], $stuff[8], $stuff[9]); } elsif ($type eq ':') { # :fqdn:n:rdata:ttl:timestamp:lo $rec->addRecord($stuff[0], &parseData($stuff[1]), $stuff[2], $stuff[3], $stuff[4]); } } foreach my $rec (values %main::names) { my $dom = $rec->{name}; if (exists($rec->{byType}->{48})) { if (!exists($rec->{byType}->{6})) { print STDERR "Warning: useless DNSKEY for $dom without SOA!\n"; } elsif (!exists($rec->{byType}->{51})) { print STDERR "ERROR: DNSKEY for $dom without NSEC3PARAM!\n"; exit(1); } } elsif (exists($rec->{byType}->{51})) { print STDERR "Warning: useless NSEC3PARAM for $dom without DNSKEY!\n"; } &findControl($rec); } foreach my $zone (@main::zones) { my $rec = $main::names{$zone}; if (!exists($rec->{keys})) { print STDERR "Info: ignore unsigned zone $zone\n"; foreach my $dom (keys %{$rec->{zone}}) { delete $main::names{$dom}; } next; } if (!exists($rec->{nsec3p})) { next; } # # Add NSEC3 RRs to zone my %hashes = (); my $n3p = $rec->{nsec3p}->[0]; # Generate hashes foreach my $name (keys %{$rec->{zone}}) { my $rr; LOOP: { do { $rr = &getOrCreateRRs($name); if (exists($rr->{hash})) { last; } $rec->{zone}->{$name} = $rr; my $hash = &nsec3hash($name, $n3p->{iterations}, $n3p->{salt}); if (exists($hashes{$hash})) { print STDERR "ERROR: hash collision on $name!?\n"; exit(1); } $hashes{$hash} = { hash => $hash, name => &base32hex($hash).".".$zone, generator => $name, zone => $zone }; $rr->{hash} = $hash; if ($name eq $zone) { last; } if ($name =~ /^[^.]*\./) { $name = $'; } } while ($rr->{name} ne $zone); } } # Find next hash in hash sort order my ($first, $prev); my @byNibble = (); my $prevNib = -1; foreach my $hash (sort keys %hashes) { my $nib = ord($hash) >> 4; if (!defined($byNibble[$nib])) { while ($prevNib < $nib) { $byNibble[++$prevNib] = defined($prev) ? [$prev->{hash}] : []; } } push @{$byNibble[$nib]}, $hash; if (!$first) { $first = $prev = $hashes{$hash}; next; } $prev->{next} = $hash; $prev = $hashes{$hash}; } $prev->{next} = $first->{hash}; unshift @{$byNibble[0]}, $prev->{hash}; for (my $nib = 1; $#{$byNibble[$nib]} < 0; $nib++) { unshift @{$byNibble[$nib]}, $prev->{hash}; } for (my $nib = $#byNibble + 1; $nib < 16; $nib++) { $byNibble[$nib] = [$prev->{hash}]; } # Generate records foreach my $hash (values %hashes) { my $rRec = &getOrCreateRRs($hash->{name}); my $gRec = &getOrCreateRRs($hash->{generator}); $rec->{zone}->{$hash->{name}} = $rRec; my $rdata = chr($n3p->{alg}).chr($n3p->{flags}) .&htons($n3p->{iterations}) .chr(length($n3p->{salt})).$n3p->{salt} .chr(length($hash->{next})).$hash->{next} .&genTypeBitmaps(keys %{$gRec->{byType}}); print "# NSEC3: ".$hash->{name}." - ".&base32hex($hash->{next})." (" .join(" ", sort keys %{$gRec->{byType}}).")\n"; $rRec->addRecord(50, $rdata, $n3p->{ttl}, $n3p->{ts}, $n3p->{lo}); &makeGenericRecord($hash->{name}, 50, $rdata, $n3p->{ttl}, $n3p->{ts}, $n3p->{lo}); print "# H(".$hash->{generator}.") -> ".$hash->{name}."\n"; #$gRec->addRecord(65281, &wireName($hash->{name}), $n3p->{ttl}, $n3p->{ts}, $n3p->{lo}); &makeGenericRecord($hash->{generator}, 65281, &wireName($hash->{name}), $n3p->{ttl}, $n3p->{ts}, $n3p->{lo}); } for (my $nib = 0; $nib < 16; $nib++) { my $name = sprintf("%x", $nib).".$zone"; my $rec = &getOrCreateRRs($name); $rec->addRecord(65282, join("", @{$byNibble[$nib]}), $n3p->{ttl}, $n3p->{ts}, $n3p->{lo}); &makeGenericRecord($name, 65282, join("", @{$byNibble[$nib]}), $n3p->{ttl}, $n3p->{ts}, $n3p->{lo}); } # Add RRSIGs to zone my (@zsks, @ksks); if (!$rec->{haveSEP} || !$rec->{haveZSK}) { @zsks = @ksks = @{$rec->{keys}}; } else { @ksks = grep { $_->{flags} & 1 } @{$rec->{keys}}; @zsks = grep { !($_->{flags} & 1) } @{$rec->{keys}}; } my $validFrom = $now - 3600; my $validUntil = $now + $main::ttl; foreach my $owner (values %{$rec->{zone}}) { my $name = $owner->{name}; my $isCutpoint = exists($owner->{byType}->{2}) && $name ne $zone; foreach my $type (keys %{$owner->{byType}}) { if ($isCutpoint && $type != 43 && $type != 50) { next; } my $labels = $name; $labels =~ s/^\*\.//; $labels =~ s/[^.]+//g; $labels = length($labels) + 1; my $ttl = $owner->{byType}->{$type}->[0]->{ttl} || 86400; foreach my $key ($type == 48 ? @ksks : @zsks) { my $keytag = $key->{key}->{basetag} + $key->{alg} + $key->{flags}; $keytag = ($keytag + ($keytag >> 16)) & 0xffff; my $rdata = &htons($type).chr($key->{alg}).chr($labels) .&htonl($ttl).&htonl($validUntil) .&htonl($validFrom) .&htons($keytag).&wireName($zone); my $toSign = $rdata; foreach my $rr (sort { $a->{rdata} cmp $b->{rdata} } @{$owner->{byType}->{$type}}) { $toSign .= &wireName($name).&htons($type)."\0\1".&htonl($ttl) .&htons(length($rr->{rdata})).$rr->{rdata}; } $key->{key}->{key}->use_pkcs1_padding(); if ($key->{alg} == 7) { $key->{key}->{key}->use_sha1_hash(); } elsif ($key->{alg} == 8) { $key->{key}->{key}->use_sha256_hash(); } elsif ($key->{alg} == 10) { $key->{key}->{key}->use_sha512_hash(); } else { print STDERR "Unsupported key type ".$key->{alg}."\n"; exit 1; } my $sig = $key->{key}->{key}->sign($toSign); if (!$sig || !$key->{key}->{key}->verify($toSign, $sig)) { print STDERR "Failed to sign $name ($type)!?\n"; exit 1; } #$key->{key}->{key}->use_no_padding(); #print "# ".unpack("H*", $key->{key}->{key}->public_decrypt($sig))."\n"; $rdata .= $sig; print "# RRSIG $type ".$key->{alg}." $labels $ttl $validUntil $validFrom " ."$keytag $name\n"; &makeGenericRecord($name, 46, $rdata, $ttl, "", ""); } } } } exit(0); package Records; sub new { my ($class, $dom) = @_; $dom =~ tr/A-Z/a-z/; my $self = { name => $dom, byType => {}, haveSEP => 0, haveZSK => 0, locs => {}, zone => {} }; $self->{zone}->{$dom} = $self; bless $self, $class; return $self; } sub addKey { my ($self, $flags, $alg, $key) = @_; if (!($flags & 0x100)) { return; } if (!exists($main::keys{$key})) { print STDERR "ERROR: encountered DNSKEY pseudo record without a matching key:\n"; print STDERR "$_\n"; exit 1; } if ($alg != 7 && $alg != 8 && $alg != 10) { print STDERR "Warning: ignoring DNSKEY with unsupported algorithm $alg\n"; return; } my $entry = { flags => $flags, alg => $alg, pubkey => $key, key => $main::keys{$key} }; if (exists($self->{keys})) { foreach my $other (@{$self->{keys}}) { if ($other->{flags} == $flags && $other->{alg} == $alg && $other->{pubkey} eq $key) { return; } } push @{$self->{keys}}, $entry; } else { $self->{keys} = [$entry]; } if ($flags & 1) { $self->{haveSEP} = 1; } else { $self->{haveZSK} = 1; } } sub addNS3P { my ($self, $alg, $flag, $iter, $salt, $ttl, $ts, $lo) = @_; if ($flag ne "0") { print STDERR "Warning: don't know about NSEC3 flags values != 0, ignoring\n"; } if ($iter > 150) { print STDERR "Warning: large iteration count $iter may lead to problems\n"; } if ($alg ne "1") { print STDERR "ERROR: NSEC3 algorithm $alg unknown. Please use algorithm 1.\n"; exit 1; } my $entry = { flags => $flag, alg => $alg, iterations => $iter, salt => pack("H*", $salt), ttl => $ttl, ts => $ts, lo => $lo }; if (exists($self->{nsec3p})) { print STDERR "Warning: ignoring additional NSEC3PARAM for ".$self->{name}."\n"; # foreach my $other (@$self->{nsec3p}) { # if ($other->{flags} == $flag && $other->{alg} == $alg # && $other->{iterations} == $iter # && $other->{salt} eq $entry->{salt}) { # return; # } # } # push @$self->{nsec3p}, $entry; } else { $self->{nsec3p} = [$entry]; } } sub addRecord { my ($self, $type, $data, $ttl, $ts, $lo) = @_; if (!defined($lo)) { $lo = ""; } if (!defined($ttl) || $ttl eq "") { $ttl = 86400; } $self->{locs}->{$lo} = 1; my $entry = { type => $type, rdata => $data, ttl => $ttl, ts => $ts, lo => $lo }; if (!exists($self->{byType}->{$type})) { $self->{byType}->{$type} = [$entry]; return; } foreach my $other (@{$self->{byType}->{$type}}) { if ($other->{rdata} eq $data && ($other->{lo} eq $lo || $lo eq "" || $other->{lo} eq "") && (($other->{ttl} eq "0") != ($ttl eq "0"))) { print STDERR "Warning: duplicate record ".$self->{name}.":$type:...\n"; return; } if (($other->{lo} eq $lo || $lo eq "" || $other->{lo} eq "") && (($other->{ttl} eq "0") == ($ttl eq "0")) && $other->{ttl} ne $ttl) { print STDERR "Warning: ttl mismatch for ".$self->{name}.":$type:...\n"; return; } } push @{$self->{byType}->{$type}}, $entry; } sub addA { my ($self, $ip, $ttl, $ts, $lo) = @_; if ($ip !~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/) { print STDERR "Warning: ignoring unparsable IPv4 address '$ip'\n"; return; } $self->addRecord(1, pack("C*", $1, $2, $3, $4), $ttl, $ts, $lo); } sub addNS { my ($self, $ns, $ttl, $ts, $lo) = @_; $self->addRecord(2, &toDomain($ns), $ttl || 259200, $ts, $lo); } sub addCNAME { my ($self, $nm, $ttl, $ts, $lo) = @_; $self->addRecord(5, &toDomain($nm), $ttl, $ts, $lo); } sub addSOA { my ($self, $ns, $hm, $ser, $ref, $ret, $exp, $min, $ttl, $ts, $lo) = @_; $self->addRecord(6, &toDomain($ns).&toDomain($hm) .pack("N*", $ser || 1, $ref || 16384, $ret || 2048, $exp || 1048576, $min || 2560), $ttl || 2560, $ts, $lo); } sub addPTR { my ($self, $nm, $ttl, $ts, $lo) = @_; $self->addRecord(12, &toDomain($nm), $ttl, $ts, $lo); } sub addMX { my ($self, $mx, $dist, $ttl, $ts, $lo) = @_; $self->addRecord(15, pack("n", $dist || 0).&toDomain($mx), $ttl, $ts, $lo); } sub addTXT { my ($self, $txt, $ttl, $ts, $lo) = @_; my $cstr = ""; while ((my $l = length($txt)) > 0) { if ($l > 255) { $l = 255; } $cstr .= chr($l).substr($txt, 0, $l); $txt = substr($txt, $l); } $self->addRecord(16, $cstr, $ttl, $ts, $lo); } sub addAAAA { my ($self, $ip, $ttl, $ts, $lo) = @_; if ($ip !~ /^[0-9a-f]{32}$/i) { print STDERR "Warning: ignoring unparsable IPv6 address '$ip'\n"; return; } $self->addRecord(28, pack("H*", $ip), $ttl, $ts, $lo); } sub toDomain { my $name = shift; $name =~ tr/A-Z/a-z/; my $res = ""; while ($name =~ /^([^.]*)\./) { $name = $'; $res .= chr(length($1)).$1; } return $res.chr(length($name)).$name."\0"; } package main; sub usage { print STDERR "$0 -g \n"; print STDERR " or\n"; print STDERR "$0 [-t ] [ ...] output\n"; exit(1); } sub findControl { my $rec = shift; if (exists($rec->{control})) { return; } my $dom = $rec->{name}; my $soa = exists($rec->{byType}->{6}); if ($soa) { $rec->{control} = $dom; $rec->{zone}->{$dom} = $rec; push @main::zones, $dom; return; } my $anc = $dom; while ($anc =~ /^[^.]*\./) { $anc = $'; if ($dom !~ /^\*\./ && exists($main::names{"*.$anc"}) && !exists($main::names{"*.$dom"})) { print STDERR "Warning: wildcard *.$anc shadowed by $dom (see RFC-1034 sect. 4.3.3)!\n"; } if (exists($main::names{$anc})) { my $ctl_rec = $main::names{$anc}; &findControl($ctl_rec); if (!exists($ctl_rec->{control})) { if (!$soa) { print STDERR "Warning: Out-of-bailiwick name '$dom' (oob parent)\n"; } return; } my $control = $ctl_rec->{control}; if ($ctl_rec->{byType}->{2}) { $control = $anc; } $ctl_rec = $main::names{$control}; if (!$ctl_rec->{byType}->{6}) { # Most likely glue... if (!exists($rec->{byType}->{1}) && !exists($rec->{byType}->{28})) { print STDERR "Warning: Out-of-bailiwick name '$dom' (below subdelegation)\n"; } } else { $rec->{control} = $control; $ctl_rec->{zone}->{$dom} = $rec; } return; } } print STDERR "Warning: Out-of-bailiwick name '$dom' (no parent)\n"; } sub parseData { my $in = shift; my $res = ""; while ($in =~ /^(.*?)\\(\d\d\d)/) { $in = $'; $res .= $1.chr(oct($2)); } return $res.$in; } sub makeGenericRecord { my ($dom, $type, $rdata, $ttl, $ts, $lo) = @_; print ":$dom:$type:"; while (length($rdata)) { my $char = substr($rdata, 0, 1); $rdata = substr($rdata, 1); if ($char =~ /[0-9a-zA-Z +*.,\/=<>\@\$-]/) { print $char; } else { printf "\\%03o", ord($char); } } print ":$ttl:$ts:$lo\n"; } sub getOrCreateRRs { my $dom = shift; $dom =~ tr/A-Z/a-z/; if (!exists($main::names{$dom})) { $main::names{$dom} = new Records($dom); } return $main::names{$dom}; } sub htons { my $n = shift; return chr($n >> 8).chr($n & 0xff); } sub htonl { my $n = shift; return &htons($n >> 16).&htons($n & 0xffff); } sub wireName { my $name = shift; my $wire = ""; while ($name =~ /^([^.]+)/) { $wire .= chr(length($1)).$1; $name = ($name eq $1) ? "" : substr($name, length($1) + 1); } return $wire."\0"; } sub nsec3hash { my ($name, $iter, $salt) = @_; my $dig = &wireName($name); while ($iter-- >= 0) { $dig = sha1($dig.$salt); } return $dig; } sub base32hex { my $data = shift; my $buf = 0; my $bits = 0; my $res = ""; while (length($data) > 0) { $buf = ($buf << 8) | ord($data); $data = substr($data, 1); $bits += 8; while ($bits >= 5) { my $dig = ($buf >> ($bits-5)) & 0x1f; $bits -= 5; if ($dig < 10) { $res .= $dig; } else { $res .= chr($dig + 87); } } } if ($bits > 0) { $buf <<= (5 - $bits); $buf &= 0x1f; if ($buf < 10) { $res .= $buf; } else { $res .= chr($buf + 87); } $bits += 3; while ($bits > 0) { if ($bits < 5) { $bits += 8; } else { $res .= "="; $bits -= 5; } } } return $res; } sub tai2unix { my $ts = shift; $ts =~ s/^40*//; return "0x$ts" - 10; } sub genTypeBitmaps { my %windows = (); if ($#_ > 0 || $#_ == 0 && $_[0] != 2) { # The type bitmaps includes all types except those contributed by NSEC3 # itself, including the signature for the NSEC3. I. e. if there's at # least one record here (except for a single NS delegation), there'll # also be an RRSIG here, eventually. push @_, 46; @_ = sort @_; } foreach my $type (@_) { my $window = $type >> 8; $type &= 0xff; if (exists($windows{$window})) { push @{$windows{$window}}, $type; } else { $windows{$window} = [$type]; } } my $tbm = ""; foreach my $window (sort keys %windows) { my $wbm = "\0" x 32; foreach my $type (@{$windows{$window}}) { substr($wbm, $type >> 3, 1) = chr(ord(substr($wbm, $type >> 3, 1)) | 1 << (7 - ($type & 7))); } $wbm =~ s/\0+$//; $tbm .= chr($window).chr(length($wbm)).$wbm; } return $tbm; } sub gen_pubkey_data { my ($n, $e, $flags, $alg, $dom) = @_; my $e_bin = $e->to_bin(); $e_bin =~ s/^\0+//; my $n_bin = $n->to_bin(); $n_bin =~ s/^\0+//; my $pubkey = chr(length($e_bin)).$e_bin.$n_bin; my $keytag = $flags + $alg + 3 * 256; # protocol is always 3 for (my $i = length($pubkey) - 1; $i >= 0; $i--) { $keytag += ord(substr($pubkey, $i)) * (($i & 1) ? 1 : 256); } if ($alg > 0) { $keytag += $keytag >> 16; $keytag &= 0xffff; } $dom =~ tr/A-Z/a-z/; my $digest = sha1_hex(&wireName($dom).&htons($flags).chr(3).chr($alg).$pubkey); return ($keytag, $pubkey, $digest); } sub addA { my ($dom, $ip, $ttl, $ts, $lo) = @_; if ($ip eq "") { return; } my $rec = &getOrCreateRRs($dom); $rec->addA($ip, $ttl, $ts, $lo); } sub addPTR { my ($ip, $dom, $ttl, $ts, $lo) = @_; my $arpa; if ($ip =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/) { $arpa = "$4.$3.$2.$1.in-addr.arpa"; } elsif ($ip =~ /^[0-9a-f]{32}$/i) { $ip =~ tr/A-Z/a-z/; $arpa = "ip6.arpa"; while ($ip =~ /^./) { $ip = $'; $arpa = "$&.$arpa"; } } else { print STDERR "Warning: ignoring unparsable IP address '$ip'\n"; return; } my $rec = getOrCreateRRs($arpa); $rec->addPTR($dom, $ttl, $ts, $lo); } sub addKey { my ($file) = @_; open(KEYFILE, "<$file") or die("Can't read $file either:"); my $rsa = Crypt::OpenSSL::RSA->new_private_key(join("", )); close KEYFILE; if (!$rsa) { print STDERR "Failed to read key from $file!\n"; exit(1); } my ($n, $e, $x1, $x2, $x3, $x4, $x5, $x6) = $rsa->get_key_parameters(); $x1 = $x2 = $x3 = $x4 = $x5 = $x6 = 0; # get rid of private stuff my ($keytag, $keydata, $fp) = &gen_pubkey_data($n, $e, 0, 0, ""); $main::keys{$keydata} = { key => $rsa, basetag => $keytag }; } sub genkey { my ($bits, $flags, $alg, $dom, $file) = @_; if ($bits < 1024 || $bits > 4096) { print STDERR "ERROR: Keys of less than 1024 or more than 4096 bits are not supported!\n"; exit 1; } if ($alg != 7 && $alg != 8 && $alg != 10) { print STDERR "ERROR: $0 only supports algorithms 7 (RSA-SHA1), 8 (RSA-SHA256)\nand 10 (RSA-SHA512).\n"; exit 1; } if (sysopen(KEYFILE, $file, O_CREAT|O_EXCL|O_WRONLY, 0600)) { my $rsa = Crypt::OpenSSL::RSA->generate_key($bits); print KEYFILE $rsa->get_private_key_string(); my ($n, $e, $x1, $x2, $x3, $x4, $x5, $x6) = $rsa->get_key_parameters(); $x1 = $x2 = $x3 = $x4 = $x5 = $x6 = 0; # get rid of private stuff my ($keytag, $keydata, $fp) = &gen_pubkey_data($n, $e, $flags, $alg, $dom); print KEYFILE "#K$dom:$flags:3:$alg:".encode_base64($keydata, "").":::\n"; print KEYFILE "#D$dom:$keytag:$alg:1:${fp}:::\n"; close KEYFILE; } else { print STDERR "Warning: couldn't create $file: $!\n"; print STDERR "Attempting to read key...\n"; open(KEYFILE, "<$file") or die("Can't read $file either:"); my $rsa = Crypt::OpenSSL::RSA->new_private_key(join("", )); close KEYFILE; if (!$rsa) { print STDERR "Failed to read key from $file!\n"; exit(1); } my ($n, $e, $x1, $x2, $x3, $x4, $x5, $x6) = $rsa->get_key_parameters(); $x1 = $x2 = $x3 = $x4 = $x5 = $x6 = 0; # get rid of private stuff my ($keytag, $keydata, $fp) = &gen_pubkey_data($n, $e, $flags, $alg, $dom); print "#K$dom:$flags:3:$alg:".encode_base64($keydata, "").":::\n"; print "#D$dom:$keytag:$alg:1:${fp}:::\n"; } exit 0; } =pod =head1 NAME tinydns-sign - Signs records in L files =head1 SYNOPSIS tinydns-sign -g bits flags algorithm domain keyfile tinydns-sign [-t ttl] [keyfile ...] outfile =head1 DESCRIPTION The first form is used to generate a public/private RSA key pair with a modulus of length I. If F exists, tinydns-sign will try to read a private key from the file and print DS and DNSKEY pseudo-records for the corresponding public key on stdout. If F does not exist, tinydns-sign will generate a new key pair and write the key plus the corresponding pseudo-records to F. In the second form, tinydns-sign reads key pairs from each given F. It then reads a L file from STDIN and writes the same file to STDOUT, with the following modifications: =over =item * It will delete all generic records with RRTYPE DS (43), RRSIG (46), DNSKEY (48), NSEC3(50), NSEC3PARAM(51) and private types 65281 and 65282. =item * It will turn each . record into a Z record and a & record. =item * It will adjust the serial number of all Z records to the current time, unless the serial number begins with two zeroes. Note that an SOA must have a fixed serial for generating a matching RRSIG record. =item * It will create new DS, DNSKEY and NSEC3PARAM records from each corresponding pseudo record (see below) present in the file. =item * It will create NSEC3 records for all names in all zones that have at least one DNSKEY and NSEC3PARAM in the file. =item * It will create a generic record with type 65281 for each name (including empty non-terminals) containing the owner of its matching NSEC3 RR. =item * It will create generic records with type 65282 for each hex digit (i. e. 0-9a-f) below the zone apex containing all NSEC3 hashes starting with that digit. =item * It will create RRSIG records for all RR-sets in all zones that have at least one DNSKEY in the file. If both DNSKEYS with and without the SEP flag set are present, then those with the SEP flag will be used only for RRSIGs on DNSKEY RRs and those without the SEP flag will be used for the remaining RR-sets. Otherwise, RRSIGs will be created using all DNSKEYs. RRSIGs will be valid beginning one hour in the past and ending at (now + I) seconds. I defaults to 432000 (5 days). =back =head2 Pseudo-Records Pseudo-records are records defined in a syntax that's only understood by tinydns-sign. To L they look like comments, i. e. they are ignored. tinydns-sign will create one or more generic records for each pseudo-record. All generic records with an RR-type for which a pseudo-record can be defined are deleted from the input. (Otherwise, removing a pseudo-record would not result in removal of the corresponding generic record.) In contrast to standard tinydns-data behaviour, trailing colons in pseudo-records are B optional. Currently, pseudo-records are defined for the following RR-types: =over =item * #Kname:flags:proto:algorithm:key:ttl:timestamp:lo This generates a DNSKEY record for I. I. I and I are decimal numbers. At the time of writing, I must be 3. tinydns-sign only supports Is 7 (RSA-SHA1), 8 (RSA-SHA256) and 10 (RSA-SHA512). I is base-64 encoded key material, depending on the selected I. I, I and I are as usual. It is an error to have a DNSKEY pseudo-record in the input without a corresponding F containing the matching private key. =item * #Dname:tag:algorithm:digest:fingerprint:ttl:timestamp:lo This generates a DS record for I. I is the key tag, I specifies the algorithm of the referenced key and I is the digest type (all in decimal). I is the hex-encoded actual digest value (omitting leading/trailing zeroes is not permitted!). I, I and I are as usual. =item * #Pname:algorithm:flags:iter:len:salt:ttl:timestamp:lo This generates an NSEC3PARAM record for I with the given I, I, Iation count, salt Igth and I. If I is empty, a new random salt with the given salt Igth (4 bytes if I is empty) will be generated. If I is non-empty, it must be a string of hex digits with even length. The salt length is derived from the given salt value, i. e. I is ignored in that case. I, I and I are as usual. tinydns-sign currently only supports I 1 (SHA-1). At the time of writing, I is defined to be 0, and the Iation count is limited depending on the key length (see L). =back =head1 EXIT STATUS tinydns-sign will exit with status 0 if it thinks all went well. Warning messages will not trigger a nonzero exit status. tinydns-sign will exit with nonzero status if an error occurred. In this case, the output is most likely incomplete and should not be used to replace an input file. =head1 SEE ALSO L, L, L, L =head1 LIMITATIONS =over =item * Location code handling is incomplete in that location codes must be present for all RRs in a zone, or for none at all. =item * Timestamps are currently mostly ignored, i. e. signatures will happily outlive the RR-sets which they sign. =item * It is currently not possible to protect the private keys with a passphrase. =item * It is not possible to have a signed zone and a signed child zone in the same data file. =item * NSEC3 RRs with Opt-Out child zones are not supported. =item * The pseudo-RRs with type 65282 contain a list of hash values. The list cannot grow bigger than 65kBytes (about 3270 hashes). This is not a problem for a typical domain, but it would be a problem if tinydns were to serve the .de zone, for example. Also, the list is searched sequentially, which can cause a performance impact long before this limit is reached. =back =head1 CAVEATS =over =item * The system clock should be reasonably close to UTC (i. e. within a few minutes). =item * Stock tinydns/axfrdns will happily work with signed data.cdb files, but they will not produce correct DNSSEC responses! =item * If a zone contains both keys with and without the SEP flag, you must make sure that both key sets cover the same set of algorithms. This is due to a requirement in RFC-4035 section 2.2. =back =head1 AUTHOR (C) 2012 Peter Conrad L This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License version 3 as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . =cut