GnuPG.pm 77.4 KB
Newer Older
Jesse Vincent's avatar
Jesse Vincent committed
1
2
3
# BEGIN BPS TAGGED BLOCK {{{
# 
# COPYRIGHT:
Jesse Vincent's avatar
Jesse Vincent committed
4
# 
Thomas Sibley's avatar
Thomas Sibley committed
5
# This software is Copyright (c) 1996-2010 Best Practical Solutions, LLC
Jesse Vincent's avatar
Jesse Vincent committed
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#                                          <jesse@bestpractical.com>
# 
# (Except where explicitly superseded by other copyright notices)
# 
# 
# LICENSE:
# 
# This work is made available to you under the terms of Version 2 of
# the GNU General Public License. A copy of that license should have
# been provided with this software, but in any event can be snarfed
# from www.gnu.org.
# 
# This work is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 or visit their web page on the internet at
Ruslan Zakirov's avatar
Ruslan Zakirov committed
27
# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
Jesse Vincent's avatar
Jesse Vincent committed
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# 
# 
# CONTRIBUTION SUBMISSION POLICY:
# 
# (The following paragraph is not intended to limit the rights granted
# to you to modify and distribute this software under the terms of
# the GNU General Public License and is only of importance to you if
# you choose to contribute your changes and enhancements to the
# community by submitting them to Best Practical Solutions, LLC.)
# 
# By intentionally submitting any modifications, corrections or
# derivatives to this work, or any other work intended for use with
# Request Tracker, to Best Practical Solutions, LLC, you confirm that
# you are the copyright holder for those contributions and you grant
# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
# royalty-free, perpetual, license to use, copy, create derivative
# works based on those contributions, and sublicense and distribute
# those contributions and any derivatives thereof.
# 
# END BPS TAGGED BLOCK }}}
48

49
50
51
use strict;
use warnings;

52
53
package RT::Crypt::GnuPG;

54
55
use IO::Handle;
use GnuPG::Interface;
56
use RT::EmailParser ();
sunnavy's avatar
sunnavy committed
57
use RT::Util 'safe_run_child';
58

Ruslan Zakirov's avatar
Ruslan Zakirov committed
59
60
=head1 NAME

61
RT::Crypt::GnuPG - encrypt/decrypt and sign/verify email messages with the GNU Privacy Guard (GPG)
Ruslan Zakirov's avatar
Ruslan Zakirov committed
62
63
64

=head1 DESCRIPTION

Jesse Vincent's avatar
Jesse Vincent committed
65
66
This module provides support for encryption and signing of outgoing messages, 
as well as the decryption and verification of incoming email.
Ruslan Zakirov's avatar
Ruslan Zakirov committed
67
68
69

=head1 CONFIGURATION

Jesse Vincent's avatar
Jesse Vincent committed
70
71
You can control the configuration of this subsystem from RT's configuration file.
Some options are available via the web interface, but to enable this functionality, you
72
MUST start in the configuration file.
Ruslan Zakirov's avatar
Ruslan Zakirov committed
73

Jesse Vincent's avatar
Jesse Vincent committed
74
75
76
77
78
There are two hashes, GnuPG and GnuPGOptions in the configuration file. The 
first one controls RT specific options. It enables you to enable/disable facility 
or change the format of messages. The second one is a hash with options for the 
'gnupg' utility. You can use it to define a keyserver, enable auto-retrieval keys 
and set almost any option 'gnupg' supports on your system.
Ruslan Zakirov's avatar
Ruslan Zakirov committed
79
80
81
82
83
84
85
86
87
88
89
90

=head2 %GnuPG

=head3 Enabling GnuPG

Set to true value to enable this subsystem:

    Set( %GnuPG,
        Enable => 1,
        ... other options ...
    );

91
However, note that you B<must> add the 'Auth::GnuPG' email filter to enable
Jesse Vincent's avatar
Jesse Vincent committed
92
the handling of incoming encrypted/signed messages.
Ruslan Zakirov's avatar
Ruslan Zakirov committed
93
94
95

=head3 Format of outgoing messages

Jesse Vincent's avatar
Jesse Vincent committed
96
Format of outgoing messages can be controlled using the 'OutgoingMessagesFormat'
Ruslan Zakirov's avatar
Ruslan Zakirov committed
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
option in the RT config:

    Set( %GnuPG,
        ... other options ...
        OutgoingMessagesFormat => 'RFC',
        ... other options ...
    );

or

    Set( %GnuPG,
        ... other options ...
        OutgoingMessagesFormat => 'Inline',
        ... other options ...
    );

Jesse Vincent's avatar
Jesse Vincent committed
113
This framework implements two formats of signing and encrypting of email messages:
Ruslan Zakirov's avatar
Ruslan Zakirov committed
114
115
116
117
118
119
120

=over

=item RFC

This format is also known as GPG/MIME and described in RFC3156 and RFC1847.
Technique described in these RFCs is well supported by many mail user
Jesse Vincent's avatar
Jesse Vincent committed
121
agents (MUA), but some MUAs support only inline signatures and encryption,
Ruslan Zakirov's avatar
Ruslan Zakirov committed
122
123
124
125
so it's possible to use inline format (see below).

=item Inline

Jesse Vincent's avatar
Jesse Vincent committed
126
127
This format doesn't take advantage of MIME, but some mail clients do
not support GPG/MIME.
Ruslan Zakirov's avatar
Ruslan Zakirov committed
128
129

We sign text parts using clear signatures. For each attachments another
130
attachment with a signature is added with '.sig' extension.
Ruslan Zakirov's avatar
Ruslan Zakirov committed
131

