1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2013 Best Practical Solutions, LLC
6 # <sales@bestpractical.com>
8 # (Except where explicitly superseded by other copyright notices)
13 # This work is made available to you under the terms of Version 2 of
14 # the GNU General Public License. A copy of that license should have
15 # been provided with this software, but in any event can be snarfed
18 # This work is distributed in the hope that it will be useful, but
19 # WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
21 # General Public License for more details.
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 # 02110-1301 or visit their web page on the internet at
27 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
30 # CONTRIBUTION SUBMISSION POLICY:
32 # (The following paragraph is not intended to limit the rights granted
33 # to you to modify and distribute this software under the terms of
34 # the GNU General Public License and is only of importance to you if
35 # you choose to contribute your changes and enhancements to the
36 # community by submitting them to Best Practical Solutions, LLC.)
38 # By intentionally submitting any modifications, corrections or
39 # derivatives to this work, or any other work intended for use with
40 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
41 # you are the copyright holder for those contributions and you grant
42 # Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
43 # royalty-free, perpetual, license to use, copy, create derivative
44 # works based on those contributions, and sublicense and distribute
45 # those contributions and any derivatives thereof.
47 # END BPS TAGGED BLOCK }}}
52 package RT::Crypt::GnuPG;
56 use RT::EmailParser ();
57 use RT::Util 'safe_run_child';
61 RT::Crypt::GnuPG - encrypt/decrypt and sign/verify email messages with the GNU Privacy Guard (GPG)
65 This module provides support for encryption and signing of outgoing messages,
66 as well as the decryption and verification of incoming email.
70 You can control the configuration of this subsystem from RT's configuration file.
71 Some options are available via the web interface, but to enable this functionality, you
72 MUST start in the configuration file.
74 There are two hashes, GnuPG and GnuPGOptions in the configuration file. The
75 first one controls RT specific options. It enables you to enable/disable facility
76 or change the format of messages. The second one is a hash with options for the
77 'gnupg' utility. You can use it to define a keyserver, enable auto-retrieval keys
78 and set almost any option 'gnupg' supports on your system.
84 Set to true value to enable this subsystem:
91 However, note that you B<must> add the 'Auth::GnuPG' email filter to enable
92 the handling of incoming encrypted/signed messages.
94 =head3 Format of outgoing messages
96 Format of outgoing messages can be controlled using the 'OutgoingMessagesFormat'
97 option in the RT config:
100 ... other options ...
101 OutgoingMessagesFormat => 'RFC',
102 ... other options ...
108 ... other options ...
109 OutgoingMessagesFormat => 'Inline',
110 ... other options ...
113 This framework implements two formats of signing and encrypting of email messages:
119 This format is also known as GPG/MIME and described in RFC3156 and RFC1847.
120 Technique described in these RFCs is well supported by many mail user
121 agents (MUA), but some MUAs support only inline signatures and encryption,
122 so it's possible to use inline format (see below).
126 This format doesn't take advantage of MIME, but some mail clients do
127 not support GPG/MIME.
129 We sign text parts using clear signatures. For each attachments another
130 attachment with a signature is added with '.sig' extension.
132 Encryption of text parts is implemented using inline format, other parts
133 are replaced with attachments with the filename extension '.pgp'.
135 This format is discouraged because modern mail clients typically don't support
140 =head3 Encrypting data in the database
142 You can allow users to encrypt data in the database using
143 option C<AllowEncryptDataInDB>. By default it's disabled.
144 Users must have rights to see and modify tickets to use
149 Use this hash to set options of the 'gnupg' program. You can define almost any
150 option you want which gnupg supports, but never try to set options which
151 change output format or gnupg's commands, such as --sign (command),
152 --list-options (option) and other.
154 Some GnuPG options take arguments while others take none. (Such as --use-agent).
155 For options without specific value use C<undef> as hash value.
156 To disable these option just comment them out or delete them from the hash
159 'option-with-value' => 'value',
160 'enabled-option-without-value' => undef,
161 # 'commented-option' => 'value or undef',
164 B<NOTE> that options may contain '-' character and such options B<MUST> be
165 quoted, otherwise you can see quite cryptic error 'gpg: Invalid option "--0"'.
171 The GnuPG home directory, by default it is set to F</opt/rt3/var/data/gpg>.
173 You can manage this data with the 'gpg' commandline utility
174 using the GNUPGHOME environment variable or --homedir option.
175 Other utilities may be used as well.
177 In a standard installation, access to this directory should be granted to
178 the web server user which is running RT's web interface, but if you're running
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
181 must have access too.
183 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.
185 To enable this features and suppress warnings about permissions on
186 the dir use --no-permission-warning.
190 This option is required in advance when RFC format for outgoing messages is
191 used. We can not get default algorithm from gpg program so RT uses 'SHA1' by
192 default. You may want to override it. You can use MD5, SHA1, RIPEMD160,
193 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.
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.
204 This option lets you set the passphrase of RT's key directly. This option is
205 special in that it isn't passed directly to GPG, but is put into a file that
206 GPG then reads (which is more secure). The downside is that anyone who has read
207 access to your RT_SiteConfig.pm file can see the passphrase, thus we recommend
208 the --use-agent option instead.
212 Read `man gpg` to get list of all options this program support.
216 =head2 Per-queue options
218 Using the web interface it's possible to enable signing and/or encrypting by
219 default. As an administrative user of RT, open 'Configuration' then 'Queues',
220 and select a queue. On the page you can see information about the queue's keys
221 at the bottom and two checkboxes to choose default actions.
223 As well, encryption is enabled for autoreplies and other notifications when
224 an encypted message enters system via mailgate interface even if queue's
227 =head2 Handling incoming messages
229 To enable handling of encrypted and signed message in the RT you should add
230 'Auth::GnuPG' mail plugin.
232 Set(@MailPlugins, 'Auth::MailFrom', 'Auth::GnuPG', ...other filter...);
234 See also `perldoc lib/RT/Interface/Email/Auth/GnuPG.pm`.
236 =head2 Errors handling
238 There are several global templates created in the database by default. RT
239 uses these templates to send error messages to users or RT's owner. These
240 templates have 'Error:' or 'Error to RT owner:' prefix in the name. You can
241 adjust the text of the messages using the web interface.
243 Note that C<$TicketObj>, C<$TransactionObj> and other variable usually available
244 in RT's templates are not available in these templates, but each template
245 used for errors reporting has set of available data structures you can use to
246 build better messages. See default templates and descriptions below.
248 As well, you can disable particular notification by deleting content of
249 a template. You can delete a template too, but in this case you'll see
250 error messages in the logs when RT can not load template you've deleted.
252 =head3 Problems with public keys
254 Template 'Error: public key' is used to inform the user that RT has problems with
255 his public key and won't be able to send him encrypted content. There are several
256 reasons why RT can't use a key. However, the actual reason is not sent to the user,
257 but sent to RT owner using 'Error to RT owner: public key'.
259 The possible reasons: "Not Found", "Ambiguous specification", "Wrong
260 key usage", "Key revoked", "Key expired", "No CRL known", "CRL too
261 old", "Policy mismatch", "Not a secret key", "Key not trusted" or
262 "No specific reason given".
264 Due to limitations of GnuPG, it's impossible to encrypt to an untrusted key,
265 unless 'always trust' mode is enabled.
267 In the 'Error: public key' template there are a few additional variables available:
271 =item $Message - user friendly error message
273 =item $Reason - short reason as listed above
275 =item $Recipient - recipient's identification
277 =item $AddressObj - L<Email::Address> object containing recipient's email address
281 A message can have several invalid recipients, to avoid sending many emails
282 to the RT owner the system sends one message to the owner, grouped by
283 recipient. In the 'Error to RT owner: public key' template a C<@BadRecipients>
284 array is available where each element is a hash reference that describes one
285 recipient using the same fields as described above. So it's something like:
288 { Message => '...', Reason => '...', Recipient => '...', ...},
289 { Message => '...', Reason => '...', Recipient => '...', ...},
293 =head3 Private key doesn't exist
295 Template 'Error: no private key' is used to inform the user that
296 he sent an encrypted email, but we have no private key to decrypt
299 In this template C<$Message> object of L<MIME::Entity> class
300 available. It's the message RT received.
304 Template 'Error: bad GnuPG data' used to inform the user that a
305 message he sent has invalid data and can not be handled.
307 There are several reasons for this error, but most of them are data
308 corruption or absence of expected information.
310 In this template C<@Messages> array is available and contains list
313 =head1 FOR DEVELOPERS
315 =head2 Documentation and references
317 * RFC1847 - Security Multiparts for MIME: Multipart/Signed and Multipart/Encrypted.
318 Describes generic MIME security framework, "mulitpart/signed" and "multipart/encrypted"
321 * RFC3156 - MIME Security with Pretty Good Privacy (PGP),
326 # gnupg options supported by GnuPG::Interface
327 # other otions should be handled via extra_args argument
328 my %supported_opt = map { $_ => 1 } qw(
354 # DEV WARNING: always pass all STD* handles to GnuPG interface even if we don't
355 # need them, just pass 'new IO::Handle' and then close it after safe_run_child.
356 # we don't want to leak anything into FCGI/Apache/MP handles, this break things.
357 # So code should look like:
358 # my $handles = GnuPG::Handles->new(
359 # stdin => ($handle{'stdin'} = new IO::Handle),
360 # stdout => ($handle{'stdout'} = new IO::Handle),
361 # stderr => ($handle{'stderr'} = new IO::Handle),
365 =head2 SignEncrypt Entity => MIME::Entity, [ Encrypt => 1, Sign => 1, ... ]
367 Signs and/or encrypts an email message with GnuPG utility.
373 During signing you can pass C<Signer> argument to set key we sign with this option
374 overrides gnupg's C<default-key> option. If C<Signer> argument is not provided
375 then address of a message sender is used.
377 As well you can pass C<Passphrase>, but if value is undefined then L</GetPassphrase>
382 During encryption you can pass a C<Recipients> array, otherwise C<To>, C<Cc> and
383 C<Bcc> fields of the message are used to fetch the list.
387 Returns a hash with the following keys:
400 my $entity = $args{'Entity'};
401 if ( $args{'Sign'} && !defined $args{'Signer'} ) {
402 $args{'Signer'} = UseKeyForSigning()
403 || (Email::Address->parse( $entity->head->get( 'From' ) ))[0]->address;
405 if ( $args{'Encrypt'} && !$args{'Recipients'} ) {
407 $args{'Recipients'} = [
408 grep $_ && !$seen{ $_ }++, map $_->address,
409 map Email::Address->parse( $entity->head->get( $_ ) ),
414 my $format = lc RT->Config->Get('GnuPG')->{'OutgoingMessagesFormat'} || 'RFC';
415 if ( $format eq 'inline' ) {
416 return SignEncryptInline( %args );
418 return SignEncryptRFC3156( %args );
422 sub SignEncryptRFC3156 {
436 my $gnupg = new GnuPG::Interface;
437 my %opt = RT->Config->Get('GnuPGOptions');
439 # handling passphrase in GnuPGOptions
440 $args{'Passphrase'} = delete $opt{'passphrase'}
441 if !defined $args{'Passphrase'};
443 $opt{'digest-algo'} ||= 'SHA1';
444 $opt{'default_key'} = $args{'Signer'}
445 if $args{'Sign'} && $args{'Signer'};
446 $gnupg->options->hash_init(
447 _PrepareGnuPGOptions( %opt ),
449 meta_interactive => 0,
452 my $entity = $args{'Entity'};
454 if ( $args{'Sign'} && !defined $args{'Passphrase'} ) {
455 $args{'Passphrase'} = GetPassphrase( Address => $args{'Signer'} );
459 if ( $args{'Sign'} && !$args{'Encrypt'} ) {
460 # required by RFC3156(Ch. 5) and RFC1847(Ch. 2.1)
461 foreach ( grep !$_->is_multipart, $entity->parts_DFS ) {
462 my $tenc = $_->head->mime_encoding;
463 unless ( $tenc =~ m/^(?:7bit|quoted-printable|base64)$/i ) {
464 $_->head->mime_attr( 'Content-Transfer-Encoding'
465 => $_->effective_type =~ m{^text/}? 'quoted-printable': 'base64'
470 my ($handles, $handle_list) = _make_gpg_handles(stdin =>IO::Handle::CRLF->new );
471 my %handle = %$handle_list;
473 $gnupg->passphrase( $args{'Passphrase'} );
476 local $SIG{'CHLD'} = 'DEFAULT';
477 my $pid = safe_run_child { $gnupg->detach_sign( handles => $handles ) };
478 $entity->make_multipart( 'mixed', Force => 1 );
480 local $SIG{'PIPE'} = 'IGNORE';
481 $entity->parts(0)->print( $handle{'stdin'} );
482 close $handle{'stdin'};
487 my @signature = readline $handle{'stdout'};
488 close $handle{'stdout'};
490 $res{'exit_code'} = $?;
491 foreach ( qw(stderr logger status) ) {
492 $res{$_} = do { local $/; readline $handle{$_} };
493 delete $res{$_} unless $res{$_} && $res{$_} =~ /\S/s;
496 $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 if ( $err || $res{'exit_code'} ) {
500 $res{'message'} = $err? $err : "gpg exitted with error code ". ($res{'exit_code'} >> 8);
504 # setup RFC1847(Ch.2.1) requirements
505 my $protocol = 'application/pgp-signature';
506 $entity->head->mime_attr( 'Content-Type' => 'multipart/signed' );
507 $entity->head->mime_attr( 'Content-Type.protocol' => $protocol );
508 $entity->head->mime_attr( 'Content-Type.micalg' => 'pgp-'. lc $opt{'digest-algo'} );
511 Disposition => 'inline',
516 if ( $args{'Encrypt'} ) {
518 $gnupg->options->push_recipients( $_ ) foreach
519 map UseKeyForEncryption($_) || $_,
520 grep !$seen{ $_ }++, map $_->address,
521 map Email::Address->parse( $entity->head->get( $_ ) ),
524 my ($tmp_fh, $tmp_fn) = File::Temp::tempfile( UNLINK => 1 );
525 binmode $tmp_fh, ':raw';
527 my ($handles, $handle_list) = _make_gpg_handles(stdout => $tmp_fh);
528 my %handle = %$handle_list;
529 $handles->options( 'stdout' )->{'direct'} = 1;
530 $gnupg->passphrase( $args{'Passphrase'} ) if $args{'Sign'};
533 local $SIG{'CHLD'} = 'DEFAULT';
534 my $pid = safe_run_child { $args{'Sign'}
535 ? $gnupg->sign_and_encrypt( handles => $handles )
536 : $gnupg->encrypt( handles => $handles ) };
537 $entity->make_multipart( 'mixed', Force => 1 );
539 local $SIG{'PIPE'} = 'IGNORE';
540 $entity->parts(0)->print( $handle{'stdin'} );
541 close $handle{'stdin'};
546 $res{'exit_code'} = $?;
547 foreach ( qw(stderr logger status) ) {
548 $res{$_} = do { local $/; readline $handle{$_} };
549 delete $res{$_} unless $res{$_} && $res{$_} =~ /\S/s;
552 $RT::Logger->debug( $res{'status'} ) if $res{'status'};
553 $RT::Logger->warning( $res{'stderr'} ) if $res{'stderr'};
554 $RT::Logger->error( $res{'logger'} ) if $res{'logger'} && $?;
556 $res{'message'} = $@? $@: "gpg exited with error code ". ($? >> 8);
560 my $protocol = 'application/pgp-encrypted';
562 $entity->head->mime_attr( 'Content-Type' => 'multipart/encrypted' );
563 $entity->head->mime_attr( 'Content-Type.protocol' => $protocol );
566 Disposition => 'inline',
567 Data => ['Version: 1',''],
571 Type => 'application/octet-stream',
572 Disposition => 'inline',
577 $entity->parts(-1)->bodyhandle->{'_dirty_hack_to_save_a_ref_tmp_fh'} = $tmp_fh;
582 sub SignEncryptInline {
585 my $entity = $args{'Entity'};
588 $entity->make_singlepart;
589 if ( $entity->is_multipart ) {
590 foreach ( $entity->parts ) {
591 %res = SignEncryptInline( @_, Entity => $_ );
592 return %res if $res{'exit_code'};
597 return _SignEncryptTextInline( @_ )
598 if $entity->effective_type =~ /^text\//i;
600 return _SignEncryptAttachmentInline( @_ );
603 sub _SignEncryptTextInline {
616 return unless $args{'Sign'} || $args{'Encrypt'};
618 my $gnupg = new GnuPG::Interface;
619 my %opt = RT->Config->Get('GnuPGOptions');
621 # handling passphrase in GnupGOptions
622 $args{'Passphrase'} = delete $opt{'passphrase'}
623 if !defined($args{'Passphrase'});
625 $opt{'digest-algo'} ||= 'SHA1';
626 $opt{'default_key'} = $args{'Signer'}
627 if $args{'Sign'} && $args{'Signer'};
628 $gnupg->options->hash_init(
629 _PrepareGnuPGOptions( %opt ),
631 meta_interactive => 0,
634 if ( $args{'Sign'} && !defined $args{'Passphrase'} ) {
635 $args{'Passphrase'} = GetPassphrase( Address => $args{'Signer'} );
638 if ( $args{'Encrypt'} ) {
639 $gnupg->options->push_recipients( $_ ) foreach
640 map UseKeyForEncryption($_) || $_,
641 @{ $args{'Recipients'} || [] };
646 my ($tmp_fh, $tmp_fn) = File::Temp::tempfile( UNLINK => 1 );
647 binmode $tmp_fh, ':raw';
649 my ($handles, $handle_list) = _make_gpg_handles(stdout => $tmp_fh);
650 my %handle = %$handle_list;
652 $handles->options( 'stdout' )->{'direct'} = 1;
653 $gnupg->passphrase( $args{'Passphrase'} ) if $args{'Sign'};
655 my $entity = $args{'Entity'};
657 local $SIG{'CHLD'} = 'DEFAULT';
658 my $method = $args{'Sign'} && $args{'Encrypt'}
660 : ($args{'Sign'}? 'clearsign': 'encrypt');
661 my $pid = safe_run_child { $gnupg->$method( handles => $handles ) };
663 local $SIG{'PIPE'} = 'IGNORE';
664 $entity->bodyhandle->print( $handle{'stdin'} );
665 close $handle{'stdin'};
669 $res{'exit_code'} = $?;
672 foreach ( qw(stderr logger status) ) {
673 $res{$_} = do { local $/; readline $handle{$_} };
674 delete $res{$_} unless $res{$_} && $res{$_} =~ /\S/s;
677 $RT::Logger->debug( $res{'status'} ) if $res{'status'};
678 $RT::Logger->warning( $res{'stderr'} ) if $res{'stderr'};
679 $RT::Logger->error( $res{'logger'} ) if $res{'logger'} && $?;
680 if ( $err || $res{'exit_code'} ) {
681 $res{'message'} = $err? $err : "gpg exitted with error code ". ($res{'exit_code'} >> 8);
685 $entity->bodyhandle( new MIME::Body::File $tmp_fn );
686 $entity->{'__store_tmp_handle_to_avoid_early_cleanup'} = $tmp_fh;
691 sub _SignEncryptAttachmentInline {
704 return unless $args{'Sign'} || $args{'Encrypt'};
706 my $gnupg = new GnuPG::Interface;
707 my %opt = RT->Config->Get('GnuPGOptions');
709 # handling passphrase in GnupGOptions
710 $args{'Passphrase'} = delete $opt{'passphrase'}
711 if !defined($args{'Passphrase'});
713 $opt{'digest-algo'} ||= 'SHA1';
714 $opt{'default_key'} = $args{'Signer'}
715 if $args{'Sign'} && $args{'Signer'};
716 $gnupg->options->hash_init(
717 _PrepareGnuPGOptions( %opt ),
719 meta_interactive => 0,
722 if ( $args{'Sign'} && !defined $args{'Passphrase'} ) {
723 $args{'Passphrase'} = GetPassphrase( Address => $args{'Signer'} );
726 my $entity = $args{'Entity'};
727 if ( $args{'Encrypt'} ) {
728 $gnupg->options->push_recipients( $_ ) foreach
729 map UseKeyForEncryption($_) || $_,
730 @{ $args{'Recipients'} || [] };
735 my ($tmp_fh, $tmp_fn) = File::Temp::tempfile( UNLINK => 1 );
736 binmode $tmp_fh, ':raw';
738 my ($handles, $handle_list) = _make_gpg_handles(stdout => $tmp_fh);
739 my %handle = %$handle_list;
740 $handles->options( 'stdout' )->{'direct'} = 1;
741 $gnupg->passphrase( $args{'Passphrase'} ) if $args{'Sign'};
744 local $SIG{'CHLD'} = 'DEFAULT';
745 my $method = $args{'Sign'} && $args{'Encrypt'}
747 : ($args{'Sign'}? 'detach_sign': 'encrypt');
748 my $pid = safe_run_child { $gnupg->$method( handles => $handles ) };
750 local $SIG{'PIPE'} = 'IGNORE';
751 $entity->bodyhandle->print( $handle{'stdin'} );
752 close $handle{'stdin'};
756 $res{'exit_code'} = $?;
759 foreach ( qw(stderr logger status) ) {
760 $res{$_} = do { local $/; readline $handle{$_} };
761 delete $res{$_} unless $res{$_} && $res{$_} =~ /\S/s;
764 $RT::Logger->debug( $res{'status'} ) if $res{'status'};
765 $RT::Logger->warning( $res{'stderr'} ) if $res{'stderr'};
766 $RT::Logger->error( $res{'logger'} ) if $res{'logger'} && $?;
767 if ( $err || $res{'exit_code'} ) {
768 $res{'message'} = $err? $err : "gpg exitted with error code ". ($res{'exit_code'} >> 8);
772 my $filename = $entity->head->recommended_filename || 'no_name';
773 if ( $args{'Sign'} && !$args{'Encrypt'} ) {
774 $entity->make_multipart;
776 Type => 'application/octet-stream',
778 Filename => "$filename.sig",
779 Disposition => 'attachment',
782 $entity->bodyhandle( new MIME::Body::File $tmp_fn );
783 $entity->effective_type('application/octet-stream');
784 $entity->head->mime_attr( $_ => "$filename.pgp" )
785 foreach (qw(Content-Type.name Content-Disposition.filename));
788 $entity->{'__store_tmp_handle_to_avoid_early_cleanup'} = $tmp_fh;
793 sub SignEncryptContent {
806 return unless $args{'Sign'} || $args{'Encrypt'};
808 my $gnupg = new GnuPG::Interface;
809 my %opt = RT->Config->Get('GnuPGOptions');
811 # handling passphrase in GnupGOptions
812 $args{'Passphrase'} = delete $opt{'passphrase'}
813 if !defined($args{'Passphrase'});
815 $opt{'digest-algo'} ||= 'SHA1';
816 $opt{'default_key'} = $args{'Signer'}
817 if $args{'Sign'} && $args{'Signer'};
818 $gnupg->options->hash_init(
819 _PrepareGnuPGOptions( %opt ),
821 meta_interactive => 0,
824 if ( $args{'Sign'} && !defined $args{'Passphrase'} ) {
825 $args{'Passphrase'} = GetPassphrase( Address => $args{'Signer'} );
828 if ( $args{'Encrypt'} ) {
829 $gnupg->options->push_recipients( $_ ) foreach
830 map UseKeyForEncryption($_) || $_,
831 @{ $args{'Recipients'} || [] };
836 my ($tmp_fh, $tmp_fn) = File::Temp::tempfile( UNLINK => 1 );
837 binmode $tmp_fh, ':raw';
839 my ($handles, $handle_list) = _make_gpg_handles(stdout => $tmp_fh);
840 my %handle = %$handle_list;
841 $handles->options( 'stdout' )->{'direct'} = 1;
842 $gnupg->passphrase( $args{'Passphrase'} ) if $args{'Sign'};
845 local $SIG{'CHLD'} = 'DEFAULT';
846 my $method = $args{'Sign'} && $args{'Encrypt'}
848 : ($args{'Sign'}? 'clearsign': 'encrypt');
849 my $pid = safe_run_child { $gnupg->$method( handles => $handles ) };
851 local $SIG{'PIPE'} = 'IGNORE';
852 $handle{'stdin'}->print( ${ $args{'Content'} } );
853 close $handle{'stdin'};
857 $res{'exit_code'} = $?;
860 foreach ( qw(stderr logger status) ) {
861 $res{$_} = do { local $/; readline $handle{$_} };
862 delete $res{$_} unless $res{$_} && $res{$_} =~ /\S/s;
865 $RT::Logger->debug( $res{'status'} ) if $res{'status'};
866 $RT::Logger->warning( $res{'stderr'} ) if $res{'stderr'};
867 $RT::Logger->error( $res{'logger'} ) if $res{'logger'} && $?;
868 if ( $err || $res{'exit_code'} ) {
869 $res{'message'} = $err? $err : "gpg exitted with error code ". ($res{'exit_code'} >> 8);
873 ${ $args{'Content'} } = '';
876 my $status = read $tmp_fh, my $buf, 4*1024;
877 unless ( defined $status ) {
878 $RT::Logger->crit( "couldn't read message: $!" );
879 } elsif ( !$status ) {
882 ${ $args{'Content'} } .= $buf;
888 sub FindProtectedParts {
889 my %args = ( Entity => undef, CheckBody => 1, @_ );
890 my $entity = $args{'Entity'};
892 # inline PGP block, only in singlepart
893 unless ( $entity->is_multipart ) {
894 my $io = $entity->open('r');
896 $RT::Logger->warning( "Entity of type ". $entity->effective_type ." has no body" );
900 # Deal with "partitioned" PGP mail, which (contrary to common
901 # sense) unnecessarily applies a base64 transfer encoding to PGP
902 # mail (whose content is already base64-encoded).
903 if ( $entity->bodyhandle->is_encoded and $entity->head->mime_encoding ) {
904 my $decoder = MIME::Decoder->new( $entity->head->mime_encoding );
909 open my $fh, '>', \$buf
910 or die "Couldn't open scalar for writing: $!";
912 $decoder->decode($io, $fh);
913 close $fh or die "Couldn't close scalar: $!";
916 or die "Couldn't re-open scalar for reading: $!";
921 $RT::Logger->error("Couldn't decode body: $@");
926 while ( defined($_ = $io->getline) ) {
927 next unless /^-----BEGIN PGP (SIGNED )?MESSAGE-----/;
928 my $type = $1? 'signed': 'encrypted';
929 $RT::Logger->debug("Found $type inline part");
940 # RFC3156, multipart/{signed,encrypted}
941 if ( ( my $type = $entity->effective_type ) =~ /^multipart\/(?:encrypted|signed)$/ ) {
942 unless ( $entity->parts == 2 ) {
943 $RT::Logger->error( "Encrypted or signed entity must has two subparts. Skipped" );
947 my $protocol = $entity->head->mime_attr( 'Content-Type.protocol' );
948 unless ( $protocol ) {
949 $RT::Logger->error( "Entity is '$type', but has no protocol defined. Skipped" );
953 if ( $type eq 'multipart/encrypted' ) {
954 unless ( $protocol eq 'application/pgp-encrypted' ) {
955 $RT::Logger->info( "Skipping protocol '$protocol', only 'application/pgp-encrypted' is supported" );
958 $RT::Logger->debug("Found encrypted according to RFC3156 part");
963 Data => $entity->parts(1),
964 Info => $entity->parts(0),
967 unless ( $protocol eq 'application/pgp-signature' ) {
968 $RT::Logger->info( "Skipping protocol '$protocol', only 'application/pgp-signature' is supported" );
971 $RT::Logger->debug("Found signed according to RFC3156 part");
976 Data => $entity->parts(0),
977 Signature => $entity->parts(1),
982 # attachments signed with signature in another part
984 foreach my $i ( 0 .. $entity->parts - 1 ) {
985 my $part = $entity->parts($i);
987 # we can not associate a signature within an attachment
989 my $fname = $part->head->recommended_filename;
992 if ( $part->effective_type eq 'application/pgp-signature' ) {
993 push @file_indices, $i;
995 elsif ( $fname =~ /\.sig$/i && $part->effective_type eq 'application/octet-stream' ) {
996 push @file_indices, $i;
1001 foreach my $i ( @file_indices ) {
1002 my $sig_part = $entity->parts($i);
1003 $skip{"$sig_part"}++;
1004 my $sig_name = $sig_part->head->recommended_filename;
1005 my ($file_name) = $sig_name =~ /^(.*?)(?:\.sig)?$/;
1007 my ($data_part_idx) =
1008 grep $file_name eq ($entity->parts($_)->head->recommended_filename||''),
1009 grep $sig_part ne $entity->parts($_),
1010 0 .. $entity->parts - 1;
1011 unless ( defined $data_part_idx ) {
1012 $RT::Logger->error("Found $sig_name attachment, but didn't find $file_name");
1015 my $data_part_in = $entity->parts($data_part_idx);
1017 $skip{"$data_part_in"}++;
1018 $RT::Logger->debug("Found signature (in '$sig_name') of attachment '$file_name'");
1021 Format => 'Attachment',
1023 Data => $data_part_in,
1024 Signature => $sig_part,
1028 # attachments with inline encryption
1029 my @encrypted_indices =
1030 grep {($entity->parts($_)->head->recommended_filename || '') =~ /\.pgp$/}
1031 0 .. $entity->parts - 1;
1033 foreach my $i ( @encrypted_indices ) {
1034 my $part = $entity->parts($i);
1036 $RT::Logger->debug("Found encrypted attachment '". $part->head->recommended_filename ."'");
1038 Type => 'encrypted',
1039 Format => 'Attachment',
1045 push @res, FindProtectedParts( Entity => $_ )
1046 foreach grep !$skip{"$_"}, $entity->parts;
1051 =head2 VerifyDecrypt Entity => undef, [ Detach => 1, Passphrase => undef, SetStatus => 1 ]
1063 my @protected = FindProtectedParts( Entity => $args{'Entity'} );
1065 # XXX: detaching may brake nested signatures
1066 foreach my $item( grep $_->{'Type'} eq 'signed', @protected ) {
1068 if ( $item->{'Format'} eq 'RFC3156' ) {
1069 push @res, { VerifyRFC3156( %$item, SetStatus => $args{'SetStatus'} ) };
1070 if ( $args{'Detach'} ) {
1071 $item->{'Top'}->parts( [ $item->{'Data'} ] );
1072 $item->{'Top'}->make_singlepart;
1074 $status_on = $item->{'Top'};
1075 } elsif ( $item->{'Format'} eq 'Inline' ) {
1076 push @res, { VerifyInline( %$item ) };
1077 $status_on = $item->{'Data'};
1078 } elsif ( $item->{'Format'} eq 'Attachment' ) {
1079 push @res, { VerifyAttachment( %$item ) };
1080 if ( $args{'Detach'} ) {
1081 $item->{'Top'}->parts( [
1082 grep "$_" ne $item->{'Signature'}, $item->{'Top'}->parts
1084 $item->{'Top'}->make_singlepart;
1086 $status_on = $item->{'Data'};
1088 if ( $args{'SetStatus'} || $args{'AddStatus'} ) {
1089 my $method = $args{'AddStatus'} ? 'add' : 'set';
1090 # Let the header be modified so continuations are handled
1091 my $modify = $status_on->head->modify;
1092 $status_on->head->modify(1);
1093 $status_on->head->$method(
1094 'X-RT-GnuPG-Status' => $res[-1]->{'status'}
1096 $status_on->head->modify($modify);
1099 foreach my $item( grep $_->{'Type'} eq 'encrypted', @protected ) {
1101 if ( $item->{'Format'} eq 'RFC3156' ) {
1102 push @res, { DecryptRFC3156( %$item ) };
1103 $status_on = $item->{'Top'};
1104 } elsif ( $item->{'Format'} eq 'Inline' ) {
1105 push @res, { DecryptInline( %$item ) };
1106 $status_on = $item->{'Data'};
1107 } elsif ( $item->{'Format'} eq 'Attachment' ) {
1108 push @res, { DecryptAttachment( %$item ) };
1109 $status_on = $item->{'Data'};
1111 if ( $args{'SetStatus'} || $args{'AddStatus'} ) {
1112 my $method = $args{'AddStatus'} ? 'add' : 'set';
1113 # Let the header be modified so continuations are handled
1114 my $modify = $status_on->head->modify;
1115 $status_on->head->modify(1);
1116 $status_on->head->$method(
1117 'X-RT-GnuPG-Status' => $res[-1]->{'status'}
1119 $status_on->head->modify($modify);
1125 sub VerifyInline { return DecryptInline( @_ ) }
1127 sub VerifyAttachment {
1128 my %args = ( Data => undef, Signature => undef, Top => undef, @_ );
1130 my $gnupg = new GnuPG::Interface;
1131 my %opt = RT->Config->Get('GnuPGOptions');
1132 $opt{'digest-algo'} ||= 'SHA1';
1133 $gnupg->options->hash_init(
1134 _PrepareGnuPGOptions( %opt ),
1135 meta_interactive => 0,
1138 foreach ( $args{'Data'}, $args{'Signature'} ) {
1139 next unless $_->bodyhandle->is_encoded;
1141 require RT::EmailParser;
1142 RT::EmailParser->_DecodeBody($_);
1145 my ($tmp_fh, $tmp_fn) = File::Temp::tempfile( UNLINK => 1 );
1146 binmode $tmp_fh, ':raw';
1147 $args{'Data'}->bodyhandle->print( $tmp_fh );
1150 my ($handles, $handle_list) = _make_gpg_handles();
1151 my %handle = %$handle_list;
1155 local $SIG{'CHLD'} = 'DEFAULT';
1156 my $pid = safe_run_child { $gnupg->verify(
1157 handles => $handles, command_args => [ '-', $tmp_fn ]
1160 local $SIG{'PIPE'} = 'IGNORE';
1161 $args{'Signature'}->bodyhandle->print( $handle{'stdin'} );
1162 close $handle{'stdin'};
1166 $res{'exit_code'} = $?;
1167 foreach ( qw(stderr logger status) ) {
1168 $res{$_} = do { local $/; readline $handle{$_} };
1169 delete $res{$_} unless $res{$_} && $res{$_} =~ /\S/s;
1172 $RT::Logger->debug( $res{'status'} ) if $res{'status'};
1173 $RT::Logger->warning( $res{'stderr'} ) if $res{'stderr'};
1174 $RT::Logger->error( $res{'logger'} ) if $res{'logger'} && $?;
1176 $res{'message'} = $@? $@: "gpg exitted with error code ". ($? >> 8);
1182 my %args = ( Data => undef, Signature => undef, Top => undef, @_ );
1184 my $gnupg = new GnuPG::Interface;
1185 my %opt = RT->Config->Get('GnuPGOptions');
1186 $opt{'digest-algo'} ||= 'SHA1';
1187 $gnupg->options->hash_init(
1188 _PrepareGnuPGOptions( %opt ),
1189 meta_interactive => 0,
1192 my ($tmp_fh, $tmp_fn) = File::Temp::tempfile( UNLINK => 1 );
1193 binmode $tmp_fh, ':raw:eol(CRLF?)';
1194 $args{'Data'}->print( $tmp_fh );
1197 my ($handles, $handle_list) = _make_gpg_handles();
1198 my %handle = %$handle_list;
1202 local $SIG{'CHLD'} = 'DEFAULT';
1203 my $pid = safe_run_child { $gnupg->verify(
1204 handles => $handles, command_args => [ '-', $tmp_fn ]
1207 local $SIG{'PIPE'} = 'IGNORE';
1208 $args{'Signature'}->bodyhandle->print( $handle{'stdin'} );
1209 close $handle{'stdin'};
1213 $res{'exit_code'} = $?;
1214 foreach ( qw(stderr logger status) ) {
1215 $res{$_} = do { local $/; readline $handle{$_} };
1216 delete $res{$_} unless $res{$_} && $res{$_} =~ /\S/s;
1219 $RT::Logger->debug( $res{'status'} ) if $res{'status'};
1220 $RT::Logger->warning( $res{'stderr'} ) if $res{'stderr'};
1221 $RT::Logger->error( $res{'logger'} ) if $res{'logger'} && $?;
1223 $res{'message'} = $@? $@: "gpg exitted with error code ". ($? >> 8);
1228 sub DecryptRFC3156 {
1233 Passphrase => undef,
1237 my $gnupg = new GnuPG::Interface;
1238 my %opt = RT->Config->Get('GnuPGOptions');
1240 # handling passphrase in GnupGOptions
1241 $args{'Passphrase'} = delete $opt{'passphrase'}
1242 if !defined($args{'Passphrase'});
1244 $opt{'digest-algo'} ||= 'SHA1';
1245 $gnupg->options->hash_init(
1246 _PrepareGnuPGOptions( %opt ),
1247 meta_interactive => 0,
1250 if ( $args{'Data'}->bodyhandle->is_encoded ) {
1251 require RT::EmailParser;
1252 RT::EmailParser->_DecodeBody($args{'Data'});
1255 $args{'Passphrase'} = GetPassphrase()
1256 unless defined $args{'Passphrase'};
1258 my ($tmp_fh, $tmp_fn) = File::Temp::tempfile( UNLINK => 1 );
1259 binmode $tmp_fh, ':raw';
1261 my ($handles, $handle_list) = _make_gpg_handles(stdout => $tmp_fh);
1262 my %handle = %$handle_list;
1263 $handles->options( 'stdout' )->{'direct'} = 1;
1267 local $SIG{'CHLD'} = 'DEFAULT';
1268 $gnupg->passphrase( $args{'Passphrase'} );
1269 my $pid = safe_run_child { $gnupg->decrypt( handles => $handles ) };
1271 local $SIG{'PIPE'} = 'IGNORE';
1272 $args{'Data'}->bodyhandle->print( $handle{'stdin'} );
1273 close $handle{'stdin'}
1278 $res{'exit_code'} = $?;
1279 foreach ( qw(stderr logger status) ) {
1280 $res{$_} = do { local $/; readline $handle{$_} };
1281 delete $res{$_} unless $res{$_} && $res{$_} =~ /\S/s;
1284 $RT::Logger->debug( $res{'status'} ) if $res{'status'};
1285 $RT::Logger->warning( $res{'stderr'} ) if $res{'stderr'};
1286 $RT::Logger->error( $res{'logger'} ) if $res{'logger'} && $?;
1288 # if the decryption is fine but the signature is bad, then without this
1289 # status check we lose the decrypted text
1290 # XXX: add argument to the function to control this check
1291 if ( $res{'status'} !~ /DECRYPTION_OKAY/ ) {
1293 $res{'message'} = $@? $@: "gpg exitted with error code ". ($? >> 8);
1299 my $parser = new RT::EmailParser;
1300 my $decrypted = $parser->ParseMIMEEntityFromFileHandle( $tmp_fh, 0 );
1301 $decrypted->{'__store_link_to_object_to_avoid_early_cleanup'} = $parser;
1302 $args{'Top'}->parts( [] );
1303 $args{'Top'}->add_part( $decrypted );
1304 $args{'Top'}->make_singlepart;
1311 Passphrase => undef,
1315 my $gnupg = new GnuPG::Interface;
1316 my %opt = RT->Config->Get('GnuPGOptions');
1318 # handling passphrase in GnuPGOptions
1319 $args{'Passphrase'} = delete $opt{'passphrase'}
1320 if !defined($args{'Passphrase'});
1322 $opt{'digest-algo'} ||= 'SHA1';
1323 $gnupg->options->hash_init(
1324 _PrepareGnuPGOptions( %opt ),
1325 meta_interactive => 0,
1328 if ( $args{'Data'}->bodyhandle->is_encoded ) {
1329 require RT::EmailParser;
1330 RT::EmailParser->_DecodeBody($args{'Data'});
1333 $args{'Passphrase'} = GetPassphrase()
1334 unless defined $args{'Passphrase'};
1336 my ($tmp_fh, $tmp_fn) = File::Temp::tempfile( UNLINK => 1 );
1337 binmode $tmp_fh, ':raw';
1339 my $io = $args{'Data'}->open('r');
1341 die "Entity has no body, never should happen";
1346 my ($had_literal, $in_block) = ('', 0);
1347 my ($block_fh, $block_fn) = File::Temp::tempfile( UNLINK => 1 );
1348 binmode $block_fh, ':raw';
1350 while ( defined(my $str = $io->getline) ) {
1351 if ( $in_block && $str =~ /^-----END PGP (?:MESSAGE|SIGNATURE)-----/ ) {
1352 print $block_fh $str;
1354 next if $in_block > 0;
1356 seek $block_fh, 0, 0;
1358 my ($res_fh, $res_fn);
1359 ($res_fh, $res_fn, %res) = _DecryptInlineBlock(
1362 BlockHandle => $block_fh,
1364 return %res unless $res_fh;
1366 print $tmp_fh "-----BEGIN OF PGP PROTECTED PART-----\n" if $had_literal;
1367 while (my $buf = <$res_fh> ) {
1370 print $tmp_fh "-----END OF PART-----\n" if $had_literal;
1372 ($block_fh, $block_fn) = File::Temp::tempfile( UNLINK => 1 );
1373 binmode $block_fh, ':raw';
1376 elsif ( $str =~ /^-----BEGIN PGP (SIGNED )?MESSAGE-----/ ) {
1378 print $block_fh $str;
1380 elsif ( $in_block ) {
1381 print $block_fh $str;
1385 $had_literal = 1 if /\S/s;
1391 # we're still in a block, this not bad not good. let's try to
1392 # decrypt what we have, it can be just missing -----END PGP...
1393 seek $block_fh, 0, 0;
1395 my ($res_fh, $res_fn);
1396 ($res_fh, $res_fn, %res) = _DecryptInlineBlock(
1399 BlockHandle => $block_fh,
1401 return %res unless $res_fh;
1403 print $tmp_fh "-----BEGIN OF PGP PROTECTED PART-----\n" if $had_literal;
1404 while (my $buf = <$res_fh> ) {
1407 print $tmp_fh "-----END OF PART-----\n" if $had_literal;
1411 $args{'Data'}->bodyhandle( new MIME::Body::File $tmp_fn );
1412 $args{'Data'}->{'__store_tmp_handle_to_avoid_early_cleanup'} = $tmp_fh;
1416 sub _DecryptInlineBlock {
1419 BlockHandle => undef,
1420 Passphrase => undef,
1423 my $gnupg = $args{'GnuPG'};
1425 my ($tmp_fh, $tmp_fn) = File::Temp::tempfile( UNLINK => 1 );
1426 binmode $tmp_fh, ':raw';
1428 my ($handles, $handle_list) = _make_gpg_handles(
1429 stdin => $args{'BlockHandle'},
1431 my %handle = %$handle_list;
1432 $handles->options( 'stdout' )->{'direct'} = 1;
1433 $handles->options( 'stdin' )->{'direct'} = 1;
1437 local $SIG{'CHLD'} = 'DEFAULT';
1438 $gnupg->passphrase( $args{'Passphrase'} );
1439 my $pid = safe_run_child { $gnupg->decrypt( handles => $handles ) };
1442 $res{'exit_code'} = $?;
1443 foreach ( qw(stderr logger status) ) {
1444 $res{$_} = do { local $/; readline $handle{$_} };
1445 delete $res{$_} unless $res{$_} && $res{$_} =~ /\S/s;
1448 $RT::Logger->debug( $res{'status'} ) if $res{'status'};
1449 $RT::Logger->warning( $res{'stderr'} ) if $res{'stderr'};
1450 $RT::Logger->error( $res{'logger'} ) if $res{'logger'} && $?;
1452 # if the decryption is fine but the signature is bad, then without this
1453 # status check we lose the decrypted text
1454 # XXX: add argument to the function to control this check
1455 if ( $res{'status'} !~ /DECRYPTION_OKAY/ ) {
1457 $res{'message'} = $@? $@: "gpg exitted with error code ". ($? >> 8);
1458 return (undef, undef, %res);
1463 return ($tmp_fh, $tmp_fn, %res);
1466 sub DecryptAttachment {
1470 Passphrase => undef,
1474 my $gnupg = new GnuPG::Interface;
1475 my %opt = RT->Config->Get('GnuPGOptions');
1477 # handling passphrase in GnuPGOptions
1478 $args{'Passphrase'} = delete $opt{'passphrase'}
1479 if !defined($args{'Passphrase'});
1481 $opt{'digest-algo'} ||= 'SHA1';
1482 $gnupg->options->hash_init(
1483 _PrepareGnuPGOptions( %opt ),
1484 meta_interactive => 0,
1487 if ( $args{'Data'}->bodyhandle->is_encoded ) {
1488 require RT::EmailParser;
1489 RT::EmailParser->_DecodeBody($args{'Data'});
1492 $args{'Passphrase'} = GetPassphrase()
1493 unless defined $args{'Passphrase'};
1495 my ($tmp_fh, $tmp_fn) = File::Temp::tempfile( UNLINK => 1 );
1496 binmode $tmp_fh, ':raw';
1497 $args{'Data'}->bodyhandle->print( $tmp_fh );
1500 my ($res_fh, $res_fn, %res) = _DecryptInlineBlock(
1503 BlockHandle => $tmp_fh,
1505 return %res unless $res_fh;
1507 $args{'Data'}->bodyhandle( new MIME::Body::File $res_fn );
1508 $args{'Data'}->{'__store_tmp_handle_to_avoid_early_cleanup'} = $res_fh;
1510 my $filename = $args{'Data'}->head->recommended_filename;
1511 $filename =~ s/\.pgp$//i;
1512 $args{'Data'}->head->mime_attr( $_ => $filename )
1513 foreach (qw(Content-Type.name Content-Disposition.filename));
1518 sub DecryptContent {
1521 Passphrase => undef,
1525 my $gnupg = new GnuPG::Interface;
1526 my %opt = RT->Config->Get('GnuPGOptions');
1528 # handling passphrase in GnupGOptions
1529 $args{'Passphrase'} = delete $opt{'passphrase'}
1530 if !defined($args{'Passphrase'});
1532 $opt{'digest-algo'} ||= 'SHA1';
1533 $gnupg->options->hash_init(
1534 _PrepareGnuPGOptions( %opt ),
1535 meta_interactive => 0,
1538 $args{'Passphrase'} = GetPassphrase()
1539 unless defined $args{'Passphrase'};
1541 my ($tmp_fh, $tmp_fn) = File::Temp::tempfile( UNLINK => 1 );
1542 binmode $tmp_fh, ':raw';
1544 my ($handles, $handle_list) = _make_gpg_handles(
1546 my %handle = %$handle_list;
1547 $handles->options( 'stdout' )->{'direct'} = 1;
1551 local $SIG{'CHLD'} = 'DEFAULT';
1552 $gnupg->passphrase( $args{'Passphrase'} );
1553 my $pid = safe_run_child { $gnupg->decrypt( handles => $handles ) };
1555 local $SIG{'PIPE'} = 'IGNORE';
1556 print { $handle{'stdin'} } ${ $args{'Content'} };
1557 close $handle{'stdin'};
1562 $res{'exit_code'} = $?;
1563 foreach ( qw(stderr logger status) ) {
1564 $res{$_} = do { local $/; readline $handle{$_} };
1565 delete $res{$_} unless $res{$_} && $res{$_} =~ /\S/s;
1568 $RT::Logger->debug( $res{'status'} ) if $res{'status'};
1569 $RT::Logger->warning( $res{'stderr'} ) if $res{'stderr'};
1570 $RT::Logger->error( $res{'logger'} ) if $res{'logger'} && $?;
1572 # if the decryption is fine but the signature is bad, then without this
1573 # status check we lose the decrypted text
1574 # XXX: add argument to the function to control this check
1575 if ( $res{'status'} !~ /DECRYPTION_OKAY/ ) {
1577 $res{'message'} = $@? $@: "gpg exitted with error code ". ($? >> 8);
1582 ${ $args{'Content'} } = '';
1585 my $status = read $tmp_fh, my $buf, 4*1024;
1586 unless ( defined $status ) {
1587 $RT::Logger->crit( "couldn't read message: $!" );
1588 } elsif ( !$status ) {
1591 ${ $args{'Content'} } .= $buf;
1597 =head2 GetPassphrase [ Address => undef ]
1599 Returns passphrase, called whenever it's required with Address as a named argument.
1604 my %args = ( Address => undef, @_ );
1610 Takes a string containing output of gnupg status stream. Parses it and returns
1611 array of hashes. Each element of array is a hash ref and represents line or
1612 group of lines in the status message.
1614 All hashes have Operation, Status and Message elements.
1620 Classification of operations gnupg performs. Now we have support
1621 for Sign, Encrypt, Decrypt, Verify, PassphraseCheck, RecipientsCheck and Data
1626 Informs about success. Value is 'DONE' on success, other values means that
1627 an operation failed, for example 'ERROR', 'BAD', 'MISSING' and may be other.
1631 User friendly message.
1635 This parser is based on information from GnuPG distribution, see also
1636 F<docs/design_docs/gnupg_details_on_output_formats> in the RT distribution.
1640 my %REASON_CODE_TO_TEXT = (
1642 1 => "No armored data",
1643 2 => "Expected a packet, but did not found one",
1644 3 => "Invalid packet found",
1645 4 => "Signature expected, but not found",
1648 0 => "No specific reason given",
1650 2 => "Ambigious specification",
1651 3 => "Wrong key usage",
1654 6 => "No CRL known",
1656 8 => "Policy mismatch",
1657 9 => "Not a secret key",
1658 10 => "Key not trusted",
1661 0 => 'not specified',
1662 4 => 'unknown algorithm',
1663 9 => 'missing public key',
1667 sub ReasonCodeToText {
1668 my $keyword = shift;
1670 return $REASON_CODE_TO_TEXT{ $keyword }{ $code }
1671 if exists $REASON_CODE_TO_TEXT{ $keyword }{ $code };
1675 my %simple_keyword = (
1677 Operation => 'RecipientsCheck',
1679 Message => 'No recipients',
1682 Operation => 'Data',
1684 Message => 'Unexpected data has been encountered',
1687 Operation => 'Data',
1689 Message => 'The ASCII armor is corrupted',
1694 my %parse_keyword = map { $_ => 1 } qw(
1696 SIG_CREATED GOODSIG BADSIG ERRSIG
1698 DECRYPTION_FAILED DECRYPTION_OKAY
1699 BAD_PASSPHRASE GOOD_PASSPHRASE
1701 NO_RECP INV_RECP NODATA UNEXPECTED
1704 # keywords we ignore without any messages as we parse them using other
1705 # keywords as starting point or just ignore as they are useless for us
1706 my %ignore_keyword = map { $_ => 1 } qw(
1707 NEED_PASSPHRASE MISSING_PASSPHRASE BEGIN_SIGNING PLAINTEXT PLAINTEXT_LENGTH
1708 BEGIN_ENCRYPTION SIG_ID VALIDSIG
1709 ENC_TO BEGIN_DECRYPTION END_DECRYPTION GOODMDC
1710 TRUST_UNDEFINED TRUST_NEVER TRUST_MARGINAL TRUST_FULLY TRUST_ULTIMATE
1716 return () unless $status;
1719 while ( $status =~ /\[GNUPG:\]\s*(.*?)(?=\[GNUPG:\]|\z)/igms ) {
1720 push @status, $1; $status[-1] =~ s/\s+/ /g; $status[-1] =~ s/\s+$//;
1722 $status = join "\n", @status;
1726 my (%user_hint, $latest_user_main_key);
1727 for ( my $i = 0; $i < @status; $i++ ) {
1728 my $line = $status[$i];
1729 my ($keyword, $args) = ($line =~ /^(\S+)\s*(.*)$/s);
1730 if ( $simple_keyword{ $keyword } ) {
1731 push @res, $simple_keyword{ $keyword };
1732 $res[-1]->{'Keyword'} = $keyword;
1735 unless ( $parse_keyword{ $keyword } ) {
1736 $RT::Logger->warning("Skipped $keyword") unless $ignore_keyword{ $keyword };
1740 if ( $keyword eq 'USERID_HINT' ) {
1741 my %tmp = _ParseUserHint($status, $line);
1742 $latest_user_main_key = $tmp{'MainKey'};
1743 if ( $user_hint{ $tmp{'MainKey'} } ) {
1744 while ( my ($k, $v) = each %tmp ) {
1745 $user_hint{ $tmp{'MainKey'} }->{$k} = $v;
1748 $user_hint{ $tmp{'MainKey'} } = \%tmp;
1752 elsif ( $keyword eq 'BAD_PASSPHRASE' || $keyword eq 'GOOD_PASSPHRASE' ) {
1755 Operation => 'PassphraseCheck',
1756 Status => $keyword eq 'BAD_PASSPHRASE'? 'BAD' : 'DONE',
1759 $res{'Status'} = 'MISSING' if $status[ $i - 1 ] =~ /^MISSING_PASSPHRASE/;
1760 foreach my $line ( reverse @status[ 0 .. $i-1 ] ) {
1761 next unless $line =~ /^NEED_PASSPHRASE\s+(\S+)\s+(\S+)\s+(\S+)/;
1762 next if $key_id && $2 ne $key_id;
1763 @res{'MainKey', 'Key', 'KeyType'} = ($1, $2, $3);
1766 $res{'Message'} = ucfirst( lc( $res{'Status'} eq 'DONE'? 'GOOD': $res{'Status'} ) ) .' passphrase';
1767 $res{'User'} = ( $user_hint{ $res{'MainKey'} } ||= {} ) if $res{'MainKey'};
1768 if ( exists $res{'User'}->{'EmailAddress'} ) {
1769 $res{'Message'} .= ' for '. $res{'User'}->{'EmailAddress'};
1771 $res{'Message'} .= " for '0x$key_id'";
1775 elsif ( $keyword eq 'END_ENCRYPTION' ) {
1777 Operation => 'Encrypt',
1779 Message => 'Data has been encrypted',
1781 foreach my $line ( reverse @status[ 0 .. $i-1 ] ) {
1782 next unless $line =~ /^BEGIN_ENCRYPTION\s+(\S+)\s+(\S+)/;
1783 @res{'MdcMethod', 'SymAlgo'} = ($1, $2);
1788 elsif ( $keyword eq 'DECRYPTION_FAILED' || $keyword eq 'DECRYPTION_OKAY' ) {
1789 my %res = ( Operation => 'Decrypt' );
1790 @res{'Status', 'Message'} =
1791 $keyword eq 'DECRYPTION_FAILED'
1792 ? ('ERROR', 'Decryption failed')
1793 : ('DONE', 'Decryption process succeeded');
1795 foreach my $line ( reverse @status[ 0 .. $i-1 ] ) {
1796 next unless $line =~ /^ENC_TO\s+(\S+)\s+(\S+)\s+(\S+)/;
1797 my ($key, $alg, $key_length) = ($1, $2, $3);
1799 my %encrypted_to = (
1800 Message => "The message is encrypted to '0x$key'",
1801 User => ( $user_hint{ $key } ||= {} ),
1803 KeyLength => $key_length,
1807 push @{ $res{'EncryptedTo'} ||= [] }, \%encrypted_to;
1812 elsif ( $keyword eq 'NO_SECKEY' || $keyword eq 'NO_PUBKEY' ) {
1813 my ($key) = split /\s+/, $args;
1814 my $type = $keyword eq 'NO_SECKEY'? 'secret': 'public';
1816 Operation => 'KeyCheck',
1817 Status => 'MISSING',
1818 Message => ucfirst( $type ) ." key '0x$key' is not available",
1822 $res{'User'} = ( $user_hint{ $key } ||= {} );
1823 $res{'User'}{ ucfirst( $type ). 'KeyMissing' } = 1;
1826 # GOODSIG, BADSIG, VALIDSIG, TRUST_*
1827 elsif ( $keyword eq 'GOODSIG' ) {
1829 Operation => 'Verify',
1831 Message => 'The signature is good',
1833 @res{qw(Key UserString)} = split /\s+/, $args, 2;
1834 $res{'Message'} .= ', signed by '. $res{'UserString'};
1836 foreach my $line ( @status[ $i .. $#status ] ) {
1837 next unless $line =~ /^TRUST_(\S+)/;
1841 $res{'Message'} .= ', trust level is '. lc( $res{'Trust'} || 'unknown');
1843 foreach my $line ( @status[ $i .. $#status ] ) {
1844 next unless $line =~ /^VALIDSIG\s+(.*)/;
1857 ) } = split /\s+/, $1, 10;
1862 elsif ( $keyword eq 'BADSIG' ) {
1864 Operation => 'Verify',
1866 Message => 'The signature has not been verified okay',
1868 @res{qw(Key UserString)} = split /\s+/, $args, 2;
1871 elsif ( $keyword eq 'ERRSIG' ) {
1873 Operation => 'Verify',
1875 Message => 'Not possible to check the signature',
1877 @res{qw(Key PubkeyAlgo HashAlgo Class Timestamp ReasonCode Other)}
1878 = split /\s+/, $args, 7;
1880 $res{'Reason'} = ReasonCodeToText( $keyword, $res{'ReasonCode'} );
1881 $res{'Message'} .= ", the reason is ". $res{'Reason'};
1885 elsif ( $keyword eq 'SIG_CREATED' ) {
1886 # SIG_CREATED <type> <pubkey algo> <hash algo> <class> <timestamp> <key fpr>
1887 my @props = split /\s+/, $args;
1889 Operation => 'Sign',
1891 Message => "Signed message",
1893 PubKeyAlgo => $props[1],
1894 HashKeyAlgo => $props[2],
1896 Timestamp => $props[4],
1897 KeyFingerprint => $props[5],
1898 User => $user_hint{ $latest_user_main_key },
1900 $res[-1]->{Message} .= ' by '. $user_hint{ $latest_user_main_key }->{'EmailAddress'}
1901 if $user_hint{ $latest_user_main_key };
1903 elsif ( $keyword eq 'INV_RECP' ) {
1904 my ($rcode, $recipient) = split /\s+/, $args, 2;
1905 my $reason = ReasonCodeToText( $keyword, $rcode );
1907 Operation => 'RecipientsCheck',
1909 Message => "Recipient '$recipient' is unusable, the reason is '$reason'",
1910 Recipient => $recipient,
1911 ReasonCode => $rcode,
1915 elsif ( $keyword eq 'NODATA' ) {
1916 my $rcode = (split /\s+/, $args)[0];
1917 my $reason = ReasonCodeToText( $keyword, $rcode );
1919 Operation => 'Data',
1921 Message => "No data has been found. The reason is '$reason'",
1922 ReasonCode => $rcode,
1927 $RT::Logger->warning("Keyword $keyword is unknown");
1930 $res[-1]{'Keyword'} = $keyword if @res && !$res[-1]{'Keyword'};
1935 sub _ParseUserHint {
1936 my ($status, $hint) = (@_);
1937 my ($main_key_id, $user_str) = ($hint =~ /^USERID_HINT\s+(\S+)\s+(.*)$/);
1938 return () unless $main_key_id;
1940 MainKey => $main_key_id,
1941 String => $user_str,
1942 EmailAddress => (map $_->address, Email::Address->parse( $user_str ))[0],
1946 sub _PrepareGnuPGOptions {
1948 my %res = map { lc $_ => $opt{ $_ } } grep $supported_opt{ lc $_ }, keys %opt;
1949 $res{'extra_args'} ||= [];
1950 foreach my $o ( grep !$supported_opt{ lc $_ }, keys %opt ) {
1951 push @{ $res{'extra_args'} }, '--'. lc $o;
1952 push @{ $res{'extra_args'} }, $opt{ $o }
1953 if defined $opt{ $o };
1960 # one arg -> return preferred key
1962 sub UseKeyForEncryption {
1965 } elsif ( @_ > 1 ) {
1967 $key{ lc($_) } = delete $key{ $_ } foreach grep lc ne $_, keys %key;
1969 return $key{ $_[0] };
1974 =head2 UseKeyForSigning
1976 Returns or sets identifier of the key that should be used for signing.
1978 Returns the current value when called without arguments.
1980 Sets new value when called with one argument and unsets if it's undef.
1985 sub UseKeyForSigning {
1992 =head2 GetKeysForEncryption
1994 Takes identifier and returns keys suitable for encryption.
1996 B<Note> that keys for which trust level is not set are
2001 sub GetKeysForEncryption {
2003 my %res = GetKeysInfo( $key_id, 'public', @_ );
2004 return %res if $res{'exit_code'};
2005 return %res unless $res{'info'};
2007 foreach my $key ( splice @{ $res{'info'} } ) {
2008 # skip disabled keys
2009 next if $key->{'Capabilities'} =~ /D/;
2010 # skip keys not suitable for encryption
2011 next unless $key->{'Capabilities'} =~ /e/i;
2012 # skip disabled, expired, revoke and keys with no trust,
2013 # but leave keys with unknown trust level
2014 next if $key->{'TrustLevel'} < 0;
2016 push @{ $res{'info'} }, $key;
2018 delete $res{'info'} unless @{ $res{'info'} };
2022 sub GetKeysForSigning {
2024 return GetKeysInfo( $key_id, 'private', @_ );
2027 sub CheckRecipients {
2028 my @recipients = (@_);
2030 my ($status, @issues) = (1, ());
2033 foreach my $address ( grep !$seen{ lc $_ }++, map $_->address, @recipients ) {
2034 my %res = GetKeysForEncryption( $address );
2035 if ( $res{'info'} && @{ $res{'info'} } == 1 && $res{'info'}[0]{'TrustLevel'} > 0 ) {
2036 # good, one suitable and trusted key
2039 my $user = RT::User->new( $RT::SystemUser );
2040 $user->LoadByEmail( $address );
2041 # it's possible that we have no User record with the email
2042 $user = undef unless $user->id;
2044 if ( my $fpr = UseKeyForEncryption( $address ) ) {
2045 if ( $res{'info'} && @{ $res{'info'} } ) {
2047 grep lc $_->{'Fingerprint'} eq lc $fpr,
2048 grep $_->{'TrustLevel'} > 0,
2054 EmailAddress => $address,
2055 $user? (User => $user) : (),
2058 $issue{'Message'} = "Selected key either is not trusted or doesn't exist anymore."; #loc
2059 push @issues, \%issue;
2064 $prefered_key = $user->PreferredKey if $user;
2065 #XXX: prefered key is not yet implemented...
2070 EmailAddress => $address,
2071 $user? (User => $user) : (),
2075 unless ( $res{'info'} && @{ $res{'info'} } ) {
2077 $issue{'Message'} = "There is no key suitable for encryption."; #loc
2079 elsif ( @{ $res{'info'} } == 1 && !$res{'info'}[0]{'TrustLevel'} ) {
2081 $issue{'Message'} = "There is one suitable key, but trust level is not set."; #loc
2085 $issue{'Message'} = "There are several keys suitable for encryption."; #loc
2087 push @issues, \%issue;
2089 return ($status, @issues);
2092 sub GetPublicKeyInfo {
2093 return GetKeyInfo( shift, 'public', @_ );
2096 sub GetPrivateKeyInfo {
2097 return GetKeyInfo( shift, 'private', @_ );
2101 my %res = GetKeysInfo(@_);
2102 $res{'info'} = $res{'info'}->[0];
2108 my $type = shift || 'public';
2112 return (exit_code => 0) unless $force;
2115 my $gnupg = new GnuPG::Interface;
2116 my %opt = RT->Config->Get('GnuPGOptions');
2117 $opt{'digest-algo'} ||= 'SHA1';
2118 $opt{'with-colons'} = undef; # parseable format
2119 $opt{'fingerprint'} = undef; # show fingerprint
2120 $opt{'fixed-list-mode'} = undef; # don't merge uid with keys
2121 $gnupg->options->hash_init(
2122 _PrepareGnuPGOptions( %opt ),
2124 meta_interactive => 0,
2129 my ($handles, $handle_list) = _make_gpg_handles();
2130 my %handle = %$handle_list;
2133 local $SIG{'CHLD'} = 'DEFAULT';
2134 my $method = $type eq 'private'? 'list_secret_keys': 'list_public_keys';
2135 my $pid = safe_run_child { $gnupg->$method( handles => $handles, $email
2136 ? (command_args => [ "--", $email])
2138 close $handle{'stdin'};
2142 my @info = readline $handle{'stdout'};
2143 close $handle{'stdout'};
2145 $res{'exit_code'} = $?;
2146 foreach ( qw(stderr logger status) ) {
2147 $res{$_} = do { local $/; readline $handle{$_} };
2148 delete $res{$_} unless $res{$_} && $res{$_} =~ /\S/s;
2151 $RT::Logger->debug( $res{'status'} ) if $res{'status'};
2152 $RT::Logger->warning( $res{'stderr'} ) if $res{'stderr'};
2153 $RT::Logger->error( $res{'logger'} ) if $res{'logger'} && $?;
2155 $res{'message'} = $@? $@: "gpg exitted with error code ". ($? >> 8);
2159 @info = ParseKeysInfo( @info );
2160 $res{'info'} = \@info;
2167 my %gpg_opt = RT->Config->Get('GnuPGOptions');
2170 foreach my $line( @lines ) {
2173 ($tag, $line) = split /:/, $line, 2;
2174 if ( $tag eq 'pub' ) {
2177 TrustChar KeyLength Algorithm Key
2178 Created Expire Empty OwnerTrustChar
2179 Empty Empty Capabilities Other
2180 ) } = split /:/, $line, 12;
2182 # workaround gnupg's wierd behaviour, --list-keys command report calculated trust levels
2183 # for any model except 'always', so you can change models and see changes, but not for 'always'
2184 # we try to handle it in a simple way - we set ultimate trust for any key with trust
2185 # level >= 0 if trust model is 'always'
2187 $always_trust = 1 if exists $gpg_opt{'always-trust'};
2188 $always_trust = 1 if exists $gpg_opt{'trust-model'} && $gpg_opt{'trust-model'} eq 'always';
2189 @info{qw(Trust TrustTerse TrustLevel)} =
2190 _ConvertTrustChar( $info{'TrustChar'} );
2191 if ( $always_trust && $info{'TrustLevel'} >= 0 ) {
2192 @info{qw(Trust TrustTerse TrustLevel)} =
2193 _ConvertTrustChar( 'u' );
2196 @info{qw(OwnerTrust OwnerTrustTerse OwnerTrustLevel)} =
2197 _ConvertTrustChar( $info{'OwnerTrustChar'} );
2198 $info{ $_ } = _ParseDate( $info{ $_ } )
2199 foreach qw(Created Expire);
2202 elsif ( $tag eq 'sec' ) {
2205 Empty KeyLength Algorithm Key
2206 Created Expire Empty OwnerTrustChar
2207 Empty Empty Capabilities Other
2208 ) } = split /:/, $line, 12;
2209 @info{qw(OwnerTrust OwnerTrustTerse OwnerTrustLevel)} =
2210 _ConvertTrustChar( $info{'OwnerTrustChar'} );
2211 $info{ $_ } = _ParseDate( $info{ $_ } )
2212 foreach qw(Created Expire);
2215 elsif ( $tag eq 'uid' ) {
2217 @info{ qw(Trust Created Expire String) }
2218 = (split /:/, $line)[0,4,5,8];
2219 $info{ $_ } = _ParseDate( $info{ $_ } )
2220 foreach qw(Created Expire);
2221 push @{ $res[-1]{'User'} ||= [] }, \%info;
2223 elsif ( $tag eq 'fpr' ) {
2224 $res[-1]{'Fingerprint'} = (split /:/, $line, 10)[8];
2234 "The key has been disabled", #loc
2235 "key disabled", #loc
2240 "The key has been revoked", #loc
2245 e => [ "The key has expired", #loc
2250 n => [ "Don't trust this key at all", #loc
2255 #gpupg docs says that '-' and 'q' may safely be treated as the same value
2257 'Unknown (no trust value assigned)', #loc
2262 'Unknown (no trust value assigned)', #loc
2267 'Unknown (this value is new to the system)', #loc
2273 "There is marginal trust in this key", #loc
2278 "The key is fully trusted", #loc
2283 "The key is ultimately trusted", #loc
2289 sub _ConvertTrustChar {
2291 return @{ $verbose{'-'} } unless $value;
2292 $value = substr $value, 0, 1;
2293 return @{ $verbose{ $value } || $verbose{'o'} };
2300 return $value unless $value;
2303 my $obj = RT::Date->new( $RT::SystemUser );
2305 if ( $value =~ /^\d+$/ ) {
2306 $obj->Set( Value => $value );
2308 $obj->Set( Format => 'unknown', Value => $value, Timezone => 'utc' );
2316 my $gnupg = new GnuPG::Interface;
2317 my %opt = RT->Config->Get('GnuPGOptions');
2318 $gnupg->options->hash_init(
2319 _PrepareGnuPGOptions( %opt ),
2320 meta_interactive => 0,
2323 my ($handles, $handle_list) = _make_gpg_handles();
2324 my %handle = %$handle_list;
2327 local $SIG{'CHLD'} = 'DEFAULT';
2328 local @ENV{'LANG', 'LC_ALL'} = ('C', 'C');
2329 my $pid = safe_run_child { $gnupg->wrap_call(
2330 handles => $handles,
2331 commands => ['--delete-secret-and-public-key'],
2332 command_args => ["--", $key],
2334 close $handle{'stdin'};
2335 while ( my $str = readline $handle{'status'} ) {
2336 if ( $str =~ /^\[GNUPG:\]\s*GET_BOOL delete_key\..*/ ) {
2337 print { $handle{'command'} } "y\n";
2343 close $handle{'stdout'};
2346 $res{'exit_code'} = $?;
2347 foreach ( qw(stderr logger status) ) {
2348 $res{$_} = do { local $/; readline $handle{$_} };
2349 delete $res{$_} unless $res{$_} && $res{$_} =~ /\S/s;
2352 $RT::Logger->debug( $res{'status'} ) if $res{'status'};
2353 $RT::Logger->warning( $res{'stderr'} ) if $res{'stderr'};
2354 $RT::Logger->error( $res{'logger'} ) if $res{'logger'} && $?;
2355 if ( $err || $res{'exit_code'} ) {
2356 $res{'message'} = $err? $err : "gpg exitted with error code ". ($res{'exit_code'} >> 8);
2364 my $gnupg = new GnuPG::Interface;
2365 my %opt = RT->Config->Get('GnuPGOptions');
2366 $gnupg->options->hash_init(
2367 _PrepareGnuPGOptions( %opt ),
2368 meta_interactive => 0,
2371 my ($handles, $handle_list) = _make_gpg_handles();
2372 my %handle = %$handle_list;
2375 local $SIG{'CHLD'} = 'DEFAULT';
2376 local @ENV{'LANG', 'LC_ALL'} = ('C', 'C');
2377 my $pid = safe_run_child { $gnupg->wrap_call(
2378 handles => $handles,
2379 commands => ['--import'],
2381 print { $handle{'stdin'} } $key;
2382 close $handle{'stdin'};
2386 close $handle{'stdout'};
2389 $res{'exit_code'} = $?;
2390 foreach ( qw(stderr logger status) ) {
2391 $res{$_} = do { local $/; readline $handle{$_} };
2392 delete $res{$_} unless $res{$_} && $res{$_} =~ /\S/s;
2395 $RT::Logger->debug( $res{'status'} ) if $res{'status'};
2396 $RT::Logger->warning( $res{'stderr'} ) if $res{'stderr'};
2397 $RT::Logger->error( $res{'logger'} ) if $res{'logger'} && $?;
2398 if ( $err || $res{'exit_code'} ) {
2399 $res{'message'} = $err? $err : "gpg exitted with error code ". ($res{'exit_code'} >> 8);
2406 Signs a small message with the key, to make sure the key exists and
2407 we have a useable passphrase. The first argument MUST be a key identifier
2408 of the signer: either email address, key id or finger print.
2410 Returns a true value if all went well.
2417 my $mime = MIME::Entity->build(
2418 Type => "text/plain",
2419 From => 'nobody@localhost',
2420 To => 'nobody@localhost',
2421 Subject => "dry sign",
2425 my %res = SignEncrypt(
2432 return $res{exit_code} == 0;
2439 This routine returns true if RT's GnuPG support is configured and working
2440 properly (and false otherwise).
2447 my $gnupg = new GnuPG::Interface;
2448 my %opt = RT->Config->Get('GnuPGOptions');
2449 $gnupg->options->hash_init(
2450 _PrepareGnuPGOptions( %opt ),
2452 meta_interactive => 0,
2455 my ($handles, $handle_list) = _make_gpg_handles();
2456 my %handle = %$handle_list;
2460 local $SIG{'CHLD'} = 'DEFAULT';
2461 my $pid = safe_run_child { $gnupg->wrap_call( commands => ['--version' ], handles => $handles ) };
2462 close $handle{'stdin'};
2467 "Probe for GPG failed."
2468 ." Couldn't run `gpg --version`: ". $@
2473 # on some systems gpg exits with code 2, but still 100% functional,
2474 # it's general error system error or incorrect command, command is correct,
2475 # but there is no way to get actuall error
2476 if ( $? && ($? >> 8) != 2 ) {
2477 my $msg = "Probe for GPG failed."
2478 ." Process exitted with code ". ($? >> 8)
2479 . ($? & 127 ? (" as recieved signal ". ($? & 127)) : '')
2481 foreach ( qw(stderr logger status) ) {
2482 my $tmp = do { local $/; readline $handle{$_} };
2483 next unless $tmp && $tmp =~ /\S/s;
2485 $msg .= "\n$_:\n$tmp\n";
2487 $RT::Logger->debug( $msg );
2494 sub _make_gpg_handles {
2495 my %handle_map = (@_);
2496 $handle_map{$_} = IO::Handle->new
2497 foreach grep !defined $handle_map{$_},
2498 qw(stdin stdout stderr logger status command);
2500 my $handles = GnuPG::Handles->new(%handle_map);
2501 return ($handles, \%handle_map);
2504 RT::Base->_ImportOverlays();
2506 # helper package to avoid using temp file
2507 package IO::Handle::CRLF;
2509 use base qw(IO::Handle);
2512 my ($self, @args) = (@_);
2513 s/\r*\n/\x0D\x0A/g foreach @args;
2514 return $self->SUPER::print( @args );