Skip to content

Conversation

@Maria-12648430
Copy link
Contributor

@Maria-12648430 Maria-12648430 commented Jan 20, 2026

This PR removes remaining usages of and and or from ssh, as well as the then unneeded nowarn_obsolete_bool_op directives.

I have not the least idea what this ssh_lib:comp/2 function was about. It was moved over from crypto where it was called equal_const_time, as far as I could ferret out 🤷‍♀️ Both the original function in crypto as well as the move to ssh_lib and rename were done by @HansN.
As far as I can tell, my revised implementation is semantically the same, except that it in the case of improper lists, my implementation will return true for any two equal improper lists, while the old implementation would return true only for two equal improper lists if their last elements were equal binaries.

@github-actions
Copy link
Contributor

github-actions bot commented Jan 20, 2026

CT Test Results

    2 files     29 suites   20m 0s ⏱️
  485 tests   479 ✅  6 💤 0 ❌
1 690 runs  1 664 ✅ 26 💤 0 ❌

Results for commit 885f27b.

♻️ This comment has been updated with latest results.

To speed up review, make sure that you have read Contributing to Erlang/OTP and that all checks pass.

See the TESTING and DEVELOPMENT HowTo guides for details about how to run test locally.

Artifacts

// Erlang/OTP Github Action Bot

@u3s u3s added the team:PS Assigned to OTP team PS label Jan 21, 2026
@IngelaAndin
Copy link
Contributor

@Maria-12648430 I believe that functions is about comparing "password strings" in "equal time" for both the matching and the nonmatching case. I think that is is doubtfully that we can guarantee equal time in an erlang implementation, but even if we could I think that password authenticating seems like an option you should not use if you want to have a secure system.

@Maria-12648430
Copy link
Contributor Author

Maria-12648430 commented Jan 22, 2026

Interesting, I never thought of that. So the attack this defends against works as follows, correct?

For simplicity, say passwords can only consist of lowercase letters, and in this case the password is "erlang". Assume the time taken for comparing two characters is always 1s, vastly exaggerated for illustration purposes. If you were trying to break the password, you would go about it as follows.

  1. You start trying "a" .. "z", in case the password consists of only 1 character. Each try will fail and take 1s, so this tells you nothing except that the password is longer.
  2. Then you try "aa" .. "za". "aa", "ba", "ca", "da" will fail and take 1s because the overall string comparison will stop after it finds the first difference, which happens to be the first character in this case. However, when you get to "ea", that will still fail but take 2s because the first character was the same and only the second failed, so you can deduce that you got the first character right: e.
  3. You then try "eb" .. "ez", in case the password consists of only 2 characters (you already tried "ea" in the previous round, so you can start at "eb"). Each try will fail and take 2s, which again will tell you nothing except that the password is longer.
  4. Then you try "eaa" .. "eza". "eaa" .. "eqa" will fail and take 2s, but when you get to "era", it will fail but take 3s, so now you also got the second character right: r.
  5. ...

To find out "erlang" this way, you need only 182 tries (568s in this example), vs a naive brute-force approach ("a" .. "z", "aa" .. "zz", "aaa" ... "zzz", ..., "aaaaaa", ..., "erlang") which would need ~124 million tries (~4.2 years in this example):

  1. "a" .. "z" -> 26 (1s each -> 26s)
  2. "aa" .. "ea" -> 5 (1s for the first 4, 2s for the 5th -> 6s)
  3. "eb" .. "ez" -> 25 (2s each -> 50s)
  4. "eaa" .. "era" -> 18 (2s for the first 17, 3s for the 18th -> 37s)
  5. "erb" .. "erz" -> 25 (3s each -> 75s)
  6. "eraa" .. "erla" -> 12 (3s for the first 2, 4s for the 3rd -> 37s)
  7. "erlb" .. "erlz" -> 25 (4s each -> 100s)
  8. "erlaa" .. "erlaa" -> 1 (4s for the first 0, 5s for the 1st -> 5s)
  9. "erlab" .. "erlaz" -> 25 (5s each -> 125s)
  10. "erlaaa" .. "erlana" -> 14 (5s for the first 13, 6s for the 14th -> 71s)
  11. "erlanb" .. "erlang" -> 6 (6s each -> 36s)