Jesse Vincent's avatar
Jesse Vincent committed
132
133
Encryption of text parts is implemented using inline format, other parts
are replaced with attachments with the filename extension '.pgp'.
Ruslan Zakirov's avatar
Ruslan Zakirov committed
134

Shawn Moore's avatar
Shawn Moore committed
135
136
137
This format is discouraged because modern mail clients typically don't support
it well.

Ruslan Zakirov's avatar
Ruslan Zakirov committed
138
139
=back

140
141
142
143
144
145
146
=head3 Encrypting data in the database

You can allow users to encrypt data in the database using
option C<AllowEncryptDataInDB>. By default it's disabled.
Users must have rights to see and modify tickets to use
this feature.

Ruslan Zakirov's avatar
Ruslan Zakirov committed
147
148
=head2 %GnuPGOptions

Jesse Vincent's avatar
Jesse Vincent committed
149
150
Use this hash to set options of the 'gnupg' program. You can define almost any
option you want which  gnupg supports, but never try to set options which
Ruslan Zakirov's avatar
Ruslan Zakirov committed
151
152
153
change output format or gnupg's commands, such as --sign (command),
--list-options (option) and other.

Jesse Vincent's avatar
Jesse Vincent committed
154
155
156
Some GnuPG options take arguments while others take none. (Such as  --use-agent).
For options without specific value use C<undef> as hash value.
To disable these option just comment them out or delete them from the hash
Ruslan Zakirov's avatar
Ruslan Zakirov committed
157
158
159
160
161
162
163

    Set(%GnuPGOptions,
        'option-with-value' => 'value',
        'enabled-option-without-value' => undef,
        # 'commented-option' => 'value or undef',
    );

164
165
166
B<NOTE> that options may contain '-' character and such options B<MUST> be
quoted, otherwise you can see quite cryptic error 'gpg: Invalid option "--0"'.

Ruslan Zakirov's avatar
Ruslan Zakirov committed
167
168
169
170
=over

=item --homedir

Jesse Vincent's avatar
Jesse Vincent committed
171
The GnuPG home directory, by default it is set to F</opt/rt3/var/data/gpg>.
Ruslan Zakirov's avatar
Ruslan Zakirov committed
172

Jesse Vincent's avatar
Jesse Vincent committed
173
174
175
You can manage this data with the 'gpg' commandline utility 
using the GNUPGHOME environment variable or --homedir option. 
Other utilities may be used as well.
Ruslan Zakirov's avatar
Ruslan Zakirov committed
176

Jesse Vincent's avatar
Jesse Vincent committed
177
178
In a standard installation, access to this directory should be granted to
the web server user which is running RT's web interface, but if you're running
Ruslan Zakirov's avatar
Ruslan Zakirov committed
179
cronjobs or other utilities that access RT directly via API and may generate
180
encrypted/signed notifications then the users you execute these scripts under
Jesse Vincent's avatar
Jesse Vincent committed
181
182
183
must have access too. 

However, granting access to the dir to many users makes your setup less secure,
184
some features, such as auto-import of keys, may not be available if you do not.
Jesse Vincent's avatar
Jesse Vincent committed
185
To enable this features and suppress warnings about permissions on
Ruslan Zakirov's avatar
Ruslan Zakirov committed
186
187
188
189
the dir use --no-permission-warning.

=item --digest-algo

190
191
This option is required in advance when RFC format for outgoing messages is
used. We can not get default algorithm from gpg program so RT uses 'SHA1' by
Ruslan Zakirov's avatar
Ruslan Zakirov committed
192
193
default. You may want to override it. You can use MD5, SHA1, RIPEMD160,
SHA256 or other, however use `gpg --version` command to get information about
194
supported algorithms by your gpg. These algorithms are listed as hash-functions.
Ruslan Zakirov's avatar
Ruslan Zakirov committed
195

196
197
=item --use-agent

198
This option lets you use GPG Agent to cache the passphrase of RT's key. See
199
L<http://www.gnupg.org/documentation/manuals/gnupg/Invoking-GPG_002dAGENT.html>
200
for information about GPG Agent.
201
202
203
204
205
206
207
208
209

=item --passphrase

This option lets you set the passphrase of RT's key directly. This option is
special in that it isn't passed directly to GPG, but is put into a file that
GPG then reads (which is more secure). The downside is that anyone who has read
access to your RT_SiteConfig.pm file can see the passphrase, thus we recommend
the --use-agent option instead.

Ruslan Zakirov's avatar
Ruslan Zakirov committed
210
211
212
213
214
215
=item other

Read `man gpg` to get list of all options this program support.

=back

Ruslan Zakirov's avatar
Ruslan Zakirov committed
216
217
218
=head2 Per-queue options

Using the web interface it's possible to enable signing and/or encrypting by
Jesse Vincent's avatar
Jesse Vincent committed
219
220
221
default. As an administrative user of RT, open 'Configuration' then 'Queues',
and select a queue. On the page you can see information about the queue's keys 
at the bottom and two checkboxes to choose default actions.
Ruslan Zakirov's avatar
Ruslan Zakirov committed
222

Ruslan Zakirov's avatar
Ruslan Zakirov committed
223
224
225
226
As well, encryption is enabled for autoreplies and other notifications when
an encypted message enters system via mailgate interface even if queue's
option is disabled.

Ruslan Zakirov's avatar
Ruslan Zakirov committed
227
228
=head2 Handling incoming messages

229
To enable handling of encrypted and signed message in the RT you should add
230
'Auth::GnuPG' mail plugin.
Ruslan Zakirov's avatar
Ruslan Zakirov committed
231

