[Rt-commit] rt branch, 4.4-trunk, updated. rt-4.4.4-173-g6a8ea9f51a

? sunnavy sunnavy at bestpractical.com
Wed Nov 18 12:39:09 EST 2020


The branch, 4.4-trunk has been updated
       via  6a8ea9f51aee3002eb9a6361f1fe22d8c4130376 (commit)
       via  eff063c2481838f072f9b6398e98328c765b5cdb (commit)
       via  8c914a0aef00e31e289533f04637987b196f91be (commit)
       via  855c9df08d09402111d1ee1597f21930dbd29988 (commit)
       via  574c2e0ec13e03f9e2392b4713c31cfa73f710b9 (commit)
       via  1baa631446e6e1fbc395df7d41044d5c45c5520d (commit)
       via  93faa6da1fba197a94c49c3b6da0da4fd1dd3941 (commit)
      from  c7c2988f7b0e6fe4d10c8eb8a6e5c57346d63a37 (commit)

Summary of changes:
 lib/RT/Crypt/GnuPG.pm                              | 64 ++++++++++++++++++++++
 lib/RT/Crypt/SMIME.pm                              | 50 +++++++++++++++++
 .../GroupRights.html => Crypt/GetGPGPubkey.html}   | 36 ++++++++----
 share/html/Elements/CryptStatus                    | 52 +++++++++++++++++-
 t/crypt/smime/extract-email-address.t              | 31 +++++++++++
 t/data/smime/keys/dianne at skoll.ca.crt              | 34 ++++++++++++
 t/data/smime/keys/smime at example.com.crt            | 33 +++++++++++
 t/web/crypt-gnupg.t                                | 22 ++++++++
 t/web/smime/outgoing.t                             | 10 ++++
 9 files changed, 320 insertions(+), 12 deletions(-)
 copy share/html/{Admin/Global/GroupRights.html => Crypt/GetGPGPubkey.html} (73%)
 create mode 100644 t/crypt/smime/extract-email-address.t
 create mode 100644 t/data/smime/keys/dianne at skoll.ca.crt
 create mode 100644 t/data/smime/keys/smime at example.com.crt

- Log -----------------------------------------------------------------
commit 93faa6da1fba197a94c49c3b6da0da4fd1dd3941
Author: Dianne Skoll <dianne at bestpractical.com>
Date:   Thu Nov 12 10:08:23 2020 -0500

    Add AgorithmName to info returned by ParseKeysInfo

diff --git a/lib/RT/Crypt/GnuPG.pm b/lib/RT/Crypt/GnuPG.pm
index d96c68f861..c9bed8ab29 100644
--- a/lib/RT/Crypt/GnuPG.pm
+++ b/lib/RT/Crypt/GnuPG.pm
@@ -1712,6 +1712,7 @@ sub ParseKeysInfo {
                 Empty Empty Capabilities Other
             ) } = split /:/, $line, 12;
 
+            $info{AlgorithmName} = $self->PubkeyAlgorithmToName($info{Algorithm}) if defined($info{Algorithm});
             # workaround gnupg's wierd behaviour, --list-keys command report calculated trust levels
             # for any model except 'always', so you can change models and see changes, but not for 'always'
             # we try to handle it in a simple way - we set ultimate trust for any key with trust
@@ -1739,6 +1740,7 @@ sub ParseKeysInfo {
                 Created Expire Empty OwnerTrustChar
                 Empty Empty Capabilities Other
             ) } = split /:/, $line, 12;
+            $info{AlgorithmName} = $self->PubkeyAlgorithmToName($info{Algorithm}) if defined($info{Algorithm});
             @info{qw(OwnerTrust OwnerTrustTerse OwnerTrustLevel)} = 
                 _ConvertTrustChar( $info{'OwnerTrustChar'} );
             $info{ $_ } = $self->ParseDate( $info{ $_ } )
@@ -1958,6 +1960,22 @@ sub _make_gpg_handles {
     return ($handles, \%handle_map);
 }
 
+# Given a PGP public-key algorithm number, return the algorithm name.
+# See https://tools.ietf.org/html/rfc4880#section-9.1
+sub PubkeyAlgorithmToName {
+    my ( $self, $alg ) = @_;
+    return 'RSA'                         if ( $alg == 1 );
+    return 'RSA Encrypt-Only'            if ( $alg == 2 );
+    return 'RSA Sign-Only'               if ( $alg == 3 );
+    return 'Elgamal Encrypt-Ony'         if ( $alg == 16 );
+    return 'DSA'                         if ( $alg == 17 );
+    return 'Reserved for EC'             if ( $alg == 18 );
+    return 'Reserved for ECDSA'          if ( $alg == 19 );
+    return 'Reserved (formerly Elgamal)' if ( $alg == 20 );
+    return 'Reserved (DH)'               if ( $alg == 21 );
+    return undef;
+}
+
 RT::Base->_ImportOverlays();
 
 1;

commit 1baa631446e6e1fbc395df7d41044d5c45c5520d
Author: Dianne Skoll <dianne at bestpractical.com>
Date:   Thu Nov 12 17:06:48 2020 -0500

    For GnuPG, add a tooltip with additional info about the signature