Also, when you have a few characters right, you could try to guess at the rest, or at least part of the rest. In this example, when you got to "erl" in step 6, "erlang" may be worth a guess.

Now, if we do not stop comparing at the first difference but always compare all characters of the attempted password, the timing attack does not work any more, and even for the brute-force method the time it takes goes up to ~23.8 years in this example.


That all said, I agree that Erlang is too "erratic" to allow for the described attack to work in a real-world system where we're not talking seconds but microseconds, and probably ever will be. I also agree that password auth is in itself insecure. Then again, even if password auth is unsafe, it would be unwise to open up another hole in it, especially since we had it closed before. So @IngelaAndin WDYT, guess I'll revert this part of the changes? (But document better what it actually does and why 😉)

@IngelaAndin
Copy link
Contributor

I agree that it would not be a good idea to change the code so that it could be perceived as less secure than it could be. I think we should keep a best effort of making the comparison in equal time , although in a real system I think such analysis probably would be less reliable than if you tried it on a system based on more sequential not garbage collected language. However I have no experience in trying such analysis on Erlang systems.

@Maria-12648430
Copy link
Contributor Author

Yes, it is probably far fetched ;) Anyway, I will undo those changes 👍

@Maria-12648430 Maria-12648430 force-pushed the ssh_remove_obsolete_boolean_operators branch from f60471d to c533017 Compare January 22, 2026 20:14
@Maria-12648430
Copy link
Contributor Author

@IngelaAndin I reverted the changes to ssh_lib:comp and improved on it (I hope 😅). The left-hand list/binary is always iterated fully, and each step does the same number and kind of operations. I guess this is as close as we can get to constant time comparison, with "constant time" meaning the same for the same length of the left-hand list/binary. Plus I added a comment that will hopefully help future contributors understand what this is about 😉

@IngelaAndin
Copy link
Contributor

@Maria-12648430 Looks good to me, I will leave final approval to @u3s that currently is our main responsible for the ssh application.

@u3s u3s added the testing currently being tested, tag is used by OTP internal CI label Jan 23, 2026
comp(R1, <<>>, Truth and false);
comp(<<B1, R1/binary>>, <<B2, R2/binary>>, Truth) ->
comp(R1, R2, B1 =:= B2 andalso Truth);
comp(<<B1 ,R1/binary>>, <<>>, _) ->
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix formatting pls. space after comma, not before if not a problem ;-)

{{_, Reason}, client} when ((StateName =/= {connected,client})
and (not Rengotation)) ->
{{_, Reason}, client} when StateName =/= {connected,client},
not Rengotation ->
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

while at it, can you fix Rengotation typo pls?

comp(R1, R2, Truth and (B1 == B2));
comp(<<_,R1/binary>>, <<>>, Truth) ->
comp(R1, <<>>, Truth and false);
comp(<<B1, R1/binary>>, <<B2, R2/binary>>, Truth) ->
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

encouraged by #10571 (comment) i would like to complain a bit ;-) andalso used in this context does not look good as it short-circuits.

since comp/3 is not exported, we can maybe change it.
for example:

  1. adjust the Truth accumulator to be integer initiated with 0.
  2. use bxor instead of =:= (bxor resulting with 0 means number is equal)
  3. use bor instead andalso for accumulating result
  4. in the end interpret Accumulator of value 0 as "provided arguments are equal" and return true
  5. would above look better assuming only small integers will be processed?

"looking more secure" might reduce chances of receiving a security report for ssh, and having a lengthy discussions on how practical is the reported potential vulnerability and why we want/can't reject it. I would like to avoid this time waste if possible.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

andalso used in this context does not look good as it short-circuits.

Not sure I agree, but...

  1. adjust the Truth accumulator to be integer initiated with 0.
  2. use bxor instead of =:= (bxor resulting with 0 means number is equal)
  3. use bor instead andalso for accumulating result
  4. in the end interpret Accumulator of value 0 as "provided arguments are equal" and return true

... since the suggestion is sound (and IMO pretty elegant), I'll do it ;)