232
    Set(@MailPlugins, 'Auth::MailFrom', 'Auth::GnuPG', ...other filter...);
Ruslan Zakirov's avatar
Ruslan Zakirov committed
233

234
See also `perldoc lib/RT/Interface/Email/Auth/GnuPG.pm`.
Ruslan Zakirov's avatar
Ruslan Zakirov committed
235

236
237
=head2 Errors handling

Jesse Vincent's avatar
Jesse Vincent committed
238
239
240
241
There are several global templates created in the database by default. RT
uses these templates to send error messages to users or RT's owner. These 
templates have 'Error:' or 'Error to RT owner:' prefix in the name. You can 
adjust the text of the messages using the web interface.
242
243
244
245
246
247

Note that C<$TicketObj>, C<$TransactionObj> and other variable usually available
in RT's templates are not available in these templates, but each template
used for errors reporting has set of available data structures you can use to
build better messages. See default templates and descriptions below.

Ruslan Zakirov's avatar
Ruslan Zakirov committed
248
249
250
251
As well, you can disable particular notification by deleting content of
a template. You can delete a template too, but in this case you'll see
error messages in the logs when RT can not load template you've deleted.

252
253
=head3 Problems with public keys

Jesse Vincent's avatar
Jesse Vincent committed
254
255
256
257
Template 'Error: public key' is used to inform the user that RT has problems with
his public key and won't be able to send him encrypted content. There are several 
reasons why RT can't use a key. However, the actual reason is not sent to the user, 
but sent to RT owner using 'Error to RT owner: public key'.
258

259
The possible reasons: "Not Found", "Ambiguous specification", "Wrong
Jesse Vincent's avatar
Jesse Vincent committed
260
261
262
key usage", "Key revoked", "Key expired", "No CRL known", "CRL too
old", "Policy mismatch", "Not a secret key", "Key not trusted" or
"No specific reason given".
263

Ruslan Zakirov's avatar
Ruslan Zakirov committed
264
265
266
267
268
269
270
271
272
273
274
275
276
Due to limitations of GnuPG, it's impossible to encrypt to an untrusted key,
unless 'always trust' mode is enabled.

In the 'Error: public key' template there are a few additional variables available:

=over 4

=item $Message - user friendly error message

=item $Reason - short reason as listed above

=item $Recipient - recipient's identification

Emmanuel Lacour's avatar
Emmanuel Lacour committed
277
=item $AddressObj - L<Email::Address> object containing recipient's email address
Ruslan Zakirov's avatar
Ruslan Zakirov committed
278
279
280
281
282
283
284
285
286
287
288
289
290
291

=back

A message can have several invalid recipients, to avoid sending many emails
to the RT owner the system sends one message to the owner, grouped by
recipient. In the 'Error to RT owner: public key' template a C<@BadRecipients>
array is available where each element is a hash reference that describes one
recipient using the same fields as described above. So it's something like:

    @BadRecipients = (
        { Message => '...', Reason => '...', Recipient => '...', ...},
        { Message => '...', Reason => '...', Recipient => '...', ...},
        ...
    )
292

293
294
=head3 Private key doesn't exist

Jesse Vincent's avatar
Jesse Vincent committed
295
296
297
Template 'Error: no private key' is used to inform the user that
he sent an encrypted email, but we have no private key to decrypt
it.
298

Jesse Vincent's avatar
Jesse Vincent committed
299
300
In this template C<$Message> object of L<MIME::Entity> class
available. It's the message RT received.
301
302
303

=head3 Invalid data

Jesse Vincent's avatar
Jesse Vincent committed
304
305
Template 'Error: bad GnuPG data' used to inform the user that a
message he sent has invalid data and can not be handled.
306

Jesse Vincent's avatar
Jesse Vincent committed
307
There are several reasons for this error, but most of them are data
308
corruption or absence of expected information.
309

Jesse Vincent's avatar
Jesse Vincent committed
310
311
In this template C<@Messages> array is available and contains list
of error messages.
312

Ruslan Zakirov's avatar
Ruslan Zakirov committed
313
314
315
316
317
318
319
320
321
322
=head1 FOR DEVELOPERS

=head2 Documentation and references

* RFC1847 - Security Multiparts for MIME: Multipart/Signed and Multipart/Encrypted.
Describes generic MIME security framework, "mulitpart/signed" and "multipart/encrypted"
MIME types.

* RFC3156 - MIME Security with Pretty Good Privacy (PGP),
updates RFC2015.
Ruslan Zakirov's avatar
Ruslan Zakirov committed
323
324
325

=cut

326
# gnupg options supported by GnuPG::Interface
Ruslan Zakirov's avatar
Ruslan Zakirov committed
327
# other otions should be handled via extra_args argument
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
my %supported_opt = map { $_ => 1 } qw(
       always_trust
       armor
       batch
       comment
       compress_algo
       default_key
       encrypt_to
       extra_args
       force_v3_sigs
       homedir
       logger_fd
       no_greeting
       no_options
       no_verbose
       openpgp
       options
       passphrase_fd
       quiet
       recipients
       rfc1991
       status_fd
       textmode
       verbose
);

354
# DEV WARNING: always pass all STD* handles to GnuPG interface even if we don't
sunnavy's avatar
sunnavy committed
355
# need them, just pass 'new IO::Handle' and then close it after safe_run_child.
356
357
358
# we don't want to leak anything into FCGI/Apache/MP handles, this break things.
# So code should look like:
#        my $handles = GnuPG::Handles->new(
359
360
361
#            stdin  => ($handle{'stdin'}  = new IO::Handle),
#            stdout => ($handle{'stdout'} = new IO::Handle),
#            stderr => ($handle{'stderr'}  = new IO::Handle),
362
363
364
#            ...
#        );

365
=head2 SignEncrypt Entity => MIME::Entity, [ Encrypt => 1, Sign => 1, ... ]
Ruslan Zakirov's avatar
Ruslan Zakirov committed
366

367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
Signs and/or encrypts an email message with GnuPG utility.

=over

=item Signing

During signing you can pass C<Signer> argument to set key we sign with this option
overrides gnupg's C<default-key> option. If C<Signer> argument is not provided
then address of a message sender is used.

As well you can pass C<Passphrase>, but if value is undefined then L</GetPassphrase>
called to get it.

=item Encrypting

During encryption you can pass a C<Recipients> array, otherwise C<To>, C<Cc> and
C<Bcc> fields of the message are used to fetch the list.

=back
Ruslan Zakirov's avatar
Ruslan Zakirov committed
386
387
388
389
390
391
392
393
394
395
396

Returns a hash with the following keys:

* exit_code
* error
* logger
* status
* message

=cut

397
sub SignEncrypt {
398
399
400
401
    my %args = (@_);

    my $entity = $args{'Entity'};
    if ( $args{'Sign'} && !defined $args{'Signer'} ) {
402
        $args{'Signer'} = UseKeyForSigning()
Emmanuel Lacour's avatar
Emmanuel Lacour committed
403
            || (Email::Address->parse( $entity->head->get( 'From' ) ))[0]->address;
404
405
406
407
408
    }
    if ( $args{'Encrypt'} && !$args{'Recipients'} ) {
        my %seen;
        $args{'Recipients'} = [
            grep $_ && !$seen{ $_ }++, map $_->address,
Emmanuel Lacour's avatar
Emmanuel Lacour committed
409
            map Email::Address->parse( $entity->head->get( $_ ) ),
410
411
412
            qw(To Cc Bcc)
        ];
    }
413
414
415
    
    my $format = lc RT->Config->Get('GnuPG')->{'OutgoingMessagesFormat'} || 'RFC';
    if ( $format eq 'inline' ) {
416
        return SignEncryptInline( %args );
417
    } else {
418
        return SignEncryptRFC3156( %args );
419
420
421
422
    }
}

