1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2015 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 }}}
53 package RT::Crypt::SMIME;
55 use Role::Basic 'with';
56 with 'RT::Crypt::Role';
60 use IPC::Run3 0.036 'run3';
61 use RT::Util 'safe_run_child';
63 use String::ShellQuote 'shell_quote';
67 RT::Crypt::SMIME - encrypt/decrypt and sign/verify email messages with the SMIME
71 You should start from reading L<RT::Crypt>.
77 OpenSSL => '/usr/bin/openssl',
78 Keyring => '/opt/rt4/var/data/smime',
79 CAPath => '/opt/rt4/var/data/smime/signing-ca.pem',
81 'queue.address@example.com' => 'passphrase',
88 Path to openssl executable.
92 Path to directory with keys and certificates for queues. Key and
93 certificates should be stored in a PEM file named, e.g.,
94 F<email.address@example.com.pem>. See L</Keyring configuration>.
98 C<CAPath> should be set to either a PEM-formatted certificate of a
99 single signing certificate authority, or a directory of such (including
100 hash symlinks as created by the openssl tool C<c_rehash>). Only SMIME
101 certificates signed by these certificate authorities will be treated as
102 valid signatures. If left unset (and C<AcceptUntrustedCAs> is unset, as
103 it is by default), no signatures will be marked as valid!
105 =head3 AcceptUntrustedCAs
107 Allows arbitrary SMIME certificates, no matter their signing entities.
108 Such mails will be marked as untrusted, but signed; C<CAPath> will be
109 used to mark which mails are signed by trusted certificate authorities.
110 This configuration is generally insecure, as it allows the possibility
111 of accepting forged mail signed by an untrusted certificate authority.
113 Setting this option also allows encryption to users with certificates
114 created by untrusted CAs.
118 C<Passphrase> may be set to a scalar (to use for all keys), an anonymous
119 function, or a hash (to look up by address). If the hash is used, the
120 '' key is used as a default.
122 =head2 Keyring configuration
124 RT looks for keys in the directory configured in the L</Keyring> option
125 of the L<RT_Config/%SMIME>. While public certificates are also stored
126 on users, private SSL keys are only loaded from disk. Keys and
127 certificates should be concatenated, in in PEM format, in files named
128 C<email.address@example.com.pem>, for example.
130 These files need be readable by the web server user which is running
131 RT's web interface; however, if you are running cronjobs or other
132 utilities that access RT directly via API, and may generate
133 encrypted/signed notifications, then the users you execute these scripts
134 under must have access too.
136 The keyring on disk will be checked before the user with the email
137 address is examined. If the file exists, it will be used in preference
138 to the certificate on the user.
143 state $cache = RT->Config->Get('SMIME')->{'OpenSSL'};
144 $cache = $_[1] if @_ > 1;
150 my $bin = $self->OpenSSLPath();
152 $RT::Logger->warning(
153 "No openssl path set; SMIME support has been disabled. ".
154 "Check the 'OpenSSL' configuration in %OpenSSL");
159 unless (-f $bin and -x _) {
160 $RT::Logger->warning(
161 "Invalid openssl path $bin; SMIME support has been disabled. ".
162 "Check the 'OpenSSL' configuration in %OpenSSL");
166 local $ENV{PATH} = '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'
167 unless defined $ENV{PATH};
168 my $path = File::Which::which( $bin );
170 $RT::Logger->warning(
171 "Can't find openssl binary '$bin' in PATH ($ENV{PATH}); SMIME support has been disabled. ".
172 "You may need to specify a full path to opensssl via the 'OpenSSL' configuration in %OpenSSL");
175 $self->OpenSSLPath( $bin = $path );
179 my ($buf, $err) = ('', '');
181 local $SIG{'CHLD'} = 'DEFAULT';
182 safe_run_child { run3( [$bin, "list-standard-commands"],
188 $RT::Logger->warning(
189 "RT's SMIME libraries couldn't successfully execute openssl.".
190 " SMIME support has been disabled") ;
192 } elsif ($buf !~ /\bsmime\b/) {
193 $RT::Logger->warning(
194 "openssl does not include smime support.".
195 " SMIME support has been disabled");
218 my $entity = $args{'Entity'};
220 if ( $args{'Encrypt'} ) {
222 $args{'Recipients'} = [
223 grep !$seen{$_}++, map $_->address, map Email::Address->parse(Encode::decode("UTF-8",$_)),
224 grep defined && length, map $entity->head->get($_), qw(To Cc Bcc)
228 $entity->make_multipart('mixed', Force => 1);
229 my ($buf, %res) = $self->_SignEncrypt(
231 Content => \$entity->parts(0)->stringify,
234 $entity->make_singlepart;
238 my $tmpdir = File::Temp::tempdir( TMPDIR => 1, CLEANUP => 1 );
239 my $parser = MIME::Parser->new();
240 $parser->output_dir($tmpdir);
241 my $newmime = $parser->parse_data($$buf);
243 # Work around https://rt.cpan.org/Public/Bug/Display.html?id=87835
244 for my $part (grep {$_->is_multipart and $_->preamble and @{$_->preamble}} $newmime->parts_DFS) {
245 $part->preamble->[-1] .= "\n"
246 if $part->preamble->[-1] =~ /\r$/;
249 $entity->parts([$newmime]);
250 $entity->make_singlepart;
255 sub SignEncryptContent {
262 my ($buf, %res) = $self->_SignEncrypt(%args);
263 ${ $args{'Content'} } = $$buf if $buf;
282 my %res = (exit_code => 0, status => '');
285 if ( $args{'Encrypt'} ) {
286 my @addresses = @{ $args{'Recipients'} };
288 foreach my $address ( @addresses ) {
289 $RT::Logger->debug( "Considering encrypting message to " . $address );
291 my %key_info = $self->GetKeysInfo( Key => $address );
292 unless ( defined $key_info{'info'} ) {
293 $res{'exit_code'} = 1;
294 my $reason = 'Key not found';
295 $res{'status'} .= $self->FormatStatus({
296 Operation => "RecipientsCheck", Status => "ERROR",
297 Message => "Recipient '$address' is unusable, the reason is '$reason'",
298 Recipient => $address,
304 if ( not $key_info{'info'}[0]{'Expire'} ) {
305 # we continue here as it's most probably a problem with the key,
306 # so later during encryption we'll get verbose errors
308 "Trying to send an encrypted message to ". $address
309 .", but we couldn't get expiration date of the key."
312 elsif ( $key_info{'info'}[0]{'Expire'}->Diff( time ) < 0 ) {
313 $res{'exit_code'} = 1;
314 my $reason = 'Key expired';
315 $res{'status'} .= $self->FormatStatus({
316 Operation => "RecipientsCheck", Status => "ERROR",
317 Message => "Recipient '$address' is unusable, the reason is '$reason'",
318 Recipient => $address,
323 push @keys, $key_info{'info'}[0]{'Content'};
326 return (undef, %res) if $res{'exit_code'};
328 my $opts = RT->Config->Get('SMIME');
331 if ( $args{'Sign'} ) {
332 my $file = $self->CheckKeyring( Key => $args{'Signer'} );
334 $res{'status'} .= $self->FormatStatus({
335 Operation => "KeyCheck", Status => "MISSING",
336 Message => "Secret key for $args{Signer} is not available",
337 Key => $args{Signer},
341 return (undef, %res);
343 $args{'Passphrase'} = $self->GetPassphrase( Address => $args{'Signer'} )
344 unless defined $args{'Passphrase'};
347 $self->OpenSSLPath, qw(smime -sign),
350 (defined $args{'Passphrase'} && length $args{'Passphrase'})
351 ? (qw(-passin env:SMIME_PASS))
355 if ( $args{'Encrypt'} ) {
356 foreach my $key ( @keys ) {
357 my $key_file = File::Temp->new;
358 print $key_file $key;
363 $self->OpenSSLPath, qw(smime -encrypt -des3),
364 map { $_->filename } @keys
368 my $buf = ${ $args{'Content'} };
369 for my $command (@commands) {
370 my ($out, $err) = ('', '');
372 local $ENV{'SMIME_PASS'} = $args{'Passphrase'};
373 local $SIG{'CHLD'} = 'DEFAULT';
374 safe_run_child { run3(
381 $RT::Logger->debug( "openssl stderr: " . $err ) if length $err;
383 # copy output from the first command to the second command
384 # similar to the pipe we used to use to pipe signing -> encryption
385 # Using the pipe forced us to invoke the shell, this avoids any use of shell.
390 $res{'status'} .= $self->FormatStatus({
391 Operation => "Sign", Status => "DONE",
392 Message => "Signed message",
394 $res{'status'} .= $self->FormatStatus({
395 Operation => "Encrypt", Status => "DONE",
396 Message => "Data has been encrypted",
397 }) if $args{'Encrypt'};
400 return (\$buf, %res);
405 my %args = ( Info => undef, @_ );
408 my $item = $args{'Info'};
409 if ( $item->{'Type'} eq 'signed' ) {
410 %res = $self->Verify( %$item );
411 } elsif ( $item->{'Type'} eq 'encrypted' ) {
412 %res = $self->Decrypt( %args, %$item );
414 die "Unknown type '". $item->{'Type'} ."' of protected item";
417 return (%res, status_on => $item->{'Data'});
422 my %args = (Data => undef, @_ );
424 my $msg = $args{'Data'}->as_string;
428 my $keyfh = File::Temp->new;
430 local $SIG{CHLD} = 'DEFAULT';
432 $self->OpenSSLPath, qw(smime -verify -noverify),
433 '-signer', $keyfh->filename,
435 safe_run_child { run3( $cmd, \$msg, \$buf, \$res{'stderr'} ) };
436 $res{'exit_code'} = $?;
438 if ( $res{'exit_code'} ) {
439 if ($res{stderr} =~ /(signature|digest) failure/) {
440 $res{'message'} = "Validation failed";
441 $res{'status'} = $self->FormatStatus({
442 Operation => "Verify", Status => "BAD",
443 Message => "The signature did not verify",
446 $res{'message'} = "openssl exited with error code ". ($? >> 8)
447 ." and error: $res{stderr}";
448 $res{'status'} = $self->FormatStatus({
449 Operation => "Verify", Status => "ERROR",
450 Message => "There was an error verifying: $res{stderr}",
452 $RT::Logger->error($res{'message'});
458 if ( my $key = do { $keyfh->seek(0, 0); local $/; readline $keyfh } ) {{
459 my %info = $self->GetCertificateInfo( Certificate => $key );
461 $signer = $info{info}[0];
462 last unless $signer and $signer->{User}[0]{String};
464 unless ( $info{info}[0]{TrustLevel} > 0 or RT->Config->Get('SMIME')->{AcceptUntrustedCAs}) {
465 # We don't trust it; give it the finger
467 $res{'message'} = "Validation failed";
468 $res{'status'} = $self->FormatStatus({
469 Operation => "Verify", Status => "BAD",
470 Message => "The signing CA was not trusted",
471 UserString => $signer->{User}[0]{String},
477 my $user = RT::User->new( $RT::SystemUser );
478 $user->LoadOrCreateByEmail( $signer->{User}[0]{String} );
479 my $current_key = $user->SMIMECertificate;
480 last if $current_key && $current_key eq $key;
482 # Never over-write existing keys with untrusted ones.
483 last if $current_key and not $info{info}[0]{TrustLevel} > 0;
485 my ($status, $msg) = $user->SetSMIMECertificate( $key );
486 $RT::Logger->error("Couldn't set SMIME certificate for user #". $user->id .": $msg")
490 my $res_entity = _extract_msg_from_buf( \$buf );
491 unless ( $res_entity ) {
492 $res{'exit_code'} = 1;
493 $res{'message'} = "verified message, but couldn't parse result";
494 $res{'status'} = $self->FormatStatus({
495 Operation => "Verify", Status => "DONE",
496 Message => "The signature is good, unknown signer",
502 $res_entity->make_multipart( 'mixed', Force => 1 );
504 $args{'Data'}->make_multipart( 'mixed', Force => 1 );
505 $args{'Data'}->parts([ $res_entity->parts ]);
506 $args{'Data'}->make_singlepart;
508 $res{'status'} = $self->FormatStatus({
509 Operation => "Verify", Status => "DONE",
510 Message => "The signature is good, signed by ".$signer->{User}[0]{String}.", trust is ".$signer->{TrustTerse},
511 UserString => $signer->{User}[0]{String},
512 Trust => uc($signer->{TrustTerse}),
520 my %args = (Data => undef, Queue => undef, @_ );
522 my $msg = $args{'Data'}->as_string;
524 push @{ $args{'Recipients'} ||= [] },
525 $args{'Queue'}->CorrespondAddress, RT->Config->Get('CorrespondAddress'),
526 $args{'Queue'}->CommentAddress, RT->Config->Get('CommentAddress')
529 my ($buf, %res) = $self->_Decrypt( %args, Content => \$args{'Data'}->as_string );
530 return %res unless $buf;
532 my $res_entity = _extract_msg_from_buf( $buf );
533 $res_entity->make_multipart( 'mixed', Force => 1 );
535 # Work around https://rt.cpan.org/Public/Bug/Display.html?id=87835
536 for my $part (grep {$_->is_multipart and $_->preamble and @{$_->preamble}} $res_entity->parts_DFS) {
537 $part->preamble->[-1] .= "\n"
538 if $part->preamble->[-1] =~ /\r$/;
541 $args{'Data'}->make_multipart( 'mixed', Force => 1 );
542 $args{'Data'}->parts([ $res_entity->parts ]);
543 $args{'Data'}->make_singlepart;
555 my ($buf, %res) = $self->_Decrypt( %args );
556 ${ $args{'Content'} } = $$buf if $buf;
562 my %args = (Content => undef, @_ );
566 grep !$seen{lc $_}++, map $_->address, map Email::Address->parse($_),
567 grep length && defined, @{$args{'Recipients'}};
569 my ($buf, $encrypted_to, %res);
571 foreach my $address ( @addresses ) {
572 my $file = $self->CheckKeyring( Key => $address );
574 my $keyring = RT->Config->Get('SMIME')->{'Keyring'};
575 $RT::Logger->debug("No key found for $address in $keyring directory");
579 local $ENV{SMIME_PASS} = $self->GetPassphrase( Address => $address );
580 local $SIG{CHLD} = 'DEFAULT';
585 (defined $ENV{'SMIME_PASS'} && length $ENV{'SMIME_PASS'})
586 ? (qw(-passin env:SMIME_PASS))
589 safe_run_child { run3( $cmd, $args{'Content'}, \$buf, \$res{'stderr'} ) };
591 $encrypted_to = $address;
592 $RT::Logger->debug("Message encrypted for $encrypted_to");
596 if ( index($res{'stderr'}, 'no recipient matches key') >= 0 ) {
597 $RT::Logger->debug("Although we have a key for $address, it is not the one that encrypted this message");
601 $res{'exit_code'} = $?;
602 $res{'message'} = "openssl exited with error code ". ($? >> 8)
603 ." and error: $res{stderr}";
604 $RT::Logger->error( $res{'message'} );
605 $res{'status'} = $self->FormatStatus({
606 Operation => 'Decrypt', Status => 'ERROR',
607 Message => 'Decryption failed',
608 EncryptedTo => $address,
610 return (undef, %res);
612 unless ( $encrypted_to ) {
613 $RT::Logger->error("Couldn't find SMIME key for addresses: ". join ', ', @addresses);
614 $res{'exit_code'} = 1;
615 $res{'status'} = $self->FormatStatus({
616 Operation => 'KeyCheck',
618 Message => "Secret key is not available",
621 return (undef, %res);
624 $res{'status'} = $self->FormatStatus({
625 Operation => 'Decrypt', Status => 'DONE',
626 Message => 'Decryption process succeeded',
627 EncryptedTo => $encrypted_to,
630 return (\$buf, %res);
638 foreach ( @status ) {
639 while ( my ($k, $v) = each %$_ ) {
640 $res .= "[SMIME:]". $k .": ". $v ."\n";
642 $res .= "[SMIME:]\n";
651 return () unless $status;
653 my @status = split /\s*(?:\[SMIME:\]\s*){2}/, $status;
654 foreach my $block ( grep length, @status ) {
656 $block = { map { s/^\s+//; s/\s+$//; $_ } map split(/:/, $_, 2), split /\s*\[SMIME:\]/, $block };
658 foreach my $block ( grep $_->{'EncryptedTo'}, @status ) {
659 $block->{'EncryptedTo'} = [{
660 EmailAddress => $block->{'EncryptedTo'},
667 sub _extract_msg_from_buf {
669 my $rtparser = RT::EmailParser->new();
670 my $parser = MIME::Parser->new();
671 $rtparser->_SetupMIMEParser($parser);
672 $parser->decode_bodies(0);
673 $parser->output_to_core(1);
674 unless ( $rtparser->{'entity'} = $parser->parse_data($$buf) ) {
675 $RT::Logger->crit("Couldn't parse MIME stream and extract the submessages");
677 # Try again, this time without extracting nested messages
678 $parser->extract_nested_messages(0);
679 unless ( $rtparser->{'entity'} = $parser->parse_data($$buf) ) {
680 $RT::Logger->crit("couldn't parse MIME stream");
684 return $rtparser->Entity;
687 sub FindScatteredParts { return () }
689 sub CheckIfProtected {
691 my %args = ( Entity => undef, @_ );
693 my $entity = $args{'Entity'};
695 my $type = $entity->effective_type;
696 if ( $type =~ m{^application/(?:x-)?pkcs7-mime$} || $type eq 'application/octet-stream' ) {
697 # RFC3851 ch.3.9 variant 1 and 3
701 my $smime_type = $entity->head->mime_attr('Content-Type.smime-type');
702 if ( $smime_type ) { # it's optional according to RFC3851
703 if ( $smime_type eq 'enveloped-data' ) {
704 $security_type = 'encrypted';
706 elsif ( $smime_type eq 'signed-data' ) {
707 $security_type = 'signed';
709 elsif ( $smime_type eq 'certs-only' ) {
710 $security_type = 'certificate management';
712 elsif ( $smime_type eq 'compressed-data' ) {
713 $security_type = 'compressed';
716 $security_type = $smime_type;
720 unless ( $security_type ) {
721 my $fname = $entity->head->recommended_filename || '';
722 if ( $fname =~ /\.p7([czsm])$/ ) {
724 if ( $type_char eq 'm' ) {
726 # it can be both encrypted and signed
727 $security_type = 'encrypted';
729 elsif ( $type_char eq 's' ) {
730 # RFC3851, ch3.4.3, multipart/signed, XXX we should never be here
731 # unless message is changed by some gateway
732 $security_type = 'signed';
734 elsif ( $type_char eq 'c' ) {
736 $security_type = 'certificate management';
738 elsif ( $type_char eq 'z' ) {
740 $security_type = 'compressed';
744 return () unless $security_type;
747 Type => $security_type,
752 if ( $security_type eq 'encrypted' ) {
753 my $top = $args{'TopEntity'}->head;
754 $res{'Recipients'} = [map {Encode::decode("UTF-8", $_)}
755 grep defined && length, map $top->get($_), 'To', 'Cc'];
760 elsif ( $type eq 'multipart/signed' ) {
761 # RFC3156, multipart/signed
762 # RFC3851, ch.3.9 variant 2
764 unless ( $entity->parts == 2 ) {
765 $RT::Logger->error( "Encrypted or signed entity must has two subparts. Skipped" );
769 my $protocol = $entity->head->mime_attr( 'Content-Type.protocol' );
770 unless ( $protocol ) {
771 $RT::Logger->error( "Entity is '$type', but has no protocol defined. Skipped" );
775 unless ( $protocol =~ m{^application/(x-)?pkcs7-signature$} ) {
776 $RT::Logger->info( "Skipping protocol '$protocol', only 'application/x-pkcs7-signature' is supported" );
779 $RT::Logger->debug("Found part signed according to RFC3156");
789 sub GetKeysForEncryption {
791 my %args = (Recipient => undef, @_);
792 return $self->GetKeysInfo( Key => delete $args{'Recipient'}, %args, Type => 'public' );
795 sub GetKeysForSigning {
797 my %args = (Signer => undef, @_);
798 return $self->GetKeysInfo( Key => delete $args{'Signer'}, %args, Type => 'private' );
810 my $email = $args{'Key'};
812 return (exit_code => 0); # unless $args{'Force'};
815 my $key = $self->GetKeyContent( %args );
816 return (exit_code => 0) unless $key;
818 return $self->GetCertificateInfo( Certificate => $key );
823 my %args = ( Key => undef, @_ );
826 if ( my $file = $self->CheckKeyring( %args ) ) {
827 open my $fh, '<:raw', $file
828 or die "Couldn't open file '$file': $!";
829 $key = do { local $/; readline $fh };
833 my $user = RT::User->new( RT->SystemUser );
834 $user->LoadByEmail( $args{'Key'} );
835 $key = $user->SMIMECertificate if $user->id;
846 my $keyring = RT->Config->Get('SMIME')->{'Keyring'};
847 return undef unless $keyring;
849 my $file = File::Spec->catfile( $keyring, $args{'Key'} .'.pem' );
850 return undef unless -f $file;
855 sub GetCertificateInfo {
858 Certificate => undef,
862 if ($args{Certificate} =~ /^-----BEGIN \s+ CERTIFICATE----- \s* $
864 ^-----END \s+ CERTIFICATE----- \s* $/smx) {
865 $args{Certificate} = MIME::Base64::decode_base64($1);
868 my $cert = Crypt::X509->new( cert => $args{Certificate} );
869 return ( exit_code => 1, stderr => $cert->error ) if $cert->error;
872 Country => 'country',
873 StateOrProvince => 'state',
874 Organization => 'org',
875 OrganizationUnit => 'ou',
877 EmailAddress => 'email',
879 my $canonicalize = sub {
882 for (keys %USER_MAP) {
883 my $method = $type . "_" . $USER_MAP{$_};
884 $data{$_} = $cert->$method if $cert->can($method);
886 $data{String} = Email::Address->new( @data{'Name', 'EmailAddress'} )->format
887 if $data{EmailAddress};
891 my $PEM = "-----BEGIN CERTIFICATE-----\n"
892 . MIME::Base64::encode_base64( $args{Certificate} )
893 . "-----END CERTIFICATE-----\n";
899 Fingerprint => Digest::SHA::sha1_hex($args{Certificate}),
900 'Serial Number' => $cert->serial,
901 Created => $self->ParseDate( $cert->not_before ),
902 Expire => $self->ParseDate( $cert->not_after ),
903 Version => sprintf("%d (0x%x)",hex($cert->version || 0)+1, hex($cert->version || 0)),
904 Issuer => [ $canonicalize->( 'issuer' ) ],
905 User => [ $canonicalize->( 'subject' ) ],
911 my $ca = RT->Config->Get('SMIME')->{'CAPath'};
915 @ca_verify = ('-CApath', $ca);
917 @ca_verify = ('-CAfile', $ca);
920 local $SIG{CHLD} = 'DEFAULT';
923 'verify', @ca_verify,
926 safe_run_child { run3( $cmd, \$PEM, \$buf, \$res{stderr} ) };
928 if ($buf =~ /^stdin: OK$/) {
929 $res{info}[0]{Trust} = "Signed by trusted CA $res{info}[0]{Issuer}[0]{String}";
930 $res{info}[0]{TrustTerse} = "full";
931 $res{info}[0]{TrustLevel} = 2;
932 } elsif ($? == 0 or ($? >> 8) == 2) {
933 $res{info}[0]{Trust} = "UNTRUSTED signing CA $res{info}[0]{Issuer}[0]{String}";
934 $res{info}[0]{TrustTerse} = "none";
935 $res{info}[0]{TrustLevel} = -1;
937 $res{exit_code} = $?;
938 $res{message} = "openssl exited with error code ". ($? >> 8)
940 $res{info}[0]{Trust} = "unknown (openssl failed)";
941 $res{info}[0]{TrustTerse} = "unknown";
942 $res{info}[0]{TrustLevel} = 0;
945 $res{info}[0]{Trust} = "unknown (no CAPath set)";
946 $res{info}[0]{TrustTerse} = "unknown";
947 $res{info}[0]{TrustLevel} = 0;
950 $res{info}[0]{Formatted} = $res{info}[0]{User}[0]{String}
951 . " (issued by $res{info}[0]{Issuer}[0]{String})";