flakes / mirc_fish_10

"FiSH 10" - a blowfish encryption script for mIRC 7, compatible to previous FiSH scripts and other clients! Come visit us in #fish10 on EFNet!
https://syndicode.org/fish_10/
86 stars 16 forks source link

1 out of every 128 DH1080 handshakes fails #58

Closed maroonbells closed 4 years ago

maroonbells commented 4 years ago

1 out of every 128.25 DH1080 handshakes fails because 1 out of every 256 outputs from $dll(%FiSH_dll,DH1080_gen,NOT_USED) returns the wrong string for the 2nd string "Public Key".

When the public key should begin with 1 or more 0x00 bytes, the mime public key string has the correct length, but it skips the leading 0x00 bytes and adds the needed number of garbage bytes at the tail end. Because the public key appears to be pseudo-random, it has a 1/256th chance to begin with 0x00, which triggers this bug.

The handshake fails if either nick generates one of these bogus public keys. The odds that 1 nick does not generate a bad pubkey is 255/256, and the odds that neither of 2 nicks generates the bad pubkey is (255/256)^2, so the odds of either nick in the handshake having a bad key is

//echo -a $calc( 1/ ( 1-((255/256)^2)) ) = 1 in 128.25 handshakes

The bug is likely caused either by

  1. OpenSSL skipping the leading 0x00's and giving a shorter binary string, which causes un-initialized data to be captured in the mime string.

  2. The code creating the mime has the bug skipping leading zeroes.

If it's the former, there appears to be a function BN_bn2binpad that might be the one which should be called instead.

As a work-around to avoid this bug, the script would need to be adapted to keep a long-term sample private/public key pair, and after each time it generates a new private/public pair, it would need to test both sides of the DH handshake to see if they both arrive at the same secret value. If they don't, which would happen 1 out of every 256 generated key pair, it would need to discard that new key pair and keep generating new pairs until the test handshake doesn't fail. A very small percent of the time, around 1 key per 16k, a bad public key can be detected quickly if the mime string begins with "/", which should not be possible when the "p" prime as a mime string begins with "+".

This work-around would be a pain, because it requires each side of the handshake to perform 2 additional compute of the secret against the long-term test key, and possibly more when it turns out that they generated a bogus public key.

As proof of this bug, someone running the FiSH script can add these 2 aliases, and run the command: /find_bad_pubkey It steals most of the focus of your client, so you can either use the F9 client to halt the demo, or remove the comment from the line that shows the Yes/No prompt after each bad key pair found.

alias F9 { timerfind_bad_pubkey off } alias find_bad_pubkey { if (find_bad_pubkey !iswm $ctimer) { :try_again tokenize 32 $dll(%FiSH_dll,DH1080_gen,NOT_USED) $dll(%FiSH_dll,DH1080_gen,NOT_USED) var %secret_B^a $dll(%FiSH_dll,DH1080_comp, $1 $4) var %secret_A^b $dll(%FiSH_dll,DH1080_comp, $3 $2) if (%secret_B^a != %secret_A^b) goto try_again .timerfind_bad_pubkey 1 1 find_bad_pubkey $1 $2 0 0 echo -s new private/public pairs will be compared against valid pair: $1 $2 return } var %dh.tested $3 , %dh.failed $4 , %private_a $1 , %public_A $2 var %i 256 , %j 0 | while (%i) { tokenize 32 $dll(%FiSH_dll,DH1080_gen,NOT_USED) | inc %dh.tested var %secret_B^a $dll(%FiSH_dll,DH1080_comp, %private_a $2) var %secret_A^b $dll(%FiSH_dll,DH1080_comp, $1 %public_A) if (%secret_B^a != %secret_A^b) { inc %dh.failed | inc %j | break } dec %i } if (%j) { echo -s fail! %i echo 3 -s private_a %private_a echo 3 -s publicA %public_A echo 4 -s private_b $1 echo 4 -s publicB $2 echo 3 -s secret_B^a %secret_B^a echo 3 -s secret_A^b %secret_A^b echo -s fail rate: 1 in $calc(%dh.tested / %dh.failed) ( %dh.failed of %dh.tested ) $asctime ; beep 5 | var %a $input(keep going?,yn) | if (!%a) { .timer $+ $ctimer off | return } write bad_fish_pubkey_pair prv $1 write bad_fish_pubkey_pair pub $2 } .timerfind_bad_pubkey $+ $rand(1,99) 1 5 find_bad_pubkey %private_a %public_A %dh.tested %dh.failed }

