From dd2989b86b1a23469165048865945d48a2fd9e7b Mon Sep 17 00:00:00 2001 From: Sadeep Madurange Date: Mon, 10 Aug 2026 11:27:28 +0800 Subject: Verify GPG signature using OpenPGP Perl module. --- chroot_test.txt | 94 ----------------------------------------- lex.cgi | 127 ++++++++++++++++++++++++-------------------------------- 2 files changed, 54 insertions(+), 167 deletions(-) delete mode 100644 chroot_test.txt diff --git a/chroot_test.txt b/chroot_test.txt deleted file mode 100644 index a1c76d8..0000000 --- a/chroot_test.txt +++ /dev/null @@ -1,94 +0,0 @@ -chroot -u www /var/www /usr/bin/perl -e ' - use URI::Escape qw(uri_escape); - - my $raw_mime = <<'\''MIME_END'\''; -Received: from mailtransmit04.runbox.com (mailtransmit04.runbox.com [185.226.149.37]) by - 99abfb82813db16bb2b923f8a789c1622dfbc9adec15037074ea0a1d0bd3d9be with SMTP id - (version=TLS1.3, cipher=TLS_AES_128_GCM_SHA256); Mon, 10 Aug 2026 - 01:23:51 GMT -Authentication-Results: mxa.mailgun.org; - dkim=pass header.d=asciimx.com header.s=selector2 header.b=n9CTxyV+; - spf=pass (domain asciimx.com designates 185.226.149.37 as permitted sender) smtp.mailfrom="sadeep@asciimx.com"; - spf=pass (domain mailtransmit04.runbox.com designates 185.226.149.37 as permitted sender) smtp.helo="mailtransmit04.runbox.com"; - dmarc=none header.from=asciimx.com -X-Mailgun-Incoming: Yes -X-Envelope-From: sadeep@asciimx.com -Received: from mailtransmit03.runbox ([10.9.9.163] helo=aibo.runbox.com) - by mailtransmit04.runbox.com with esmtps (TLS1.2) tls TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 - (Exim 4.93) - (envelope-from ) - id 1wtEk1-00AOZb-03 - for lex@info.asciimx.com; Mon, 10 Aug 2026 03:23:49 +0200 -DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=asciimx.com - ; s=selector2; h=Content-Type:MIME-Version:Message-ID:Subject:To:From:Date; - bh=LcStAB7n3m8zTsu0AENI8MHLDQClt2p0YtEX7Mj98uo=; b=n9CTxyV+0K9gtkqLjdbPw57eJ - l5i4DmiJCsO4wKHeGsvNK8YQ7q1dqnW205gpNXTK4eC0yXI7fT2t0OmOTJROVr7MOyen0ZdR0J+VL - PEWlfqNkSzlnonzrREOGlCrZYnukxaPxGDUKEZ1AG+isCnzUt29R540rtQo/mi9v9sBG0/BbVWew8 - tkT9BGoRx351kTaDVw7VAwc6FLi4Cs06XuhxFOYrGGHXY9NyFlGjAdTuLxa2ObVibBvy+MklzHe7S - eUArWBPBVyCgU3ejhVbqfDfIZ+pl12iSYuF4luF2lwMAOOmY2+/i49ktjqHhl+632UJhPYQRZkPxe - OuoomaU9g==; -Received: from [10.9.9.74] (helo=submission03.runbox) - by mailtransmit03.runbox with esmtp (Exim 4.86_2) - (envelope-from ) - id 1wtEk0-0001MT-FI - for lex@info.asciimx.com; Mon, 10 Aug 2026 03:23:48 +0200 -Received: by submission03.runbox with esmtpsa [Authenticated ID (1110424)] (TLS1.2:ECDHE_SECP256R1__RSA_PSS_RSAE_SHA512__AES_256_GCM:256) - (Exim 4.95) - id 1wtEjg-009saV-DO - for lex@info.asciimx.com; - Mon, 10 Aug 2026 03:23:28 +0200 -Date: Mon, 10 Aug 2026 09:23:24 +0800 -From: Sadeep -To: lex@info.asciimx.com -Subject: Test -Message-ID: -MIME-Version: 1.0 -Content-Type: multipart/signed; micalg=pgp-sha256; - protocol="application/pgp-signature"; boundary="+lN9vFx72/4iTYkI" -Content-Disposition: inline - - ---+lN9vFx72/4iTYkI -Content-Type: text/plain; charset=us-ascii -Content-Disposition: inline -Content-Transfer-Encoding: quoted-printable - -This is a test email. - --- -Sadeep - ---+lN9vFx72/4iTYkI -Content-Type: application/pgp-signature; name=signature.asc - ------BEGIN PGP SIGNATURE----- - -iQHPBAEBCAA5FiEEqfBXeZvaBlev9/CoEDv54+dQv34FAmp5KAYbFIAAAAAABAAO -bWFudTIsMi41KzEuMTIsMiwzAAoJEBA7+ePnUL9+LAwMAM42Hp1A5YLHqOVSL716 -Ag42MCRldlJUklPUMonzrTxbRwoReG9yGqrxuB6VPDZ4pWwzpupivaUhi2ULw9Ru -fAc8xoKZQTsjB1bo7BXw0jXwYHUCLv6HkMWgL6nJsKvcSJzIRP9arTyu4ZEqH/vI -Io6IZIiWGwaMM+NPK1HjFbcjkKenHcapmT14aEsUQ4QjUlyyFOvvzKDZ7fgNmh/o -hRHdXkivYmlww6dIR49bwH2pIRu3SKXtq0DqMF2t0KjjNZ0ledSUKG7P1+U7GIY2 -W0fTyK9Bf/kB8fLwCKUDQbytw4qTgHMtvE6v+h/3VUK/Lt9JEwjySTfVzrHvwNRG -XycQXlM/5sc/tbpm0fVc5K+yg2OauOrSUI9r4HGvNS/FawV5rTuSi7KNDLB6G8O6 -GKugMgDMJnanBRI2U7quKNmpf7s95ouFN8vJ84ZslE4bzrtjDE0IQ1FOsO1yW64Y -FE51iC4OY0un/HsYLSsFPHlebd/52WrQikVljQNDgvr/Vg== -=GOo2 ------END PGP SIGNATURE----- - ---+lN9vFx72/4iTYkI-- -MIME_END - - my $payload = "body-mime=" . uri_escape($raw_mime); - - $ENV{CONTENT_TYPE} = "application/x-www-form-urlencoded"; - $ENV{CONTENT_LENGTH} = length($payload); - $ENV{REMOTE_ADDR} = "35.212.14.65"; - $ENV{HTTP_USER_AGENT}= "Go-http-client/2.0"; - - open(my $fh, "|-", "/usr/bin/perl", "/cgi-bin/lex.cgi") or die "Cannot pipe to lex.cgi: $!"; - print $fh $payload; - close($fh); - - print "Exit status: " . ($? >> 8) . "\n"; -' diff --git a/lex.cgi b/lex.cgi index e4a1994..9c11b28 100755 --- a/lex.cgi +++ b/lex.cgi @@ -2,22 +2,48 @@ use strict; use warnings; +use File::Spec; +use File::Find; use File::Path qw(make_path); use Time::HiRes qw(time); use Email::MIME; use OpenBSD::Pledge; use OpenBSD::Unveil; +my $start_time = time(); + +# Load Crypt::OpenPGP and all Crypt:: sub-modules before unveil() +BEGIN { + require Crypt::OpenPGP; + + for my $inc_dir (@INC) { + next unless -d $inc_dir; + my $crypt_dir = File::Spec->catdir($inc_dir, 'Crypt'); + next unless -d $crypt_dir; + + find(sub { + return unless /\.pm$/; + my $rel = File::Spec->abs2rel($File::Find::name, $inc_dir); + $rel =~ s/\.pm$//; + my $module = join('::', File::Spec->splitdir($rel)); + eval "require $module;"; + }, $crypt_dir); + } + + # Trigger backend instantiation (Math::BigInt, AutoLoader, Ciphers) + eval { + my $pgp = Crypt::OpenPGP->new(); + }; +} + my $base_dir = "/var/lex"; my $keyring_file = "$base_dir/pubring.gpg"; -unveil("/usr/local/bin/gpg", "rx") or die "unveil gpg failed: $!"; -unveil("/dev/null", "rw") or die "unveil /dev/null failed: $!"; -unveil("/tmp", "rwc") or die "unveil /tmp failed: $!"; -unveil($base_dir, "rwc") or die "unveil $base_dir failed: $!"; -unveil() or die "unveil lock failed: $!"; +unveil("/dev/null", "rw") or die "unveil /dev/null failed: $!"; +unveil($base_dir, "rwc") or die "unveil $base_dir failed: $!"; +unveil() or die "unveil lock failed: $!"; -pledge('stdio rpath wpath cpath proc exec') or die "pledge failed: $!"; +pledge('stdio rpath wpath cpath') or die "pledge failed: $!"; my $log_file = "$base_dir/debug.log"; my $max_log_bytes = 1024 * 1024; # 1 MB cap @@ -25,6 +51,7 @@ my $max_log_bytes = 1024 * 1024; # 1 MB cap sub log_msg { my ($msg) = @_; + # Truncate if >$max_log_bytes if (-e $log_file && -s $log_file >= $max_log_bytes) { my $clrh; if (open($clrh, '>', $log_file)) { @@ -36,7 +63,6 @@ sub log_msg { if (open($lh, '>>', $log_file)) { my $tz_offset = 8 * 3600; my ($sec, $min, $hour, $mday, $mon, $year) = gmtime(time() + $tz_offset); - my $timestamp = sprintf( "%04d-%02d-%02d %02d:%02d:%02d", $year + 1900, $mon + 1, $mday, $hour, $min, $sec @@ -57,74 +83,28 @@ sub verify_pgp_signature { my ($signed_data, $signature) = @_; unless (-f $keyring_file) { - log_msg("ERROR: Keyring missing at $keyring_file"); + log_msg("ERROR: No keyring at $keyring_file"); return 0; } - my $tmp_dir = "/tmp/lex"; - unless (-d $tmp_dir) { - make_path($tmp_dir); - } - - my $msg_id = sprintf("msg_%.6f_$$", time()); - my $tmp_data = "${tmp_dir}/${msg_id}.mime"; - my $tmp_sig = "${tmp_dir}/${msg_id}.sig"; - - # Ensure CRLF line endings on the signed data block + # Ensure CRLF line endings per OpenPGP canonical specification $signed_data =~ s/\r?\n/\r\n/g; - my $dh; - unless (open($dh, '>:raw', $tmp_data)) { - log_msg("ERROR: Could not create temp data file: $!"); - return 0; - } - print $dh $signed_data; - close($dh); - - # Write sig file with trailing CRLF - my $sh; - unless (open($sh, '>:raw', $tmp_sig)) { - log_msg("ERROR: Could not create temp sig file: $!"); - unlink($tmp_data); - return 0; - } - print $sh $signature . "\r\n"; - close($sh); - - my $pipe; - my $gpg_output = ""; - my $pid = open($pipe, "-|") // die "Cannot fork: $!"; - - if ($pid == 0) { - open(STDERR, '>&', STDOUT); - exec( - '/usr/local/bin/gpg', - '--batch', - '--no-default-keyring', - '--keyring', $keyring_file, - '--trust-model', 'always', - '--verify', - $tmp_sig, - $tmp_data - ) or exit(127); - } + my $pgp = Crypt::OpenPGP->new( + PubRing => $keyring_file, + ); - while (my $line = <$pipe>) { - $gpg_output .= $line; + unless ($pgp) { + log_msg("ERROR: Failed to initialize PGP"); + return 0; } - close($pipe); - my $exit_code = $? >> 8; - unlink($tmp_data, $tmp_sig); + my $verified = $pgp->verify( + Data => $signed_data, + Signature => $signature, + ); - if ($exit_code == 0) { - return 1; - } else { - $gpg_output =~ s/\r?\n/ /g; - log_msg("DEBUG: gpg output: $gpg_output"); - log_msg("DEBUG: gpg exited with code $exit_code"); - return 0; - } + return $verified ? 1 : 0; } my $content_type = $ENV{'CONTENT_TYPE'} // ''; @@ -132,7 +112,7 @@ my $content_length = $ENV{'CONTENT_LENGTH'} // 0; my $remote_ip = $ENV{'REMOTE_ADDR'} // 'unknown'; my $user_agent = $ENV{'HTTP_USER_AGENT'} // 'unknown'; -log_msg("INFO: Request received from $remote_ip [$user_agent]"); +log_msg("INFO: New request from $remote_ip [$user_agent]"); unless ($content_length > 0) { log_msg("WARN: content_length is 0 or missing"); @@ -165,7 +145,7 @@ for my $pair (split(/&/, $raw_data)) { my $mime_raw = $params{'body-mime'} // ''; if ($mime_raw eq '') { - log_msg("WARN: 'body-mime' parameter is empty or missing"); + log_msg("WARN: No MIME message in payload"); respond_ok(); } @@ -191,7 +171,7 @@ unless ($pgp_sig) { } unless (length($signed_part) > 0 && $pgp_sig) { - log_msg("WARN: Missing signed_part content or PGP signature block"); + log_msg("WARN: Missing PGP signature block"); respond_ok(); } @@ -206,7 +186,7 @@ $plain_text =~ s/\n-- \n.*$//s; $plain_text =~ s/^\s+|\s+$//g; unless (length($plain_text) > 0) { - log_msg("WARN: Extracted plain_text is empty after stripping"); + log_msg("WARN: Message is empty after stripping"); respond_ok(); } @@ -219,8 +199,9 @@ unless (open($qfh, '>>:utf8', $queue_file)) { print $qfh $plain_text . "\n"; close($qfh); -my $msg_id = sprintf("msg_%.6f_$$", time()); +my $elapsed_ms = sprintf("%.2fms", (time() - $start_time) * 1000); +my $msg_id = sprintf("msg_%.6f_$$", time()); my $content_length_kb = sprintf("%.2f KB", $content_length / 1024); -log_msg("INFO: Processed $msg_id successfully ($content_length_kb)"); -respond_ok(); +log_msg("INFO: Processed $msg_id ($content_length_kb) in $elapsed_ms"); +respond_ok(); -- cgit v1.2.3