sub SignEncryptRFC3156 {
423
424
    my %args = (
        Entity => undef,
425

426
        Sign => 1,
427
        Signer => undef,
428
        Passphrase => undef,
429
430
431
432

        Encrypt => 1,
        Recipients => undef,

433
434
        @_
    );
435

436
    my $gnupg = new GnuPG::Interface;
437
    my %opt = RT->Config->Get('GnuPGOptions');
438
439
440
441
442

    # handling passphrase in GnuPGOptions
    $args{'Passphrase'} = delete $opt{'passphrase'}
        if !defined $args{'Passphrase'};

443
    $opt{'digest-algo'} ||= 'SHA1';
444
445
    $opt{'default_key'} = $args{'Signer'}
        if $args{'Sign'} && $args{'Signer'};
446
447
448
449
450
451
    $gnupg->options->hash_init(
        _PrepareGnuPGOptions( %opt ),
        armor => 1,
        meta_interactive => 0,
    );

452
453
454
455
456
457
    my $entity = $args{'Entity'};

    if ( $args{'Sign'} && !defined $args{'Passphrase'} ) {
        $args{'Passphrase'} = GetPassphrase( Address => $args{'Signer'} );
    }

458
459
460
    my %res;
    if ( $args{'Sign'} && !$args{'Encrypt'} ) {
        # required by RFC3156(Ch. 5) and RFC1847(Ch. 2.1)
461
462
463
464
465
466
467
468
        foreach ( grep !$_->is_multipart, $entity->parts_DFS ) {
            my $tenc = $_->head->mime_encoding;
            unless ( $tenc =~ m/^(?:7bit|quoted-printable|base64)$/i ) {
                $_->head->mime_attr( 'Content-Transfer-Encoding'
                    => $_->effective_type =~ m{^text/}? 'quoted-printable': 'base64'
                );
            }
        }
469

Ruslan Zakirov's avatar
minor    
Ruslan Zakirov committed
470
471
        my ($handles, $handle_list) = _make_gpg_handles(stdin =>IO::Handle::CRLF->new );
        my %handle = %$handle_list;
472

473
474
475
        $gnupg->passphrase( $args{'Passphrase'} );

        eval {
476
            local $SIG{'CHLD'} = 'DEFAULT';
sunnavy's avatar
sunnavy committed
477
            my $pid = safe_run_child { $gnupg->detach_sign( handles => $handles ) };
478
            $entity->make_multipart( 'mixed', Force => 1 );
479
480
481
482
483
            {
                local $SIG{'PIPE'} = 'IGNORE';
                $entity->parts(0)->print( $handle{'stdin'} );
                close $handle{'stdin'};
            }
484
485
            waitpid $pid, 0;
        };
Ruslan Zakirov's avatar
indent    
Ruslan Zakirov committed
486
        my $err = $@;
487
488
        my @signature = readline $handle{'stdout'};
        close $handle{'stdout'};
489
490

        $res{'exit_code'} = $?;
491
        foreach ( qw(stderr logger status) ) {
492
493
494
495
496
            $res{$_} = do { local $/; readline $handle{$_} };
            delete $res{$_} unless $res{$_} && $res{$_} =~ /\S/s;
            close $handle{$_};
        }
        $RT::Logger->debug( $res{'status'} ) if $res{'status'};
497
        $RT::Logger->warning( $res{'stderr'} ) if $res{'stderr'};
498
        $RT::Logger->error( $res{'logger'} ) if $res{'logger'} && $?;
499
500
        if ( $err || $res{'exit_code'} ) {
            $res{'message'} = $err? $err : "gpg exitted with error code ". ($res{'exit_code'} >> 8);
501
            return %res;
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
        }

        # setup RFC1847(Ch.2.1) requirements
        my $protocol = 'application/pgp-signature';
        $entity->head->mime_attr( 'Content-Type' => 'multipart/signed' );
        $entity->head->mime_attr( 'Content-Type.protocol' => $protocol );
        $entity->head->mime_attr( 'Content-Type.micalg'   => 'pgp-'. lc $opt{'digest-algo'} );
        $entity->attach(
            Type        => $protocol,
            Disposition => 'inline',
            Data        => \@signature,
            Encoding    => '7bit',
        );
    }
    if ( $args{'Encrypt'} ) {
        my %seen;
518
        $gnupg->options->push_recipients( $_ ) foreach 
519
            map UseKeyForEncryption($_) || $_,
520
            grep !$seen{ $_ }++, map $_->address,
Emmanuel Lacour's avatar
Emmanuel Lacour committed
521
            map Email::Address->parse( $entity->head->get( $_ ) ),
522
523
            qw(To Cc Bcc);

524
        my ($tmp_fh, $tmp_fn) = File::Temp::tempfile( UNLINK => 1 );
525
526
        binmode $tmp_fh, ':raw';

527
528
        my ($handles, $handle_list) = _make_gpg_handles(stdout => $tmp_fh);
        my %handle = %$handle_list;
529
530
531
532
        $handles->options( 'stdout'  )->{'direct'} = 1;
        $gnupg->passphrase( $args{'Passphrase'} ) if $args{'Sign'};

        eval {
533
            local $SIG{'CHLD'} = 'DEFAULT';
sunnavy's avatar
sunnavy committed
534
            my $pid = safe_run_child { $args{'Sign'}
535
536
                ? $gnupg->sign_and_encrypt( handles => $handles )
                : $gnupg->encrypt( handles => $handles ) };
537
            $entity->make_multipart( 'mixed', Force => 1 );
538
539
540
541
542
            {
                local $SIG{'PIPE'} = 'IGNORE';
                $entity->parts(0)->print( $handle{'stdin'} );
                close $handle{'stdin'};
            }
543
544
545
546
            waitpid $pid, 0;
        };

        $res{'exit_code'} = $?;
547
        foreach ( qw(stderr logger status) ) {
548
549
550
551
552
            $res{$_} = do { local $/; readline $handle{$_} };
            delete $res{$_} unless $res{$_} && $res{$_} =~ /\S/s;
            close $handle{$_};
        }
        $RT::Logger->debug( $res{'status'} ) if $res{'status'};
553
        $RT::Logger->warning( $res{'stderr'} ) if $res{'stderr'};
554
555
        $RT::Logger->error( $res{'logger'} ) if $res{'logger'} && $?;
        if ( $@ || $? ) {
556
            $res{'message'} = $@? $@: "gpg exited with error code ". ($? >> 8);
557
            return %res;
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
        }

        my $protocol = 'application/pgp-encrypted';
        $entity->parts([]);
        $entity->head->mime_attr( 'Content-Type' => 'multipart/encrypted' );
        $entity->head->mime_attr( 'Content-Type.protocol' => $protocol );
        $entity->attach(
            Type        => $protocol,
            Disposition => 'inline',
            Data        => ['Version: 1',''],
            Encoding    => '7bit',
        );
        $entity->attach(
            Type        => 'application/octet-stream',
            Disposition => 'inline',
            Path        => $tmp_fn,
            Filename    => '',
            Encoding    => '7bit',
        );
        $entity->parts(-1)->bodyhandle->{'_dirty_hack_to_save_a_ref_tmp_fh'} = $tmp_fh;
    }
Ruslan Zakirov's avatar
Ruslan Zakirov committed
579
    return %res;
580
581
}

582
sub SignEncryptInline {
Ruslan Zakirov's avatar
Ruslan Zakirov committed
583
584
585
    my %args = ( @_ );

    my $entity = $args{'Entity'};
586
587
588
589
590

    my %res;
    $entity->make_singlepart;
    if ( $entity->is_multipart ) {
        foreach ( $entity->parts ) {
591
            %res = SignEncryptInline( @_, Entity => $_ );
592
593
594
595
596
            return %res if $res{'exit_code'};
        }
        return %res;
    }

597
    return _SignEncryptTextInline( @_ )
598
599
        if $entity->effective_type =~ /^text\//i;

600
    return _SignEncryptAttachmentInline( @_ );
601
602
603
604
605
}

sub _SignEncryptTextInline {
    my %args = (
        Entity => undef,
Ruslan Zakirov's avatar
Ruslan Zakirov committed
606

607
608
609
        Sign => 1,
        Signer => undef,
        Passphrase => undef,
Ruslan Zakirov's avatar
Ruslan Zakirov committed
610
611
612
613

        Encrypt => 1,
        Recipients => undef,

614
615
616
617
618
619
        @_
    );
    return unless $args{'Sign'} || $args{'Encrypt'};

    my $gnupg = new GnuPG::Interface;
    my %opt = RT->Config->Get('GnuPGOptions');
620
621
622
623
624

    # handling passphrase in GnupGOptions
    $args{'Passphrase'} = delete $opt{'passphrase'}
        if !defined($args{'Passphrase'});

625
    $opt{'digest-algo'} ||= 'SHA1';
626
627
    $opt{'default_key'} = $args{'Signer'}
        if $args{'Sign'} && $args{'Signer'};
628
629
630
631
632
633
    $gnupg->options->hash_init(
        _PrepareGnuPGOptions( %opt ),
        armor => 1,
        meta_interactive => 0,
    );

634
635
636
637
    if ( $args{'Sign'} && !defined $args{'Passphrase'} ) {
        $args{'Passphrase'} = GetPassphrase( Address => $args{'Signer'} );
    }

638
    if ( $args{'Encrypt'} ) {
639
        $gnupg->options->push_recipients( $_ ) foreach 
640
            map UseKeyForEncryption($_) || $_,
641
            @{ $args{'Recipients'} || [] };
642
643
644
645
    }

    my %res;

646
    my ($tmp_fh, $tmp_fn) = File::Temp::tempfile( UNLINK => 1 );
647
648
    binmode $tmp_fh, ':raw';

649
650
651
    my ($handles, $handle_list) = _make_gpg_handles(stdout => $tmp_fh);
    my %handle = %$handle_list;

652
653
654
    $handles->options( 'stdout'  )->{'direct'} = 1;
    $gnupg->passphrase( $args{'Passphrase'} ) if $args{'Sign'};

655
    my $entity = $args{'Entity'};
656
657
658
659
660
    eval {
        local $SIG{'CHLD'} = 'DEFAULT';
        my $method = $args{'Sign'} && $args{'Encrypt'}
            ? 'sign_and_encrypt'
            : ($args{'Sign'}? 'clearsign': 'encrypt');
sunnavy's avatar
sunnavy committed
661
        my $pid = safe_run_child { $gnupg->$method( handles => $handles ) };
662
663
664
665
666
        {
            local $SIG{'PIPE'} = 'IGNORE';
            $entity->bodyhandle->print( $handle{'stdin'} );
            close $handle{'stdin'};
        }
667
668
669
670
671
        waitpid $pid, 0;
    };
    $res{'exit_code'} = $?;
    my $err = $@;

672
    foreach ( qw(stderr logger status) ) {
673
674
675
676
677
        $res{$_} = do { local $/; readline $handle{$_} };
        delete $res{$_} unless $res{$_} && $res{$_} =~ /\S/s;
        close $handle{$_};
    }
    $RT::Logger->debug( $res{'status'} ) if $res{'status'};
678
    $RT::Logger->warning( $res{'stderr'} ) if $res{'stderr'};
679
680
681
682
683
684
685
686
687
688
689
690
691
    $RT::Logger->error( $res{'logger'} ) if $res{'logger'} && $?;
    if ( $err || $res{'exit_code'} ) {
        $res{'message'} = $err? $err : "gpg exitted with error code ". ($res{'exit_code'} >> 8);
        return %res;
    }

    $entity->bodyhandle( new MIME::Body::File $tmp_fn );
    $entity->{'__store_tmp_handle_to_avoid_early_cleanup'} = $tmp_fh;

    return %res;
}