maroonbells commented 4 years ago

/* { As a work-around for the bug where a bad pub key is created 1 of every 128 times, this alias should fix the problem at the cost of a few extra ticks doing the DH handshake. Instead of asking fish_10.dll to simply generate the key pair then use what's provided, this alias tests the generated key pair to see if the public key was corrupted. Since the mime string of the safe prime begins with "+" and the public key must always be less than prime, any public key mime string beginning with "/" must be invalid. In addition to that, this alias uses an existing test key pair to test the newly generated key pair being used on both sides of the handshake. When there's a bad public key, the secrets generated on each side of the handshake will either be $null or be different from each other. If a bad public key is detected, it repeats the key generation until it's a good key. You can add the -l switch to make this alias local, if you don't want it to be accessible from outside the same script. maroon

status window shows this replacing an average of 2 bad keys: /timer 256 1 Bugfix_of_FiSH_DH1080_Generate

Simply add this alias to the fish_10.ini script, then find the other 2 places in the script containing

$dll(%FiSH_dll,DH1080_gen,NOT_USED)

and replace that with

$Bugfix_of_FiSH_DH1080_Generate } */

alias Bugfix_of_FiSH_DH1080_Generate { var %test_priv This+is+a+test+private+key+used+to+check+FiSH+DH1080+handshake+for+bad+public+key+caused+by+OpenSSL+zeropad+bug++If+public+key+is+bad+then+script+should+generate+another+pair////// var %test_pub 92NDRLzvLlsCQtHP4zxZDKyDONnppkJjIoe5UMxuPyiYEw9iYOq7bMqQnLhXQvxX4DD5wHIZMbEM1RV9CQNJc4eYl/P6iKdMxBSDX4EQg0s4MRBOPKjRIH52PIPJu3iaGae/pIszm4/iWTUpxxovI7AKkOBWGpXjtmLtMyrXt0jh0rwVCDNw :generate_again tokenize 32 $dll(%FiSH_dll,DH1080_gen,NOT_USED) | if (/* iswm $2) { echo -s  fail | goto pubfail } if ($2 == $null) { echo -s fail! DH1080_gen returns null key | return }
var %secret1 $dll(%FiSH_dll,DH1080_comp, $1 %test_pub) var %secret2 $dll(%FiSH_dll,DH1080_comp, %test_priv $2) if ((%secret1 !=== %secret2) || (%secret1 == $null)) { :pubfail
echo -s $chr(3) $+ 0,4 Generating new DH key to replace bad pair! | goto generate_again } return $1 $2 }

maroonbells commented 4 years ago

I underestimated the rate of DH handshake failures, because the bug where leading 0x00's being stripped also affects the calculation of the 1080-bit shared secret. When the 135-byte input to the SHA256 hash of the shared secret should begin with 1+ leading 0x00's, these leading 0x00's are being stripping the same way they were removed in causing a garbled public key. However, instead of generating a bogus 1080-bit value as happens when the public key is damaged, FiSH returns $null when calculating the secret encounters leading zeroes being stripped. It appears the leading 0x00's causes 'DH_compute_key' to have an unexpected return value.

When I added a counter to the above "Bugfix_of_FiSH_DH1080_Generate" alias to count how many times it generates a key pair, and also counted how many times the generated key pairs failed to generate matching secrets in the test handshake, it turns out that 2 per 256 handshakes failed, not just 1 per 256.

When I inspected the rejected key pairs, half of them had the wrong public key due to missing the leading 0x00's, and failed against 100% of handshakes. However, the remaining half of the rejected public keys had the %secret being $null for both sides of the handshake, instead of each side generating a different secret. Yet, the vast majority of all of these key pairs which generated $null secret against the test pair were working just fine against every other key pair generated. And the same thing happened no matter which test pair I substituted into the Bugfix alias.

Every generated key pair is guaranteed to have a 1 in 256 chance of failure against an unknown public key generated by the other nick, even when neither side is using a damaged key pair detected by the Bugfix alias - because that private key in combination with the other nick's public key has that 1/256 chance of having leading 0x00 stripped.

