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.
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: https://pastebin.com/ZrL1UDEg
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're not talking about chip-wide temperatures, we'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's almost never buggy (especially relative to other components that provide entropy), it'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'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't implementation, the physics of CPUs simply insist on introducing entropy when trying to repeatedly hash something quickly.
I have tested this method on over 100 different CPUs and I have never seen such consistent output. I'm genuinely surprised to see that you only hit 92 bits of entropy, but that can trivially be fixed by doing 10x the iterations. 500 iterations is still going to put you under a millisecond of cost.
And, for what it's worth, code I've actually shipped has combined the above technique with Fortuna, and has typically targeted 2000 bits of entropy rather than 128 (for security buffer).
EDIT: I reviewed his code, and he's not hashing between calls to check the clock; the hash call itself causes the CPU to heat up in arbitrary ways which changes the timing between hashes and introduces more entropy; removing that call basically entirely defeats the idea behind the technique, these results are fully invalid.
---
I updated the code to insert the hash call, this is what I got for his original code on my machine, and the updated code with hashing on my machine (and the difference is cryptographically meaningful):
=== Original C — no hashing ===
Clock resolution: 0.000000001
Deltas (ns): 50 34 19 19 13 13 13 13 13 14 13 13 13 13 13 14 13 13 14 12 13 14 13 13 13 14 13 13 14 12 13 14 13 13 14 12 13 14 13 14 13 12 13 14 14 13 13 13 13
Deltas of deltas: -16 -15 0 -6 0 0 0 0 1 -1 0 0 0 0 1 -1 0 1 -2 1 1 -1 0 0 1 -1 0 1 -2 1 1 -1 0 1 -2 1 1 -1 1 -1 -1 1 1 0 -1 0 0 0
Maximum entropy: 90
=== C with SHA-256 between clock reads ===
Clock resolution: 0.000000001
Deltas (ns): 756852 1287 542 470 472 445 442 436 434 439 488 435 433 434 440 439 439 435 432 433 435 432 433 433 429 433 453 441 437 437 431 433 432 430 431 438 436 434 431 433 435 436 435 433 430 436 435 437 428
Deltas of deltas: -755565 -745 -72 2 -27 -3 -6 -2 5 49 -53 -2 1 6 -1 0 -4 -3 1 2 -3 1 0 -4 4 20 -12 -4 0 -6 2 -1 -2 1 7 -2 -2 -3 2 2 1 -1 -2 -3 6 -1 2 -9
Maximum entropy: 188Can you run the program 10 times and show me how much variance there actually is in the first column? Because if all the values lie between (say) 756000 and 757000 that's actually just 10 bits of entropy, not 19.5, and if the same applies to the other values, you're much closer to the original 90 bits.
And here are the results of running that code:
=== No hashing ===
Clock resolution: 0.000000001 seconds
Clock reads: 500,000
Second-difference outcomes: 499,998
Retained outcomes: 449,998 (90.000%)
Average Shannon information: 1.755579 bits/retained outcome
Marginal min-entropy estimate: 1.339460 bits/retained outcome
Lag-1 conditional min-entropy: 0.960079 bits/retained adjacent outcome
Conservative descriptive proxy: 0.960079 bits/retained outcome
Proxy scaled per clock iteration: 0.864067 bits/iteration
These are empirical timing statistics, not a proven entropy rate.
=== One SHA-256 between clock reads ===
Clock resolution: 0.000000001 seconds
Clock reads: 500,000
Second-difference outcomes: 499,998
Retained outcomes: 449,998 (90.000%)
Average Shannon information: 4.205076 bits/retained outcome
Marginal min-entropy estimate: 3.610848 bits/retained outcome
Lag-1 conditional min-entropy: 3.351217 bits/retained adjacent outcome
Conservative descriptive proxy: 3.351217 bits/retained outcome
Proxy scaled per clock iteration: 3.016082 bits/iteration
These are empirical timing statistics, not a proven entropy rate.
------------As GPT helpfully points out, this isn't a proven guarantee, but a reasonable estimate is somewhere between 3 and 4 bits of entropy per hash. That means 50 is actually enough, though if you want to be conservative I don't think there's any harm in doing 500 or even 5,000 instead of 50. And, if you are going to be using this in a hostile environment, it doesn't hurt to also add a fortuna-like accumulator that resets your entropy every once in a while.
I said this in another reply as well, but 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'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're not talking about chip-wide temperatures, we'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's almost never buggy (especially relative to other components that provide entropy), it'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'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.
#include <time.h>
#include <stdio.h>
#include <stdint.h>
int main() {
struct timespec foo;
int z;
uint8_t buffer[512];
for(z=0;z<128;z++) {
clock_gettime(CLOCK_REALTIME,&foo);
buffer[z * 4] = (foo.tv_nsec >> 24) & 0xff;
buffer[z * 4 + 1] = (foo.tv_nsec >> 16) & 0xff;
buffer[z * 4 + 2] = (foo.tv_nsec >> 8) & 0xff;
buffer[z * 4 + 3] = (foo.tv_nsec) & 0xff;
}
for(z=0;z<512;z++) {
printf("%02x ",buffer[z]);
if(z % 16 == 15) {puts("");}
}
return 0;
}
(code is public domain)Here, we see, running it on Windows, at least 1 but of entropy per clock_gettime() call. For people who argue kernel entropy is somehow more secure, perhaps they should become familiar with how kernels before Linux 5.6 or so on some devices had issues where (u)random wouldn’t provide enough entropy to be really secure (people would use haveged to make sure they had enough entropy).
The point is this: Getting micro-timing won’t give us as much entropy as we want, but it will still give us entropy. So it’s a perfectly good yet-another-source of entropy to feed in to an entropy pool (such as the input to a XOF).
If those Coldcard devices had used this code as one source of entropy, and this source of entropy was the only entropy still working, they never would had been compromised.
(I won’t update my 18-year-old PRNG to use this code, of course, since that code is now 18 years old and there are no known weaknesses in said code)
EDIT: I reviewed his code, and he's not hashing between calls to check the clock; the hash call itself causes the CPU to heat up in arbitrary ways which changes the timing between hashes and introduces more entropy; removing that call basically entirely defeats the idea behind the technique, these results are fully invalid.