summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSadeep Madurange <sadeep@asciimx.com>2026-08-17 17:29:28 +0800
committerSadeep Madurange <sadeep@asciimx.com>2026-08-17 17:29:28 +0800
commitae06cd0db80d688ab2c82cd2b03b8f1d6cf9f595 (patch)
tree10fc8fb102cb94047ca91efd92ff4e460a13c1b5
parentbdb2060407df9c798fd8460b8aaa1c395d7a16b4 (diff)
downloadlex-ae06cd0db80d688ab2c82cd2b03b8f1d6cf9f595.tar.gz
Improve PGP error logs, get things working with K-mail.
-rwxr-xr-xlex.cgi49
1 files changed, 43 insertions, 6 deletions
diff --git a/lex.cgi b/lex.cgi
index ea181c7..da17fc5 100755
--- a/lex.cgi
+++ b/lex.cgi
@@ -75,11 +75,23 @@ sub respond_ok {
exit 0;
}
+my $pgp_error;
+
+sub set_pgp_error {
+ my ($msg) = @_;
+ $msg //= 'unknown error';
+ $msg =~ s/\s+/ /g; # collapse any newlines/whitespace runs to single spaces
+ $msg =~ s/^\s+|\s+$//g; # trim leading/trailing space
+ $pgp_error = $msg;
+}
+
sub verify_pgp_signature {
my ($signed_data, $signature) = @_;
+ $pgp_error = undef;
+
unless (-f $keyring_file) {
- log_msg("ERROR: No keyring at $keyring_file");
+ set_pgp_error("No keyring at $keyring_file");
return 0;
}
@@ -91,7 +103,7 @@ sub verify_pgp_signature {
);
unless ($pgp) {
- log_msg("ERROR: Failed to initialize PGP");
+ set_pgp_error(Crypt::OpenPGP->errstr // 'unknown PGP init error');
return 0;
}
@@ -100,6 +112,10 @@ sub verify_pgp_signature {
Signature => $signature,
);
+ unless ($verified) {
+ set_pgp_error($pgp->errstr // 'unknown error');
+ }
+
return $verified ? 1 : 0;
}
@@ -188,8 +204,29 @@ unless (@parts >= 2) {
respond_ok();
}
-# Extract canonical signed part directly (Part 0 string representation)
-my $signed_part = $parts[0]->as_string;
+# Extract the boundary string from the outer Content-Type header so the
+# signed part can be sliced out of the raw message verbatim. PGP/MIME
+# signature verification (RFC 3156) requires the exact original octets of
+# the signed entity; re-serializing it via Email::MIME's ->as_string()
+# is not safe, since header folding, whitespace, and field order can be
+# normalized differently on reserialization than the sender produced,
+# which breaks the signature hash even though the content is unchanged.
+my ($boundary) = $parsed->content_type =~ /boundary="?([^";]+)"?/;
+
+unless ($boundary) {
+ log_msg("WARN: No MIME boundary found in Content-Type header");
+ respond_ok();
+}
+
+# Slice the literal bytes between the first boundary delimiter and the
+# CRLF immediately preceding the second boundary delimiter. That trailing
+# CRLF belongs to the boundary line itself, not the signed content, per
+# the MIME multipart spec.
+my $signed_part;
+if ($mime_raw =~ /--\Q$boundary\E\r?\n(.*?)\r?\n--\Q$boundary\E/s) {
+ $signed_part = $1;
+ $signed_part =~ s/\r?\n/\r\n/g; # canonical OpenPGP line endings
+}
# Extract PGP signature block
my $sig_part_raw = $parts[1]->body_raw // '';
@@ -200,13 +237,13 @@ unless ($pgp_sig) {
($pgp_sig) = $sig_part_str =~ /(-----BEGIN PGP SIGNATURE-----[\s\S]*?-----END PGP SIGNATURE-----)/;
}
-unless (length($signed_part) > 0 && $pgp_sig) {
+unless (defined $signed_part && length($signed_part) > 0 && $pgp_sig) {
log_msg("WARN: Missing PGP signature block");
respond_ok();
}
unless (verify_pgp_signature($signed_part, $pgp_sig)) {
- log_msg("WARN: PGP signature verification failed");
+ log_msg("WARN: PGP signature verification failed: " . ($pgp_error // 'unknown error'));
respond_ok();
}