To test this effect, you can generate a long list of output from 'Bugfix_of_FiSH_DH1080_Generate' and /write all the key-pairs to a disk file. The Bugfix alias has already filtered out the bad keys which fail for 100% of handshakes due the missing leading 0x00 bytes in the public key, so none of those type of bad public keys should be among the key pairs output by the alias.

Once you have a long list of sample key pairs, you can then take a new key-pair from 'Bugfix_of_FiSH_DH1080_Generate' output, and have it perform a DH handshake against each of the keys in that disk file's list of sample key-pairs. If the list has 2560 key pairs in it, then on average that last new key-pair will fail the handshake against 10 key-pairs from among those 2560 key pairs. Each time you select a randomly new key pair to test against this group of 2560, there will be an average of 10 failures, and it will be against a different 10 each time.

Until both of these bugs are fixed, the workaround is to use an updated 'Bugfix_of_FiSH_DH1080_Generate' to reject the key pair where the public key has been trashed due to missing 0x00 bytes at the front, without the command rejecting keys which generate a $null secret.

But also, the :DH1080_INIT*: event should call call this alias with the other nick's public key as a parameter: $Bugfix_of_FiSH_DH1080_Generate($2)

... because it would also be $null when the other nick tries to calculate the same secret using their private key and your public key.

In addition, :NOTICE:DH1080_FINISH*: needs to be edited to check if %secret is $null due to the other nick not first checking if their %secret was also $null. As the script is currently written, this event handler calls FiSH.WriteKey even when %secret is $null, which silently does nothing when the %secret is $null.

This is a 3rd 1-in-256 event to avoid, so the frequency of handshake failures is closer to:

//echo -a $calc( 1/ ( 1-((255/256)^3)) ) = 1 in 85.6 handshakes

1/256 handshakes fail when nick1 has damaged public key 1/256 of remaining handshakes fail when nick2 has damaged public key 1/256 of remaining handshakes fail when private key from one of the nicks combines with public key from the other nick to generate $null secret.

; Call this alias to replace the 2 dll calls: $dll(%FiSH_dll,DH1080_gen,NOT_USED) ; On entry, $1 should either be $null, or contain other nick's true pub key ; In :NOTICE:DH1080_INIT*: event, should be replaced with: $Bugfix_of_FiSH_DH1080_Generate($2) ; in FiSH.DH1080_INIT should be replaced with: $Bugfix_of_FiSH_DH1080_Generate