sub _SignEncryptAttachmentInline {
Ruslan Zakirov's avatar
Ruslan Zakirov committed
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
    my %args = (
        Entity => undef,

        Sign => 1,
        Signer => undef,
        Passphrase => undef,

        Encrypt => 1,
        Recipients => undef,

        @_
    );
    return unless $args{'Sign'} || $args{'Encrypt'};

    my $gnupg = new GnuPG::Interface;
    my %opt = RT->Config->Get('GnuPGOptions');
708
709
710
711
712

    # handling passphrase in GnupGOptions
    $args{'Passphrase'} = delete $opt{'passphrase'}
        if !defined($args{'Passphrase'});

Ruslan Zakirov's avatar
Ruslan Zakirov committed
713
    $opt{'digest-algo'} ||= 'SHA1';
714
715
    $opt{'default_key'} = $args{'Signer'}
        if $args{'Sign'} && $args{'Signer'};
Ruslan Zakirov's avatar
Ruslan Zakirov committed
716
717
718
719
720
721
    $gnupg->options->hash_init(
        _PrepareGnuPGOptions( %opt ),
        armor => 1,
        meta_interactive => 0,
    );

722
723
724
725
    if ( $args{'Sign'} && !defined $args{'Passphrase'} ) {
        $args{'Passphrase'} = GetPassphrase( Address => $args{'Signer'} );
    }

Ruslan Zakirov's avatar
Ruslan Zakirov committed
726
727
    my $entity = $args{'Entity'};
    if ( $args{'Encrypt'} ) {
728
        $gnupg->options->push_recipients( $_ ) foreach
729
            map UseKeyForEncryption($_) || $_,
730
            @{ $args{'Recipients'} || [] };
Ruslan Zakirov's avatar
Ruslan Zakirov committed
731
732
733
734
    }

    my %res;

735
    my ($tmp_fh, $tmp_fn) = File::Temp::tempfile( UNLINK => 1 );
Ruslan Zakirov's avatar
Ruslan Zakirov committed
736
737
    binmode $tmp_fh, ':raw';

738
739
    my ($handles, $handle_list) = _make_gpg_handles(stdout => $tmp_fh);
    my %handle = %$handle_list;
Ruslan Zakirov's avatar
Ruslan Zakirov committed
740
741
742
743
744
745
746
747
    $handles->options( 'stdout'  )->{'direct'} = 1;
    $gnupg->passphrase( $args{'Passphrase'} ) if $args{'Sign'};

    eval {
        local $SIG{'CHLD'} = 'DEFAULT';
        my $method = $args{'Sign'} && $args{'Encrypt'}
            ? 'sign_and_encrypt'
            : ($args{'Sign'}? 'detach_sign': 'encrypt');
sunnavy's avatar
sunnavy committed
748
        my $pid = safe_run_child { $gnupg->$method( handles => $handles ) };
749
750
751
752
753
        {
            local $SIG{'PIPE'} = 'IGNORE';
            $entity->bodyhandle->print( $handle{'stdin'} );
            close $handle{'stdin'};
        }
Ruslan Zakirov's avatar
Ruslan Zakirov committed
754
755
756
757
758
        waitpid $pid, 0;
    };
    $res{'exit_code'} = $?;
    my $err = $@;

759
    foreach ( qw(stderr logger status) ) {
Ruslan Zakirov's avatar
Ruslan Zakirov committed
760
761
762
763
764
        $res{$_} = do { local $/; readline $handle{$_} };
        delete $res{$_} unless $res{$_} && $res{$_} =~ /\S/s;
        close $handle{$_};
    }
    $RT::Logger->debug( $res{'status'} ) if $res{'status'};
765
    $RT::Logger->warning( $res{'stderr'} ) if $res{'stderr'};
Ruslan Zakirov's avatar
Ruslan Zakirov committed
766
767
768
769
770
771
772
773
774
775
    $RT::Logger->error( $res{'logger'} ) if $res{'logger'} && $?;
    if ( $err || $res{'exit_code'} ) {
        $res{'message'} = $err? $err : "gpg exitted with error code ". ($res{'exit_code'} >> 8);
        return %res;
    }

    my $filename = $entity->head->recommended_filename || 'no_name';
    if ( $args{'Sign'} && !$args{'Encrypt'} ) {
        $entity->make_multipart;
        $entity->attach(
Shawn Moore's avatar
Shawn Moore committed
776
            Type     => 'application/octet-stream',
Ruslan Zakirov's avatar
Ruslan Zakirov committed
777
778
779
780
781
782
            Path     => $tmp_fn,
            Filename => "$filename.sig",
            Disposition => 'attachment',
        );
    } else {
        $entity->bodyhandle( new MIME::Body::File $tmp_fn );
Shawn Moore's avatar
Shawn Moore committed
783
        $entity->effective_type('application/octet-stream');
784
        $entity->head->mime_attr( $_ => "$filename.pgp" )
Ruslan Zakirov's avatar
Ruslan Zakirov committed
785
786
787
788
789
790
            foreach (qw(Content-Type.name Content-Disposition.filename));

    }
    $entity->{'__store_tmp_handle_to_avoid_early_cleanup'} = $tmp_fh;

    return %res;
791
792
}

793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
sub SignEncryptContent {
    my %args = (
        Content => undef,

        Sign => 1,
        Signer => undef,
        Passphrase => undef,

        Encrypt => 1,
        Recipients => undef,

        @_
    );
    return unless $args{'Sign'} || $args{'Encrypt'};

    my $gnupg = new GnuPG::Interface;
    my %opt = RT->Config->Get('GnuPGOptions');
810
811
812
813
814

    # handling passphrase in GnupGOptions
    $args{'Passphrase'} = delete $opt{'passphrase'}
        if !defined($args{'Passphrase'});

815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
    $opt{'digest-algo'} ||= 'SHA1';
    $opt{'default_key'} = $args{'Signer'}
        if $args{'Sign'} && $args{'Signer'};
    $gnupg->options->hash_init(
        _PrepareGnuPGOptions( %opt ),
        armor => 1,
        meta_interactive => 0,
    );

    if ( $args{'Sign'} && !defined $args{'Passphrase'} ) {
        $args{'Passphrase'} = GetPassphrase( Address => $args{'Signer'} );
    }

    if ( $args{'Encrypt'} ) {
        $gnupg->options->push_recipients( $_ ) foreach 
            map UseKeyForEncryption($_) || $_,
            @{ $args{'Recipients'} || [] };
    }

    my %res;

836
    my ($tmp_fh, $tmp_fn) = File::Temp::tempfile( UNLINK => 1 );
837
838
    binmode $tmp_fh, ':raw';

839
840
    my ($handles, $handle_list) = _make_gpg_handles(stdout => $tmp_fh);
    my %handle = %$handle_list;
841
842
843
844
845
846
847
848
    $handles->options( 'stdout'  )->{'direct'} = 1;
    $gnupg->passphrase( $args{'Passphrase'} ) if $args{'Sign'};

    eval {
        local $SIG{'CHLD'} = 'DEFAULT';
        my $method = $args{'Sign'} && $args{'Encrypt'}
            ? 'sign_and_encrypt'
            : ($args{'Sign'}? 'clearsign': 'encrypt');
sunnavy's avatar
sunnavy committed
849
        my $pid = safe_run_child { $gnupg->$method( handles => $handles ) };
850
851
852
853
854
        {
            local $SIG{'PIPE'} = 'IGNORE';
            $handle{'stdin'}->print( ${ $args{'Content'} } );
            close $handle{'stdin'};
        }
855
856
857
858
859
        waitpid $pid, 0;
    };
    $res{'exit_code'} = $?;
    my $err = $@;

860
    foreach ( qw(stderr logger status) ) {
861
862
863
864
865
        $res{$_} = do { local $/; readline $handle{$_} };
        delete $res{$_} unless $res{$_} && $res{$_} =~ /\S/s;
        close $handle{$_};
    }
    $RT::Logger->debug( $res{'status'} ) if $res{'status'};
866
    $RT::Logger->warning( $res{'stderr'} ) if $res{'stderr'};
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
    $RT::Logger->error( $res{'logger'} ) if $res{'logger'} && $?;
    if ( $err || $res{'exit_code'} ) {
        $res{'message'} = $err? $err : "gpg exitted with error code ". ($res{'exit_code'} >> 8);
        return %res;
    }

    ${ $args{'Content'} } = '';
    seek $tmp_fh, 0, 0;
    while (1) {
        my $status = read $tmp_fh, my $buf, 4*1024;
        unless ( defined $status ) {
            $RT::Logger->crit( "couldn't read message: $!" );
        } elsif ( !$status ) {
            last;
        }
        ${ $args{'Content'} } .= $buf;
    }

    return %res;
}

