package Echolot::Conf; # # $Id$ # # This file is part of Echolot - a Pinger for anonymous remailers. # # Copyright (c) 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2012, 2014 Peter Palfrader # # This program is free software. you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # 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, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # =pod =head1 Name Echolot::Conf - remailer Configuration/Capabilities =head1 DESCRIPTION This package provides functions for requesting, parsing, and analyzing remailer-conf and remailer-key replies. =head1 CAVEATS When parsing OpenPGP keys only the address of the primary user id is taken into account (This is the one with the latest self signature I think). =cut use strict; use Echolot::Log; use GnuPG::Interface; sub is_not_a_remailer($) { my ($reply) = @_; if ($reply =~ /^\s* not \s+ a \s+ remailer\b/xi) { return 1; } else { return 0; }; }; sub send_requests($;$) { my ($scheduled_for, $which) = @_; $which = '' unless defined $which; my $call_intervall = Echolot::Config::get()->{'getkeyconf_interval'}; my $send_every_n_calls = Echolot::Config::get()->{'getkeyconf_every_nth_time'}; my $timemod = int ($scheduled_for / $call_intervall); my $this_call_id = $timemod % $send_every_n_calls; my $session_id = int ($scheduled_for / ($call_intervall * $send_every_n_calls)); Echolot::Globals::get()->{'storage'}->delay_commit(); for my $remailer (Echolot::Globals::get()->{'storage'}->get_addresses()) { next unless ($remailer->{'status'} eq 'active'); next unless ($remailer->{'fetch'}); my $address = $remailer->{'address'}; next unless ( $which eq 'all' || $which eq $address || $which eq ''); for my $type (qw{conf key help stats adminkey}) { next unless ( $which eq $address || $which eq 'all' || (($which eq '') && ($this_call_id == (Echolot::Tools::makeShortNumHash($address.$type.$session_id) % $send_every_n_calls)))); Echolot::Log::debug("Sending $type request to ".$address."."); my $source_text = Echolot::Config::get()->{'remailerxxxtext'}; my $template = HTML::Template->new( scalarref => \$source_text, strict => 0, global_vars => 1 ); $template->param ( address => $address ); $template->param ( operator_address => Echolot::Config::get()->{'operator_address'} ); my $body = $template->output(); Echolot::Tools::send_message( 'To' => $address, 'Subject' => 'remailer-'.$type, 'Token' => $type.'.'.$remailer->{'id'}, 'Body' => $body); Echolot::Globals::get()->{'storage'}->decrease_ttl($address) if (($type eq 'conf') && ($which eq '')); }; }; Echolot::Globals::get()->{'storage'}->enable_commit(); }; sub check_resurrection() { Echolot::Globals::get()->{'storage'}->delay_commit(); for my $remailer (Echolot::Globals::get()->{'storage'}->get_addresses()) { next unless ($remailer->{'status'} eq 'ttl timeout'); next unless ($remailer->{'fetch'}); next unless ($remailer->{'resurrection_ttl'}); Echolot::Log::debug("Sending request to ".$remailer->{'address'}." to check for resurrection."); for my $type (qw{conf key help stats adminkey}) { Echolot::Tools::send_message( 'To' => $remailer->{'address'}, 'Subject' => 'remailer-'.$type, 'Token' => $type.'.'.$remailer->{'id'}) }; Echolot::Globals::get()->{'storage'}->decrease_resurrection_ttl($remailer->{'address'}); }; Echolot::Globals::get()->{'storage'}->enable_commit(); }; sub remailer_caps($$$;$) { my ($conf, $token, $time, $dontexpire) = @_; my ($id) = $token =~ /^conf\.(\d+)$/; (defined $id) or Echolot::Log::info("Returned token '$token' has no id at all."), return 0; Echolot::Log::info("Could not find id in token '$token'."), return 0 unless defined $id; my ($remailer_type) = ($conf =~ /^\s*Remailer-Type:\s* (.*?) \s*$/imx); Echolot::Log::info("No remailer type found in remailer_caps from '$token'."), return 0 unless defined $remailer_type; my ($remailer_caps) = ($conf =~ /^\s*( \$remailer{".*"} \s*=\s* "<.*@.*>.*"; )\s*$/imx); Echolot::Log::info("No remailer caps found in remailer_caps from '$token'."), return 0 unless defined $remailer_caps; my ($remailer_nick, $remailer_address) = ($remailer_caps =~ /^\s* \$remailer{"(.*)"} \s*=\s* "<(.*@.*)>.*"; \s*$/ix); Echolot::Log::info("No remailer nick found in remailer_caps from '$token': '$remailer_caps'."), return 0 unless defined $remailer_nick; Echolot::Log::info("No remailer address found in remailer_caps from '$token': '$remailer_caps'."), return 0 unless defined $remailer_address; my $remailer = Echolot::Globals::get()->{'storage'}->get_address_by_id($id); Echolot::Log::info("No remailer found for id '$id'."), return 0 unless defined $remailer; if ($remailer->{'address'} ne $remailer_address) { # Address mismatch -> Ignore reply and add $remailer_address to prospective addresses Echolot::Log::info("Remailer address mismatch $remailer->{'address'} vs $remailer_address. Adding latter to prospective remailers."); Echolot::Globals::get()->{'storage'}->add_prospective_address($remailer_address, 'self-capsstring-conf', $remailer_address); } else { Echolot::Log::debug("Setting capabilities for $remailer_address"); Echolot::Globals::get()->{'storage'}->restore_ttl( $remailer->{'address'} ); Echolot::Globals::get()->{'storage'}->set_caps($remailer_type, $remailer_caps, $remailer_nick, $remailer_address, $time, $dontexpire); # if remailer is cpunk and not pgponly if (($remailer_caps =~ /\bcpunk\b/) && !($remailer_caps =~ /\bpgponly\b/)) { Echolot::Globals::get()->{'storage'}->set_key( 'cpunk-clear', $remailer_nick, $remailer->{'address'}, 'N/A', 'none', 'N/A', 'N/A', 'N/A', $time); } } # Fetch prospective remailers from reliable's remailer-conf reply: my @lines = split /\r?\n/, $conf; while (1) { my $head; while (@lines) { $head = $lines[0]; chomp $head; shift @lines; last if ($head eq 'SUPPORTED CPUNK (TYPE I) REMAILERS' || $head eq 'SUPPORTED MIXMASTER (TYPE II) REMAILERS'); }; last unless defined $head; my $wanting = $head eq 'SUPPORTED CPUNK (TYPE I) REMAILERS' ? 1 : $head eq 'SUPPORTED MIXMASTER (TYPE II) REMAILERS' ? 2 : undef; last unless defined $wanting; while (@lines) { $head = $lines[0]; chomp $head; shift @lines; if ($wanting == 1) { last unless ($head =~ /<(.*?@.*?)>/); Echolot::Globals::get()->{'storage'}->add_prospective_address($1, 'reliable-caps-reply-type1', $remailer_address); } elsif ($wanting == 2) { last unless ($head =~ /\s(.*?@.*?)\s/); Echolot::Globals::get()->{'storage'}->add_prospective_address($1, 'reliable-caps-reply-type2', $remailer_address); } else { Echolot::Log::confess("Shouldn't be here. wanting == $wanting."); }; }; }; return 1; }; sub remailer_conf($$$) { my ($reply, $token, $time) = @_; my ($id) = $token =~ /^conf\.(\d+)$/; (defined $id) or Echolot::Log::info ("Returned token '$token' has no id at all."), return 0; my $remailer = Echolot::Globals::get()->{'storage'}->get_address_by_id($id); Echolot::Log::info("No remailer found for id '$id'."), return 0 unless defined $remailer; Echolot::Log::debug("Received remailer-conf reply for $remailer."), Echolot::Globals::get()->{'storage'}->not_a_remailer($id), return 1 if (is_not_a_remailer($reply)); Echolot::Thesaurus::save_thesaurus('conf', $id, $reply); remailer_caps($reply, $token, $time); }; sub set_caps_manually($$) { my ($addr, $caps) = @_; defined $addr or Echolot::Log::info("Address not defined."), return 0; defined $caps or Echolot::Log::info("Caps not defined."), return 0; Echolot::Log::info("Setting caps for $addr manually to $caps."); my $remailer = Echolot::Globals::get()->{'storage'}->get_address($addr); defined $remailer or Echolot::Log::info("Remailer address $addr did not give a valid remailer."), return 0; my $id = $remailer->{'id'}; defined $id or Echolot::Log::info("Remailer address $addr did not give a remailer with an id."), return 0; my $token = 'conf.'.$id; my $conf = "Remailer-Type: set-manually\n$caps"; remailer_caps($conf, $token, time, 1); return 1; }; sub parse_mix_key($$$) { my ($reply, $time, $remailer) = @_; # -----Begin Mix Key----- # 7f6d997678b19ccac110f6e669143126 # 258 # AASyedeKiP1/UKyfrBz2K6gIhv4jfXIaHo8dGmwD # KqkG3DwytgSySSY3wYm0foT7KvEnkG2aTi/uJva/ # gymE+tsuM8l8iY1FOiXwHWLDdyUBPbrLjRkgm7GD # Y7ogSjPhVLeMpzkSyO/ryeUfLZskBUBL0LxjLInB # YBR3o6p/RiT0EQAAAAAAAAAAAAAAAAAAAAAAAAAA # AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA # AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA # AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA # AAAAAAAAAAAAAAAAAAAAAQAB # -----End Mix Key----- my %mixmasters; # rot26 rot26@mix.uucico.de 7f6d997678b19ccac110f6e669143126 2.9b33 MC my @mix_confs = ($reply =~ /^ [a-z0-9]+ \s+ \S+\@\S+ \s+ [0-9a-f]{32} .*?$/xmg); my @mix_keys = ($reply =~ /^-----Begin \s Mix \s Key-----\r?\n [0-9a-f]{32}\r?\n \d+\r?\n (?:[a-zA-Z0-9+\/]*\r?\n)+ -----End \s Mix \s Key-----$/xmg ); for (@mix_confs) { my ($nick, $address, $keyid, $version, $caps, $created, $expires) = /^ ([a-z0-9]+) \s+ (\S+@\S+) \s+ ([0-9a-f]{32}) (?: [ \t]+ (\S+) (?: [ \t]+ (\S+) (?: [ \t]+ (\d{4}-\d{2}-\d{2}) (?: [ \t]+ (\d{4}-\d{2}-\d{2}) )? )? )? )? .*?/x; $mixmasters{$keyid} = { nick => $nick, address => $address, version => $version, caps => $caps, created => $created, expires => $expires, summary => $_ }; }; for (@mix_keys) { my ($keyid) = /^-----Begin \s Mix \s Key-----\r?\n ([0-9a-f]{32})\r?\n \d+\r?\n (?:[a-zA-Z0-9+\/]*\r?\n)+ -----End \s Mix \s Key-----$/xmg; $mixmasters{$keyid}->{'key'} = $_; }; for my $keyid (keys %mixmasters) { my $remailer_address = $mixmasters{$keyid}->{'address'}; (defined $mixmasters{$keyid}->{'nick'}) or Echolot::Log::info("Could not parse a remailer-key reply."), next; (defined $mixmasters{$keyid}->{'nick'} && ! defined $mixmasters{$keyid}->{'key'}) and Echolot::Log::info("Mixmaster key header without key in reply from $remailer_address."), next; (! defined $mixmasters{$keyid}->{'nick'} && defined $mixmasters{$keyid}->{'key'}) and Echolot::Log::info("Mixmaster key without key header in reply from $remailer_address."), next; my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday) = gmtime(); my $today = sprintf("%04d-%02d-%02d", $year+1900, $mon+1, $mday); (defined $mixmasters{$keyid}->{'created'} && ($today lt $mixmasters{$keyid}->{'created'})) and Echolot::Log::info("Mixmaster key for $remailer_address created in the future ($today < ".$mixmasters{$keyid}->{'created'}.")."), next; (defined $mixmasters{$keyid}->{'expires'} && ($mixmasters{$keyid}->{'expires'} lt $today)) and Echolot::Log::info("Mixmaster key for $remailer_address expired (".$mixmasters{$keyid}->{'expires'}." < $today)."), next; if ($remailer->{'address'} ne $remailer_address) { # Address mismatch -> Ignore reply and add $remailer_address to prospective addresses Echolot::Log::info("Remailer address mismatch $remailer->{'address'} vs $remailer_address. Adding latter to prospective remailers."); Echolot::Globals::get()->{'storage'}->add_prospective_address($remailer_address, 'self-capsstring-key', $remailer_address); } else { Echolot::Log::debug("Setting mix key for $remailer_address: $keyid"); Echolot::Globals::get()->{'storage'}->restore_ttl( $remailer->{'address'} ); Echolot::Globals::get()->{'storage'}->set_key( 'mix', $mixmasters{$keyid}->{'nick'}, $mixmasters{$keyid}->{'address'}, $mixmasters{$keyid}->{'key'}, $keyid, $mixmasters{$keyid}->{'version'}, $mixmasters{$keyid}->{'caps'}, $mixmasters{$keyid}->{'summary'}, $time); } }; return 1; }; sub parse_cpunk_key($$$) { my ($reply, $time, $remailer) = @_; my $GnuPG = new GnuPG::Interface; $GnuPG->call( Echolot::Config::get()->{'gnupg'} ) if (Echolot::Config::get()->{'gnupg'}); $GnuPG->options->hash_init( homedir => Echolot::Config::get()->{'gnupghome'} ); $GnuPG->options->meta_interactive( 0 ); my %cypherpunk; my @pgp_keys = ($reply =~ /^-----BEGIN \s PGP \s PUBLIC \s KEY \s BLOCK-----\r?\n (?:.+\r?\n)* \r?\n (?:[a-zA-Z0-9+\/=]*\r?\n)+ -----END \s PGP \s PUBLIC \s KEY \s BLOCK-----$/xmg ); for my $key (@pgp_keys) { my ( $stdin_fh, $stdout_fh, $stderr_fh, $status_fh, $handles ) = Echolot::Tools::make_gpg_fds(); my $pid = $GnuPG->wrap_call( commands => [qw{--with-colons --no-options --no-secmem-warning --no-default-keyring --fast-list-mode}], handles => $handles ); my ($stdout, $stderr, $status) = Echolot::Tools::readwrite_gpg($key, $stdin_fh, $stdout_fh, $stderr_fh, $status_fh); waitpid $pid, 0; ($stderr eq '') or Echolot::Log::info("GnuPG returned something in stderr: '$stderr' when checking key '$key'; So what?"); ($status eq '') or Echolot::Log::info("GnuPG returned something in status '$status' when checking key '$key': So what?"); my @included_keys = $stdout =~ /^pub:.*$/mg; (scalar @included_keys >= 2) && # FIXME handle more than one key per block nicely Echolot::Log::debug ("Cannot handle more than one key per block nicely (correctly) yet. Found ".(scalar @included_keys)." in one block from ".$remailer->{'address'}."."); for my $included_key (@included_keys) { my ($type, $keyid, $uid) = $included_key =~ /pub::\d+:(\d+):([0-9A-F]+):[^:]+:[^:]*:::([^:]+):/; (defined $uid) or Echolot::Log::info ("Unexpected format of '$included_key' by ".$remailer->{'address'}."; Skipping."), next; my ($address) = $uid =~ /<(.*?)>/; $cypherpunk{$keyid} = { address => $address, type => $type, key => $key # FIXME handle more than one key per block correctly }; }; }; for my $keyid (keys %cypherpunk) { my $remailer_address = $cypherpunk{$keyid}->{'address'}; if ($remailer->{'address'} ne $remailer_address) { # Address mismatch -> Ignore reply and add $remailer_address to prospective addresses Echolot::Log::info("Remailer address mismatch $remailer->{'address'} vs $remailer_address id key $keyid. Adding latter to prospective remailers."); Echolot::Globals::get()->{'storage'}->add_prospective_address($remailer_address, 'self-capsstring-key', $remailer_address); } else { Echolot::Globals::get()->{'storage'}->restore_ttl( $remailer->{'address'} ); # 1 .. RSA # 17 .. DSA if ($cypherpunk{$keyid}->{'type'} == 1 || $cypherpunk{$keyid}->{'type'} == 17 ) { Echolot::Log::debug("Setting cpunk key for $remailer_address: $keyid; type ".$cypherpunk{$keyid}->{'type'}); Echolot::Globals::get()->{'storage'}->set_key( (($cypherpunk{$keyid}->{'type'} == 1) ? 'cpunk-rsa' : (($cypherpunk{$keyid}->{'type'} == 17) ? 'cpunk-dsa' : 'ERROR')), $keyid, # as nick $cypherpunk{$keyid}->{'address'}, $cypherpunk{$keyid}->{'key'}, $keyid, 'N/A', 'N/A', 'N/A', $time); } else { Echolot::Log::info("$keyid from $remailer_address has algoid ".$cypherpunk{$keyid}->{'type'}.". Cannot handle those."); }; } }; return 1; }; sub remailer_key($$$) { my ($reply, $token, $time) = @_; my $cp_reply = $reply; $cp_reply =~ s/^- -/-/gm; # PGP Signed messages my ($id) = $token =~ /^key\.(\d+)$/; (defined $id) or Echolot::Log::info ("Returned token '$token' has no id at all."), return 0; my $remailer = Echolot::Globals::get()->{'storage'}->get_address_by_id($id); Echolot::Log::info("No remailer found for id '$id'."), return 0 unless defined $remailer; Echolot::Log::debug("Received remailer-keys reply for $remailer."), Echolot::Globals::get()->{'storage'}->not_a_remailer($id), return 1 if (is_not_a_remailer($reply)); Echolot::Thesaurus::save_thesaurus('key', $id, $reply); parse_mix_key($cp_reply, $time, $remailer); parse_cpunk_key($cp_reply, $time, $remailer); return 1; }; sub remailer_stats($$$) { my ($reply, $token, $time) = @_; my ($id) = $token =~ /^stats\.(\d+)$/; (defined $id) or Echolot::Log::info ("Returned token '$token' has no id at all."), return 0; my $remailer = Echolot::Globals::get()->{'storage'}->get_address_by_id($id); Echolot::Log::info("No remailer found for id '$id'."), return 0 unless defined $remailer; Echolot::Log::debug("Received remailer-stats reply for $remailer."), Echolot::Globals::get()->{'storage'}->not_a_remailer($id), return 1 if (is_not_a_remailer($reply)); Echolot::Thesaurus::save_thesaurus('stats', $id, $reply); }; sub remailer_help($$$) { my ($reply, $token, $time) = @_; my ($id) = $token =~ /^help\.(\d+)$/; (defined $id) or Echolot::Log::info ("Returned token '$token' has no id at all."), return 0; my $remailer = Echolot::Globals::get()->{'storage'}->get_address_by_id($id); Echolot::Log::info("No remailer found for id '$id'."), return 0 unless defined $remailer; Echolot::Log::debug("Received remailer-help reply for $remailer."), Echolot::Globals::get()->{'storage'}->not_a_remailer($id), return 1 if (is_not_a_remailer($reply)); Echolot::Thesaurus::save_thesaurus('help', $id, $reply); }; sub remailer_adminkey($$$) { my ($reply, $token, $time) = @_; my ($id) = $token =~ /^adminkey\.(\d+)$/; (defined $id) or Echolot::Log::info ("Returned token '$token' has no id at all."), return 0; my $remailer = Echolot::Globals::get()->{'storage'}->get_address_by_id($id); Echolot::Log::info("No remailer found for id '$id'."), return 0 unless defined $remailer; Echolot::Log::debug("Received remailer-adminkey reply for $remailer."), Echolot::Globals::get()->{'storage'}->not_a_remailer($id), return 1 if (is_not_a_remailer($reply)); Echolot::Thesaurus::save_thesaurus('adminkey', $id, $reply); }; 1; # vim: set ts=4 shiftwidth=4: