[Rt-commit] rt branch, 4.0-trunk, updated. rt-4.0.24-56-g27eda90

Shawn Moore shawn at bestpractical.com
Wed Jul 12 10:26:54 EDT 2017


The branch, 4.0-trunk has been updated
       via  27eda905a3df2379bb9b8ddbb0ccf2ead4749383 (commit)
       via  94f391df9f714fd6466fac907643d7aa90902532 (commit)
       via  358034bcbd6f2725915963e4d38acede402cb875 (commit)
       via  5bea1fce0d936332ed913ad8ac7d2385644675cb (commit)
       via  f25c207ce9fb2e43429df59e79623f25dcde46a6 (commit)
       via  a394f9888e54577fa195724ea1b3d6985ac06e8e (commit)
      from  12572858ebbdba79f7223186d379e02e1f1f6a71 (commit)

Summary of changes:
 lib/RT/User.pm           |  33 +++--
 lib/RT/Util.pm           |  66 +++++++++
 t/api/constant_time_eq.t |  89 ++++++++++++
 t/api/user.t             |  41 +++++-
 t/data/input/blns.txt    | 362 +++++++++++++++++++++++++++++++++++++++++++++++
 5 files changed, 582 insertions(+), 9 deletions(-)
 create mode 100644 t/api/constant_time_eq.t
 create mode 100644 t/data/input/blns.txt

- Log -----------------------------------------------------------------
commit a394f9888e54577fa195724ea1b3d6985ac06e8e
Author: Aaron Kondziela <aaron at bestpractical.com>
Date:   Tue Jan 24 18:20:28 2017 -0500

    Fix timing sidechannel vulnerability in password checking
    
    "eq" operators for comparing against passwords are replaced by a new
    RT::Util::constant_time_eq to resolve a timing sidechannel vulnerability.
    
    This addresses CVE-2017-5361.
    
    Fixes: T#161960

diff --git a/lib/RT/User.pm b/lib/RT/User.pm
index b089b7f..c5e0e54 100644
--- a/lib/RT/User.pm
+++ b/lib/RT/User.pm
@@ -82,6 +82,7 @@ use RT::Principals;
 use RT::ACE;
 use RT::Interface::Email;
 use Text::Password::Pronounceable;