(Just for the record, the short-circuiting behavior of andalso is AFAICS of not consequence here. Or rather, for that reason, I put the comparisons on the left hand side of it (vs how it was before), so it will always be done. The andalso itself will also be always executed. The only difference we could have here is that andalso could potentially exhibit different runtime characteristic depending on whether the left hand side is true or false, I don't know. Another thing that may happen in theory is that the compiler could optimizie away ... andalso false to false if it could reason that the left hand side will always return a boolean (vs something else or failing); I don't think it does, but then again, I don't know, and it may in the future.)

  1. would above look better assuming only small integers will be processed?

What do you mean by that? 🤔

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would above look better assuming only small integers will be processed?
What do you mean by that? 🤔

binary operators execute differently for small integers than for large ?

Copy link
Contributor

@jhogberg jhogberg Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, although they are "guaranteed small" in this case as it's all bitwise operations on bytes.

However, the length is leaked by the control flow diverging as soon as either list/bitstring ends. For starters it appears to crash if the provided password is longer than the correct one, and even if that were fixed, the divergent control flow is enough of a leak on its own.

I don't think it's worth trying to fix this part. Hashing is the way to go.

@jhogberg
Copy link
Contributor

jhogberg commented Jan 26, 2026

For what it's worth, comp/2 may as well be replaced with =:= for all the good it can do. It cannot be done in a fully timing-safe manner so we may as well not bother, if we're going to support passwords it's better to hash and compare with crypto:hash_equals/2.

That all said, I agree that Erlang is too "erratic" to allow for the described attack to work in a real-world system where we're not talking seconds but microseconds, and probably ever will be.

The funny thing with timing attacks is that with enough samples it doesn't matter how jittery the network is, you will see the difference anyway.

@Maria-12648430 Maria-12648430 force-pushed the ssh_remove_obsolete_boolean_operators branch from c533017 to 885f27b Compare January 26, 2026 09:46
@Maria-12648430
Copy link
Contributor Author

I implemented @u3s' suggestion in the last commit before I read @jhogberg's comment... Well then... just tell me what to do 😉

@IngelaAndin
Copy link
Contributor

IngelaAndin commented Jan 26, 2026

The hash_equals/2 in crypto was introduced here #4750 and actually it was merged by Hans N. The existing function in ssh was previously moved from crypto code to ssh. I do not know why he did not uses the new crypto function in ssh, maybe to support really old OpenSSL versions?! That should no longer be an issue. I think that for avoiding vulnerability reports on this functionality ( which I think is the goal) crypto:hash_equals makes the most sense. @u3s what do you think?

comp(R1, R2, Truth and (B1 == B2));
comp(<<_,R1/binary>>, <<>>, Truth) ->
comp(R1, <<>>, Truth and false);
comp(<<B1, R1/binary>>, <<B2, R2/binary>>, Truth) ->
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would above look better assuming only small integers will be processed?
What do you mean by that? 🤔

binary operators execute differently for small integers than for large ?

@u3s
Copy link
Contributor

u3s commented Jan 26, 2026

I agree using crypto:hash_equals/2 would be optimal. I badly assumed it would be a bigger change out of initial scope of this PR.
But it should be fairly simple - convert lists to binaries and check their size equalness before calling hash_equals.

I accepted current proposal, but it would be great if we could refine it to use crypto:hash_equals/2.

@jhogberg
Copy link
Contributor

But it should be fairly simple - convert lists to binaries and check their size equalness before calling hash_equals.

That leaks the length of the password, and is the main reason it's called hash_equals/2 instead of constant_time_compare/2: it's only safe to use on guaranteed-same-length arguments, which in practice limits it to comparing hashes. If we're going to use hash_equals/2 we may as well do it correctly, pre-hashing the correct password with PKDF2 or similar, then hashing passwords as they come in and comparing them with hash_equals/2.

It doesn't look like a huge modification, but then again this isn't my turf.

@u3s
Copy link
Contributor

u3s commented Jan 26, 2026

we should do crypto:hash_equals/2 with using hashed passwords.

@Maria-12648430 let me know if you have will and time to extend this PR.
I could also contribute to it if needed.

@Maria-12648430
Copy link
Contributor Author

@u3s I'll see what I can do 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

team:PS Assigned to OTP team PS testing currently being tested, tag is used by OTP internal CI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants