diff options
| author | Sadeep Madurange <sadeep@asciimx.com> | 2026-08-10 11:12:33 +0800 |
|---|---|---|
| committer | Sadeep Madurange <sadeep@asciimx.com> | 2026-08-10 11:12:33 +0800 |
| commit | 78eb31baf3a34ba255701c249d71dda7fd0d75e5 (patch) | |
| tree | 4a7e6fcb545c2077d4aa7459d5e5d8b68199d164 | |
| parent | 09292783439f705d70b13463fb8aa23ab667bc67 (diff) | |
| download | lex-78eb31baf3a34ba255701c249d71dda7fd0d75e5.tar.gz | |
Verify GPG signature.
| -rw-r--r-- | README.txt | 11 | ||||
| -rw-r--r-- | chroot_test.txt | 94 | ||||
| -rwxr-xr-x | lex.cgi | 142 |
3 files changed, 205 insertions, 42 deletions
@@ -7,6 +7,17 @@ TEST # slowcgi -d -v -s /var/www/run/slowcgi_debug.sock +Without PGP: + +# echo -n "body-mime=hello%20world" | chroot -u www /var/www /usr/bin/perl -e ' + $ENV{CONTENT_TYPE} = "application/x-www-form-urlencoded"; + $ENV{CONTENT_LENGTH} = 23; + $ENV{REMOTE_ADDR} = "127.0.0.1"; + $ENV{HTTP_USER_AGENT}= "test-cli"; + do "/cgi-bin/lex.cgi"; + print $@ if $@; +' + CHROOT: # Create the jail root diff --git a/chroot_test.txt b/chroot_test.txt new file mode 100644 index 0000000..a1c76d8 --- /dev/null +++ b/chroot_test.txt @@ -0,0 +1,94 @@ +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"; +' @@ -8,12 +8,18 @@ use Email::MIME; use OpenBSD::Pledge; use OpenBSD::Unveil; -unveil("/tmp", "rwc") or die "unveil /tmp failed: $!"; -unveil() or die "unveil lock failed: $!"; +my $base_dir = "/var/lex"; +my $keyring_file = "$base_dir/pubring.gpg"; -pledge('stdio rpath wpath cpath') or die "pledge failed: $!"; +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: $!"; -my $log_file = "/tmp/lex.log"; +pledge('stdio rpath wpath cpath proc exec') or die "pledge failed: $!"; + +my $log_file = "$base_dir/debug.log"; my $max_log_bytes = 1024 * 1024; # 1 MB cap sub log_msg { @@ -28,7 +34,13 @@ sub log_msg { my $lh; if (open($lh, '>>', $log_file)) { - my $timestamp = sprintf("%d", time()); + 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 + ); print $lh "[$timestamp] [$$] $msg\n"; close($lh); } @@ -41,10 +53,84 @@ sub respond_ok { exit 0; } -my $content_type = $ENV{'CONTENT_TYPE'} // ''; +sub verify_pgp_signature { + my ($signed_data, $signature) = @_; + + unless (-f $keyring_file) { + log_msg("ERROR: Keyring missing 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 + $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); + } + + while (my $line = <$pipe>) { + $gpg_output .= $line; + } + close($pipe); + my $exit_code = $? >> 8; + + unlink($tmp_data, $tmp_sig); + + 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; + } +} + +my $content_type = $ENV{'CONTENT_TYPE'} // ''; my $content_length = $ENV{'CONTENT_LENGTH'} // 0; -my $remote_ip = $ENV{'REMOTE_ADDR'} // 'unknown'; +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]"); @@ -57,7 +143,6 @@ binmode(STDIN); my $raw_data = ''; read(STDIN, $raw_data, $content_length); -# Parse application/x-www-form-urlencoded payload unless ($content_type =~ /application\/x-www-form-urlencoded/i) { log_msg("WARN: Content-Type is not urlencoded: '$content_type'"); respond_ok(); @@ -78,7 +163,6 @@ for my $pair (split(/&/, $raw_data)) { $params{$key} //= $val; } -# Extract MIME message for PGP check my $mime_raw = $params{'body-mime'} // ''; if ($mime_raw eq '') { log_msg("WARN: 'body-mime' parameter is empty or missing"); @@ -89,19 +173,15 @@ if ($mime_raw eq '') { my $parsed = Email::MIME->new($mime_raw); my @parts = $parsed->parts; -# Signed content + PGP signature must be present unless (@parts >= 2) { log_msg("WARN: MIME parts count < 2 (found " . scalar(@parts) . ")"); respond_ok(); } -# Part 0: canonical signed content (headers + body) +# Extract canonical signed part directly (Part 0 string representation) my $signed_part = $parts[0]->as_string; -# Normalize line endings to \r\n for canonical PGP verification -$signed_part =~ s/\r?\n/\r\n/g; - -# Part 1: detached PGP signature +# Extract PGP signature block my $sig_part_raw = $parts[1]->body_raw // ''; my ($pgp_sig) = $sig_part_raw =~ /(-----BEGIN PGP SIGNATURE-----[\s\S]*?-----END PGP SIGNATURE-----)/; @@ -115,34 +195,11 @@ unless (length($signed_part) > 0 && $pgp_sig) { respond_ok(); } -my $dir = "/tmp/lex"; -unless (-d $dir) { - make_path($dir); -} - -my $msg_id = sprintf("msg_%.6f_$$", time()); -my $sig_file = "${dir}/${msg_id}.sig"; -my $mime_file = "${dir}/${msg_id}.mime"; - -# Write MIME file -my $mfh; -unless (open($mfh, '>:raw', $mime_file)) { - log_msg("ERROR: Failed to write $mime_file: $!"); - respond_ok(); -} -print $mfh $signed_part; -close($mfh); - -# Write sig file -my $sfh; -unless (open($sfh, '>:raw', $sig_file)) { - log_msg("ERROR: Failed to write $sig_file: $!"); +unless (verify_pgp_signature($signed_part, $pgp_sig)) { + log_msg("WARN: PGP signature verification failed"); respond_ok(); } -print $sfh $pgp_sig . "\r\n"; -close($sfh); -# Extract body, strip signature block, leading and trailing whitespaces my $plain_text = $parts[0]->body_str // ''; $plain_text =~ s/\r\n/\n/g; $plain_text =~ s/\n-- \n.*$//s; @@ -153,8 +210,7 @@ unless (length($plain_text) > 0) { respond_ok(); } -# Append plain text to queue file -my $queue_file = "/tmp/lex_queue.txt"; +my $queue_file = "$base_dir/queue.txt"; my $qfh; unless (open($qfh, '>>:utf8', $queue_file)) { log_msg("ERROR: Failed to append to $queue_file: $!"); @@ -163,6 +219,8 @@ unless (open($qfh, '>>:utf8', $queue_file)) { print $qfh $plain_text . "\n"; close($qfh); +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(); + |