alias Bugfix_of_FiSH_DH1080_Generate { var %test_priv This+is+a+test+private+key+used+to+check+FiSH+DH1080+handshake+for+bad+public+key+caused+by+OpenSSL+zeropad+bug++If+public+key+is+bad+then+script+should+generate+another+pair////// var %test_pub 92NDRLzvLlsCQtHP4zxZDKyDONnppkJjIoe5UMxuPyiYEw9iYOq7bMqQnLhXQvxX4DD5wHIZMbEM1RV9CQNJc4eYl/P6iKdMxBSDX4EQg0s4MRBOPKjRIH52PIPJu3iaGae/pIszm4/iWTUpxxovI7AKkOBWGpXjtmLtMyrXt0jh0rwVCDNw ; tmp is either null or contains other nick's true pub key var %tmp $1 :generate_again tokenize 32 $dll(%FiSH_dll,DH1080_gen,NOT_USED) ; quick test finds 1/64 public keys having leading 0x00 stripped if (/* iswm $2) goto pub_greater_than_p var %secret1 $dll(%FiSH_dll,DH1080_comp, $1 %test_pub) var %secret2 $dll(%FiSH_dll,DH1080_comp, %test_priv $2) ; if secret1 != secret2 then $2 is a damaged public key ; reject null because 1/64 damaged public keys also generates null secret if ((%secret1 !=== %secret2) || (%secret2 == $null)) { :pub_greater_than_p echo -s $chr(3) $+ 0,4 Generating new DH key to replace bad pair! | goto generate_again } ; test if other nick's true pubkey combines with this key pair to have null secret if ((%tmp != $null) && ($dll(%FiSH_dll,DH1080_comp, $1 %tmp) == $null)) goto pub_greater_than_p return $1 $2 }

flakes commented 4 years ago

Nice find. I will check if it's possible to integrate the workaround into the DLL instead of doing it in the script.

flakes commented 4 years ago

Should be fixed in the new version by means of BN_bn2binpad, can you doublecheck?

maroonbells commented 4 years ago

I have not yet detected a bad public key being generated in the new Oct 6 compile, but the problem with bad shared secret is still there. When the raw secret begins with at least 1 0x00 byte, DH1080_comp still returns $null. Test vector:

private key: dt47lJ11aoJz7/6KSa635swQnIC8ySiNSN8J9ndJvW9ALKXhZoHfqIMN/nT0NwnpvtCAL6ENsHwGmr2GpD8V2WUh8afnZ/wkYS+rVx5scgd69Ut2QpopMXI0xlfuvms19ofneamz1Km56xgDKf5VCvaLGHfeerW2hXoGbW/fvRG+Rb42hVFgA publick key: 92NDRLzvLlsCQtHP4zxZDKyDONnppkJjIoe5UMxuPyiYEw9iYOq7bMqQnLhXQvxX4DD5wHIZMbEM1RV9CQNJc4eYl/P6iKdMxBSDX4EQg0s4MRBOPKjRIH52PIPJu3iaGae/pIszm4/iWTUpxxovI7AKkOBWGpXjtmLtMyrXt0jh0rwVCDNw

raw secret with leading zero is: 00 87 CF C6 7C AA 8E C4 2C F5 37 9A 48 AD 69 2D 41 5F F1 C6 5B 8D 3A 4B 12 18 BD 12 72 93 66 61 08 51 D2 EA D8 05 E9 34 B1 D1 70 35 DF A7 7F A2 D7 CD 7D EC 92 1C FC FC FF F5 DB 78 FF 11 15 43 4B 60 4B B9 A4 3C 89 C8 50 2A EE DC 06 46 19 F8 7B 3B A2 DD F2 81 3B D1 88 5E 0E CF 59 7F 64 6C 1B 36 AC 8F EB FA 6A 9A 63 1A 43 31 A5 3E 0D 70 60 7D 81 FA FE BA 78 D5 1E F7 6F 6C D6 9F 90 D4 BF 9F C7 1C 92 BC B0 sha256(secret) = f14347306a85284b3d7570b78331331d92bbe973d98f49f85b052c868aa1f7d3 result should be: mime(hash) = 8UNHMGqFKEs9dXC3gzEzHZK76XPZj0n4WwUshoqh99M

flakes commented 4 years ago

This should fix it. Needs some more review. https://github.com/flakes/mirc_fish_10/commit/07f63d344a142b116cb5a2e2e5a0b880db824e96

maroonbells commented 4 years ago

I can't get my VS2017 to compile, so can't verify, though it looks like it should work for the zero padded secrets issue. For another test vector, here's a key pair which generate a secret with 2 leading 0x00's private: a+test+private+key+to+generate+shared+secret+beginning+with+more+than+one+zero+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/ public: a+test+public+key+to+generate+shared+secret+beginning+with+more+than+one+zero+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+//ACJe secret as hex: 00 00 31 D1 E4 C1 96 FF 79 78 2D 5A 35 D7 D0 08 E1 F1 A2 31 56 3E AE B7 EA 1B 35 5F 37 EA B4 42 19 9E 39 A3 56 5F 19 C1 B8 59 AD 6A 16 92 E5 41 71 2D 95 7D D0 1A A5 D0 25 F4 41 1C 5D 9D D3 A1 5A 83 A5 BF E0 F7 6A 21 1F 0E AB B0 65 C6 51 CA 83 1E 10 6B DE DF 71 6C 44 1C F7 70 CF 33 FE C4 73 D2 B8 9B 93 CE 51 30 39 93 5D 6A 32 E2 11 19 8E FA FE C5 8B 9E 2F BB E4 E9 02 45 FF CB AD 41 EA 9C 66 54 82 93 E1 sha256(secret) = c91c98b366489c0a04330e527ba4c4633e0e90f17e8a97e60020e33a55a17d3d result should be: mime(hash) = yRyYs2ZInAoEMw5Se6TEYz4OkPF+ipfmACDjOlWhfT0

flakes commented 4 years ago

looks good, very impressive how you managed to construct such test data

https://i.imgur.com/7X07svl.png

flakes commented 4 years ago

Fix will be part of the next release