888
sub FindProtectedParts {
889
    my %args = ( Entity => undef, CheckBody => 1, @_ );
890
891
892
893
894
    my $entity = $args{'Entity'};

    # inline PGP block, only in singlepart
    unless ( $entity->is_multipart ) {
        my $io = $entity->open('r');
895
896
897
898
        unless ( $io ) {
            $RT::Logger->warning( "Entity of type ". $entity->effective_type ." has no body" );
            return ();
        }
899
        while ( defined($_ = $io->getline) ) {
900
            next unless /^-----BEGIN PGP (SIGNED )?MESSAGE-----/;
Ruslan Zakirov's avatar
Ruslan Zakirov committed
901
902
            my $type = $1? 'signed': 'encrypted';
            $RT::Logger->debug("Found $type inline part");
903
            return {
904
905
                Type    => $type,
                Format  => 'Inline',
906
                Data  => $entity,
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
            };
        }
        $io->close;
        return ();
    }

    # RFC3156, multipart/{signed,encrypted}
    if ( ( my $type = $entity->effective_type ) =~ /^multipart\/(?:encrypted|signed)$/ ) {
        unless ( $entity->parts == 2 ) {
            $RT::Logger->error( "Encrypted or signed entity must has two subparts. Skipped" );
            return ();
        }

        my $protocol = $entity->head->mime_attr( 'Content-Type.protocol' );
        unless ( $protocol ) {
            $RT::Logger->error( "Entity is '$type', but has no protocol defined. Skipped" );
            return ();
        }

        if ( $type eq 'multipart/encrypted' ) {
            unless ( $protocol eq 'application/pgp-encrypted' ) {
                $RT::Logger->info( "Skipping protocol '$protocol', only 'application/pgp-encrypted' is supported" );
                return ();
            }
Ruslan Zakirov's avatar
Ruslan Zakirov committed
931
            $RT::Logger->debug("Found encrypted according to RFC3156 part");
932
            return {
933
934
                Type    => 'encrypted',
                Format  => 'RFC3156',
935
936
                Top   => $entity,
                Data  => $entity->parts(1),
937
                Info    => $entity->parts(0),
938
939
940
941
942
943
            };
        } else {
            unless ( $protocol eq 'application/pgp-signature' ) {
                $RT::Logger->info( "Skipping protocol '$protocol', only 'application/pgp-signature' is supported" );
                return ();
            }
Ruslan Zakirov's avatar
Ruslan Zakirov committed
944
            $RT::Logger->debug("Found signed according to RFC3156 part");
945
946
947
            return {
                Type      => 'signed',
                Format    => 'RFC3156',
948
949
                Top     => $entity,
                Data    => $entity->parts(0),
950
951
952
953
954
955
                Signature => $entity->parts(1),
            };
        }
    }

    # attachments signed with signature in another part
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
    my @file_indices;
    foreach my $i ( 0 .. $entity->parts - 1 ) {
        my $part = $entity->parts($i);

        # we can not associate a signature within an attachment
        # without file names
        my $fname = $part->head->recommended_filename;
        next unless $fname;

        if ( $part->effective_type eq 'application/pgp-signature' ) {
            push @file_indices, $i;
        }
        elsif ( $fname =~ /\.sig$/i && $part->effective_type eq 'application/octet-stream' ) {
            push @file_indices, $i;
        }
    }
972
973

    my (@res, %skip);
974
975
    foreach my $i ( @file_indices ) {
        my $sig_part = $entity->parts($i);
976
977
        $skip{"$sig_part"}++;
        my $sig_name = $sig_part->head->recommended_filename;
978
        my ($file_name) = $sig_name =~ /^(.*?)(?:\.sig)?$/;
Shawn Moore's avatar
Shawn Moore committed
979
980
981
982
983
984

        my ($data_part_idx) =
            grep $file_name eq ($entity->parts($_)->head->recommended_filename||''),
            grep $sig_part  ne  $entity->parts($_),
                0 .. $entity->parts - 1;
        unless ( defined $data_part_idx ) {
985
986
987
            $RT::Logger->error("Found $sig_name attachment, but didn't find $file_name");
            next;
        }
Shawn Moore's avatar
Shawn Moore committed
988
        my $data_part_in = $entity->parts($data_part_idx);
989

990
        $skip{"$data_part_in"}++;
991
        $RT::Logger->debug("Found signature (in '$sig_name') of attachment '$file_name'");
992
993
994
        push @res, {
            Type      => 'signed',
            Format    => 'Attachment',
995
996
            Top       => $entity,
            Data      => $data_part_in,
997
998
999
1000
            Signature => $sig_part,
        };
    }

For faster browsing, not all history is shown. View entire blame