+use RT::Util;
 
 sub _OverlayAccessible {
     {
@@ -945,7 +946,10 @@ sub IsPassword {
         # If it's a new-style (>= RT 4.0) password, it starts with a '!'
         my (undef, $method, $salt, undef) = split /!/, $stored;
         if ($method eq "sha512") {
-            return $self->_GeneratePassword_sha512($value, $salt) eq $stored;
+            return 0 unless RT::Util::constant_time_eq(
+                $self->_GeneratePassword_sha512($value, $salt),
+                $stored
+            );
         } else {
             $RT::Logger->warn("Unknown hash method $method");
             return 0;
@@ -955,16 +959,28 @@ sub IsPassword {
         my $hash = MIME::Base64::decode_base64($stored);
         # Decoding yields 30 byes; first 4 are the salt, the rest are substr(SHA256,0,26)
         my $salt = substr($hash, 0, 4, "");
-        return 0 unless substr(Digest::SHA::sha256($salt . Digest::MD5::md5(Encode::encode( "UTF-8", $value))), 0, 26) eq $hash;
+        return 0 unless RT::Util::constant_time_eq(
+            substr(Digest::SHA::sha256($salt . Digest::MD5::md5(Encode::encode( "UTF-8", $value))), 0, 26),
+            $hash
+        );
     } elsif (length $stored == 32) {
         # Hex nonsalted-md5
-        return 0 unless Digest::MD5::md5_hex(Encode::encode( "UTF-8", $value)) eq $stored;
+        return 0 unless RT::Util::constant_time_eq(
+            Digest::MD5::md5_hex(Encode::encode( "UTF-8", $value)),
+            $stored
+        );
     } elsif (length $stored == 22) {
         # Base64 nonsalted-md5
-        return 0 unless Digest::MD5::md5_base64(Encode::encode( "UTF-8", $value)) eq $stored;
+        return 0 unless RT::Util::constant_time_eq(
+            Digest::MD5::md5_base64(Encode::encode( "UTF-8", $value)),
+            $stored
+        );
     } elsif (length $stored == 13) {
         # crypt() output
-        return 0 unless crypt(Encode::encode( "UTF-8", $value), $stored) eq $stored;
+        return 0 unless RT::Util::constant_time_eq(
+            crypt(Encode::encode( "UTF-8", $value), $stored),
+            $stored
+        );
     } else {
         $RT::Logger->warning("Unknown password form");
         return 0;
@@ -1060,19 +1076,20 @@ sub GenerateAuthString {
 
 =head3 ValidateAuthString
 
-Takes auth string and protected string. Returns true is protected string
+Takes auth string and protected string. Returns true if protected string
 has been protected by user's L</AuthToken>. See also L</GenerateAuthString>.
 
 =cut
 
 sub ValidateAuthString {
     my $self = shift;
-    my $auth_string = shift;
+    my $auth_string_to_validate = shift;
     my $protected = shift;
 
     my $str = Encode::encode( "UTF-8", $self->AuthToken . $protected );
+    my $valid_auth_string = substr(Digest::MD5::md5_hex($str),0,16);
 
-    return $auth_string eq substr(Digest::MD5::md5_hex($str),0,16);
+    return RT::Util::constant_time_eq( $auth_string_to_validate, $valid_auth_string );
 }
 
 =head2 SetDisabled
diff --git a/lib/RT/Util.pm b/lib/RT/Util.pm
index 8268b21..9fc24c5 100644
--- a/lib/RT/Util.pm
+++ b/lib/RT/Util.pm
@@ -54,6 +54,8 @@ use warnings;
 use base 'Exporter';
 our @EXPORT = qw/safe_run_child mime_recommended_filename/;
 
+use Encode qw/encode/;
+
 sub safe_run_child (&) {
     my $our_pid = $$;
 
@@ -150,6 +152,58 @@ sub assert_bytes {
 }
 
 
+=head2 C<constant_time_eq($a, $b)>
+
+Compares two strings for equality in constant-time. Replacement for the C<eq>
+operator designed to avoid timing side-channel vulnerabilities. Returns zero
+or one.
+
+This is intended for use in cryptographic subsystems for comparing well-formed
+data such as hashes - not for direct use with user input or as a general
+replacement for the C<eq> operator.
+
+The two string arguments B<MUST> be of equal length. If the lengths differ,
+this function will call C<die()>, as proceeding with execution would create
+a timing vulnerability. Length is defined by characters, not bytes.
+
+This code has been tested to do what it claims. Do not change it without
+thorough statistical timing analysis to validate the changes.
+
+Added to resolve CVE-2017-5361
+
+For more on timing attacks, see this Wikipedia article:
+B<https://en.wikipedia.org/wiki/Timing_attack>
+
+=cut
+
+sub constant_time_eq {
+    my ($a, $b) = @_;
+
+    my $result = 0;
+
+    # generic error message avoids potential information leaks
+    my $generic_error = "Cannot compare values";
+    die $generic_error unless defined $a and defined $b;
+    die $generic_error unless length $a == length $b;
+    die $generic_error if ref($a) or ref($b);
+
+    for (my $i = 0; $i < length($a); $i++) {
+        my $a_char = substr($a, $i, 1);
+        my $b_char = substr($b, $i, 1);
+
+        # encode() is set to die on malformed
+        my @a_octets = unpack("C*", encode('UTF-8', $a_char, Encode::FB_CROAK));
+        my @b_octets = unpack("C*", encode('UTF-8', $b_char, Encode::FB_CROAK));
+        die $generic_error if (scalar @a_octets) != (scalar @b_octets);
+
+        for (my $j = 0; $j < scalar @a_octets; $j++) {
+            $result |= $a_octets[$j] ^ $b_octets[$j];
+        }
+    }
+    return 0 + not $result;
+}
+
+
 RT::Base->_ImportOverlays();
 
 1;
diff --git a/t/api/constant_time_eq.t b/t/api/constant_time_eq.t
new file mode 100644
index 0000000..4256dd2
--- /dev/null
+++ b/t/api/constant_time_eq.t
@@ -0,0 +1,81 @@
+use utf8;
+use warnings;
+use strict;
+use RT;
+use RT::Test tests => undef;
+use Test::Exception;
+
+use_ok "RT::Util";
+
+my ($a, $b) = (1, 2);
+
+# die-worthy error conditions
+_dies_ok (eval { RT::Util::constant_time_eq(undef, undef) }, 'Should die: both args undefined');
+_dies_ok (eval { RT::Util::constant_time_eq('', undef)    }, 'Should die: first arg undefined');
+_dies_ok (eval { RT::Util::constant_time_eq(undef, '')    }, 'Should die: second arg undefined');
+_dies_ok (eval { RT::Util::constant_time_eq('', 'a')      }, 'Should die: empty string and non-empty string (different length)');
+_dies_ok (eval { RT::Util::constant_time_eq('a', '')      }, 'Should die: empty string and non-empty string (different length)');
+_dies_ok (eval { RT::Util::constant_time_eq('abcde', 'abcdef')    }, 'strings different lengths should die');
+_dies_ok (eval { RT::Util::constant_time_eq('ff', 'ff')    }, 'string "ff" and ligature "ff" should die (different lengths)');
+_dies_ok (eval { RT::Util::constant_time_eq(\$a, '')      }, 'First arg ref should die');
+_dies_ok (eval { RT::Util::constant_time_eq('', \$b)      }, 'Second arg ref should die');
+_dies_ok (eval { RT::Util::constant_time_eq(\$a, \$a)     }, 'Both args same ref should die');
+
+# Check unicode chars
+_lives_and (eval { RT::Util::constant_time_eq('', '') },       1, 'both args empty strings is true');
+_lives_and (eval { RT::Util::constant_time_eq('a', 'a') },     1, 'both args one-byte chars is true');
+_lives_and (eval { RT::Util::constant_time_eq('þ', 'þ') },     1, 'both args two-byte utf8 chars is true');
+_dies_ok   (eval { RT::Util::constant_time_eq('þ', 'þ') },       'a two-byte utf8 char and its mojibake dies');
+_lives_and (eval { RT::Util::constant_time_eq('þ', 'ÿ') },     0, 'two-byte utf8 chars which differ by one bit');
+_lives_and (eval { RT::Util::constant_time_eq('þ', '¾') },     0, 'two-byte utf8 chars which differ by one bit');
+_lives_and (eval { RT::Util::constant_time_eq('þ', '¿') },     0, 'two-byte utf8 chars which differ by two bits');
+_dies_ok   (eval { RT::Util::constant_time_eq('þ', 'xx') },       'two-byte utf8 and two one-byte chars is false');
+_lives_and (eval { RT::Util::constant_time_eq('試', '試') },   1, 'both args three-byte utf8 chars is true');
+_dies_ok   (eval { RT::Util::constant_time_eq('試', 'xxx') },     'three-byte utf8 and three one-byte chars is false');
+_lives_and (eval { RT::Util::constant_time_eq('😎', '😎') },   1, 'both args four-byte utf8 chars is true');
+_dies_ok   (eval { RT::Util::constant_time_eq('😎', 'xxxx') },    'four-byte utf8 and four one-byte chars is false');
+_lives_and (eval { RT::Util::constant_time_eq('😎✈️'x1024, '😎✈️'x1024) }, 1, 'both long strings of utf8 chars is true');
+
+# Longer strings
+_lives_and (eval { RT::Util::constant_time_eq('a'x4096, 'a'x4096) },             1, 'both args equal long strings is true');
+_lives_and (eval { RT::Util::constant_time_eq('a'x4096 . 'c', 'a'x4096 . 'b') }, 0, 'both args unequal long strings is false');
+
+# Numeric values would be stringified before comparison. This should never
+# be used this way, but if so the behaviour should remain consistent.
+_lives_and (eval { RT::Util::constant_time_eq(0, 0) },                 1, 'both args equal zero ints is true');
+_lives_and (eval { RT::Util::constant_time_eq(123456789, 123456789) }, 1, 'both args equal long ints is true');
+_lives_and (eval { RT::Util::constant_time_eq(123.456, 123.456) },     1, 'both args equal floats is true');
+_lives_and (eval { RT::Util::constant_time_eq(0, 1) },                 0, 'both args unequal ints is false');
+
+# Big List of Naughty Strings (https://github.com/minimaxir/big-list-of-naughty-strings)
+my $f;
+open ($f, '<', 't/data/input/blns.txt') or die "can't open blns.txt";
+my $line = 0;
+while(<$f>) {
+    $line++;
+    next if length($_) == 0;
+    next if $_ =~ /^#\w/;
+    my $string = $_;
+    _lives_and (eval { RT::Util::constant_time_eq($string, $string) }, 1, "Big List of Naughty String, blns.txt line $line");
+}
+close $f;
+
+# TODO: statistical analysis of the timing performance of RT::Util::constant_time_eq
+# This is probably very difficult to do and have work across myriad systems that could
+# end up running RT. This note serves to remind you to test manually if you change
+# the function.
+
+done_testing();
+
+# Helpers inspired by Test::Exception
+
+sub _lives_and {
+    my ($t, $r, $message) = @_;
+    is ($@, '', $message);
+    is ($t, $r, $message);
+}
+
+sub _dies_ok {
+    my ($t, $message) = @_;
+    isnt ($@, '', $message);
+}
diff --git a/t/api/user.t b/t/api/user.t
index e6b891f..e5f43fe 100644
--- a/t/api/user.t
+++ b/t/api/user.t
@@ -2,7 +2,7 @@
 use strict;
 use warnings;
 use RT;
-use RT::Test tests => 111;
+use RT::Test tests => undef;
 
 
 {
@@ -335,3 +335,42 @@ ok($rqv, "Revoked the right successfully - $rqm");
 
 }
 
+{
+# Test the auth token mechanisms used for iCal feeds
+
+# Set up a test user
+my $user = RT::Test->load_or_create_user(Name => "AuthTokenTestUser");
+
+ok($user && $user->id, "Create AuthTokenTestUser");
+
+# Generate an auth token
+my $token = $user->GenerateAuthToken();
+
+ok($token, "GenerateAuthToken()");
+unlike($user->AuthToken, qr/[^0-9A-Fa-f]/, "GenerateAuthToken() returns hex digits");
+
+# Protect a string with an auth token
+my $protected_string = "The quick brown RT jumped over the lazy [redacted competitor's product]";
+my $auth_string = $user->GenerateAuthString($protected_string);
+
+ok($auth_string, "GenerateAuthString() returns a string");
+unlike($auth_string, qr/[^0-9A-Fa-f]/, "GenerateAuthString() returns hex digits");
+
+# Validate a string is protected by an auth token
+ok($user->ValidateAuthString($auth_string, $protected_string),
+   "ValidateAuthString() returns true when string is protected by token");
+
+my $bad_protected_string = "This string is not protected by the token";
+
+ok( ! $user->ValidateAuthString($auth_string, $bad_protected_string),
+   "ValidateAuthString() returns false with invalid string and valid token");
+
+my $bad_auth_string = "L04DD3ADB33FC0DE";
+ok( ! $user->ValidateAuthString($bad_auth_string, $protected_string),
+   "ValidateAuthString() returns false with valid string and invalid token");
+
+ok( ! $user->ValidateAuthString($bad_auth_string, $bad_protected_string),
+   "ValidateAuthString() returns false with invalid string and invalid token");
+}
+
+done_testing();
diff --git a/t/data/input/blns.txt b/t/data/input/blns.txt
new file mode 100644
index 0000000..90c0b29
--- /dev/null
+++ b/t/data/input/blns.txt
@@ -0,0 +1,362 @@
+# The MIT License (MIT)
+
+# Copyright (c) 2015 Max Woolf
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+
+# See: https://github.com/minimaxir/big-list-of-naughty-strings
+
+# Trimmed down for use in RT
+
+#	Reserved Strings
+#
+#	Strings which may be used elsewhere in code
+
+undefined
+undef
+null
+NULL
+(null)
+nil
+NIL
+true
+false
+True
+False
+TRUE
+FALSE
+None
+hasOwnProperty
+\
+\\
+
+#	Numeric Strings
+#
+#	Strings which can be interpreted as numeric
+
+0
+1
+1.00
+$1.00
+1/2
+1E2
+1E02
+1E+02
+-1
+-1.00
+-$1.00
+-1/2
+-1E2
+-1E02
+-1E+02
+1/0
+0/0
+-2147483648/-1
+-9223372036854775808/-1
+-0
+-0.0
++0
++0.0
+0.00
+0..0
+.
+0.0.0
+0,00
+0,,0
+,
+0,0,0
+0.0/0
+1.0/0.0
+0.0/0.0
+1,0/0,0
+0,0/0,0
+--1
+-
+-.
+-,
+999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999
+NaN
+Infinity
+-Infinity
+INF
+1#INF
+-1#IND
+1#QNAN
+1#SNAN
+1#IND
+0x0
+0xffffffff
+0xffffffffffffffff
+0xabad1dea
+123456789012345678901234567890123456789
+1,000.00
+1 000.00
+1'000.00
+1,000,000.00
+1 000 000.00
+1'000'000.00
+1.000,00
+1 000,00
+1'000,00
+1.000.000,00
+1 000 000,00
+1'000'000,00
+01000
+08
+09
+2.2250738585072011e-308
+
+#	Special Characters
+#
+# ASCII punctuation.  All of these characters may need to be escaped in some
+# contexts.  Divided into three groups based on (US-layout) keyboard position.
+
+,./;'[]\-=
+<>?:"{}|_+
+!@#$%^&*()`~
+
+# Non-whitespace C0 controls: U+0001 through U+0008, U+000E through U+001F,
+# and U+007F (DEL)
+# Often forbidden to appear in various text-based file formats (e.g. XML),
+# or reused for internal delimiters on the theory that they should never
+# appear in input.
+# The next line may appear to be blank or mojibake in some viewers.
+



+
+# Non-whitespace C1 controls: U+0080 through U+0084 and U+0086 through U+009F.
+# Commonly misinterpreted as additional graphic characters.
+# The next line may appear to be blank, mojibake, or dingbats in some viewers.
+€‚ƒ„†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ
+
+# Whitespace: all of the characters with category Zs, Zl, or Zp (in Unicode
+# version 8.0.0), plus U+0009 (HT), U+000B (VT), U+000C (FF), U+0085 (NEL),
+# and U+200B (ZERO WIDTH SPACE), which are in the C categories but are often
+# treated as whitespace in some contexts.
+# This file unfortunately cannot express strings containing
+# U+0000, U+000A, or U+000D (NUL, LF, CR).
+# The next line may appear to be blank or mojibake in some viewers.
+# The next line may be flagged for "trailing whitespace" in some viewers.
+	

 
             ​

   
+
+# Unicode additional control characters: all of the characters with
+# general category Cf (in Unicode 8.0.0).
+# The next line may appear to be blank or mojibake in some viewers.
+­؀؁؂؃؄؅؜۝܏᠎​‌‍‎‏‪‫‬‭‮⁠⁡⁢⁣⁤⁦⁧⁨⁩𑂽𛲠𛲡𛲢𛲣𝅳𝅴𝅵𝅶𝅷𝅸𝅹𝅺󠀁󠀠󠀡󠀢󠀣󠀤󠀥󠀦󠀧󠀨󠀩󠀪󠀫󠀬󠀭󠀮󠀯󠀰󠀱󠀲󠀳󠀴󠀵󠀶󠀷󠀸󠀹󠀺󠀻󠀼󠀽󠀾󠀿󠁀󠁁󠁂󠁃󠁄󠁅󠁆󠁇󠁈󠁉󠁊󠁋󠁌󠁍󠁎󠁏󠁐󠁑󠁒󠁓󠁔󠁕󠁖󠁗󠁘󠁙󠁚󠁛󠁜󠁝󠁞󠁟󠁠󠁡󠁢󠁣󠁤󠁥󠁦󠁧󠁨󠁩󠁪󠁫󠁬󠁭󠁮󠁯󠁰󠁱󠁲󠁳󠁴󠁵󠁶󠁷󠁸󠁹󠁺󠁻󠁼󠁽󠁾󠁿
+
+# "Byte order marks", U+FEFF and U+FFFE, each on its own line.
+# The next two lines may appear to be blank or mojibake in some viewers.
+
+￾
+
+#	Unicode Symbols
+#
+#	Strings which contain common unicode symbols (e.g. smart quotes)
+
+Ω≈ç√∫˜µ≤≥÷
+åß∂ƒ©˙∆˚¬…æ
+œ∑´®†¥¨ˆøπ“‘
+¡™£¢∞§¶•ªº–≠
+¸˛Ç◊ı˜Â¯˘¿
+ÅÍÎÏ˝ÓÔÒÚÆ☃
+Œ„´‰ˇÁ¨ˆØ∏”’
+`⁄€‹›fifl‡°·‚—±
+⅛⅜⅝⅞
+ЁЂЃЄЅІЇЈЉЊЋЌЍЎЏАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюя
+٠١٢٣٤٥٦٧٨٩
+
+#	Unicode Subscript/Superscript/Accents
+#
+#	Strings which contain unicode subscripts/superscripts; can cause rendering issues
+
+⁰⁴⁵
+₀₁₂
+⁰⁴⁵₀₁₂
+ด้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็ ด้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็ ด้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็
+
+#	Quotation Marks
+#
+#	Strings which contain misplaced quotation marks; can cause encoding errors
+
+'
+"
+''
+""
+'"'
+"''''"'"
+"'"'"''''"
+<foo val=“bar” />
+<foo val=“bar” />
+<foo val=”bar“ />
+<foo val=`bar' />
+
+#	Two-Byte Characters
+#
+#	Strings which contain two-byte characters: can cause rendering issues or character-length issues
+
+田中さんにあげて下さい
+パーティーへ行かないか
+和製漢語
+部落格
+사회과학원 어학연구소
+찦차를 타고 온 펲시맨과 쑛다리 똠방각하
+社會科學院語學研究所
+울란바토르
+𠜎𠜱𠝹𠱓𠱸𠲖𠳏
+
+#	Changing length when lowercased
+#
+#	Characters which increase in length (2 to 3 bytes) when lowercased
+#	Credit: https://twitter.com/jifa/status/625776454479970304
+
+Ⱥ
+Ⱦ
+
+#	Japanese Emoticons
+#
+#	Strings which consists of Japanese-style emoticons which are popular on the web
+
+ヽ༼ຈل͜ຈ༽ノ ヽ༼ຈل͜ຈ༽ノ
+(。◕ ∀ ◕。)
+`ィ(´∀`∩
+__ロ(,_,*)
+・( ̄∀ ̄)・:*:
+゚・✿ヾ╲(。◕‿◕。)╱✿・゚
+,。・:*:・゜’( ☻ ω ☻ )。・:*:・゜’
+(╯°□°)╯︵ ┻━┻)
+(ノಥ益ಥ)ノ ┻━┻
+┬─┬ノ( º _ ºノ)
+( ͡° ͜ʖ ͡°)
+
+#	Emoji
+#
+#	Strings which contain Emoji; should be the same behavior as two-byte characters, but not always
+
+😍
+👩🏽
+👾 🙇 💁 🙅 🙆 🙋 🙎 🙍
+🐵 🙈 🙉 🙊
+❤️ 💔 💌 💕 💞 💓 💗 💖 💘 💝 💟 💜 💛 💚 💙
+✋🏿 💪🏿 👐🏿 🙌🏿 👏🏿 🙏🏿
+🚾 🆒 🆓 🆕 🆖 🆗 🆙 🏧
+0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ 🔟
+
+#       Regional Indicator Symbols
+#
+#       Regional Indicator Symbols can be displayed differently across
+#       fonts, and have a number of special behaviors
+
+🇺🇸🇷🇺🇸 🇦🇫🇦🇲🇸
+🇺🇸🇷🇺🇸🇦🇫🇦🇲
+🇺🇸🇷🇺🇸🇦
+
+#	Unicode Numbers
+#
+#	Strings which contain unicode numbers; if the code is localized, it should see the input as numeric
+
+123
+١٢٣
+
+#	Right-To-Left Strings
+#
+#	Strings which contain text that should be rendered RTL if possible (e.g. Arabic, Hebrew)
+
+ثم نفس سقطت وبالتحديد،, جزيرتي باستخدام أن دنو. إذ هنا؟ الستار وتنصيب كان. أهّل ايطاليا، بريطانيا-فرنسا قد أخذ. سليمان، إتفاقية بين ما, يذكر الحدود أي بعد, معاملة بولندا، الإطلاق عل إيو.
+בְּרֵאשִׁית, בָּרָא אֱלֹהִים, אֵת הַשָּׁמַיִם, וְאֵת הָאָרֶץ
+הָיְתָהtestالصفحات التّحول
+﷽
+ﷺ
+مُنَاقَشَةُ سُبُلِ اِسْتِخْدَامِ اللُّغَةِ فِي النُّظُمِ الْقَائِمَةِ وَفِيم يَخُصَّ التَّطْبِيقَاتُ الْحاسُوبِيَّةُ، 
+
+#	Trick Unicode
+#
+#	Strings which contain unicode with unusual properties (e.g. Right-to-left override) (c.f. http://www.unicode.org/charts/PDF/U2000.pdf)
+
+‪‪test‪
+‫test‫
+
test

+test⁠test‫
+⁦test⁧
+
+#	Zalgo Text
+#
+#	Strings which contain "corrupted" text. The corruption will not appear in non-HTML text, however. (via http://www.eeemo.net)
+
+Ṱ̺̺̕o͞ ̷i̲̬͇̪͙n̝̗͕v̟̜̘̦͟o̶̙̰̠kè͚̮̺̪̹̱̤ ̖t̝͕̳̣̻̪͞h̼͓̲̦̳̘̲e͇̣̰̦̬͎ ̢̼̻̱̘h͚͎͙̜̣̲ͅi̦̲̣̰̤v̻͍e̺̭̳̪̰-m̢iͅn̖̺̞̲̯̰d̵̼̟͙̩̼̘̳ ̞̥̱̳̭r̛̗̘e͙p͠r̼̞̻̭̗e̺̠̣͟s̘͇̳͍̝͉e͉̥̯̞̲͚̬͜ǹ̬͎͎̟̖͇̤t͍̬̤͓̼̭͘ͅi̪̱n͠g̴͉ ͏͉ͅc̬̟h͡a̫̻̯͘o̫̟̖͍̙̝͉s̗̦̲.̨̹͈̣
+̡͓̞ͅI̗̘̦͝n͇͇͙v̮̫ok̲̫̙͈i̖͙̭̹̠̞n̡̻̮̣̺g̲͈͙̭͙̬͎ ̰t͔̦h̞̲e̢̤ ͍̬̲͖f̴̘͕̣è͖ẹ̥̩l͖͔͚i͓͚̦͠n͖͍̗͓̳̮g͍ ̨o͚̪͡f̘̣̬ ̖̘͖̟͙̮c҉͔̫͖͓͇͖ͅh̵̤̣͚͔á̗̼͕ͅo̼̣̥s̱͈̺̖̦̻͢.̛̖̞̠̫̰
+̗̺͖̹̯͓Ṯ̤͍̥͇͈h̲́e͏͓̼̗̙̼̣͔ ͇̜̱̠͓͍ͅN͕͠e̗̱z̘̝̜̺͙p̤̺̹͍̯͚e̠̻̠͜r̨̤͍̺̖͔̖̖d̠̟̭̬̝͟i̦͖̩͓͔̤a̠̗̬͉̙n͚͜ ̻̞̰͚ͅh̵͉i̳̞v̢͇ḙ͎͟-҉̭̩̼͔m̤̭̫i͕͇̝̦n̗͙ḍ̟ ̯̲͕͞ǫ̟̯̰̲͙̻̝f ̪̰̰̗̖̭̘͘c̦͍̲̞͍̩̙ḥ͚a̮͎̟̙͜ơ̩̹͎s̤.̝̝ ҉Z̡̖̜͖̰̣͉̜a͖̰͙̬͡l̲̫̳͍̩g̡̟̼̱͚̞̬ͅo̗͜.̟
+̦H̬̤̗̤͝e͜ ̜̥̝̻͍̟́w̕h̖̯͓o̝͙̖͎̱̮ ҉̺̙̞̟͈W̷̼̭a̺̪͍į͈͕̭͙̯̜t̶̼̮s̘͙͖̕ ̠̫̠B̻͍͙͉̳ͅe̵h̵̬͇̫͙i̹͓̳̳̮͎̫̕n͟d̴̪̜̖ ̰͉̩͇͙̲͞ͅT͖̼͓̪͢h͏͓̮̻e̬̝̟ͅ ̤̹̝W͙̞̝͔͇͝ͅa͏͓͔̹̼̣l̴͔̰̤̟͔ḽ̫.͕
+Z̮̞̠͙͔ͅḀ̗̞͈̻̗Ḷ͙͎̯̹̞͓G̻O̭̗̮
+
+#	Unicode Upsidedown
+#
+#	Strings which contain unicode with an "upsidedown" effect (via http://www.upsidedowntext.com)
+
+˙ɐnbᴉlɐ ɐuƃɐɯ ǝɹolop ʇǝ ǝɹoqɐl ʇn ʇunpᴉpᴉɔuᴉ ɹodɯǝʇ poɯsnᴉǝ op pǝs 'ʇᴉlǝ ƃuᴉɔsᴉdᴉpɐ ɹnʇǝʇɔǝsuoɔ 'ʇǝɯɐ ʇᴉs ɹolop ɯnsdᴉ ɯǝɹo˥
+00˙Ɩ$-
+
+#	Unicode font
+#
+#	Strings which contain bold/italic/etc. versions of normal characters
+
+The quick brown fox jumps over the lazy dog
+𝐓𝐡𝐞 𝐪𝐮𝐢𝐜𝐤 𝐛𝐫𝐨𝐰𝐧 𝐟𝐨𝐱 𝐣𝐮𝐦𝐩𝐬 𝐨𝐯𝐞𝐫 𝐭𝐡𝐞 𝐥𝐚𝐳𝐲 𝐝𝐨𝐠
+𝕿𝖍𝖊 𝖖𝖚𝖎𝖈𝖐 𝖇𝖗𝖔𝖜𝖓 𝖋𝖔𝖝 𝖏𝖚𝖒𝖕𝖘 𝖔𝖛𝖊𝖗 𝖙𝖍𝖊 𝖑𝖆𝖟𝖞 𝖉𝖔𝖌
+𝑻𝒉𝒆 𝒒𝒖𝒊𝒄𝒌 𝒃𝒓𝒐𝒘𝒏 𝒇𝒐𝒙 𝒋𝒖𝒎𝒑𝒔 𝒐𝒗𝒆𝒓 𝒕𝒉𝒆 𝒍𝒂𝒛𝒚 𝒅𝒐𝒈
+𝓣𝓱𝓮 𝓺𝓾𝓲𝓬𝓴 𝓫𝓻𝓸𝔀𝓷 𝓯𝓸𝔁 𝓳𝓾𝓶𝓹𝓼 𝓸𝓿𝓮𝓻 𝓽𝓱𝓮 𝓵𝓪𝔃𝔂 𝓭𝓸𝓰
+𝕋𝕙𝕖 𝕢𝕦𝕚𝕔𝕜 𝕓𝕣𝕠𝕨𝕟 𝕗𝕠𝕩 𝕛𝕦𝕞𝕡𝕤 𝕠𝕧𝕖𝕣 𝕥𝕙𝕖 𝕝𝕒𝕫𝕪 𝕕𝕠𝕘
+𝚃𝚑𝚎 𝚚𝚞𝚒𝚌𝚔 𝚋𝚛𝚘𝚠𝚗 𝚏𝚘𝚡 𝚓𝚞𝚖𝚙𝚜 𝚘𝚟𝚎𝚛 𝚝𝚑𝚎 𝚕𝚊𝚣𝚢 𝚍𝚘𝚐
+⒯⒣⒠ ⒬⒰⒤⒞⒦ ⒝⒭⒪⒲⒩ ⒡⒪⒳ ⒥⒰⒨⒫⒮ ⒪⒱⒠⒭ ⒯⒣⒠ ⒧⒜⒵⒴ ⒟⒪⒢
+
+#	Unwanted Interpolation
+#
+#	Strings which can be accidentally expanded into different strings if evaluated in the wrong context, e.g. used as a printf format string or via Perl or shell eval. Might expose sensitive data from the program doing the interpolation, or might just represent the wrong string.
+
+$HOME
+$ENV{'HOME'}
+%d
+%s
+{0}
+%*.*s
+File:///
+
+#	File Inclusion
+#
+#	Strings which can cause user to pull in files that should not be a part of a web server
+
+../../../../../../../../../../../etc/passwd%00
+../../../../../../../../../../../etc/hosts
+
+#	MSDOS/Windows Special Filenames
+#
+#	Strings which are reserved characters in MSDOS/Windows
+
+CON
+PRN
+AUX
+CLOCK$
+NUL
+A:
+ZZ:
+COM1
+LPT1
+LPT2
+LPT3
+COM2
+COM3
+COM4

commit f25c207ce9fb2e43429df59e79623f25dcde46a6
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Mon Jul 10 16:02:49 2017 +0000

    Tests for constant_time_eq which mirror the SHA256 failure mode
    
    The encode_utf8 tests match the SHA256 branch of IsPassword failure
    mode: it provides two bytestrings of the same length (in the case of
    this test, 2 (0xC2 0xBC) and 2 (0x61 0x61), but which end up as
    characters strings of different length after UTF8 decoding (in this
    test, 1 ("¼") and 2 ("aa") respectively).

diff --git a/t/api/constant_time_eq.t b/t/api/constant_time_eq.t
index 4256dd2..61d471b 100644
--- a/t/api/constant_time_eq.t
+++ b/t/api/constant_time_eq.t
@@ -4,6 +4,7 @@ use strict;
 use RT;
 use RT::Test tests => undef;
 use Test::Exception;
+use Encode;
 
 use_ok "RT::Util";
 
@@ -36,6 +37,14 @@ _lives_and (eval { RT::Util::constant_time_eq('😎', '😎') },   1, 'both args
 _dies_ok   (eval { RT::Util::constant_time_eq('😎', 'xxxx') },    'four-byte utf8 and four one-byte chars is false');
 _lives_and (eval { RT::Util::constant_time_eq('😎✈️'x1024, '😎✈️'x1024) }, 1, 'both long strings of utf8 chars is true');
 
+diag 'Binary strings of same length but different length after UTF-8 decode';
+_dies_ok (eval { RT::Util::constant_time_eq('¼', 'a') }, 'Dies with no binary set');
+_dies_ok (eval { RT::Util::constant_time_eq(encode_utf8('¼'), 'aa') }, 'Dies with no binary set');
+_dies_ok (eval { RT::Util::constant_time_eq('¼', 'a', 0) }, 'Dies with binary set to false');
+_dies_ok (eval { RT::Util::constant_time_eq(encode_utf8('¼'), 'aa', 0) }, 'Dies with binary set to false');
+_lives_and (eval { RT::Util::constant_time_eq('¼', 'a', 1) }, 0, 'Lives with false result with binary set to true');
+_lives_and (eval { RT::Util::constant_time_eq(encode_utf8('¼'), 'aa', 1) }, 0, 'Lives with false result with binary set to true');
+
 # Longer strings
 _lives_and (eval { RT::Util::constant_time_eq('a'x4096, 'a'x4096) },             1, 'both args equal long strings is true');
 _lives_and (eval { RT::Util::constant_time_eq('a'x4096 . 'c', 'a'x4096 . 'b') }, 0, 'both args unequal long strings is false');

commit 5bea1fce0d936332ed913ad8ac7d2385644675cb
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Mon Jul 10 11:48:28 2017 -0400

    Add a "binary" option to opt out of UTF8 encoding
    
    The SHA256 branch of IsPassword generates binary values to compare,
    which may lead to comparing two strings with a different number of
    Unicode characters, even when both strings have 26 octets (since UTF8 is
    a variable-length encoding). This triggers an error in constant_time_eq
    which demands both strings are the same length.
    
    When comparing binary values pass this flag to avoid treating the
    inputs as UTF8.

diff --git a/lib/RT/User.pm b/lib/RT/User.pm
index c5e0e54..969c464 100644
--- a/lib/RT/User.pm
+++ b/lib/RT/User.pm
@@ -961,7 +961,7 @@ sub IsPassword {
         my $salt = substr($hash, 0, 4, "");
         return 0 unless RT::Util::constant_time_eq(
             substr(Digest::SHA::sha256($salt . Digest::MD5::md5(Encode::encode( "UTF-8", $value))), 0, 26),
-            $hash
+            $hash, 1
         );
     } elsif (length $stored == 32) {
         # Hex nonsalted-md5
diff --git a/lib/RT/Util.pm b/lib/RT/Util.pm
index 9fc24c5..0399208 100644
--- a/lib/RT/Util.pm
+++ b/lib/RT/Util.pm
@@ -166,6 +166,9 @@ The two string arguments B<MUST> be of equal length. If the lengths differ,
 this function will call C<die()>, as proceeding with execution would create
 a timing vulnerability. Length is defined by characters, not bytes.
 
+Strings that should be treated as binary octets rather than Unicode text
+should pass a true value for the binary flag.
+
 This code has been tested to do what it claims. Do not change it without
 thorough statistical timing analysis to validate the changes.
 
@@ -177,7 +180,7 @@ B<https://en.wikipedia.org/wiki/Timing_attack>
 =cut
 
 sub constant_time_eq {
-    my ($a, $b) = @_;
+    my ($a, $b, $binary) = @_;
 
     my $result = 0;
 
@@ -191,9 +194,18 @@ sub constant_time_eq {
         my $a_char = substr($a, $i, 1);
         my $b_char = substr($b, $i, 1);
 
-        # encode() is set to die on malformed
-        my @a_octets = unpack("C*", encode('UTF-8', $a_char, Encode::FB_CROAK));
-        my @b_octets = unpack("C*", encode('UTF-8', $b_char, Encode::FB_CROAK));
+        my (@a_octets, @b_octets);
+
+        if ($binary) {
+            @a_octets = ord($a_char);
+            @b_octets = ord($b_char);
+        }
+        else {
+            # encode() is set to die on malformed
+            @a_octets = unpack("C*", encode('UTF-8', $a_char, Encode::FB_CROAK));
+            @b_octets = unpack("C*", encode('UTF-8', $b_char, Encode::FB_CROAK));
+        }
+
         die $generic_error if (scalar @a_octets) != (scalar @b_octets);
 
         for (my $j = 0; $j < scalar @a_octets; $j++) {

commit 358034bcbd6f2725915963e4d38acede402cb875
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Mon Jun 26 16:18:26 2017 -0400

    Assign correct parameter to message for output

diff --git a/t/api/constant_time_eq.t b/t/api/constant_time_eq.t
index 61d471b..d82bc35 100644
--- a/t/api/constant_time_eq.t
+++ b/t/api/constant_time_eq.t
@@ -85,6 +85,6 @@ sub _lives_and {
 }
 
 sub _dies_ok {
-    my ($t, $message) = @_;
+    my ($message) = @_;
     isnt ($@, '', $message);
 }

commit 94f391df9f714fd6466fac907643d7aa90902532
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Mon Jun 26 16:19:14 2017 -0400

    Remove unused Test::Exception

diff --git a/t/api/constant_time_eq.t b/t/api/constant_time_eq.t
index d82bc35..facba5f 100644
--- a/t/api/constant_time_eq.t
+++ b/t/api/constant_time_eq.t
@@ -3,7 +3,6 @@ use warnings;
 use strict;
 use RT;
 use RT::Test tests => undef;
-use Test::Exception;
 use Encode;
 
 use_ok "RT::Util";

commit 27eda905a3df2379bb9b8ddbb0ccf2ead4749383
Merge: 1257285 94f391d
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Jul 12 14:26:34 2017 +0000

    Merge branch '4.0/compare-old-passwords' into 4.0-trunk


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


More information about the rt-commit mailing list