1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2014 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($_),
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},
343 $args{'Passphrase'} = $self->GetPassphrase( Address => $args{'Signer'} )
344 unless defined $args{'Passphrase'};
346 push @command, join ' ', shell_quote(
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;
362 push @command, join ' ', shell_quote(
363 $self->OpenSSLPath, qw(smime -encrypt -des3),
364 map { $_->filename } @keys
368 my ($buf, $err) = ('', '');
370 local $ENV{'SMIME_PASS'} = $args{'Passphrase'};
371 local $SIG{'CHLD'} = 'DEFAULT';
372 safe_run_child { run3(
373 join( ' | ', @command ),
378 $RT::Logger->debug( "openssl stderr: " . $err ) if length $err;
381 $res{'status'} .= $self->FormatStatus({
382 Operation => "Sign", Status => "DONE",
383 Message => "Signed message",
385 $res{'status'} .= $self->FormatStatus({
386 Operation => "Encrypt", Status => "DONE",
387 Message => "Data has been encrypted",
388 }) if $args{'Encrypt'};
391 return (\$buf, %res);
396 my %args = ( Info => undef, @_ );
399 my $item = $args{'Info'};
400 if ( $item->{'Type'} eq 'signed' ) {
401 %res = $self->Verify( %$item );
402 } elsif ( $item->{'Type'} eq 'encrypted' ) {
403 %res = $self->Decrypt( %args, %$item );
405 die "Unknown type '". $item->{'Type'} ."' of protected item";
408 return (%res, status_on => $item->{'Data'});
413 my %args = (Data => undef, @_ );
415 my $msg = $args{'Data'}->as_string;
419 my $keyfh = File::Temp->new;
421 local $SIG{CHLD} = 'DEFAULT';
423 $self->OpenSSLPath, qw(smime -verify -noverify),
424 '-signer', $keyfh->filename,
426 safe_run_child { run3( $cmd, \$msg, \$buf, \$res{'stderr'} ) };
427 $res{'exit_code'} = $?;
429 if ( $res{'exit_code'} ) {
430 if ($res{stderr} =~ /(signature|digest) failure/) {
431 $res{'message'} = "Validation failed";
432 $res{'status'} = $self->FormatStatus({
433 Operation => "Verify", Status => "BAD",
434 Message => "The signature did not verify",
437 $res{'message'} = "openssl exited with error code ". ($? >> 8)
438 ." and error: $res{stderr}";
439 $res{'status'} = $self->FormatStatus({
440 Operation => "Verify", Status => "ERROR",
441 Message => "There was an error verifying: $res{stderr}",
443 $RT::Logger->error($res{'message'});
449 if ( my $key = do { $keyfh->seek(0, 0); local $/; readline $keyfh } ) {{
450 my %info = $self->GetCertificateInfo( Certificate => $key );
452 $signer = $info{info}[0];
453 last unless $signer and $signer->{User}[0]{String};
455 unless ( $info{info}[0]{TrustLevel} > 0 or RT->Config->Get('SMIME')->{AcceptUntrustedCAs}) {
456 # We don't trust it; give it the finger
458 $res{'message'} = "Validation failed";
459 $res{'status'} = $self->FormatStatus({
460 Operation => "Verify", Status => "BAD",
461 Message => "The signing CA was not trusted",
462 UserString => $signer->{User}[0]{String},
468 my $user = RT::User->new( $RT::SystemUser );
469 $user->LoadOrCreateByEmail( $signer->{User}[0]{String} );
470 my $current_key = $user->SMIMECertificate;
471 last if $current_key && $current_key eq $key;
473 # Never over-write existing keys with untrusted ones.
474 last if $current_key and not $info{info}[0]{TrustLevel} > 0;
476 my ($status, $msg) = $user->SetSMIMECertificate( $key );
477 $RT::Logger->error("Couldn't set SMIME certificate for user #". $user->id .": $msg")
481 my $res_entity = _extract_msg_from_buf( \$buf );
482 unless ( $res_entity ) {
483 $res{'exit_code'} = 1;
484 $res{'message'} = "verified message, but couldn't parse result";
485 $res{'status'} = $self->FormatStatus({
486 Operation => "Verify", Status => "DONE",
487 Message => "The signature is good, unknown signer",
493 $res_entity->make_multipart( 'mixed', Force => 1 );
495 $args{'Data'}->make_multipart( 'mixed', Force => 1 );
496 $args{'Data'}->parts([ $res_entity->parts ]);
497 $args{'Data'}->make_singlepart;
499 $res{'status'} = $self->FormatStatus({
500 Operation => "Verify", Status => "DONE",
501 Message => "The signature is good, signed by ".$signer->{User}[0]{String}.", trust is ".$signer->{TrustTerse},
502 UserString => $signer->{User}[0]{String},
503 Trust => uc($signer->{TrustTerse}),
511 my %args = (Data => undef, Queue => undef, @_ );
513 my $msg = $args{'Data'}->as_string;
515 push @{ $args{'Recipients'} ||= [] },
516 $args{'Queue'}->CorrespondAddress, RT->Config->Get('CorrespondAddress'),
517 $args{'Queue'}->CommentAddress, RT->Config->Get('CommentAddress')
520 my ($buf, %res) = $self->_Decrypt( %args, Content => \$args{'Data'}->as_string );
521 return %res unless $buf;
523 my $res_entity = _extract_msg_from_buf( $buf );
524 $res_entity->make_multipart( 'mixed', Force => 1 );
526 # Work around https://rt.cpan.org/Public/Bug/Display.html?id=87835
527 for my $part (grep {$_->is_multipart and $_->preamble and @{$_->preamble}} $res_entity->parts_DFS) {
528 $part->preamble->[-1] .= "\n"
529 if $part->preamble->[-1] =~ /\r$/;
532 $args{'Data'}->make_multipart( 'mixed', Force => 1 );
533 $args{'Data'}->parts([ $res_entity->parts ]);
534 $args{'Data'}->make_singlepart;
546 my ($buf, %res) = $self->_Decrypt( %args );
547 ${ $args{'Content'} } = $$buf if $buf;
553 my %args = (Content => undef, @_ );
557 grep !$seen{lc $_}++, map $_->address, map Email::Address->parse($_),
558 grep length && defined, @{$args{'Recipients'}};
560 my ($buf, $encrypted_to, %res);
562 foreach my $address ( @addresses ) {
563 my $file = $self->CheckKeyring( Key => $address );
565 my $keyring = RT->Config->Get('SMIME')->{'Keyring'};
566 $RT::Logger->debug("No key found for $address in $keyring directory");
570 local $ENV{SMIME_PASS} = $self->GetPassphrase( Address => $address );
571 local $SIG{CHLD} = 'DEFAULT';
576 (defined $ENV{'SMIME_PASS'} && length $ENV{'SMIME_PASS'})
577 ? (qw(-passin env:SMIME_PASS))
580 safe_run_child { run3( $cmd, $args{'Content'}, \$buf, \$res{'stderr'} ) };
582 $encrypted_to = $address;
583 $RT::Logger->debug("Message encrypted for $encrypted_to");
587 if ( index($res{'stderr'}, 'no recipient matches key') >= 0 ) {
588 $RT::Logger->debug("Although we have a key for $address, it is not the one that encrypted this message");
592 $res{'exit_code'} = $?;
593 $res{'message'} = "openssl exited with error code ". ($? >> 8)
594 ." and error: $res{stderr}";
595 $RT::Logger->error( $res{'message'} );
596 $res{'status'} = $self->FormatStatus({
597 Operation => 'Decrypt', Status => 'ERROR',
598 Message => 'Decryption failed',
599 EncryptedTo => $address,
601 return (undef, %res);
603 unless ( $encrypted_to ) {
604 $RT::Logger->error("Couldn't find SMIME key for addresses: ". join ', ', @addresses);
605 $res{'exit_code'} = 1;
606 $res{'status'} = $self->FormatStatus({
607 Operation => 'KeyCheck',
609 Message => "Secret key is not available",
612 return (undef, %res);
615 $res{'status'} = $self->FormatStatus({
616 Operation => 'Decrypt', Status => 'DONE',
617 Message => 'Decryption process succeeded',
618 EncryptedTo => $encrypted_to,
621 return (\$buf, %res);
629 foreach ( @status ) {
630 while ( my ($k, $v) = each %$_ ) {
631 $res .= "[SMIME:]". $k .": ". $v ."\n";
633 $res .= "[SMIME:]\n";
642 return () unless $status;
644 my @status = split /\s*(?:\[SMIME:\]\s*){2}/, $status;
645 foreach my $block ( grep length, @status ) {
647 $block = { map { s/^\s+//; s/\s+$//; $_ } map split(/:/, $_, 2), split /\s*\[SMIME:\]/, $block };
649 foreach my $block ( grep $_->{'EncryptedTo'}, @status ) {
650 $block->{'EncryptedTo'} = [{
651 EmailAddress => $block->{'EncryptedTo'},
658 sub _extract_msg_from_buf {
660 my $rtparser = RT::EmailParser->new();
661 my $parser = MIME::Parser->new();
662 $rtparser->_SetupMIMEParser($parser);
663 $parser->decode_bodies(0);
664 $parser->output_to_core(1);
665 unless ( $rtparser->{'entity'} = $parser->parse_data($$buf) ) {
666 $RT::Logger->crit("Couldn't parse MIME stream and extract the submessages");
668 # Try again, this time without extracting nested messages
669 $parser->extract_nested_messages(0);
670 unless ( $rtparser->{'entity'} = $parser->parse_data($$buf) ) {
671 $RT::Logger->crit("couldn't parse MIME stream");
675 return $rtparser->Entity;
678 sub FindScatteredParts { return () }
680 sub CheckIfProtected {
682 my %args = ( Entity => undef, @_ );
684 my $entity = $args{'Entity'};
686 my $type = $entity->effective_type;
687 if ( $type =~ m{^application/(?:x-)?pkcs7-mime$} || $type eq 'application/octet-stream' ) {
688 # RFC3851 ch.3.9 variant 1 and 3
692 my $smime_type = $entity->head->mime_attr('Content-Type.smime-type');
693 if ( $smime_type ) { # it's optional according to RFC3851
694 if ( $smime_type eq 'enveloped-data' ) {
695 $security_type = 'encrypted';
697 elsif ( $smime_type eq 'signed-data' ) {
698 $security_type = 'signed';
700 elsif ( $smime_type eq 'certs-only' ) {
701 $security_type = 'certificate management';
703 elsif ( $smime_type eq 'compressed-data' ) {
704 $security_type = 'compressed';
707 $security_type = $smime_type;
711 unless ( $security_type ) {
712 my $fname = $entity->head->recommended_filename || '';
713 if ( $fname =~ /\.p7([czsm])$/ ) {
715 if ( $type_char eq 'm' ) {
717 # it can be both encrypted and signed
718 $security_type = 'encrypted';
720 elsif ( $type_char eq 's' ) {
721 # RFC3851, ch3.4.3, multipart/signed, XXX we should never be here
722 # unless message is changed by some gateway
723 $security_type = 'signed';
725 elsif ( $type_char eq 'c' ) {
727 $security_type = 'certificate management';
729 elsif ( $type_char eq 'z' ) {
731 $security_type = 'compressed';
735 return () unless $security_type;
738 Type => $security_type,
743 if ( $security_type eq 'encrypted' ) {
744 my $top = $args{'TopEntity'}->head;
745 $res{'Recipients'} = [grep defined && length, map $top->get($_), 'To', 'Cc'];
750 elsif ( $type eq 'multipart/signed' ) {
751 # RFC3156, multipart/signed
752 # RFC3851, ch.3.9 variant 2
754 unless ( $entity->parts == 2 ) {
755 $RT::Logger->error( "Encrypted or signed entity must has two subparts. Skipped" );
759 my $protocol = $entity->head->mime_attr( 'Content-Type.protocol' );
760 unless ( $protocol ) {
761 $RT::Logger->error( "Entity is '$type', but has no protocol defined. Skipped" );
765 unless ( $protocol =~ m{^application/(x-)?pkcs7-signature$} ) {
766 $RT::Logger->info( "Skipping protocol '$protocol', only 'application/x-pkcs7-signature' is supported" );
769 $RT::Logger->debug("Found part signed according to RFC3156");
779 sub GetKeysForEncryption {
781 my %args = (Recipient => undef, @_);
782 return $self->GetKeysInfo( Key => delete $args{'Recipient'}, %args, Type => 'public' );
785 sub GetKeysForSigning {
787 my %args = (Signer => undef, @_);
788 return $self->GetKeysInfo( Key => delete $args{'Signer'}, %args, Type => 'private' );
800 my $email = $args{'Key'};
802 return (exit_code => 0); # unless $args{'Force'};
805 my $key = $self->GetKeyContent( %args );
806 return (exit_code => 0) unless $key;
808 return $self->GetCertificateInfo( Certificate => $key );
813 my %args = ( Key => undef, @_ );
816 if ( my $file = $self->CheckKeyring( %args ) ) {
817 open my $fh, '<:raw', $file
818 or die "Couldn't open file '$file': $!";
819 $key = do { local $/; readline $fh };
823 my $user = RT::User->new( RT->SystemUser );
824 $user->LoadByEmail( $args{'Key'} );
825 $key = $user->SMIMECertificate if $user->id;
836 my $keyring = RT->Config->Get('SMIME')->{'Keyring'};
837 return undef unless $keyring;
839 my $file = File::Spec->catfile( $keyring, $args{'Key'} .'.pem' );
840 return undef unless -f $file;
845 sub GetCertificateInfo {
848 Certificate => undef,
852 if ($args{Certificate} =~ /^-----BEGIN \s+ CERTIFICATE----- \s* $
854 ^-----END \s+ CERTIFICATE----- \s* $/smx) {
855 $args{Certificate} = MIME::Base64::decode_base64($1);
858 my $cert = Crypt::X509->new( cert => $args{Certificate} );
859 return ( exit_code => 1, stderr => $cert->error ) if $cert->error;
862 Country => 'country',
863 StateOrProvince => 'state',
864 Organization => 'org',
865 OrganizationUnit => 'ou',
867 EmailAddress => 'email',
869 my $canonicalize = sub {
872 for (keys %USER_MAP) {
873 my $method = $type . "_" . $USER_MAP{$_};
874 $data{$_} = $cert->$method if $cert->can($method);
876 $data{String} = Email::Address->new( @data{'Name', 'EmailAddress'} )->format
877 if $data{EmailAddress};
881 my $PEM = "-----BEGIN CERTIFICATE-----\n"
882 . MIME::Base64::encode_base64( $args{Certificate} )
883 . "-----END CERTIFICATE-----\n";
889 Fingerprint => Digest::SHA::sha1_hex($args{Certificate}),
890 'Serial Number' => $cert->serial,
891 Created => $self->ParseDate( $cert->not_before ),
892 Expire => $self->ParseDate( $cert->not_after ),
893 Version => sprintf("%d (0x%x)",hex($cert->version || 0)+1, hex($cert->version || 0)),
894 Issuer => [ $canonicalize->( 'issuer' ) ],
895 User => [ $canonicalize->( 'subject' ) ],
901 my $ca = RT->Config->Get('SMIME')->{'CAPath'};
905 @ca_verify = ('-CApath', $ca);
907 @ca_verify = ('-CAfile', $ca);
910 local $SIG{CHLD} = 'DEFAULT';
913 'verify', @ca_verify,
916 safe_run_child { run3( $cmd, \$PEM, \$buf, \$res{stderr} ) };
918 if ($buf =~ /^stdin: OK$/) {
919 $res{info}[0]{Trust} = "Signed by trusted CA $res{info}[0]{Issuer}[0]{String}";
920 $res{info}[0]{TrustTerse} = "full";
921 $res{info}[0]{TrustLevel} = 2;
922 } elsif ($? == 0 or ($? >> 8) == 2) {
923 $res{info}[0]{Trust} = "UNTRUSTED signing CA $res{info}[0]{Issuer}[0]{String}";
924 $res{info}[0]{TrustTerse} = "none";
925 $res{info}[0]{TrustLevel} = -1;
927 $res{exit_code} = $?;
928 $res{message} = "openssl exited with error code ". ($? >> 8)
930 $res{info}[0]{Trust} = "unknown (openssl failed)";
931 $res{info}[0]{TrustTerse} = "unknown";
932 $res{info}[0]{TrustLevel} = 0;
935 $res{info}[0]{Trust} = "unknown (no CAPath set)";
936 $res{info}[0]{TrustTerse} = "unknown";
937 $res{info}[0]{TrustLevel} = 0;