1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2018 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"],
187 if ($err && $err =~ /Invalid command/) {
188 ($buf, $err) = ('', '');
189 safe_run_child { run3( [$bin, "list", "-commands"],
196 $RT::Logger->warning(
197 "RT's SMIME libraries couldn't successfully execute openssl.".
198 " SMIME support has been disabled") ;
200 } elsif ($buf !~ /\bsmime\b/) {
201 $RT::Logger->warning(
202 "openssl does not include smime support.".
203 " SMIME support has been disabled");
226 my $entity = $args{'Entity'};
228 if ( $args{'Encrypt'} ) {
230 $args{'Recipients'} = [
231 grep !$seen{$_}++, map $_->address, map Email::Address->parse(Encode::decode("UTF-8",$_)),
232 grep defined && length, map $entity->head->get($_), qw(To Cc Bcc)
236 $entity->make_multipart('mixed', Force => 1);
237 my ($buf, %res) = $self->_SignEncrypt(
239 Content => \$entity->parts(0)->stringify,
242 $entity->make_singlepart;
246 my $tmpdir = File::Temp::tempdir( TMPDIR => 1, CLEANUP => 1 );
247 my $parser = MIME::Parser->new();
248 $parser->output_dir($tmpdir);
249 my $newmime = $parser->parse_data($$buf);
251 # Work around https://rt.cpan.org/Public/Bug/Display.html?id=87835
252 for my $part (grep {$_->is_multipart and $_->preamble and @{$_->preamble}} $newmime->parts_DFS) {
253 $part->preamble->[-1] .= "\n"
254 if $part->preamble->[-1] =~ /\r$/;
257 $entity->parts([$newmime]);
258 $entity->make_singlepart;
263 sub SignEncryptContent {
270 my ($buf, %res) = $self->_SignEncrypt(%args);
271 ${ $args{'Content'} } = $$buf if $buf;
290 my %res = (exit_code => 0, status => '');
293 if ( $args{'Encrypt'} ) {
294 my @addresses = @{ $args{'Recipients'} };
296 foreach my $address ( @addresses ) {
297 $RT::Logger->debug( "Considering encrypting message to " . $address );
299 my %key_info = $self->GetKeysInfo( Key => $address );
300 unless ( defined $key_info{'info'} ) {
301 $res{'exit_code'} = 1;
302 my $reason = 'Key not found';
303 $res{'status'} .= $self->FormatStatus({
304 Operation => "RecipientsCheck", Status => "ERROR",
305 Message => "Recipient '$address' is unusable, the reason is '$reason'",
306 Recipient => $address,
312 if ( not $key_info{'info'}[0]{'Expire'} ) {
313 # we continue here as it's most probably a problem with the key,
314 # so later during encryption we'll get verbose errors
316 "Trying to send an encrypted message to ". $address
317 .", but we couldn't get expiration date of the key."
320 elsif ( $key_info{'info'}[0]{'Expire'}->Diff( time ) < 0 ) {
321 $res{'exit_code'} = 1;
322 my $reason = 'Key expired';
323 $res{'status'} .= $self->FormatStatus({
324 Operation => "RecipientsCheck", Status => "ERROR",
325 Message => "Recipient '$address' is unusable, the reason is '$reason'",
326 Recipient => $address,
331 push @keys, $key_info{'info'}[0]{'Content'};
334 return (undef, %res) if $res{'exit_code'};
336 my $opts = RT->Config->Get('SMIME');
339 if ( $args{'Sign'} ) {
340 my $file = $self->CheckKeyring( Key => $args{'Signer'} );
342 $res{'status'} .= $self->FormatStatus({
343 Operation => "KeyCheck", Status => "MISSING",
344 Message => "Secret key for $args{Signer} is not available",
345 Key => $args{Signer},
349 return (undef, %res);
351 $args{'Passphrase'} = $self->GetPassphrase( Address => $args{'Signer'} )
352 unless defined $args{'Passphrase'};
355 $self->OpenSSLPath, qw(smime -sign),
358 (defined $args{'Passphrase'} && length $args{'Passphrase'})
359 ? (qw(-passin env:SMIME_PASS))
363 if ( $args{'Encrypt'} ) {
364 foreach my $key ( @keys ) {
365 my $key_file = File::Temp->new;
366 print $key_file $key;
371 $self->OpenSSLPath, qw(smime -encrypt -des3),
372 map { $_->filename } @keys
376 my $buf = ${ $args{'Content'} };
377 for my $command (@commands) {
378 my ($out, $err) = ('', '');
380 local $ENV{'SMIME_PASS'} = $args{'Passphrase'};
381 local $SIG{'CHLD'} = 'DEFAULT';
382 safe_run_child { run3(
389 $RT::Logger->debug( "openssl stderr: " . $err ) if length $err;
391 # copy output from the first command to the second command
392 # similar to the pipe we used to use to pipe signing -> encryption
393 # Using the pipe forced us to invoke the shell, this avoids any use of shell.
398 $res{'status'} .= $self->FormatStatus({
399 Operation => "Sign", Status => "DONE",
400 Message => "Signed message",
402 $res{'status'} .= $self->FormatStatus({
403 Operation => "Encrypt", Status => "DONE",
404 Message => "Data has been encrypted",
405 }) if $args{'Encrypt'};
408 return (\$buf, %res);
413 my %args = ( Info => undef, @_ );
416 my $item = $args{'Info'};
417 if ( $item->{'Type'} eq 'signed' ) {
418 %res = $self->Verify( %$item );
419 } elsif ( $item->{'Type'} eq 'encrypted' ) {
420 %res = $self->Decrypt( %args, %$item );
422 die "Unknown type '". $item->{'Type'} ."' of protected item";
425 return (%res, status_on => $item->{'Data'});
430 my %args = (Data => undef, @_ );
432 my $msg = $args{'Data'}->as_string;
436 my $keyfh = File::Temp->new;
438 local $SIG{CHLD} = 'DEFAULT';
440 $self->OpenSSLPath, qw(smime -verify -noverify),
441 '-signer', $keyfh->filename,
443 safe_run_child { run3( $cmd, \$msg, \$buf, \$res{'stderr'} ) };
444 $res{'exit_code'} = $?;
446 if ( $res{'exit_code'} ) {
447 if ($res{stderr} =~ /(signature|digest) failure/) {
448 $res{'message'} = "Validation failed";
449 $res{'status'} = $self->FormatStatus({
450 Operation => "Verify", Status => "BAD",
451 Message => "The signature did not verify",
454 $res{'message'} = "openssl exited with error code ". ($? >> 8)
455 ." and error: $res{stderr}";
456 $res{'status'} = $self->FormatStatus({
457 Operation => "Verify", Status => "ERROR",
458 Message => "There was an error verifying: $res{stderr}",
460 $RT::Logger->error($res{'message'});
466 if ( my $key = do { $keyfh->seek(0, 0); local $/; readline $keyfh } ) {{
467 my %info = $self->GetCertificateInfo( Certificate => $key );
469 $signer = $info{info}[0];
470 last unless $signer and $signer->{User}[0]{String};
472 unless ( $info{info}[0]{TrustLevel} > 0 or RT->Config->Get('SMIME')->{AcceptUntrustedCAs}) {
473 # We don't trust it; give it the finger
475 $res{'message'} = "Validation failed";
476 $res{'status'} = $self->FormatStatus({
477 Operation => "Verify", Status => "BAD",
478 Message => "The signing CA was not trusted",
479 UserString => $signer->{User}[0]{String},
485 my $user = RT::User->new( $RT::SystemUser );
486 $user->LoadOrCreateByEmail( $signer->{User}[0]{String} );
487 my $current_key = $user->SMIMECertificate;
488 last if $current_key && $current_key eq $key;
490 # Never over-write existing keys with untrusted ones.
491 last if $current_key and not $info{info}[0]{TrustLevel} > 0;
493 my ($status, $msg) = $user->SetSMIMECertificate( $key );
494 $RT::Logger->error("Couldn't set SMIME certificate for user #". $user->id .": $msg")
498 my $res_entity = _extract_msg_from_buf( \$buf );
499 unless ( $res_entity ) {
500 $res{'exit_code'} = 1;
501 $res{'message'} = "verified message, but couldn't parse result";
502 $res{'status'} = $self->FormatStatus({
503 Operation => "Verify", Status => "DONE",
504 Message => "The signature is good, unknown signer",
510 $res_entity->make_multipart( 'mixed', Force => 1 );
512 $args{'Data'}->make_multipart( 'mixed', Force => 1 );
513 $args{'Data'}->parts([ $res_entity->parts ]);
514 $args{'Data'}->make_singlepart;
516 $res{'status'} = $self->FormatStatus({
517 Operation => "Verify", Status => "DONE",
518 Message => "The signature is good, signed by ".$signer->{User}[0]{String}.", trust is ".$signer->{TrustTerse},
519 UserString => $signer->{User}[0]{String},
520 Trust => uc($signer->{TrustTerse}),
528 my %args = (Data => undef, Queue => undef, @_ );
530 my $msg = $args{'Data'}->as_string;
532 push @{ $args{'Recipients'} ||= [] },
533 $args{'Queue'}->CorrespondAddress, RT->Config->Get('CorrespondAddress'),
534 $args{'Queue'}->CommentAddress, RT->Config->Get('CommentAddress')
537 my ($buf, %res) = $self->_Decrypt( %args, Content => \$args{'Data'}->as_string );
538 return %res unless $buf;
540 my $res_entity = _extract_msg_from_buf( $buf );
541 $res_entity->make_multipart( 'mixed', Force => 1 );
543 # Work around https://rt.cpan.org/Public/Bug/Display.html?id=87835
544 for my $part (grep {$_->is_multipart and $_->preamble and @{$_->preamble}} $res_entity->parts_DFS) {
545 $part->preamble->[-1] .= "\n"
546 if $part->preamble->[-1] =~ /\r$/;
549 $args{'Data'}->make_multipart( 'mixed', Force => 1 );
550 $args{'Data'}->parts([ $res_entity->parts ]);
551 $args{'Data'}->make_singlepart;
563 my ($buf, %res) = $self->_Decrypt( %args );
564 ${ $args{'Content'} } = $$buf if $buf;
570 my %args = (Content => undef, @_ );
574 grep !$seen{lc $_}++, map $_->address, map Email::Address->parse($_),
575 grep length && defined, @{$args{'Recipients'}};
577 my ($buf, $encrypted_to, %res);
579 foreach my $address ( @addresses ) {
580 my $file = $self->CheckKeyring( Key => $address );
582 my $keyring = RT->Config->Get('SMIME')->{'Keyring'};
583 $RT::Logger->debug("No key found for $address in $keyring directory");
587 local $ENV{SMIME_PASS} = $self->GetPassphrase( Address => $address );
588 local $SIG{CHLD} = 'DEFAULT';
593 (defined $ENV{'SMIME_PASS'} && length $ENV{'SMIME_PASS'})
594 ? (qw(-passin env:SMIME_PASS))
597 safe_run_child { run3( $cmd, $args{'Content'}, \$buf, \$res{'stderr'} ) };
599 $encrypted_to = $address;
600 $RT::Logger->debug("Message encrypted for $encrypted_to");
604 if ( index($res{'stderr'}, 'no recipient matches key') >= 0 ) {
605 $RT::Logger->debug("Although we have a key for $address, it is not the one that encrypted this message");
609 $res{'exit_code'} = $?;
610 $res{'message'} = "openssl exited with error code ". ($? >> 8)
611 ." and error: $res{stderr}";
612 $RT::Logger->error( $res{'message'} );
613 $res{'status'} = $self->FormatStatus({
614 Operation => 'Decrypt', Status => 'ERROR',
615 Message => 'Decryption failed',
616 EncryptedTo => $address,
618 return (undef, %res);
620 unless ( $encrypted_to ) {
621 $RT::Logger->error("Couldn't find SMIME key for addresses: ". join ', ', @addresses);
622 $res{'exit_code'} = 1;
623 $res{'status'} = $self->FormatStatus({
624 Operation => 'KeyCheck',
626 Message => "Secret key is not available",
629 return (undef, %res);
632 $res{'status'} = $self->FormatStatus({
633 Operation => 'Decrypt', Status => 'DONE',
634 Message => 'Decryption process succeeded',
635 EncryptedTo => $encrypted_to,
638 return (\$buf, %res);
646 foreach ( @status ) {
647 while ( my ($k, $v) = each %$_ ) {
648 $res .= "[SMIME:]". $k .": ". $v ."\n";
650 $res .= "[SMIME:]\n";
659 return () unless $status;
661 my @status = split /\s*(?:\[SMIME:\]\s*){2}/, $status;
662 foreach my $block ( grep length, @status ) {
664 $block = { map { s/^\s+//; s/\s+$//; $_ } map split(/:/, $_, 2), split /\s*\[SMIME:\]/, $block };
666 foreach my $block ( grep $_->{'EncryptedTo'}, @status ) {
667 $block->{'EncryptedTo'} = [{
668 EmailAddress => $block->{'EncryptedTo'},
675 sub _extract_msg_from_buf {
677 my $rtparser = RT::EmailParser->new();
678 my $parser = MIME::Parser->new();
679 $rtparser->_SetupMIMEParser($parser);
680 $parser->decode_bodies(0);
681 $parser->output_to_core(1);
682 unless ( $rtparser->{'entity'} = $parser->parse_data($$buf) ) {
683 $RT::Logger->crit("Couldn't parse MIME stream and extract the submessages");
685 # Try again, this time without extracting nested messages
686 $parser->extract_nested_messages(0);
687 unless ( $rtparser->{'entity'} = $parser->parse_data($$buf) ) {
688 $RT::Logger->crit("couldn't parse MIME stream");
692 return $rtparser->Entity;
695 sub FindScatteredParts { return () }
697 sub CheckIfProtected {
699 my %args = ( Entity => undef, @_ );
701 my $entity = $args{'Entity'};
703 my $type = $entity->effective_type;
704 if ( $type =~ m{^application/(?:x-)?pkcs7-mime$} || $type eq 'application/octet-stream' ) {
705 # RFC3851 ch.3.9 variant 1 and 3
709 my $smime_type = $entity->head->mime_attr('Content-Type.smime-type');
710 if ( $smime_type ) { # it's optional according to RFC3851
711 if ( $smime_type eq 'enveloped-data' ) {
712 $security_type = 'encrypted';
714 elsif ( $smime_type eq 'signed-data' ) {
715 $security_type = 'signed';
717 elsif ( $smime_type eq 'certs-only' ) {
718 $security_type = 'certificate management';
720 elsif ( $smime_type eq 'compressed-data' ) {
721 $security_type = 'compressed';
724 $security_type = $smime_type;
728 unless ( $security_type ) {
729 my $fname = $entity->head->recommended_filename || '';
730 if ( $fname =~ /\.p7([czsm])$/ ) {
732 if ( $type_char eq 'm' ) {
734 # it can be both encrypted and signed
735 $security_type = 'encrypted';
737 elsif ( $type_char eq 's' ) {
738 # RFC3851, ch3.4.3, multipart/signed, XXX we should never be here
739 # unless message is changed by some gateway
740 $security_type = 'signed';
742 elsif ( $type_char eq 'c' ) {
744 $security_type = 'certificate management';
746 elsif ( $type_char eq 'z' ) {
748 $security_type = 'compressed';
752 return () unless $security_type;
755 Type => $security_type,
760 if ( $security_type eq 'encrypted' ) {
761 my $top = $args{'TopEntity'}->head;
762 $res{'Recipients'} = [map {Encode::decode("UTF-8", $_)}
763 grep defined && length, map $top->get($_), 'To', 'Cc'];
768 elsif ( $type eq 'multipart/signed' ) {
769 # RFC3156, multipart/signed
770 # RFC3851, ch.3.9 variant 2
772 unless ( $entity->parts == 2 ) {
773 $RT::Logger->error( "Encrypted or signed entity must has two subparts. Skipped" );
777 my $protocol = $entity->head->mime_attr( 'Content-Type.protocol' );
778 unless ( $protocol ) {
779 $RT::Logger->error( "Entity is '$type', but has no protocol defined. Skipped" );
783 unless ( $protocol =~ m{^application/(x-)?pkcs7-signature$} ) {
784 $RT::Logger->info( "Skipping protocol '$protocol', only 'application/x-pkcs7-signature' is supported" );
787 $RT::Logger->debug("Found part signed according to RFC3156");
797 sub GetKeysForEncryption {
799 my %args = (Recipient => undef, @_);
800 return $self->GetKeysInfo( Key => delete $args{'Recipient'}, %args, Type => 'public' );
803 sub GetKeysForSigning {
805 my %args = (Signer => undef, @_);
806 return $self->GetKeysInfo( Key => delete $args{'Signer'}, %args, Type => 'private' );
818 my $email = $args{'Key'};
820 return (exit_code => 0); # unless $args{'Force'};
823 my $key = $self->GetKeyContent( %args );
824 return (exit_code => 0) unless $key;
826 return $self->GetCertificateInfo( Certificate => $key );
831 my %args = ( Key => undef, @_ );
834 if ( my $file = $self->CheckKeyring( %args ) ) {
835 open my $fh, '<:raw', $file
836 or die "Couldn't open file '$file': $!";
837 $key = do { local $/; readline $fh };
841 my $user = RT::User->new( RT->SystemUser );
842 $user->LoadByEmail( $args{'Key'} );
843 $key = $user->SMIMECertificate if $user->id;
854 my $keyring = RT->Config->Get('SMIME')->{'Keyring'};
855 return undef unless $keyring;
857 my $file = File::Spec->catfile( $keyring, $args{'Key'} .'.pem' );
858 return undef unless -f $file;
863 sub GetCertificateInfo {
866 Certificate => undef,
870 if ($args{Certificate} =~ /^-----BEGIN \s+ CERTIFICATE----- \s* $
872 ^-----END \s+ CERTIFICATE----- \s* $/smx) {
873 $args{Certificate} = MIME::Base64::decode_base64($1);
876 my $cert = Crypt::X509->new( cert => $args{Certificate} );
877 return ( exit_code => 1, stderr => $cert->error ) if $cert->error;
880 Country => 'country',
881 StateOrProvince => 'state',
882 Organization => 'org',
883 OrganizationUnit => 'ou',
885 EmailAddress => 'email',
887 my $canonicalize = sub {
890 for (keys %USER_MAP) {
891 my $method = $type . "_" . $USER_MAP{$_};
892 $data{$_} = $cert->$method if $cert->can($method);
894 $data{String} = Email::Address->new( @data{'Name', 'EmailAddress'} )->format
895 if $data{EmailAddress};
899 my $PEM = "-----BEGIN CERTIFICATE-----\n"
900 . MIME::Base64::encode_base64( $args{Certificate} )
901 . "-----END CERTIFICATE-----\n";
907 Fingerprint => Digest::SHA::sha1_hex($args{Certificate}),
908 'Serial Number' => $cert->serial,
909 Created => $self->ParseDate( $cert->not_before ),
910 Expire => $self->ParseDate( $cert->not_after ),
911 Version => sprintf("%d (0x%x)",hex($cert->version || 0)+1, hex($cert->version || 0)),
912 Issuer => [ $canonicalize->( 'issuer' ) ],
913 User => [ $canonicalize->( 'subject' ) ],
919 my $ca = RT->Config->Get('SMIME')->{'CAPath'};
923 @ca_verify = ('-CApath', $ca);
925 @ca_verify = ('-CAfile', $ca);
928 local $SIG{CHLD} = 'DEFAULT';
931 'verify', @ca_verify,
934 safe_run_child { run3( $cmd, \$PEM, \$buf, \$res{stderr} ) };
936 if ($buf =~ /^stdin: OK$/) {
937 $res{info}[0]{Trust} = "Signed by trusted CA $res{info}[0]{Issuer}[0]{String}";
938 $res{info}[0]{TrustTerse} = "full";
939 $res{info}[0]{TrustLevel} = 2;
940 } elsif ($? == 0 or ($? >> 8) == 2) {
941 $res{info}[0]{Trust} = "UNTRUSTED signing CA $res{info}[0]{Issuer}[0]{String}";
942 $res{info}[0]{TrustTerse} = "none";
943 $res{info}[0]{TrustLevel} = -1;
945 $res{exit_code} = $?;
946 $res{message} = "openssl exited with error code ". ($? >> 8)
948 $res{info}[0]{Trust} = "unknown (openssl failed)";
949 $res{info}[0]{TrustTerse} = "unknown";
950 $res{info}[0]{TrustLevel} = 0;
953 $res{info}[0]{Trust} = "unknown (no CAPath set)";
954 $res{info}[0]{TrustTerse} = "unknown";
955 $res{info}[0]{TrustLevel} = 0;
958 $res{info}[0]{Formatted} = $res{info}[0]{User}[0]{String}
959 . " (issued by $res{info}[0]{Issuer}[0]{String})";