#!/usr/bin/perl use warnings; use strict; use v5.10; use Data::Dumper; use Email::Date; use Email::MessageID; use Getopt::Long; use GnuPG::Interface; use JSON; use List::MoreUtils; use Mail::GPG; use MIME::Entity; use Pod::Usage; use String::Random qw(random_string); use Text::Template; use Try::Tiny; =head1 NAME masterkey.pl =head1 SYNOPSIS masterkey.pl [options] Send verification mails to the owners of the listed GPG keys. Options: --help, -h short help message --dry-run, -n do not perform any permanent actions --from, -f GPG ID used to send the email --tokenfile Record tokens in this file --debug Output debugging information =cut sub main { my %templates = ( 'verification' => { 'subject' => 'Master Key Verification for {$recipient_name} ({$recipient_key})', 'body' => 'Hi, This mail is about having your GPG key signed by an Arch Linux master key. Please reply with an email that is signed with your key ({$recipient_key}) and contains the token listed below. Your token: {$token} Best Regards, SAMKIVS (Simple Automated Master Key Identity Verification System) on behalf of {$sender_name} ({$sender_key}) ', }, 'confirmation' => { 'subject' => 'Master Key Signature Confirmation for {$recipient_name} ({$recipient_key})', 'body' => 'Hi, Your GPG key ({$recipient_key}) has been successfully signed by an Arch Linux master key. Best Regards, SAMKIVS (Simple Automated Master Key Identity Verification System) on behalf of {$sender_name} ({$sender_key}) ', }, ); my %opts = (); Getopt::Long::Configure ("bundling"); GetOptions(\%opts, "help|h", "dry-run|n", "from|f=s", "tokenfile=s", "debug") or pod2usage(2); pod2usage(0) if $opts{help}; pod2usage(-verbose => 0) if (@ARGV== 0); my $command = shift @ARGV; # TODO: print all errors at once die "Error: --from option is required but not set\n" if not $opts{from}; die "Error: --tokenfile option is required but not set\n" if not $opts{tokenfile}; die "Error: no or invalid command\n" unless $templates{$command}; for my $id (@ARGV) { say STDERR "Processing $id"; try { die "key ID has length != 40\n" unless length($id) == 40; my $mail_subject = $templates{$command}{'subject'}; my $mail_body = $templates{$command}{'body'}; my $token = random_string('.' x 25); my $msg = build_email($command, $opts{from}, $id, $mail_subject, $mail_body, $token); if ($command eq 'verification') { save_token($id, $token, $opts{tokenfile}) unless $opts{'dry-run'}; } send_email($msg) unless $opts{'dry-run'}; say $msg->as_string if $opts{debug}; } catch { warn "$_\nSkipping $id due to uncaught error\n"; } } } sub save_token { my $id = shift; my $token = shift; my $file = shift; open my $fh, '>>', $file or die "Failed to open '$file': $!"; say $fh "$id $token"; close $fh; } sub fill_template { my $template = shift; my $values = shift; my $result = Text::Template::fill_in_string($template, HASH => $values) or die "Failed to fill in template: $Text::Template::ERROR"; return $result; } sub gpg_get_user { my $key = shift; my $gpg = GnuPG::Interface->new(); my @keys = $gpg->get_public_keys($key); die "No key found" if 0+@keys == 0; my $user = $keys[0]->user_ids_ref->[0]->as_string; unless ($user =~ m/^(?.*?) (?:\((?.*?)\) )?\<(?.*?@.*?)\>$/) { die "Failed to parse GPG user information for key $key; got $user"; } my $name = $+{name}; my $email = $+{email}; return ($name, $email); } sub build_email { my $command = shift; my $sender_key = shift; my $recipient_key = shift; my $subject = shift; my $body = shift; my $token = shift; # get from gpg keys my ($sender_name, $sender_addr) = gpg_get_user($sender_key); my ($recipient_name, $recipient_addr) = gpg_get_user($recipient_key); my %values; $values{token} = $token; $values{sender_key} = $sender_key; $values{sender_name} = $sender_name; $values{sender_addr} = $sender_addr; $values{recipient_key} = $recipient_key; $values{recipient_name} = $recipient_name; $values{recipient_addr} = $recipient_addr; $subject = fill_template($subject, \%values); $body = fill_template($body, \%values); my $mgpg = Mail::GPG->new( default_key_id => $sender_key, default_passphrase => '', ); my $msg = MIME::Entity->build( From => Encode::encode('iso-8859-1', $sender_name). " <$sender_addr>", To => $recipient_addr, Subject => Encode::encode('iso-8859-1', $subject), # TODO: necessary? #TimeZone => 'Europe/Vienna', Encoding => 'quoted-printable', Charset => 'utf8', Date => Email::Date::format_date(), Data => [$body], ); $msg->add("Message-ID", Email::MessageID->new->in_brackets); $msg->replace("Return-Path", "<$sender_addr>"); if ($command eq 'verification') { return $mgpg->mime_sign_encrypt( entity => $msg, recipients => [$sender_key, $recipient_key], ); } return $mgpg->mime_sign( entity => $msg, recipients => [$sender_key, $recipient_key], ); } sub send_email { my $msg = shift; open my $mail, "|msmtp -t"; print $mail $msg->as_string; close $mail; } main();