diff --git a/lib/RT/Crypt/GnuPG.pm b/lib/RT/Crypt/GnuPG.pm
index c9bed8ab29..021670bdbf 100644
--- a/lib/RT/Crypt/GnuPG.pm
+++ b/lib/RT/Crypt/GnuPG.pm
@@ -1490,6 +1490,15 @@ sub ParseStatus {
 
             foreach my $line ( @status[ $i .. $#status ] ) {
                 next unless $line =~ /^VALIDSIG\s+(.*)/;
+                # Fingerprint = key fingerprint in hex
+                # CreationDate = key creation date (YYYY-MM-DD)
+                # Timestamp = signature creation time (seconds from UNIX epoch)
+                # ExpireTimestamp = signature expiration time (since epoch) or 0 for "never expires"
+                # Version = signature version straight from the packet
+                # PubkeyAlgo = Public key algorithm (https://tools.ietf.org/html/rfc4880#section-9.1)
+                # HashAlgo = Hash algorithm (https://tools.ietf.org/html/rfc4880#section-9.4)
+                # Class = Signature type (https://tools.ietf.org/html/rfc4880#section-5.2.1)
+                # PKFingerprint = Primary Key Fingerprint
                 @res{ qw(
                     Fingerprint
                     CreationDate
@@ -1503,6 +1512,8 @@ sub ParseStatus {
                     PKFingerprint
                     Other
                 ) } = split /\s+/, $1, 10;
+                $res{HashAlgoName} = $self->HashAlgorithmToName($res{HashAlgo}) if defined($res{HashAlgo});
+                $res{PubkeyAlgoName} = $self->PubkeyAlgorithmToName($res{PubkeyAlgo}) if defined($res{PubkeyAlgo});
                 last;
             }
             push @res, \%res;
@@ -1960,6 +1971,21 @@ sub _make_gpg_handles {
     return ($handles, \%handle_map);
 }
 
+# Gigne a PGP hash algorithm number, return the algorithm name.
+# See https://tools.ietf.org/html/rfc4880#section-9.4
+sub HashAlgorithmToName {
+    my ( $self, $alg ) = @_;
+    return 'MD5'         if ( $alg == 1 );
+    return 'SHA-1'       if ( $alg == 2 );
+    return 'RIPE-MD/160' if ( $alg == 3 );
+    return 'Reserved'    if ( $alg >= 4 && $alg <= 7 );
+    return 'SHA256'      if ( $alg == 8 );
+    return 'SHA384'      if ( $alg == 9 );
+    return 'SHA512'      if ( $alg == 10 );
+    return 'SHA224'      if ( $alg == 11 );
+    return undef;
+}
+
 # Given a PGP public-key algorithm number, return the algorithm name.
 # See https://tools.ietf.org/html/rfc4880#section-9.1
 sub PubkeyAlgorithmToName {
diff --git a/share/html/Elements/CryptStatus b/share/html/Elements/CryptStatus
index e6c0e7ba8f..be94c41d7c 100644
--- a/share/html/Elements/CryptStatus
+++ b/share/html/Elements/CryptStatus
@@ -71,6 +71,32 @@ foreach ( $Message->SplitHeaders ) {
 
 return unless @runs or $needs_unsigned_warning;
 
+# Returns string representing a date in UNIX timestamp format
+sub DisplayDate {
+    my ($ts) = @_;
+    my $date = RT::Date->new($session{'CurrentUser'});
+    $date->Unix($ts);
+    return $date->AsString();
+}
+
+# Generate a little tooltip with additional info about a signature
+sub VerifyTooltip {
+    my ($line) = @_;
+    my $tooltip = '';
+    $tooltip .= "\n" . loc('Fingerprint') . ': ' . $line->{Fingerprint} if $line->{Fingerprint};
+    $tooltip .= "\n" . loc('Signature Created') . ': ' . DisplayDate($line->{Timestamp}) if $line->{Timestamp};
+    $tooltip .= "\n" . loc('Key Expires') . ': ';
+    if ($line->{ExpireTimestamp}) {
+        $tooltip .= DisplayDate($line->{Timestamp});
+    } else {
+        $tooltip .= loc('Never');
+    }
+    $tooltip .= "\n" . loc('Public Key Algorithm') . ': ' . $line->{PubkeyAlgoName} if $line->{PubkeyAlgoName};
+    $tooltip .= "\n" . loc('Hash Algorithm') . ': ' . $line->{HashAlgoName} if $line->{HashAlgoName};
+    $tooltip =~ s/^\s+//;
+    return $tooltip;
+}
+
 my $reverify_cb = sub {
     my $top = shift;
 
@@ -170,7 +196,7 @@ foreach my $run ( @runs ) {
             push @messages, {
                 Tag     => $protocol,
                 Classes => ['verify', lc $line->{Status}, 'trust-'.($line->{Trust} || 'UNKNOWN')],
-                Value   => $m->interp->apply_escapes( loc( $line->{'Message'} ), 'h'),
+                Value   => '<span title="' . $m->interp->apply_escapes(VerifyTooltip($line), 'h') . '">' . $m->interp->apply_escapes(loc( $line->{'Message'} ), 'h') . '</span>',
             };
         }
         else {

commit 574c2e0ec13e03f9e2392b4713c31cfa73f710b9
Author: Dianne Skoll <dianne at bestpractical.com>
Date:   Thu Nov 12 17:06:48 2020 -0500

    Add ability to download GnuPG public keys

diff --git a/lib/RT/Crypt/GnuPG.pm b/lib/RT/Crypt/GnuPG.pm
index 021670bdbf..1117c43094 100644
--- a/lib/RT/Crypt/GnuPG.pm
+++ b/lib/RT/Crypt/GnuPG.pm
@@ -2002,6 +2002,26 @@ sub PubkeyAlgorithmToName {
     return undef;
 }
 
+# Given a public-key fingerprint, return the public key (in PEM format)
+# if we have it, undef if we do not.
+sub GetPubkey {
+    my ( $self, $fingerprint ) = @_;
+
+    # Sanity-check the fingerprint, which must be a string of
+    # hex digits
+    if ( $fingerprint =~ /[^0-9a-fA-F]/ ) {
+        return undef;
+    }
+
+    my @pubkey;
+    my %res = $self->CallGnuPG(
+        Command     => 'export_keys',
+        CommandArgs => $fingerprint,
+        Output      => \@pubkey
+    );
+    return join( '', @pubkey );
+}
+
 RT::Base->_ImportOverlays();
 
 1;
diff --git a/share/html/Crypt/GetGPGPubkey.html b/share/html/Crypt/GetGPGPubkey.html
new file mode 100644
index 0000000000..22b7af4ba8
--- /dev/null
+++ b/share/html/Crypt/GetGPGPubkey.html
@@ -0,0 +1,77 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2020 Best Practical Solutions, LLC
+%#                                          <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<& /Elements/Header,
+   Title => $title
+ &>
+<& /Elements/Tabs &>
+
+<& /Elements/ListActions, actions => \@results &>
+
+<%ARGS>
+$Fingerprint => undef
+$title => loc('Download GnuPG Public Key')
+</%ARGS>
+
+<%INIT>
+my @results;
+if (!$Fingerprint) {
+    push(@results, loc('Fingerprint must be supplied to download a public key.'));
+} else {
+    my $key = RT::Crypt::GnuPG->GetPubkey($Fingerprint);
+    if (!$key) {
+        push(@results, loc('Could not find GnuPG public key with fingerprint [_1].', $Fingerprint));
+    } else {
+        $r->content_type('application/pgp-keys');
+        $r->header_out('Content-Disposition' => "attachment; filename=\"$Fingerprint.pub\"");
+        $m->out($key);
+        $m->flush_buffer;
+        $m->abort();
+    }
+}
+
+</%INIT>
diff --git a/share/html/Elements/CryptStatus b/share/html/Elements/CryptStatus
index be94c41d7c..4f99b7ed83 100644
--- a/share/html/Elements/CryptStatus
+++ b/share/html/Elements/CryptStatus
@@ -97,6 +97,18 @@ sub VerifyTooltip {
     return $tooltip;
 }
 
+# Generate a download link for the public key
+sub KeyDownloadLink {
+    my ($protocol, $line) = @_;
+    my $txt = '';
+    if ($protocol eq 'GnuPG') {
+        if ($line->{Fingerprint} && $line->{Fingerprint} !~ /[^0-9A-F]/i) {
+            $txt = '<a href="' . RT->Config->Get('WebPath') . '/Crypt/GetGPGPubkey.html?Fingerprint=' . $line->{Fingerprint} . '"> ' . loc('(Download Public Key)') . '</a>';
+        }
+    }
+    return $txt;
+}
+
 my $reverify_cb = sub {
     my $top = shift;
 
@@ -196,7 +208,7 @@ foreach my $run ( @runs ) {
             push @messages, {
                 Tag     => $protocol,
                 Classes => ['verify', lc $line->{Status}, 'trust-'.($line->{Trust} || 'UNKNOWN')],
-                Value   => '<span title="' . $m->interp->apply_escapes(VerifyTooltip($line), 'h') . '">' . $m->interp->apply_escapes(loc( $line->{'Message'} ), 'h') . '</span>',
+                Value   => '<span title="' . $m->interp->apply_escapes(VerifyTooltip($line), 'h') . '">' . $m->interp->apply_escapes(loc( $line->{'Message'} ), 'h') . '</span>' . KeyDownloadLink($protocol, $line),
             };
         }
         else {

commit 855c9df08d09402111d1ee1597f21930dbd29988
Author: Dianne Skoll <dianne at bestpractical.com>
Date:   Fri Nov 13 09:29:38 2020 -0500

    Add tests for our tooltip and for GnuPG public-key download link.

diff --git a/t/web/crypt-gnupg.t b/t/web/crypt-gnupg.t
index b106362618..b75b736182 100644
--- a/t/web/crypt-gnupg.t
+++ b/t/web/crypt-gnupg.t
@@ -183,8 +183,30 @@ MAIL
 
     like($attachments[0]->Content, qr/Some other content/, "RT's mail includes copy of ticket text");
     like($attachments[0]->Content, qr/$RT::rtname/, "RT's mail includes this instance's name");
+    $m->get("$baseurl/Ticket/History.html?id=$id");
+    $m->content_like(
+        qr/<span title="Fingerprint: EC1E81E7DC3DB42788FB0E4E9FA662C06DE22FC2\nSignature Created: .*\nKey Expires: Never\nPublic Key Algorithm: DSA\nHash Algorithm: SHA-1">/m,
+        "Tooltip was added"
+    );
+    $m->follow_link_ok( { text => '(Download Public Key)' }, 'Download link for public key was added' );
+    $m->text_like(qr/-----BEGIN PGP PUBLIC KEY BLOCK-----/, "Download link returned a public key");
 }
 
+# Try fetching a nonexistent pubic key
+$m->get_ok("$baseurl/Crypt/GetGPGPubkey.html?Fingerprint=EC1E81E7DC3DB42788FB0E4E9FA662C06DE22FCEEEEEEE");
+$m->text_contains('Could not find GnuPG public key with fingerprint EC1E81E7DC3DB42788FB0E4E9FA662C06DE22FCEEEEEEE', "Got correct error message");
+
+# Try fetching with invalid fingerprint
+$m->get_ok("$baseurl/Crypt/GetGPGPubkey.html?Fingerprint=wookie%3B%3C%3Erm%20/etc/passwd");
+$m->content_contains('Could not find GnuPG public key with fingerprint wookie;<>rm /etc/passwd', "Got correct error message (dangerous characters HTML-escaped)");
+
+# Try fetching with no fingerprint
+$m->get_ok("$baseurl/Crypt/GetGPGPubkey.html?Irrelevant=3");
+$m->text_contains(
+    'Fingerprint must be supplied to download a public key',
+    "Got correct error message when no fingerprint supplied"
+);
+
 $m->get("$baseurl/Admin/Queues/Modify.html?id=$qid");
 $m->form_with_fields('Sign', 'Encrypt');
 $m->field(Encrypt => 1);

commit 8c914a0aef00e31e289533f04637987b196f91be
Author: Dianne Skoll <dianne at bestpractical.com>
Date:   Fri Nov 13 11:53:26 2020 -0500

    Store and display additional info about S/MIME signatures.
    
    Specifically, we track the certificate period of validity, and
    display this, the signer and the issuer info in a tooltip.

diff --git a/lib/RT/Crypt/SMIME.pm b/lib/RT/Crypt/SMIME.pm
index 15f2661027..46f557c1fb 100644
--- a/lib/RT/Crypt/SMIME.pm
+++ b/lib/RT/Crypt/SMIME.pm
@@ -490,6 +490,8 @@ sub Verify {
                 Operation => "Verify", Status => "BAD",
                 Message => "The signing CA was not trusted",
                 UserString => $signer->{User}[0]{String},
+                ExpireTimestamp => $signer->{Expire}->Unix(),
+                CreatedTimestamp => $signer->{Created}->Unix(),
                 Trust => "NONE",
             });
             return %res;
@@ -521,6 +523,8 @@ sub Verify {
         $res{'status'} = $self->FormatStatus({
             Operation => "Verify", Status => "DONE",
             Message => "The signature is good, unknown signer",
+            ExpireTimestamp => $signer->{Expire}->Unix(),
+            CreatedTimestamp => $signer->{Created}->Unix(),
             Trust => "UNKNOWN",
         });
         return %res;
@@ -537,6 +541,9 @@ sub Verify {
         Message => "The signature is good, signed by ".$signer->{User}[0]{String}.", assured by " . $signer->{Issuer}[0]{String} . ", trust is ".$signer->{TrustTerse},
         UserString => $signer->{User}[0]{String},
         Trust => uc($signer->{TrustTerse}),
+        Issuer => $signer->{Issuer}[0]{String},
+        ExpireTimestamp => $signer->{Expire}->Unix(),
+        CreatedTimestamp => $signer->{Created}->Unix(),
     });
 
     return %res;
diff --git a/share/html/Elements/CryptStatus b/share/html/Elements/CryptStatus
index 4f99b7ed83..681387fb41 100644
--- a/share/html/Elements/CryptStatus
+++ b/share/html/Elements/CryptStatus
@@ -81,18 +81,25 @@ sub DisplayDate {
 
 # Generate a little tooltip with additional info about a signature
 sub VerifyTooltip {
-    my ($line) = @_;
+    my ($protocol, $line) = @_;
     my $tooltip = '';
-    $tooltip .= "\n" . loc('Fingerprint') . ': ' . $line->{Fingerprint} if $line->{Fingerprint};
-    $tooltip .= "\n" . loc('Signature Created') . ': ' . DisplayDate($line->{Timestamp}) if $line->{Timestamp};
-    $tooltip .= "\n" . loc('Key Expires') . ': ';
+    $tooltip .= "\n" . loc('Fingerprint') . ': ' . $line->{Fingerprint} if $line->{Fingerprint};                                # GNUPG
+    $tooltip .= "\n" . loc('Signature Created') . ': ' . DisplayDate($line->{Timestamp}) if $line->{Timestamp};                 # GNUPG
+    $tooltip .= "\n" . loc('Signer') . ': ' . $line->{UserString} if $line->{UserString};                                       # SMIME
+    $tooltip .= "\n" . loc('Issuer') . ': ' . $line->{Issuer} if $line->{Issuer};                                               # SMIME
+    $tooltip .= "\n" . loc('Certificate Created') . ': ' . DisplayDate($line->{CreatedTimestamp}) if $line->{CreatedTimestamp}; # SMIME
+    if ($protocol eq 'SMIME') {
+        $tooltip .= "\n" . loc('Certificate Expires') . ': ';
+    } else {
+        $tooltip .= "\n" . loc('Key Expires') . ': ';
+    }
     if ($line->{ExpireTimestamp}) {
-        $tooltip .= DisplayDate($line->{Timestamp});
+        $tooltip .= DisplayDate($line->{ExpireTimestamp});
     } else {
         $tooltip .= loc('Never');
     }
-    $tooltip .= "\n" . loc('Public Key Algorithm') . ': ' . $line->{PubkeyAlgoName} if $line->{PubkeyAlgoName};
-    $tooltip .= "\n" . loc('Hash Algorithm') . ': ' . $line->{HashAlgoName} if $line->{HashAlgoName};
+    $tooltip .= "\n" . loc('Public Key Algorithm') . ': ' . $line->{PubkeyAlgoName} if $line->{PubkeyAlgoName};                 # GNUPG
+    $tooltip .= "\n" . loc('Hash Algorithm') . ': ' . $line->{HashAlgoName} if $line->{HashAlgoName};                           # GNUPG
     $tooltip =~ s/^\s+//;
     return $tooltip;
 }
@@ -106,6 +113,11 @@ sub KeyDownloadLink {
             $txt = '<a href="' . RT->Config->Get('WebPath') . '/Crypt/GetGPGPubkey.html?Fingerprint=' . $line->{Fingerprint} . '"> ' . loc('(Download Public Key)') . '</a>';
         }
     }
+
+    # There isn't really a feasible way to download the S/MIME
+    # certificate, unfortunately.  However, since RT makes the
+    # original message available, the S/MIME cert could be
+    # extracted from that if necessary.
     return $txt;
 }
 
@@ -208,7 +220,7 @@ foreach my $run ( @runs ) {
             push @messages, {
                 Tag     => $protocol,
                 Classes => ['verify', lc $line->{Status}, 'trust-'.($line->{Trust} || 'UNKNOWN')],
-                Value   => '<span title="' . $m->interp->apply_escapes(VerifyTooltip($line), 'h') . '">' . $m->interp->apply_escapes(loc( $line->{'Message'} ), 'h') . '</span>' . KeyDownloadLink($protocol, $line),
+                Value   => '<span title="' . $m->interp->apply_escapes(VerifyTooltip($protocol, $line), 'h') . '">' . $m->interp->apply_escapes(loc( $line->{'Message'} ), 'h') . '</span>' . KeyDownloadLink($protocol, $line),
             };
         }
         else {
diff --git a/t/web/crypt-gnupg.t b/t/web/crypt-gnupg.t
index b75b736182..119bd91350 100644
--- a/t/web/crypt-gnupg.t
+++ b/t/web/crypt-gnupg.t
@@ -185,7 +185,7 @@ MAIL
     like($attachments[0]->Content, qr/$RT::rtname/, "RT's mail includes this instance's name");
     $m->get("$baseurl/Ticket/History.html?id=$id");
     $m->content_like(
-        qr/<span title="Fingerprint: EC1E81E7DC3DB42788FB0E4E9FA662C06DE22FC2\nSignature Created: .*\nKey Expires: Never\nPublic Key Algorithm: DSA\nHash Algorithm: SHA-1">/m,
+        qr/<span title="Fingerprint: EC1E81E7DC3DB42788FB0E4E9FA662C06DE22FC2\nSignature Created: .*\nSigner: general <general\@example.com>\nKey Expires: Never\nPublic Key Algorithm: DSA\nHash Algorithm: SHA-1">/m,
         "Tooltip was added"
     );
     $m->follow_link_ok( { text => '(Download Public Key)' }, 'Download link for public key was added' );
diff --git a/t/web/smime/outgoing.t b/t/web/smime/outgoing.t
index 2f80c7ec35..d85c2ca111 100644
--- a/t/web/smime/outgoing.t
+++ b/t/web/smime/outgoing.t
@@ -215,6 +215,16 @@ foreach my $mail ( map cleanup_headers($_), @{ $mail{'signed_encrypted'} } ) {
     my ($status, $id) = RT::Test->send_via_mailgate($mail);
     is ($status >> 8, 0, "The mail gateway exited normally");
     ok ($id, "got id of a newly created ticket - $id");
+    $m->get_ok("/Ticket/History.html?id=$id");
+
+    $m->content_like(
+        qr/The signature is good, signed by "sender" <sender\@example.com>, assured by "CA Owner" <ca.owner\@example.com>, trust is full/,
+        'Signature status correctly displayed'
+    );
+    $m->content_like(
+        qr{<span title="Signer: "sender" <sender\@example.com>\nIssuer: "CA Owner" <ca.owner\@example.com>\nCertificate Created: .* 2013\nCertificate Expires: .* 2023">}m,
+        'Tooltip correctly displayed'
+    );
 
     my $tick = RT::Ticket->new( $RT::SystemUser );
     $tick->Load( $id );

commit eff063c2481838f072f9b6398e98328c765b5cdb
Author: Dianne Skoll <dianne at bestpractical.com>
Date:   Tue Nov 17 10:41:06 2020 -0500

    Extract email addresses from S/MIME certificates as specified in RFC 5750.
    
    Add a certificate with the email address only in SubjectAltName and add
    a unit test to ensure that email addresses are extracted correctly.
    
    Since RFC 5750 permits S/MIME certificates to contain no email addresses,
    do not attempt to set the SMIMECertificate for an RT::User if we could
    not extract an email address.

diff --git a/lib/RT/Crypt/SMIME.pm b/lib/RT/Crypt/SMIME.pm
index 46f557c1fb..d78eb14146 100644
--- a/lib/RT/Crypt/SMIME.pm
+++ b/lib/RT/Crypt/SMIME.pm
@@ -941,6 +941,13 @@ sub GetCertificateInfo {
             my $method = $type . "_" . $USER_MAP{$_};
             $data{$_} = $cert->$method if $cert->can($method);
         }
+
+        # Use the correct procedure as per
+        # https://tools.ietf.org/html/rfc5750#section-3
+        # to extract the subject's email address
+        if ($type eq 'subject') {
+            $data{EmailAddress} = $self->ExtractSubjectEmailAddress($cert);
+        }
         if ($data{EmailAddress}) {
             $data{String} = Email::Address->new( @data{'Name', 'EmailAddress'} )->format;
         } else {
@@ -1014,4 +1021,40 @@ sub GetCertificateInfo {
     return %res;
 }
 
+# Extract the subject email address from an S/MIME certificate.
+# https://tools.ietf.org/html/rfc5750#section-3
+sub ExtractSubjectEmailAddress {
+    my $self = shift;
+    my $cert = shift;
+
+    # 1: Check SubjectAltName
+    # "The email address SHOULD be in the subjectAltName extension"
+
+    my $altnames = $cert->SubjectAltName;
+    if ( $altnames && ( ref($altnames) eq 'ARRAY' ) ) {
+
+        # Pick the first email address from the array.
+        foreach my $alt (@$altnames) {
+            if ( $alt =~ s/^rfc822name=//i ) {
+                return $alt;
+            }
+        }
+    }
+
+    # 2: Check subject_email
+    my $ret = $cert->subject_email;
+    return $ret if ( defined($ret) && ( $ret ne '' ) );
+
+    # 3: If the subject_cn looks like an email address,
+    # return that
+    my $email_address;
+    eval { ($email_address) = Email::Address->parse( $cert->subject_cn ); };
+    if ($email_address) {
+        return $email_address->address;
+    }
+
+    # No sensible email address found
+    return undef;
+}
+
 1;
diff --git a/t/crypt/smime/extract-email-address.t b/t/crypt/smime/extract-email-address.t
new file mode 100644
index 0000000000..ab709c58c7
--- /dev/null
+++ b/t/crypt/smime/extract-email-address.t
@@ -0,0 +1,31 @@
+use strict;
+use warnings;
+
+use RT::Test::SMIME tests => undef;
+
+sub extract_email_address
+{
+    my ($base) = @_;
+    my $cert;
+    {
+        local $/;
+        open(my $fh, "<t/data/smime/keys/$base.crt") or die ("Cannot open t/data/smime/keys/$base.crt: $!");
+        $cert = <$fh>;
+        close($fh);
+    }
+    if ($cert =~ /^-----BEGIN \s+ CERTIFICATE----- \s* $
+    (.*?)
+    ^-----END \s+ CERTIFICATE----- \s* $/smx) {
+        $cert = MIME::Base64::decode_base64($1);
+    }
+
+    my $c = Crypt::X509->new(cert => $cert);
+    return RT::Crypt::SMIME->ExtractSubjectEmailAddress($c);
+}
+
+foreach my $addr ('dianne at skoll.ca', 'root at example.com', 'sender at example.com', 'smime at example.com') {
+    is (extract_email_address($addr), $addr, "$addr: Correct email address extracted from S/MIME certificate");
+}
+
+
+done_testing;
diff --git a/t/data/smime/keys/dianne at skoll.ca.crt b/t/data/smime/keys/dianne at skoll.ca.crt
new file mode 100644
index 0000000000..e629e26977
--- /dev/null
+++ b/t/data/smime/keys/dianne at skoll.ca.crt
@@ -0,0 +1,34 @@
+-----BEGIN CERTIFICATE-----
+MIIF2DCCA8CgAwIBAgIQeWotbvwZpc0h6+xjcJRMHzANBgkqhkiG9w0BAQsFADCB
+gTELMAkGA1UEBhMCSVQxEDAOBgNVBAgMB0JlcmdhbW8xGTAXBgNVBAcMEFBvbnRl
+IFNhbiBQaWV0cm8xFzAVBgNVBAoMDkFjdGFsaXMgUy5wLkEuMSwwKgYDVQQDDCNB
+Y3RhbGlzIENsaWVudCBBdXRoZW50aWNhdGlvbiBDQSBHMzAeFw0yMDEwMjYxOTQx
+MjJaFw0yMTEwMjYxODQxMjJaMBoxGDAWBgNVBAMMD2RpYW5uZUBza29sbC5jYTCC
+ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALf0P/jWD9Ik6kc+vW8OGZvw
+kz7hAuMnIaO/BWlzVYCfcao5Yy4nZ2IERDJVn5ZCJ7mSsva0BComzvFad0M6FpzY
+G/RejmZKYF+PHEVPKbSFh2g0FlE/XWqH7XHw/LUS+kr5ZbydmV69BFkE9gZvW0Gw
+FXSvm9WOVucyom1MZNErrG6s1xBMnmDmwbIhWUPlCWCimmwJETfoCBfz7TDAVhaD
+DikSUN7PgZqpJgKyUo0uk8KvmgeHFAxwmNqrpMUqcA9TDJxygX8PCRMeShula8kD
+SM1VJpbfnDt179v/1aGc46NzDX1oWMSVmrF6hqD+FmVzBCck+WvinD3CFKggbdEC
+AwEAAaOCAbAwggGsMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUvpepqoS/gL8Q
+U30JMvnhLjIbz3cwfgYIKwYBBQUHAQEEcjBwMDsGCCsGAQUFBzAChi9odHRwOi8v
+Y2FjZXJ0LmFjdGFsaXMuaXQvY2VydHMvYWN0YWxpcy1hdXRjbGlnMzAxBggrBgEF
+BQcwAYYlaHR0cDovL29jc3AwOS5hY3RhbGlzLml0L1ZBL0FVVEhDTC1HMzAaBgNV
+HREEEzARgQ9kaWFubmVAc2tvbGwuY2EwRwYDVR0gBEAwPjA8BgYrgR8BGAEwMjAw
+BggrBgEFBQcCARYkaHR0cHM6Ly93d3cuYWN0YWxpcy5pdC9hcmVhLWRvd25sb2Fk
+MB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDBDBIBgNVHR8EQTA/MD2gO6A5
+hjdodHRwOi8vY3JsMDkuYWN0YWxpcy5pdC9SZXBvc2l0b3J5L0FVVEhDTC1HMy9n
+ZXRMYXN0Q1JMMB0GA1UdDgQWBBTKp2AtrJ2Uv2Gp8P7nnWQeFvb5eTAOBgNVHQ8B
+Af8EBAMCBaAwDQYJKoZIhvcNAQELBQADggIBAAJN7u/aFe6yLTgGJHxiPdPRv4mO
+fw3js/qAf2QTVjanMBhsvvvLFc9tprkfXOltRacYylEk3VqSDuMOwinAZvVx0g7d
+JU8jieYVAclS5bKx0LGfKWKVTEODXtNBSrxVfPQVjJLye4aXqWBfERHl7YRGKGUU
+LvaMJcYbJZI2IR4KYutGz5BIP19Jc6Lwyg2pju/2ifugYnYYpvLTwhQmaH3hf6Gx
+cL3OFoBfWZYLSBl22tdJURxTI0LOyIsR24eslxmazvGuXCpu6SR6bCc0aW4fJ5yk
+3dDlJ4PT8zdLGKxkeI+q/JXR+zZieqObkaQinhzibecq/UhqllnCb7aICuC7M8tU
+ZkIpYSHMzPJatM99/r4HcgVDpBiAJd5Py7uwuZ11PDqt6u7i469D+Z8fUWCs7I65
+BtfDWPOBo0liq3qm5qvchg+c2VF/9Bk//N/eJpmnzMsaAPLQSxnpVqxO3seA9+KM
++Ni1qjX8dN0L4/uMhLJY0Sz1L19h/8298zGrPgW6qL6kGHGJuimbHchPysLZN6x2
+dLcejDyrCnDFOnoG7pNkORKNF5RRHCfp1pCcEBIiCiZdT/YzNRYPzzSaAN3SgQpx
+VfA142WIeFE/0wqwDV2f9y9ng1AgZ6j6NoxY/SMBhvh1LMjgpW/n0K9zKXlHuA+H
+Zs2fapeVA9JAMrIG
+-----END CERTIFICATE-----
diff --git a/t/data/smime/keys/smime at example.com.crt b/t/data/smime/keys/smime at example.com.crt
new file mode 100644
index 0000000000..575c2ec094
--- /dev/null
+++ b/t/data/smime/keys/smime at example.com.crt
@@ -0,0 +1,33 @@
+-----BEGIN CERTIFICATE-----
+MIIFuzCCA6OgAwIBAgIJAJT5WGYIhaquMA0GCSqGSIb3DQEBCwUAMEkxCzAJBgNV
+BAYTAkNBMRAwDgYDVQQIDAdPbnRhcmlvMRUwEwYDVQQKDAxTa29sbCBGYW1pbHkx
+ETAPBgNVBAMMCHNrb2xsLmNhMB4XDTIwMTExNzE1MzczOFoXDTMwMTExNTE1Mzcz
+OFowYDELMAkGA1UEBhMCQ0ExEDAOBgNVBAgMB09udGFyaW8xDzANBgNVBAcMBk90
+dGF3YTEVMBMGA1UECgwMU2tvbGwgRmFtaWx5MRcwFQYDVQQDDA5NYWRhbWUgRXhh
+bXBsZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAL9JC7y84m48WknJ
+jCwLLjryWTWHgT/p3WD1TwIh4cEvkduy10OIcTh0GEwV5SvMakN3o3Zi570QvMBa
+pMMvgnve5T7ypzAApyCDv1xawx3xI3nv9qWkOyDWsV9JcMP4M6ZzSOuwQkTlo9Ss
+MC0MqT1MEjD9/yS8dfbp2NvP0D9BKI7k0LUbpRStyMknCu+Iu+X0KDfZ+HjMJ2Eo
+gt41WyuiVSqYkg/n5chSIG/7Uye/mj1rWpJ5ftK8Q+HSF4X1vSjzhG92qMkg4ER3
+eyMzcy+KT5eAEEwKQzmN2NIESdqS4hcYXtvDQ179RJzZLvbYR981Jfn4bDiMXXel
+UMA3hQ//7y7AAJk2ZvcKLvi2Zur7S+WoYrYmzAnqc/4tiJihnLg9vwXLChnEwzgM
+3atesBYanhTeDw1wVDcIVVcufWouI94T16f3sF1hkdElb+4Zbd2k6L52aaBP7QOA
+rFiiUjx3vozCBjfa1ptTYhFI+TmveueDz+pG5+aaxTLsDCk9XJyjIYiWPVugQK8z
+6pVVtk5zzn7s6M7nABaHt52HzdJewkoEohBIYUhGVe+IHmtCU6p7B6idMxpel1nu
+lJWMV9XT6q4z3ThCMoFrFqtQvY8PePDdFKthesEwTQ1I8vq1silRXq02xWthpOmX
+ZLpLIeOKonzueDHAKTzS9KDcr5sPAgMBAAGjgY4wgYswCQYDVR0TBAIwADALBgNV
+HQ8EBAMCBeAwHAYDVR0RBBUwE4ERc21pbWVAZXhhbXBsZS5jb20wHQYDVR0OBBYE
+FM7e9xTZpgqeryoHDwbCSJ+492FCMB8GA1UdIwQYMBaAFKb+i2ED4UPuop6r4fZK
+w5u8nMekMBMGA1UdJQQMMAoGCCsGAQUFBwMEMA0GCSqGSIb3DQEBCwUAA4ICAQAJ
+Y/ECUbBRh89w919Wj57oxclHQXTMYAqIvGSpwhhztCPW0O6EsFxtrvkAFhUYIdXr
+ayQw1s1yqJEcKQR1Yso0B3d4wA4appR+L01pThnUYYSmAkw3b4Yxs02uDH2A9nny
+dQd/XksAEBAycYC1U0d+dIAWtWo0Y8Ohn1Mek5uz324pzvILleBvLxOjUZlhrA/w
+X1KlHDofQBVAtYpMBTHRkrorPurGeXfapBJwxYDyL5Hv7VtSIq0jW9He8TfN5LVf
+6HP6AS0JFs3MMC7/wi13FwxjHsKlFbCvo6RW6xESxjatBow+1ZckQxQNYKzLsXtL
+96cZMCUuQPIRFQAjaY7ENQ/tZAIA4g+safoevMBLR7fk7Kn9kIOexYLPHsZ6JQx9
+6wF/rNZZatwFGvr2Xn2s6MkcWGSTlrQ78FK1Pq3e3gbFAgFKc95HBE6+eP2srLrm
+Bneurx6VPeatXfcgOkCL65aLip55inSsYjjBWM6zw97WF8Jq3TtnX7ZVq1/xjEn5
+f+FwEvgCabpptqPheMjwwAJ233bAaIz4OFy6fYBIzuwHDhw0HYicYjzXgnHld8wP
+qDwCdQmX8AD1qQXNQQscSHLMcc+L7Fd/C8Xo9f4BWf89iMUCfcqFLVZOLkkzNVmV
+4blIYQkFYbNPHq7QIwWxNScU5EMuAX+rRY0GRPZVIQ==
+-----END CERTIFICATE-----

commit 6a8ea9f51aee3002eb9a6361f1fe22d8c4130376
Merge: c7c2988f7b eff063c248
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Thu Nov 19 01:23:54 2020 +0800

    Merge branch '4.4/provide-more-public-key-details' into 4.4-trunk

diff --cc share/html/Elements/CryptStatus
index c43e0313a1,681387fb41..a608d7ca62
--- a/share/html/Elements/CryptStatus
+++ b/share/html/Elements/CryptStatus
@@@ -64,16 -63,64 +64,66 @@@ foreach ( $Message->SplitHeaders ) 
      }
  
      $needs_unsigned_warning = 0 if /^X-RT-Incoming-Signature:/;
 -
 -    # if this is not set, then the email is generated by RT, and so we don't
 -    # need "email is unsigned" warnings
 -    $needs_unsigned_warning = 0 if not /^Received:/;
 +    $seen_received = 1 if /^Received:/;
  }
  
 +# If there are no Received: headers, then the email
 +# is generated by RT and we don't need "email is unsigned"
 +# warnings.
 +$needs_unsigned_warning = 0 unless $seen_received;
 +
  return unless @runs or $needs_unsigned_warning;
  
+ # Returns string representing a date in UNIX timestamp format
+ sub DisplayDate {
+     my ($ts) = @_;
+     my $date = RT::Date->new($session{'CurrentUser'});
+     $date->Unix($ts);
+     return $date->AsString();
+ }
+ 
+ # Generate a little tooltip with additional info about a signature
+ sub VerifyTooltip {
+     my ($protocol, $line) = @_;
+     my $tooltip = '';
+     $tooltip .= "\n" . loc('Fingerprint') . ': ' . $line->{Fingerprint} if $line->{Fingerprint};                                # GNUPG
+     $tooltip .= "\n" . loc('Signature Created') . ': ' . DisplayDate($line->{Timestamp}) if $line->{Timestamp};                 # GNUPG
+     $tooltip .= "\n" . loc('Signer') . ': ' . $line->{UserString} if $line->{UserString};                                       # SMIME
+     $tooltip .= "\n" . loc('Issuer') . ': ' . $line->{Issuer} if $line->{Issuer};                                               # SMIME
+     $tooltip .= "\n" . loc('Certificate Created') . ': ' . DisplayDate($line->{CreatedTimestamp}) if $line->{CreatedTimestamp}; # SMIME
+     if ($protocol eq 'SMIME') {
+         $tooltip .= "\n" . loc('Certificate Expires') . ': ';
+     } else {
+         $tooltip .= "\n" . loc('Key Expires') . ': ';
+     }
+     if ($line->{ExpireTimestamp}) {
+         $tooltip .= DisplayDate($line->{ExpireTimestamp});
+     } else {
+         $tooltip .= loc('Never');
+     }
+     $tooltip .= "\n" . loc('Public Key Algorithm') . ': ' . $line->{PubkeyAlgoName} if $line->{PubkeyAlgoName};                 # GNUPG
+     $tooltip .= "\n" . loc('Hash Algorithm') . ': ' . $line->{HashAlgoName} if $line->{HashAlgoName};                           # GNUPG
+     $tooltip =~ s/^\s+//;
+     return $tooltip;
+ }
+ 
+ # Generate a download link for the public key
+ sub KeyDownloadLink {
+     my ($protocol, $line) = @_;
+     my $txt = '';
+     if ($protocol eq 'GnuPG') {
+         if ($line->{Fingerprint} && $line->{Fingerprint} !~ /[^0-9A-F]/i) {
+             $txt = '<a href="' . RT->Config->Get('WebPath') . '/Crypt/GetGPGPubkey.html?Fingerprint=' . $line->{Fingerprint} . '"> ' . loc('(Download Public Key)') . '</a>';
+         }
+     }
+ 
+     # There isn't really a feasible way to download the S/MIME
+     # certificate, unfortunately.  However, since RT makes the
+     # original message available, the S/MIME cert could be
+     # extracted from that if necessary.
+     return $txt;
+ }
+ 
  my $reverify_cb = sub {
      my $top = shift;
  

-----------------------------------------------------------------------


More information about the rt-commit mailing list