summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSadeep Madurange <sadeep@asciimx.com>2026-08-10 11:27:28 +0800
committerSadeep Madurange <sadeep@asciimx.com>2026-08-10 12:28:15 +0800
commitdd2989b86b1a23469165048865945d48a2fd9e7b (patch)
treec48171afb619eb71255b24dbed84db4e8da7dcc6
parent78eb31baf3a34ba255701c249d71dda7fd0d75e5 (diff)
downloadlex-dd2989b86b1a23469165048865945d48a2fd9e7b.tar.gz
Verify GPG signature using OpenPGP Perl module.
-rw-r--r--chroot_test.txt94
-rwxr-xr-xlex.cgi127
2 files changed, 54 insertions, 167 deletions
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
- <undefined> (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 <sadeep@asciimx.com>)
- 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 <sadeep@asciimx.com>)
- 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 <sadeep@asciimx.com>
-To: lex@info.asciimx.com
-Subject: Test
-Message-ID: <ankoDK-hx0-riv1A@wd>
-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();