line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
# <@LICENSE> |
2
|
|
|
|
|
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); |
3
|
|
|
|
|
|
|
# you may not use this file except in compliance with the License. |
4
|
|
|
|
|
|
|
# You may obtain a copy of the License at |
5
|
|
|
|
|
|
|
# |
6
|
|
|
|
|
|
|
# http://www.apache.org/licenses/LICENSE-2.0 |
7
|
|
|
|
|
|
|
# |
8
|
|
|
|
|
|
|
# Unless required by applicable law or agreed to in writing, software |
9
|
|
|
|
|
|
|
# distributed under the License is distributed on an "AS IS" BASIS, |
10
|
|
|
|
|
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
11
|
|
|
|
|
|
|
# See the License for the specific language governing permissions and |
12
|
|
|
|
|
|
|
# limitations under the License. |
13
|
|
|
|
|
|
|
# @LICENSE> |
14
|
|
|
|
|
|
|
|
15
|
|
|
|
|
|
|
package Mail::SpamAssassin::Plugin::OpenPGP; |
16
|
|
|
|
|
|
|
|
17
|
|
|
|
|
|
|
=head1 NAME |
18
|
|
|
|
|
|
|
|
19
|
|
|
|
|
|
|
Mail::SpamAssassin::Plugin::OpenPGP - A SpamAssassin plugin that validates OpenPGP signed email. |
20
|
|
|
|
|
|
|
|
21
|
|
|
|
|
|
|
=head1 VERSION |
22
|
|
|
|
|
|
|
|
23
|
|
|
|
|
|
|
Version 1.0.4 |
24
|
|
|
|
|
|
|
|
25
|
|
|
|
|
|
|
=cut |
26
|
|
|
|
|
|
|
|
27
|
|
|
|
|
|
|
our $VERSION = '1.0.4'; |
28
|
|
|
|
|
|
|
|
29
|
|
|
|
|
|
|
#TODO maybe use OpenPGP.pm.PL to generate this file (see perldoc Module::Build "code" section) and include etc/26_openpgp.cf automatically |
30
|
|
|
|
|
|
|
|
31
|
|
|
|
|
|
|
=head1 SYNOPSIS |
32
|
|
|
|
|
|
|
|
33
|
|
|
|
|
|
|
Install this module by running: |
34
|
|
|
|
|
|
|
|
35
|
|
|
|
|
|
|
cpan Mail::SpamAssassin::Plugin::OpenPGP |
36
|
|
|
|
|
|
|
|
37
|
|
|
|
|
|
|
Tell SpamAssassin to use it by putting the following (from this module's F) in a configuration file |
38
|
|
|
|
|
|
|
|
39
|
|
|
|
|
|
|
loadplugin Mail::SpamAssassin::Plugin::OpenPGP |
40
|
|
|
|
|
|
|
|
41
|
|
|
|
|
|
|
Configure the plugin by putting the following (from this module's F) in a configuration file (see L) |
42
|
|
|
|
|
|
|
|
43
|
|
|
|
|
|
|
ifplugin Mail::SpamAssassin::Plugin::OpenPGP |
44
|
|
|
|
|
|
|
|
45
|
|
|
|
|
|
|
rawbody OPENPGP_SIGNED eval:check_openpgp_signed() |
46
|
|
|
|
|
|
|
describe OPENPGP_SIGNED OpenPGP: message body is signed |
47
|
|
|
|
|
|
|
|
48
|
|
|
|
|
|
|
rawbody OPENPGP_ENCRYPTED eval:check_openpgp_encrypted() |
49
|
|
|
|
|
|
|
describe OPENPGP_ENCRYPTED OpenPGP: message body is encrypted |
50
|
|
|
|
|
|
|
|
51
|
|
|
|
|
|
|
rawbody OPENPGP_SIGNED_GOOD eval:check_openpgp_signed_good() |
52
|
|
|
|
|
|
|
describe OPENPGP_SIGNED_GOOD OpenPGP: message body is signed with a valid signature |
53
|
|
|
|
|
|
|
tflags OPENPGP_SIGNED_GOOD nice |
54
|
|
|
|
|
|
|
|
55
|
|
|
|
|
|
|
rawbody OPENPGP_SIGNED_BAD eval:check_openpgp_signed_bad() |
56
|
|
|
|
|
|
|
describe OPENPGP_SIGNED_BAD OpenPGP: message body is signed but the signature is invalid, or doesn't match with email's date or sender |
57
|
|
|
|
|
|
|
|
58
|
|
|
|
|
|
|
endif # Mail::SpamAssassin::Plugin::OpenPGP |
59
|
|
|
|
|
|
|
|
60
|
|
|
|
|
|
|
Set up some rules to your liking, for example: |
61
|
|
|
|
|
|
|
|
62
|
|
|
|
|
|
|
score OPENPGP_SIGNED -1 |
63
|
|
|
|
|
|
|
# this would total to -2 |
64
|
|
|
|
|
|
|
score OPENPGP_SIGNED_GOOD -1 |
65
|
|
|
|
|
|
|
# this would total to 0 |
66
|
|
|
|
|
|
|
score OPENPGP_SIGNED_BAD 1 |
67
|
|
|
|
|
|
|
|
68
|
|
|
|
|
|
|
=head1 DESCRIPTION |
69
|
|
|
|
|
|
|
|
70
|
|
|
|
|
|
|
This uses Mail::GPG which uses GnuPG::Interface which uses Gnu Privacy Guard via IPC. |
71
|
|
|
|
|
|
|
|
72
|
|
|
|
|
|
|
Make sure the homedir you use for gnupg has a gpg.conf with something like the following in it, so that it will automatically fetch public keys. And make sure that the directory & files are only readable by owner (a gpg security requirement). |
73
|
|
|
|
|
|
|
|
74
|
|
|
|
|
|
|
keyserver-options auto-key-retrieve timeout=5 |
75
|
|
|
|
|
|
|
# any keyserver will do |
76
|
|
|
|
|
|
|
keyserver x-hkp://random.sks.keyserver.penguin.de |
77
|
|
|
|
|
|
|
|
78
|
|
|
|
|
|
|
If a public key cannot be retrieved, the email will be marked as SIGNED but neither GOOD nor BAD. To ensure that your local public keys don't get out of date, you should probably set up a scheduled job to delete pubring.gpg regularly |
79
|
|
|
|
|
|
|
|
80
|
|
|
|
|
|
|
For project information, see L |
81
|
|
|
|
|
|
|
|
82
|
|
|
|
|
|
|
=head1 USER SETTINGS |
83
|
|
|
|
|
|
|
|
84
|
|
|
|
|
|
|
gpg_executable /path/to/gpg |
85
|
|
|
|
|
|
|
gpg_homedir /var/foo/gpg-homedir-for-spamassassin |
86
|
|
|
|
|
|
|
openpgp_add_header_fingerprint 1 # default 1 (true) |
87
|
|
|
|
|
|
|
openpgp_add_header_failure_info 0 # default 1 (true) |
88
|
|
|
|
|
|
|
|
89
|
|
|
|
|
|
|
The OpenPGP headers are never added to emails without a signature. |
90
|
|
|
|
|
|
|
|
91
|
|
|
|
|
|
|
=cut |
92
|
|
|
|
|
|
|
|
93
|
|
|
|
|
|
|
=head1 TAGS |
94
|
|
|
|
|
|
|
|
95
|
|
|
|
|
|
|
The following per-message SpamAssassin "tags" are set. |
96
|
|
|
|
|
|
|
|
97
|
|
|
|
|
|
|
=head2 openpgp_checked |
98
|
|
|
|
|
|
|
|
99
|
|
|
|
|
|
|
Set to 1 after the email has been checked for an OpenPGP signature |
100
|
|
|
|
|
|
|
|
101
|
|
|
|
|
|
|
=head2 openpgp_signed |
102
|
|
|
|
|
|
|
|
103
|
|
|
|
|
|
|
Set to 1 if the email has an OpenPGP signature |
104
|
|
|
|
|
|
|
|
105
|
|
|
|
|
|
|
=head2 openpgp_signed_good |
106
|
|
|
|
|
|
|
|
107
|
|
|
|
|
|
|
Set to 1 if the email has a "good" OpenPGP signature |
108
|
|
|
|
|
|
|
|
109
|
|
|
|
|
|
|
=head2 openpgp_signed_bad |
110
|
|
|
|
|
|
|
|
111
|
|
|
|
|
|
|
Set to 1 if the email has a "bad" OpenPGP signature |
112
|
|
|
|
|
|
|
|
113
|
|
|
|
|
|
|
=head2 openpgp_encrypted |
114
|
|
|
|
|
|
|
|
115
|
|
|
|
|
|
|
Set to 1 if the email is encrypted with OpenPGP |
116
|
|
|
|
|
|
|
|
117
|
|
|
|
|
|
|
=head2 openpgp_fingerprint |
118
|
|
|
|
|
|
|
|
119
|
|
|
|
|
|
|
Set to the OpenPGP fingerprint from the signature |
120
|
|
|
|
|
|
|
|
121
|
|
|
|
|
|
|
=cut |
122
|
|
|
|
|
|
|
|
123
|
1
|
|
|
1
|
|
26386
|
use warnings; |
|
1
|
|
|
|
|
2
|
|
|
1
|
|
|
|
|
39
|
|
124
|
1
|
|
|
1
|
|
7
|
use strict; |
|
1
|
|
|
|
|
2
|
|
|
1
|
|
|
|
|
37
|
|
125
|
1
|
|
|
1
|
|
490
|
use Mail::SpamAssassin::Plugin; |
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
126
|
|
|
|
|
|
|
use Mail::SpamAssassin::Logger; |
127
|
|
|
|
|
|
|
use Mail::SpamAssassin::Timeout; |
128
|
|
|
|
|
|
|
use Mail::GPG; |
129
|
|
|
|
|
|
|
|
130
|
|
|
|
|
|
|
use vars qw(@ISA); |
131
|
|
|
|
|
|
|
@ISA = qw(Mail::SpamAssassin::Plugin); |
132
|
|
|
|
|
|
|
|
133
|
|
|
|
|
|
|
sub new { |
134
|
|
|
|
|
|
|
my $class = shift; |
135
|
|
|
|
|
|
|
my $mailsaobject = shift; |
136
|
|
|
|
|
|
|
|
137
|
|
|
|
|
|
|
# some boilerplate... |
138
|
|
|
|
|
|
|
$class = ref($class) || $class; |
139
|
|
|
|
|
|
|
my $self = $class->SUPER::new($mailsaobject); |
140
|
|
|
|
|
|
|
bless ($self, $class); |
141
|
|
|
|
|
|
|
|
142
|
|
|
|
|
|
|
dbg "openpgp: created"; |
143
|
|
|
|
|
|
|
|
144
|
|
|
|
|
|
|
$self->register_eval_rule ("check_openpgp_signed"); |
145
|
|
|
|
|
|
|
$self->register_eval_rule ("check_openpgp_signed_good"); |
146
|
|
|
|
|
|
|
$self->register_eval_rule ("check_openpgp_signed_bad"); |
147
|
|
|
|
|
|
|
$self->register_eval_rule ("check_openpgp_encrypted"); |
148
|
|
|
|
|
|
|
# TODO: trusted none, marginal, full, ultimate |
149
|
|
|
|
|
|
|
|
150
|
|
|
|
|
|
|
$self->set_config($mailsaobject->{conf}); |
151
|
|
|
|
|
|
|
|
152
|
|
|
|
|
|
|
return $self; |
153
|
|
|
|
|
|
|
} |
154
|
|
|
|
|
|
|
|
155
|
|
|
|
|
|
|
# SA 3.1 style of parsing config options |
156
|
|
|
|
|
|
|
sub set_config { |
157
|
|
|
|
|
|
|
my($self, $conf) = @_; |
158
|
|
|
|
|
|
|
my @cmds = (); |
159
|
|
|
|
|
|
|
|
160
|
|
|
|
|
|
|
# see Mail::SpamAssassin::Conf::Parser for expected format of the "config blocks" stored in @cmds |
161
|
|
|
|
|
|
|
|
162
|
|
|
|
|
|
|
push(@cmds, { |
163
|
|
|
|
|
|
|
setting => 'gpg_homedir', |
164
|
|
|
|
|
|
|
# FIXME: default => 1, |
165
|
|
|
|
|
|
|
type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING, |
166
|
|
|
|
|
|
|
}); |
167
|
|
|
|
|
|
|
push(@cmds, { |
168
|
|
|
|
|
|
|
setting => 'gpg_executable', |
169
|
|
|
|
|
|
|
type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING, |
170
|
|
|
|
|
|
|
}); |
171
|
|
|
|
|
|
|
push(@cmds, { |
172
|
|
|
|
|
|
|
setting => 'openpgp_add_header_fingerprint', |
173
|
|
|
|
|
|
|
default => 1, |
174
|
|
|
|
|
|
|
type => $Mail::SpamAssassin::Conf::CONF_TYPE_BOOLEAN, |
175
|
|
|
|
|
|
|
}); |
176
|
|
|
|
|
|
|
push(@cmds, { |
177
|
|
|
|
|
|
|
setting => 'openpgp_add_header_failure_info', |
178
|
|
|
|
|
|
|
default => 1, |
179
|
|
|
|
|
|
|
type => $Mail::SpamAssassin::Conf::CONF_TYPE_BOOLEAN, |
180
|
|
|
|
|
|
|
}); |
181
|
|
|
|
|
|
|
# FIXME do we even need this |
182
|
|
|
|
|
|
|
# FIXME use fingerprints, not email address |
183
|
|
|
|
|
|
|
push (@cmds, { |
184
|
|
|
|
|
|
|
setting => 'whitelist_from_openpgp', |
185
|
|
|
|
|
|
|
code => sub { |
186
|
|
|
|
|
|
|
my ($self, $key, $value, $line) = @_; |
187
|
|
|
|
|
|
|
dbg "openpgp: handling whitelist_from_openpgp"; |
188
|
|
|
|
|
|
|
unless (defined $value && $value !~ /^$/) { |
189
|
|
|
|
|
|
|
return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE; |
190
|
|
|
|
|
|
|
} |
191
|
|
|
|
|
|
|
dbg "openpgp: value: $value"; |
192
|
|
|
|
|
|
|
unless ($value =~ /^(\S+)(?:\s+(\S+))?$/) { |
193
|
|
|
|
|
|
|
return $Mail::SpamAssassin::Conf::INVALID_VALUE; |
194
|
|
|
|
|
|
|
} |
195
|
|
|
|
|
|
|
my $address = $1; |
196
|
|
|
|
|
|
|
dbg "openpgp: address: $address"; |
197
|
|
|
|
|
|
|
my $signer = (defined $2 ? $2 : $1); |
198
|
|
|
|
|
|
|
dbg "openpgp: signer: $signer"; |
199
|
|
|
|
|
|
|
|
200
|
|
|
|
|
|
|
unless (defined $2) { |
201
|
|
|
|
|
|
|
$signer =~ s/^.*@(.*)$/$1/; |
202
|
|
|
|
|
|
|
} |
203
|
|
|
|
|
|
|
dbg "openpgp: signer: $signer"; |
204
|
|
|
|
|
|
|
# FIXME use fingerprint |
205
|
|
|
|
|
|
|
$self->{parser}->add_to_addrlist_rcvd ('whitelist_from_openpgp', $address, $signer); |
206
|
|
|
|
|
|
|
} |
207
|
|
|
|
|
|
|
}); |
208
|
|
|
|
|
|
|
|
209
|
|
|
|
|
|
|
# grr, why isn't register_commands documented? |
210
|
|
|
|
|
|
|
$conf->{parser}->register_commands(\@cmds); |
211
|
|
|
|
|
|
|
} |
212
|
|
|
|
|
|
|
|
213
|
|
|
|
|
|
|
sub check_openpgp_signed_good { |
214
|
|
|
|
|
|
|
my ($self, $scan) = @_; |
215
|
|
|
|
|
|
|
dbg "openpgp: running check_openpgp_signed_good"; |
216
|
|
|
|
|
|
|
$self->_check_openpgp($scan); |
217
|
|
|
|
|
|
|
return $scan->{openpgp_signed_good}; |
218
|
|
|
|
|
|
|
} |
219
|
|
|
|
|
|
|
sub check_openpgp_signed_bad { |
220
|
|
|
|
|
|
|
my ($self, $scan) = @_; |
221
|
|
|
|
|
|
|
dbg "openpgp: running check_openpgp_signed_bad"; |
222
|
|
|
|
|
|
|
$self->_check_openpgp($scan); |
223
|
|
|
|
|
|
|
return $scan->{openpgp_signed_bad}; |
224
|
|
|
|
|
|
|
} |
225
|
|
|
|
|
|
|
sub check_openpgp_signed { |
226
|
|
|
|
|
|
|
my ($self, $scan) = @_; |
227
|
|
|
|
|
|
|
dbg "openpgp: running check_openpgp_signed"; |
228
|
|
|
|
|
|
|
$self->_check_openpgp($scan); |
229
|
|
|
|
|
|
|
return $scan->{openpgp_signed}; |
230
|
|
|
|
|
|
|
} |
231
|
|
|
|
|
|
|
sub check_openpgp_encrypted { |
232
|
|
|
|
|
|
|
my ($self, $scan) = @_; |
233
|
|
|
|
|
|
|
dbg "openpgp: running check_openpgp_encrypted"; |
234
|
|
|
|
|
|
|
$self->_check_openpgp($scan); |
235
|
|
|
|
|
|
|
return $scan->{openpgp_encrypted}; |
236
|
|
|
|
|
|
|
} |
237
|
|
|
|
|
|
|
|
238
|
|
|
|
|
|
|
# taken from Mail::SpamAssassin::PerMsgStatus's _get |
239
|
|
|
|
|
|
|
sub _just_email { |
240
|
|
|
|
|
|
|
my $result = shift; |
241
|
|
|
|
|
|
|
$result =~ s/\s+/ /g; # reduce whitespace |
242
|
|
|
|
|
|
|
$result =~ s/^\s+//; # leading whitespace |
243
|
|
|
|
|
|
|
$result =~ s/\s+$//; # trailing whitespace |
244
|
|
|
|
|
|
|
|
245
|
|
|
|
|
|
|
# Get the email address out of the header |
246
|
|
|
|
|
|
|
# All of these should result in "jm@foo": |
247
|
|
|
|
|
|
|
# jm@foo |
248
|
|
|
|
|
|
|
# jm@foo (Foo Blah) |
249
|
|
|
|
|
|
|
# jm@foo, jm@bar |
250
|
|
|
|
|
|
|
# display: jm@foo (Foo Blah), jm@bar ; |
251
|
|
|
|
|
|
|
# Foo Blah |
252
|
|
|
|
|
|
|
# "Foo Blah" |
253
|
|
|
|
|
|
|
# "'Foo Blah'" |
254
|
|
|
|
|
|
|
# "_$B!z8=6b$=$N>l$GEv$?$j!*!zEv_(B_$B$?$k!*!)$/$8!z7|>^%\%s%P! (bug 3979) |
255
|
|
|
|
|
|
|
# |
256
|
|
|
|
|
|
|
# strip out the (comments) |
257
|
|
|
|
|
|
|
$result =~ s/\s*\(.*?\)//g; |
258
|
|
|
|
|
|
|
# strip out the "quoted text" |
259
|
|
|
|
|
|
|
$result =~ s/(?
|
260
|
|
|
|
|
|
|
# Foo Blah or |
261
|
|
|
|
|
|
|
$result =~ s/^[^<]*?<(.*?)>.*$/$1/; |
262
|
|
|
|
|
|
|
# multiple addresses on one line? remove all but first |
263
|
|
|
|
|
|
|
$result =~ s/,.*$//; |
264
|
|
|
|
|
|
|
return $result; |
265
|
|
|
|
|
|
|
} |
266
|
|
|
|
|
|
|
|
267
|
|
|
|
|
|
|
# TODO contribute back to Mail::GPG::Result |
268
|
|
|
|
|
|
|
sub _gpg_result_date { |
269
|
|
|
|
|
|
|
my $result = shift; |
270
|
|
|
|
|
|
|
my $gpg_status = $result->get_gpg_status; |
271
|
|
|
|
|
|
|
## dbg "openpgp: status: " . $$gpg_status; |
272
|
|
|
|
|
|
|
# based on Mail::GPG::Result's analyze_result |
273
|
|
|
|
|
|
|
pos($$gpg_status) = undef; # reset /g modifier since this module uses the following regex multiple times |
274
|
|
|
|
|
|
|
while ( $$gpg_status && $$gpg_status =~ m{^\[GNUPG:\]\s+(.*)$}mg ) { |
275
|
|
|
|
|
|
|
my $line = $1; |
276
|
|
|
|
|
|
|
## dbg "openpgp: line: " . $line; |
277
|
|
|
|
|
|
|
# 3rd field after VALIDSIG |
278
|
|
|
|
|
|
|
if ( $line =~ /^VALIDSIG\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)/ ) { |
279
|
|
|
|
|
|
|
#$sign_fingerprint = $1; |
280
|
|
|
|
|
|
|
return $3; |
281
|
|
|
|
|
|
|
} |
282
|
|
|
|
|
|
|
} |
283
|
|
|
|
|
|
|
} |
284
|
|
|
|
|
|
|
|
285
|
|
|
|
|
|
|
# TODO contribute back to Mail::GPG::Result |
286
|
|
|
|
|
|
|
# it's get_sign_fingerprint does signing key, not primary key if signing key is a subkey |
287
|
|
|
|
|
|
|
sub _gpg_result_primary_key_fingerprint { |
288
|
|
|
|
|
|
|
my $result = shift; |
289
|
|
|
|
|
|
|
my $gpg_status = $result->get_gpg_status; |
290
|
|
|
|
|
|
|
pos($$gpg_status) = undef; # reset /g modifier since this module uses the following regex multiple times |
291
|
|
|
|
|
|
|
# based on Mail::GPG::Result's analyze_result |
292
|
|
|
|
|
|
|
while ( $$gpg_status && $$gpg_status =~ m{^\[GNUPG:\]\s+(.*)$}mg ) { |
293
|
|
|
|
|
|
|
my $line = $1; |
294
|
|
|
|
|
|
|
# if signed with a subkey, subkey comes first and primary key comes later |
295
|
|
|
|
|
|
|
# [GNUPG:] VALIDSIG D1892B5C772E643EBB97397E6737EA5562EFBB73 2008-01-21 1200891462 0 3 0 1 10 01 EAB0FABEDEA81AD4086902FE56F0526F9BB3CE70 |
296
|
|
|
|
|
|
|
# some gnupg versions may only output 3 fields after VALIDSIG |
297
|
|
|
|
|
|
|
# get last 40hex-digit sequence |
298
|
|
|
|
|
|
|
if ( $line =~ /^VALIDSIG.+([0-9A-F]{40})/ ) { |
299
|
|
|
|
|
|
|
return $1; |
300
|
|
|
|
|
|
|
} |
301
|
|
|
|
|
|
|
} |
302
|
|
|
|
|
|
|
} |
303
|
|
|
|
|
|
|
|
304
|
|
|
|
|
|
|
sub _check_openpgp { |
305
|
|
|
|
|
|
|
my ($self, $scan) = @_; |
306
|
|
|
|
|
|
|
return if $scan->{openpgp_checked}; |
307
|
|
|
|
|
|
|
|
308
|
|
|
|
|
|
|
$scan->{openpgp_checked} = 0; |
309
|
|
|
|
|
|
|
$scan->{openpgp_signed} = 0; |
310
|
|
|
|
|
|
|
$scan->{openpgp_signed_good} = 0; |
311
|
|
|
|
|
|
|
$scan->{openpgp_signed_bad} = 0; |
312
|
|
|
|
|
|
|
|
313
|
|
|
|
|
|
|
my %opts; |
314
|
|
|
|
|
|
|
if (defined $scan->{conf}->{gpg_executable}) { |
315
|
|
|
|
|
|
|
$opts{gpg_call} = $scan->{conf}->{gpg_executable}; |
316
|
|
|
|
|
|
|
} |
317
|
|
|
|
|
|
|
# see GnuPG::Interface's hash_init (correlates to gpg commandline arguments) |
318
|
|
|
|
|
|
|
$opts{gnupg_hash_init} = { |
319
|
|
|
|
|
|
|
homedir => $scan->{conf}->{gpg_homedir} |
320
|
|
|
|
|
|
|
}; |
321
|
|
|
|
|
|
|
|
322
|
|
|
|
|
|
|
my $gpg = Mail::GPG->new(%opts); |
323
|
|
|
|
|
|
|
# TODO: use SA-parsed entity instead of having Mail::GPG reparse it into a MIME::Entity? |
324
|
|
|
|
|
|
|
my $entity = Mail::GPG->parse(mail_sref => \$scan->{msg}->get_pristine()); |
325
|
|
|
|
|
|
|
# TODO: configurable option to use is_signed_quick |
326
|
|
|
|
|
|
|
if ($gpg->is_signed(entity => $entity)) { |
327
|
|
|
|
|
|
|
$scan->{openpgp_signed} = 1; |
328
|
|
|
|
|
|
|
dbg "openpgp: is signed"; |
329
|
|
|
|
|
|
|
} |
330
|
|
|
|
|
|
|
if ($gpg->is_encrypted(entity => $entity)) { |
331
|
|
|
|
|
|
|
$scan->{openpgp_encrypted} = 1; |
332
|
|
|
|
|
|
|
dbg "openpgp: is encrypted"; |
333
|
|
|
|
|
|
|
} |
334
|
|
|
|
|
|
|
|
335
|
|
|
|
|
|
|
if ($scan->{openpgp_signed}) { |
336
|
|
|
|
|
|
|
my $result = $gpg->verify(entity => $entity); |
337
|
|
|
|
|
|
|
if (!$result->get_is_signed) { |
338
|
|
|
|
|
|
|
warn "openpgp: \$gpg->is_signed != \$result->get_is_signed"; |
339
|
|
|
|
|
|
|
$scan->{openpgp_signed} = 1; |
340
|
|
|
|
|
|
|
} else { |
341
|
|
|
|
|
|
|
#dbg "openpgp: " . $result->as_string(); |
342
|
|
|
|
|
|
|
if (${$result->get_gpg_stdout}) { |
343
|
|
|
|
|
|
|
dbg "openpgp: gpg stdout:" . ${$result->get_gpg_stdout}; |
344
|
|
|
|
|
|
|
} |
345
|
|
|
|
|
|
|
if (${$result->get_gpg_stderr}) { |
346
|
|
|
|
|
|
|
dbg "openpgp: gpg stderr:" . ${$result->get_gpg_stderr}; |
347
|
|
|
|
|
|
|
} |
348
|
|
|
|
|
|
|
if ($result->get_gpg_rc != 0) { |
349
|
|
|
|
|
|
|
my $err = "Error running gpg: " . ${$result->get_gpg_stdout} . ${$result->get_gpg_stderr}; |
350
|
|
|
|
|
|
|
dbg "openpgp: $err"; |
351
|
|
|
|
|
|
|
if ($scan->{conf}->{openpgp_add_header_fingerprint}) { |
352
|
|
|
|
|
|
|
$scan->{conf}->{headers_spam}->{'OpenPGP-Failure'} = $err; |
353
|
|
|
|
|
|
|
$scan->{conf}->{headers_ham}->{'OpenPGP-Failure'} = $err; |
354
|
|
|
|
|
|
|
} |
355
|
|
|
|
|
|
|
} else { |
356
|
|
|
|
|
|
|
$scan->{openpgp_fingerprint} = _gpg_result_primary_key_fingerprint($result); |
357
|
|
|
|
|
|
|
$scan->{openpgp_signed_good} = $result->get_sign_ok; |
358
|
|
|
|
|
|
|
$scan->{openpgp_signed_bad} = !$result->get_sign_ok; |
359
|
|
|
|
|
|
|
|
360
|
|
|
|
|
|
|
if ($scan->{conf}->{openpgp_add_header_fingerprint}) { |
361
|
|
|
|
|
|
|
$scan->{conf}->{headers_spam}->{'OpenPGP-Fingerprint'} = $scan->{openpgp_fingerprint}; |
362
|
|
|
|
|
|
|
$scan->{conf}->{headers_ham}->{'OpenPGP-Fingerprint'} = $scan->{openpgp_fingerprint}; |
363
|
|
|
|
|
|
|
} |
364
|
|
|
|
|
|
|
} |
365
|
|
|
|
|
|
|
|
366
|
|
|
|
|
|
|
if ($scan->{openpgp_signed_bad}) { |
367
|
|
|
|
|
|
|
my $err = "bad signature: " . ${$result->get_gpg_stderr}; |
368
|
|
|
|
|
|
|
dbg "openpgp: $err"; |
369
|
|
|
|
|
|
|
if ($scan->{conf}->{openpgp_add_header_fingerprint}) { |
370
|
|
|
|
|
|
|
$scan->{conf}->{headers_spam}->{'OpenPGP-Failure'} = $err; |
371
|
|
|
|
|
|
|
$scan->{conf}->{headers_ham}->{'OpenPGP-Failure'} = $err; |
372
|
|
|
|
|
|
|
} |
373
|
|
|
|
|
|
|
} |
374
|
|
|
|
|
|
|
|
375
|
|
|
|
|
|
|
# additional checks if good |
376
|
|
|
|
|
|
|
if ($scan->{openpgp_signed_good}) { |
377
|
|
|
|
|
|
|
# From address must match one in the public key |
378
|
|
|
|
|
|
|
# TODO check 'Sender:' ? |
379
|
|
|
|
|
|
|
my $from_email_address = $scan->get('From:addr'); |
380
|
|
|
|
|
|
|
my $from_ok = 0; |
381
|
|
|
|
|
|
|
if ($from_email_address eq _just_email($result->get_sign_mail)) { |
382
|
|
|
|
|
|
|
$from_ok = 1; |
383
|
|
|
|
|
|
|
} else { |
384
|
|
|
|
|
|
|
foreach my $key_alias (@{$result->get_sign_mail_aliases}) { |
385
|
|
|
|
|
|
|
if ($from_email_address eq _just_email($key_alias)) { |
386
|
|
|
|
|
|
|
$from_ok = 1; |
387
|
|
|
|
|
|
|
last; |
388
|
|
|
|
|
|
|
} |
389
|
|
|
|
|
|
|
} |
390
|
|
|
|
|
|
|
} |
391
|
|
|
|
|
|
|
if (!$from_ok) { |
392
|
|
|
|
|
|
|
my $err = 'from address ' . $from_email_address . ' not in list of email addresses on public key ' . $scan->{openpgp_fingerprint}; |
393
|
|
|
|
|
|
|
dbg "openpgp: $err"; |
394
|
|
|
|
|
|
|
if ($scan->{conf}->{openpgp_add_header_fingerprint}) { |
395
|
|
|
|
|
|
|
$scan->{conf}->{headers_spam}->{'OpenPGP-Failure'} = $err; |
396
|
|
|
|
|
|
|
$scan->{conf}->{headers_ham}->{'OpenPGP-Failure'} = $err; |
397
|
|
|
|
|
|
|
} |
398
|
|
|
|
|
|
|
$scan->{openpgp_signed_good} = 0; |
399
|
|
|
|
|
|
|
$scan->{openpgp_signed_bad} = 1; |
400
|
|
|
|
|
|
|
} else { |
401
|
|
|
|
|
|
|
dbg "openpgp: fingerprint: " . $scan->{openpgp_fingerprint}; |
402
|
|
|
|
|
|
|
} |
403
|
|
|
|
|
|
|
} |
404
|
|
|
|
|
|
|
if ($scan->{openpgp_signed_good}) { |
405
|
|
|
|
|
|
|
# date of email must be close to that of the signature |
406
|
|
|
|
|
|
|
my $sent_date = Mail::SpamAssassin::Util::parse_rfc822_date($scan->get('Date')); |
407
|
|
|
|
|
|
|
my $signature_date = _gpg_result_date($result); |
408
|
|
|
|
|
|
|
|
409
|
|
|
|
|
|
|
|
410
|
|
|
|
|
|
|
# TODO configurable threshold |
411
|
|
|
|
|
|
|
my $threshold = 60*60; |
412
|
|
|
|
|
|
|
if (abs($sent_date - $signature_date) > $threshold) { |
413
|
|
|
|
|
|
|
my $err = "mail sent date and signature data are more than $threshold seconds apart: $sent_date vs $signature_date"; |
414
|
|
|
|
|
|
|
dbg "openpgp: $err"; |
415
|
|
|
|
|
|
|
if ($scan->{conf}->{openpgp_add_header_fingerprint}) { |
416
|
|
|
|
|
|
|
$scan->{conf}->{headers_spam}->{'OpenPGP-Failure'} = $err; |
417
|
|
|
|
|
|
|
$scan->{conf}->{headers_ham}->{'OpenPGP-Failure'} = $err; |
418
|
|
|
|
|
|
|
} |
419
|
|
|
|
|
|
|
$scan->{openpgp_signed_good} = 0; |
420
|
|
|
|
|
|
|
$scan->{openpgp_signed_bad} = 1; |
421
|
|
|
|
|
|
|
} |
422
|
|
|
|
|
|
|
} |
423
|
|
|
|
|
|
|
} |
424
|
|
|
|
|
|
|
} |
425
|
|
|
|
|
|
|
|
426
|
|
|
|
|
|
|
$scan->{openpgp_checked} = 1; |
427
|
|
|
|
|
|
|
} |
428
|
|
|
|
|
|
|
|
429
|
|
|
|
|
|
|
1; # End of Mail::SpamAssassin::Plugin::OpenPGP |
430
|
|
|
|
|
|
|
__END__ |