From 30cf2e5a9f2360c52d07abb7b58ba4de64245ea5 Mon Sep 17 00:00:00 2001 From: Marc van der Wal Date: Wed, 25 Oct 2023 15:50:23 +0200 Subject: [PATCH] =?UTF-8?q?API=20REST=20pour=20l=E2=80=99=C3=A9metteur=20d?= =?UTF-8?q?e=20mails?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On peut générer des clefs DKIM mais on ne peut pas encore choisir quel sélecteur utiliser pour signer les mails sortants. Une fois DKIM activé pour un domaine, on ne peut pas non plus le désactiver. --- sender/Dockerfile | 50 ++++++ sender/etc/opendkim/opendkim.conf | 6 +- sender/etc/s6-overlay/s6-rc.d/api/run | 2 + sender/etc/s6-overlay/s6-rc.d/api/type | 1 + .../s6-overlay/s6-rc.d/user/contents.d/api | 0 sender/web-api/.dancer | 0 sender/web-api/MANIFEST | 24 +++ sender/web-api/MANIFEST.SKIP | 17 ++ sender/web-api/Makefile.PL | 26 +++ sender/web-api/bin/app.psgi | 9 + sender/web-api/config.yml | 4 + sender/web-api/cpanfile | 11 ++ sender/web-api/environments/development.yml | 14 ++ sender/web-api/environments/production.yml | 16 ++ .../lib/Email/SpoofingDemo/API/Sender.pm | 74 +++++++++ sender/web-api/lib/Email/SpoofingDemo/DKIM.pm | 157 ++++++++++++++++++ sender/web-api/public/dispatch.cgi | 16 ++ sender/web-api/public/dispatch.fcgi | 18 ++ 18 files changed, 442 insertions(+), 3 deletions(-) create mode 100644 sender/etc/s6-overlay/s6-rc.d/api/run create mode 100644 sender/etc/s6-overlay/s6-rc.d/api/type create mode 100644 sender/etc/s6-overlay/s6-rc.d/user/contents.d/api create mode 100644 sender/web-api/.dancer create mode 100644 sender/web-api/MANIFEST create mode 100644 sender/web-api/MANIFEST.SKIP create mode 100644 sender/web-api/Makefile.PL create mode 100644 sender/web-api/bin/app.psgi create mode 100644 sender/web-api/config.yml create mode 100644 sender/web-api/cpanfile create mode 100644 sender/web-api/environments/development.yml create mode 100644 sender/web-api/environments/production.yml create mode 100644 sender/web-api/lib/Email/SpoofingDemo/API/Sender.pm create mode 100644 sender/web-api/lib/Email/SpoofingDemo/DKIM.pm create mode 100644 sender/web-api/public/dispatch.cgi create mode 100644 sender/web-api/public/dispatch.fcgi diff --git a/sender/Dockerfile b/sender/Dockerfile index 3c8ce43..b152cb4 100644 --- a/sender/Dockerfile +++ b/sender/Dockerfile @@ -14,6 +14,54 @@ RUN apk add \ s6-overlay \ vim +# Dependencies for REST API +RUN apk add \ + gcc \ + libc-dev \ + make \ + perl-app-cpanminus \ + perl-clone \ + perl-config-any \ + perl-data-optlist \ + perl-dev \ + perl-exporter-tiny \ + perl-extutils-config \ + perl-extutils-helpers \ + perl-extutils-installpaths \ + perl-file-sharedir \ + perl-file-sharedir-install \ + perl-file-slurp \ + perl-file-which \ + perl-hash-merge-simple \ + perl-hash-multivalue \ + perl-http-date \ + perl-http-headers-fast \ + perl-import-into \ + perl-json-maybexs \ + perl-module-build \ + perl-module-build-tiny \ + perl-module-implementation \ + perl-module-runtime \ + perl-moo \ + perl-params-util \ + perl-params-validate \ + perl-path-tiny \ + perl-plack \ + perl-readonly \ + perl-ref-util \ + perl-role-tiny \ + perl-safe-isa \ + perl-sub-exporter \ + perl-sub-install \ + perl-sub-quote \ + perl-template-toolkit \ + perl-type-tiny \ + perl-yaml + +RUN cpanm -n -v \ + Dancer2 \ + Module::Pluggable::Object + RUN newaliases RUN install -m 0700 -o opendkim -g opendkim -d /run/opendkim @@ -23,6 +71,8 @@ COPY etc/s6-overlay /etc/s6-overlay COPY etc/postfix /etc/postfix COPY etc/opendkim /etc/opendkim +COPY web-api /src/api + ENTRYPOINT ["/init"] # Ne pas positionner USER, ou sinon les services ne démarreront pas de manière diff --git a/sender/etc/opendkim/opendkim.conf b/sender/etc/opendkim/opendkim.conf index ca65dff..0bae792 100644 --- a/sender/etc/opendkim/opendkim.conf +++ b/sender/etc/opendkim/opendkim.conf @@ -17,6 +17,6 @@ SendReports yes ## Il vaut donc mieux paramétrer une SigningTable (qui liste les expéditeurs ## pour lesquels on signe) et une KeyTable (qui liste les emplacements des ## clefs privées). -# -# SigningTable file:/etc/opendkim/signing_table -# KeyTable file:/etc/opendkim/key_table + +SigningTable file:/etc/opendkim/signing_table +KeyTable file:/etc/opendkim/key_table diff --git a/sender/etc/s6-overlay/s6-rc.d/api/run b/sender/etc/s6-overlay/s6-rc.d/api/run new file mode 100644 index 0000000..2d9058d --- /dev/null +++ b/sender/etc/s6-overlay/s6-rc.d/api/run @@ -0,0 +1,2 @@ +#!/bin/execlineb -P +/usr/bin/env perl /src/api/bin/app.psgi \ No newline at end of file diff --git a/sender/etc/s6-overlay/s6-rc.d/api/type b/sender/etc/s6-overlay/s6-rc.d/api/type new file mode 100644 index 0000000..1780f9f --- /dev/null +++ b/sender/etc/s6-overlay/s6-rc.d/api/type @@ -0,0 +1 @@ +longrun \ No newline at end of file diff --git a/sender/etc/s6-overlay/s6-rc.d/user/contents.d/api b/sender/etc/s6-overlay/s6-rc.d/user/contents.d/api new file mode 100644 index 0000000..e69de29 diff --git a/sender/web-api/.dancer b/sender/web-api/.dancer new file mode 100644 index 0000000..e69de29 diff --git a/sender/web-api/MANIFEST b/sender/web-api/MANIFEST new file mode 100644 index 0000000..aada075 --- /dev/null +++ b/sender/web-api/MANIFEST @@ -0,0 +1,24 @@ +MANIFEST +MANIFEST.SKIP +.dancer +Makefile.PL +config.yml +cpanfile +views/index.tt +views/layouts/main.tt +lib/Email/SpoofingDemo/API/DNS.pm +t/002_index_route.t +t/001_base.t +environments/production.yml +environments/development.yml +bin/app.psgi +public/500.html +public/dispatch.cgi +public/dispatch.fcgi +public/favicon.ico +public/404.html +public/javascripts/jquery.js +public/css/error.css +public/css/style.css +public/images/perldancer-bg.jpg +public/images/perldancer.jpg diff --git a/sender/web-api/MANIFEST.SKIP b/sender/web-api/MANIFEST.SKIP new file mode 100644 index 0000000..bbfb365 --- /dev/null +++ b/sender/web-api/MANIFEST.SKIP @@ -0,0 +1,17 @@ +^\.git\/ +maint +^tags$ +.last_cover_stats +Makefile$ +^blib +^pm_to_blib +^.*.bak +^.*.old +^t.*sessions +^cover_db +^.*\.log +^.*\.swp$ +MYMETA.* +^.gitignore +^.svn\/ +^Email-SpoofingDemo-API-DNS- diff --git a/sender/web-api/Makefile.PL b/sender/web-api/Makefile.PL new file mode 100644 index 0000000..6077c4f --- /dev/null +++ b/sender/web-api/Makefile.PL @@ -0,0 +1,26 @@ +use strict; +use warnings; +use ExtUtils::MakeMaker; + +# Normalize version strings like 6.30_02 to 6.3002, +# so that we can do numerical comparisons on it. +my $eumm_version = $ExtUtils::MakeMaker::VERSION; +$eumm_version =~ s/_//; + +WriteMakefile( + NAME => 'Email::SpoofingDemo::API::DNS', + AUTHOR => q{Marc van der Wal }, + VERSION_FROM => 'lib/Email/SpoofingDemo/API/Sender.pm', + ABSTRACT => 'Email spoofing demo: REST API for Sender', + ($eumm_version >= 6.3001 + ? ('LICENSE'=> 'all-rights-reserved') + : ()), + PL_FILES => {}, + PREREQ_PM => { + 'Test::More' => 0, + 'YAML' => 0, + 'Dancer2' => 0.300000, + }, + dist => { COMPRESS => 'gzip -9f', SUFFIX => 'gz', }, + clean => { FILES => 'Email-SpoofingDemo-API-Sender-*' }, +); diff --git a/sender/web-api/bin/app.psgi b/sender/web-api/bin/app.psgi new file mode 100644 index 0000000..d0cb56f --- /dev/null +++ b/sender/web-api/bin/app.psgi @@ -0,0 +1,9 @@ +#!/usr/bin/perl + +use strict; +use warnings; +use FindBin; +use lib "$FindBin::Bin/../lib"; + +use Email::SpoofingDemo::API::Sender; +Email::SpoofingDemo::API::Sender->to_app; diff --git a/sender/web-api/config.yml b/sender/web-api/config.yml new file mode 100644 index 0000000..42e24b6 --- /dev/null +++ b/sender/web-api/config.yml @@ -0,0 +1,4 @@ + +appname: "Email::SpoofingDemo::API::Sender" +charset: "UTF-8" +serializer: JSON diff --git a/sender/web-api/cpanfile b/sender/web-api/cpanfile new file mode 100644 index 0000000..28436b4 --- /dev/null +++ b/sender/web-api/cpanfile @@ -0,0 +1,11 @@ +requires "Dancer2" => "0.300000"; + +recommends "YAML" => "0"; +recommends "URL::Encode::XS" => "0"; +recommends "CGI::Deurl::XS" => "0"; +recommends "HTTP::Parser::XS" => "0"; + +on "test" => sub { + requires "Test::More" => "0"; + requires "HTTP::Request::Common" => "0"; +}; diff --git a/sender/web-api/environments/development.yml b/sender/web-api/environments/development.yml new file mode 100644 index 0000000..0887b4b --- /dev/null +++ b/sender/web-api/environments/development.yml @@ -0,0 +1,14 @@ + +logger: "console" +log: "core" + +# should Dancer2 consider warnings as critical errors? +warnings: 1 + +# should Dancer2 show a stacktrace when an 5xx error is caught? +# if set to yes, public/500.html will be ignored and either +# views/500.tt, 'error_template' template, or a default error template will be used. +show_errors: 1 + +# print the banner +startup_info: 1 diff --git a/sender/web-api/environments/production.yml b/sender/web-api/environments/production.yml new file mode 100644 index 0000000..41b436f --- /dev/null +++ b/sender/web-api/environments/production.yml @@ -0,0 +1,16 @@ +# configuration file for production environment + +# only log warning and error messsages +log: "warning" + +# log message to a file in logs/ +logger: "file" + +# don't consider warnings critical +warnings: 0 + +# hide errors +show_errors: 0 + +# disable server tokens in production environments +no_server_tokens: 1 diff --git a/sender/web-api/lib/Email/SpoofingDemo/API/Sender.pm b/sender/web-api/lib/Email/SpoofingDemo/API/Sender.pm new file mode 100644 index 0000000..a873dbc --- /dev/null +++ b/sender/web-api/lib/Email/SpoofingDemo/API/Sender.pm @@ -0,0 +1,74 @@ +package Email::SpoofingDemo::API::Sender; + +use Dancer2; + +use Email::SpoofingDemo::DKIM qw(read_signing_table read_key_table + write_signing_table write_key_table + generate_dkim_key); + +our $VERSION = '0.1'; + +my $signing_table = "/etc/opendkim/signing_table"; +my $key_table = "/etc/opendkim/key_table"; +my $key_dir = "/etc/opendkim/keys"; + +get '/' => sub { return "Welcome"; }; + +get '/installed-keys' => sub { + my $signing_table = read_signing_table($signing_table); + my $key_table = read_key_table($key_table); + + my @result; + + for my $domain (sort keys %$key_table) { + push @result, { + domain => $domain, + available_keys => $key_table->{$domain}, + current_key => $signing_table->{$domain} + }; + } + + return \@result; +}; + +post '/generate-dkim-key' => sub { + my $domain = body_parameters->get('domain'); + my $selector = body_parameters->get('selector'); + my $key_size = body_parameters->get('key_size'); + + # Generate key + my $txt_data = generate_dkim_key($domain, $selector, $key_size, + $key_table, $key_dir, $signing_table); + + my $txt_record = sprintf("%-30s. TXT %s", + qq{$selector._domainkey.$domain}, + $txt_data); + + return { + txt_record => $txt_record + }; +}; + +post '/send-email/confirmation_email' => sub { + system("/home/expediteur/scripts/send_confirmation_email.sh"); + my $status = ($? >> 8); + if ($status != 0) { + status(500); + return "E-mail script exited with status $status"; + } +}; + +post '/send-email/newsletter' => sub { + system("/home/expediteur/scripts/send_newsletter.sh"); + my $status = ($? >> 8); + if ($status != 0) { + status(500); + return "E-mail script exited with status $status"; + } +}; + +any qr{.*} => sub { status 'not_found'; return "Invalid route" }; + +dance; + +true; diff --git a/sender/web-api/lib/Email/SpoofingDemo/DKIM.pm b/sender/web-api/lib/Email/SpoofingDemo/DKIM.pm new file mode 100644 index 0000000..281cdf0 --- /dev/null +++ b/sender/web-api/lib/Email/SpoofingDemo/DKIM.pm @@ -0,0 +1,157 @@ +package Email::SpoofingDemo::DKIM; + +use strict; +use warnings; +use v5.10; +use utf8; + +use Exporter 'import'; + +our @EXPORT_OK = qw(read_signing_table read_key_table + write_signing_table write_key_table + generate_dkim_key); + +sub generate_dkim_key { + my ($domain, $selector, $key_size, $key_table_name, $key_dir, $signing_table_name) = @_; + + die if $domain =~ /\.\./; + + my $key_domain_dir = "$key_dir/$domain"; + + # Generate the key + system("mkdir", "-p", $key_domain_dir); + system("opendkim-genkey", + "-D", $key_domain_dir, + "-d", $domain, + "-s", $selector, + "-b", $key_size); + system("chown", "-R", "opendkim", $key_domain_dir); + + # Read in the public key + my $public_key_file = "$key_domain_dir/$selector.txt"; + open(my $fh, '<', $public_key_file) or die "$key_domain_dir: $!"; + my $data = eval { + local $/ = undef; + my $raw_record = <$fh>; + my ($owner, $class, $type, $data) = split(" ", $raw_record, 4); + $data =~ s/\s*;.*$//; + return $data; + }; + close($fh); + + # Update key table + my $key_table = read_key_table($key_table_name); + push @{$key_table->{$domain}}, $selector; + write_key_table($key_table_name, $key_dir, $key_table); + + # Update signing table if it’s the first key for the domain + my $signing_table = read_signing_table($signing_table_name); + if (not exists $signing_table->{$domain}) { + $signing_table->{$domain} = $selector; + write_signing_table($signing_table_name, $signing_table); + } + + # Done! + reload_opendkim(); + + return $data; +} + +sub read_signing_table { + my ($filename) = @_; + + my %sign_table; + + open(my $fh, '<', $filename) or die "$filename: $!"; + while (<$fh>) { + chomp; + s/#.*$//; + next if /^\s*$/; + + my ($domain_or_email, $key_id) = split(" ", $_, 2); + my $domain = ($domain_or_email =~ s/^.*@//r); + my $selector = ($key_id =~ s/\._domainkey.$domain$//r); + + $sign_table{$domain} = $selector; + } + close($fh); + + return \%sign_table; +} + +sub write_signing_table { + my ($filename, $contents) = @_; + + open(my $fh, '>', $filename) or die "$filename: $!"; + binmode($fh, ':utf8'); + print $fh <<'EOF'; +## +## FORMAT DE LA TABLE +## +## +## +## L’adresse mail peut être un wildcard (ex. *@expediteur.example). +## + +EOF + for my $domain (sort keys %$contents) { + my $selector = $contents->{$domain}; + my $key_id = "$selector._domainkey.$domain"; + printf $fh "%-30s %s\n", $domain, $key_id; + } + close($fh); +} + +sub read_key_table { + my ($filename) = @_; + + # We only care about the list of keys that exist for a given domain. + # The rest of the data can be deduced from that mapping. + + my %key_table; + + open(my $fh, '<', $filename) or die "$filename: $!\n"; + while (<$fh>) { + chomp; + s/#.*$//; + next if /^\s*$/; + + my ($key_id, $key_spec) = split(" ", $_, 2); + my ($domain, $selector, $key_location) = split(":", $key_spec, 3); + + push @{$key_table{$domain}}, $selector; + } + + return \%key_table; +} + +sub write_key_table { + my ($filename, $key_dir, $contents) = @_; + + open(my $fh, '>', $filename) or die "$filename: $!\n"; + binmode($fh, ':utf8'); + print $fh < :: +## + +EOF + for my $domain (sort keys %$contents) { + for my $selector (@{$contents->{$domain}}) { + my $key_id = "$selector._domainkey.$domain"; + my $key_file = "$key_dir/$domain/$selector.private"; + + printf $fh "%-30s %s:%s:%s\n", $key_id, $domain, $selector, $key_file; + } + } + close($fh); +} + +sub reload_opendkim { + system(qw(killall -USR1 opendkim)); + return (($? >> 8) == 0); +} + +1; diff --git a/sender/web-api/public/dispatch.cgi b/sender/web-api/public/dispatch.cgi new file mode 100644 index 0000000..706ba0c --- /dev/null +++ b/sender/web-api/public/dispatch.cgi @@ -0,0 +1,16 @@ +#!/usr/bin/env perl +BEGIN { $ENV{DANCER_APPHANDLER} = 'PSGI';} +use Dancer2; +use FindBin '$RealBin'; +use Plack::Runner; + +# For some reason Apache SetEnv directives don't propagate +# correctly to the dispatchers, so forcing PSGI and env here +# is safer. +set apphandler => 'PSGI'; +set environment => 'production'; + +my $psgi = path($RealBin, '..', 'bin', 'app.psgi'); +die "Unable to read startup script: $psgi" unless -r $psgi; + +Plack::Runner->run($psgi); diff --git a/sender/web-api/public/dispatch.fcgi b/sender/web-api/public/dispatch.fcgi new file mode 100644 index 0000000..ad42deb --- /dev/null +++ b/sender/web-api/public/dispatch.fcgi @@ -0,0 +1,18 @@ +#!/usr/bin/env perl +BEGIN { $ENV{DANCER_APPHANDLER} = 'PSGI';} +use Dancer2; +use FindBin '$RealBin'; +use Plack::Handler::FCGI; + +# For some reason Apache SetEnv directives don't propagate +# correctly to the dispatchers, so forcing PSGI and env here +# is safer. +set apphandler => 'PSGI'; +set environment => 'production'; + +my $psgi = path($RealBin, '..', 'bin', 'app.psgi'); +my $app = do($psgi); +die "Unable to read startup script: $@" if $@; +my $server = Plack::Handler::FCGI->new(nproc => 5, detach => 1); + +$server->run($app);