‹ BackHN Continuity

Thread

AMD's random number generator can't generate a 0?

288 points · 220 comments · BruceEel

  1. strenholme · · focus · HN ↗
    This is why I use, in security critical contents of my software (where the numbers have to be computationally infeasible to produce), a type of random number generator called an XOF (extendable-output function).

    It takes entropy from multiple different sources, makes it all input to the XOF, then the XOF uses cryptography to output a stream that has as much entropy as the combined entropy of all of its sources of randomness. So if an XOF, for example, takes 100 runs of rdrand16, along with the system time in microseconds and the number of milliseconds between receiving 100 packets over the network, the XOF will output a completely random stream without artifacts like never returning 0x0000, even if rdrand16 never outputs 0x0000.

    1. Taek · · focus · HN ↗
      You can effectively achieve the same result with this simple operation:

        hash = sha256(current_time());
        for i := 0; i < n; i++ {
            hash = sha256(hash.append(current_time()))
        }
      
      
      This is because the number of nanoseconds between hashes is actually itself variable, and this is true for physics reasons that are basically beyond the control of any attacker trying to manipulate your entropy. If your time() function has a resolution of nanoseconds, you only need your loop to iterate about 50 times to get a cryptographically secure amount of entropy. If your time() function has a resolution of milliseconds, you need to let this run for more like 20 milliseconds, and if your time() function has a resolution of seconds you need to let it run for more like 5 seconds.

      The reason I like doing it this way is that it happens entirely in userspace, it's genuinely a secure method of generating entropy, and it has no dependencies on potentially buggy firmware or microcode outside of the time() call, which is both fairly narrow, fairly heavily used (meaning a bug is likely to be discovered during testing, as the implementation is likely heavily scrutinized), and also fairly easy to test independently - just look at the number of nanoseconds that elapse at each consecutive call to sha256(current_time()) and verify that there's some statistical variance. The above suggestions are assuming about 2.5 bits of variance between calls, meaning there should be a range of at least 20 nanoseconds between your slowest and fastest hash call. This has been true on every CPU I've ever measured, including microcontrollers.

      1. sltkr · · focus · HN ↗
        This comment demonstrates everything that's wrong with people trying to be clever and rolling their own crypto.

        The security of your system depends on time() providing enough entropy, even though that's not what it's designed to do. It's built on top of the wrong primitive from the start.

        > The reason I like doing it this way is that it happens entirely in userspace

        On Linux this is often true, but there is no portable way to get the current time that is _guaranteed_ not to do any system calls.

        > If your time() function has a resolution of nanoseconds, you only need your loop to iterate about 50 times to get a cryptographically secure amount of entropy.

        You haven't proven that at all. It's easy to imagine that on a CPU running at a fixed frequency the interval between reads is constant, so if anyone knows (or can guess) the start time the resulting seed is entirely predictable.

        This is completely independent of timer resolution. You seem to realize that as you were writing that:

        > just look at the number of nanoseconds that elapse at each consecutive call to sha256(current_time()) and verify that there's some statistical variance

        Oh yes, because evaluating the quality of a random number generator is such a trivial thing to do, it's not like there is decades of research behind it or anything.

        And assuming you are able to verify the statistical variance: are you going to put that logic in the loop, making it significantly more complex?

        Or are you going to do this test on your machine and then ship your code on the assumption that if it works on your machine, it will work everywhere else, too?

        > if your time() function has a resolution of seconds you need to let it run for more like 5 seconds.

        So not only is it insecure, it's agonizingly slow by design. Why do a system call that takes milliseconds at best, when we can run a loop in userspace for 5 seconds?

        All this just so you can avoid writing the obviously correct oneliner:

            if (getentropy(&seed, sizeof(seed)) != 0) abort();
        1. sltkr · · focus · HN ↗
          And to show my objections are not just theoretical I wrote a little program to check:

              #include <time.h>
              #include <stdio.h>
              
              static int estimate_entropy(long l) {
                  int bits = 1; /* for the sign bit */
                  if (l < 0) l = -l;
                  while (l > 0) {
                      ++bits;
                      l >>= 1;
                  }
                  return bits;
              }
              
              int main() {
                  struct timespec ts;
                  if (clock_getres(CLOCK_REALTIME, &ts) != 0) {
                      perror("clock_getres");
                      return 1;
                  }
                  printf("Clock resolution: %ld.%09ld\n", (long) ts.tv_sec, (long) ts.tv_nsec);
                  
                  #define N 50  /* number of samples */
                  struct timespec samples[N];
                  for (int i = 0; i < N; ++i) {
                      clock_gettime(CLOCK_REALTIME, &samples[i]);
                  }
              
                  printf("Deltas (ns):");
                  long deltas[N - 1];
                  for (int i = 0; i < N - 1; ++i) {
                      deltas[i] = 
                          (samples[i + 1].tv_sec - samples[i].tv_sec)*1000000000L
                          + (samples[i + 1].tv_nsec - samples[i].tv_nsec);
                      printf(" %4ld", deltas[i]);
                  }
                  printf("\n");
                  long entropy = 0;
                  printf("Deltas of deltas: ");
                  for (int i = 0; i < N - 2; ++i) {
                      long dd = deltas[i + 1] - deltas[i];
                      printf(" %4ld", dd);
                      entropy += estimate_entropy(dd);
                  }
                  printf("\n");
                  printf("Maximum entropy: %lld\n", entropy);
              }
          
          On my system this prints:

              Clock resolution: 0.000000001
              Deltas (ns):   55   51   23   23   25   24   24   24   24   24   25   25   24   24   24   24   24   25   24   24   24   25   25   24   24   23   25   24   24   25   24   23   25   25   26   23   25   24   24   25   26   24   23   25   25   26   24   25   24
              Deltas of deltas:    -4  -28    0    2   -1    0    0    0    0    1    0   -1    0    0    0    0    1   -1    0    0    1    0   -1    0   -1    2   -1    0    1   -1   -1    2    0    1   -3    2   -1    0    1    1   -2   -1    2    0    1   -2    1   -1
              Maximum entropy: 92
          
          So no, 50 iterations of that loop does not provide 256 bits of entropy due to random fluctuations in nanontime between calls.
          1. Taek · · focus · HN ↗
            Hold on I have to go edit the rest of my responses because I just assumed you wrote the code correctly; you did not.

            You are not hashing between calls to the timer. The sha256 hash itself is responsible for doing physical things to the chip (heating up some parts unevenly during the hashing computation) which introduces meaningful entropy between calls to the current time.

            You can't just do calls to clock_gettime(), you have do an actual sequential sha256() call between them. Please run this code again and tell me what results you get.

            1. sltkr · · focus · HN ↗
              You're missing the point, which is that although timings may vary on the system you are testing on, there is no system guarantee from hardware _or_ software that this always happens.

              Case in point:

              > The sha256 hash itself is responsible for doing physical things to the chip (heating up some parts unevenly during the hashing computation)

              Some CPUs do thermal throttling, others run at a fixed frequency or are so underclocked that thermal throttling doesn't kick in during your 50 iterations. This is exactly the source of randomness that is just not guaranteed to exist across systems.

              -----

              > You can't just do calls to clock_gettime(), you have do an actual sequential sha256() call between them. Please run this code again and tell me what results you get.

              OK, I'll humor you, but to reiterate: it isn't really my point.

              After adding hashing in the loop:

                  Clock resolution: 0.000000001
                  Hash: a8531a79fc350a3b35b3e82e33b759f6caa97a12efd16a715acb99065b6f3e89
                  Deltas (ns): 21662  452  335  297  290  288  288  291  289  293  290  289  290  284  287  297  289  289  295  288  287  286  292  291  287  287  301  289  299  290  292  288  291  292  296  294  295  293  290  287  297  292  292  292  288  295  291  289  296
                  Deltas of deltas:  -21210 -117  -38   -7   -2    0    3   -2    4   -3   -1    1   -6    3   10   -8    0    6   -7   -1   -1    6   -1   -4    0   14  -12   10   -9    2   -4    3    1    4   -2    1   -2   -3   -3   10   -5    0    0   -4    7   -4   -2    7
                  Maximum entropy: 177
              
              Here it's mostly the first few iterations that are slow, the remaining ones are both fast and surprisingly consistent (the value 289 appears six times for example).

              It's more obvious if you run it a few times in a row:

                  Deltas (ns): 21662  452  335  297  290  288  288  291  289  293  290  289  290  284  287  297  289  289  295  288  287  286  292  291  287  287  301  289  299  290  292  288  291  292  296  294  295  293  290  287  297  292  292  292  288  295  291  289  296
                  Deltas (ns): 22213  486  361  318  290  290  290  289  289  291  289  291  287  289  285  289  294  289  289  287  294  292  293  292  295  295  286  298  288  291  292  295  291  292  291  292  297  294  293  297  289  288  299  288  299  295  292  291  293
                  Deltas (ns): 23042  475  312  309  290  292  294  291  291  289  290  293  287  291  290  297  299  288  289  294  289  289  297  294  295  295  288  295  291  287  290  287  300  293  289  290  292  287  293  295  292  291  289  292  288  294  290  287  290
                  Deltas (ns): 22209  478  360  301  295  293  290  291  290  290  293  284  291  290  289  290  294  289  294  293  290  301  288  298  287  295  300  295  292  300  293  296  295  294  294  293  291  289  295  293  291  299  292  299  292  291  295  298  292
              
              The loop timings are quite consistent at least on a single system. That's a problem if an attacker is able to run the same program on the same system to establish baseline timings.

              If I estimate the entropy as the logarithm of the difference between maximum and minimum I get only 146 bits of entropy in this case. Technically above your standard of 128 bit, but my point was: nothing guarantees you get even this much entropy on a less noisy system.

              This also shows the problem with your "just run more iterations" advice: in the above sample, the first five columns provide 24 bit of entropy per column, and the remaing 45 columns only 2.6 bits. So adding more iterations at the tail end wouldn't double the entropy obtained.

              The code I used is here: <a href="https:&#x2F;&#x2F;pastebin.com&#x2F;ZrL1UDEg" rel="nofollow">https:&#x2F;&#x2F;pastebin.com&#x2F;ZrL1UDEg

              1. Taek · · focus · HN ↗
                The reason that you get 3-4 bits of entropy per hash is because of the fundamental nature of CPUs. In addition to having considerable professional experience with cryptography, I also have considerable professional experience with hardware; hardware is fickle as hell, especially when your transistors are tens of nanometers large. Every time you flip a bit, you expend some energy, which heats up the chip, and the heat changes the timing of the next clock cycle. Chips are composed of literally billions of transistors, and each one is going to have a different temperature, because clock cycles last less than a nanosecond (well, embedded hardware is slower but the same idea still applies reliably) and that&#x27;s not enough time for temperature deltas to dissipate across the chip.

                Hashing is particularly chaotic because it lights up a different set of transistors on each clock cycle, which means the hotspots on the chip are being jerked around. Some transistors are going to light up 5-10 times in a row, and others are going to be idle 5-10 times in a row, and then randomly that changes. And all of this changes the number of picoseconds that it takes for a clock cycle to complete, which means that each clock cycle is genuinely going to take a different amount of time to complete, and stuff like temperature throttling is completely not at play whatsoever, because we&#x27;re not talking about chip-wide temperatures, we&#x27;re literally talking about temperature deltas between transistor a and transistor b.

                That makes it a really wonderful source of entropy for cryptographic applications, because the CPU clock is so critical that it&#x27;s almost never buggy (especially relative to other components that provide entropy), it&#x27;s also almost impossible to manipulate reliably by an attacker (unless the attacker has an exploit that allows them to set the value of the clock directly - which is possible, but it&#x27;s a very narrow surface area relative to other entropy sources), and you can completely take advantage of this entropy entirely in userspace, which once again heavily minimizes attack surface area and exposure to bugs.

                I have searched far and wide for a CPU that does not reliably generate entropy using the iterated-hashing-against-the-clock method, and I have not found a single example of a CPU that consistently takes the same amount of time to complete a hash. And the reason isn&#x27;t implementation, the physics of CPUs simply insist on introducing entropy when trying to repeatedly hash something quickly.

Open on Hacker News to reply ↗

Unofficial Hacker News client; not affiliated with Y Combinator.