Implement a PRNG based on ascon_xof & crypto tests#2280
Implement a PRNG based on ascon_xof & crypto tests#2280nextgens wants to merge 3 commits intomeshcore-dev:devfrom
Conversation
Fix a bunch of bias related bugs in the process
$ pio test -e heltec_v4_companion_radio_ble TEST_START ╔════════════════════════════════════════════════════╗ ║ MeshCore Crypto Test Suite (Arduino) ║ ╚════════════════════════════════════════════════════╝ test/test_crypto/main.cpp:54:test_ed25519_create_keypair_deterministic:PASS test/test_crypto/main.cpp:55:test_ed25519_create_keypair_different_seeds:PASS test/test_crypto/main.cpp:56:test_ed25519_key_exchange_commutative:PASS test/test_crypto/main.cpp:57:test_ed25519_key_exchange_different_peers:PASS test/test_crypto/main.cpp:58:test_ed25519_sign_verify_valid_signature:PASS test/test_crypto/main.cpp:59:test_ed25519_sign_verify_invalid_signature_wrong_message:PASS test/test_crypto/main.cpp:60:test_ed25519_sign_verify_invalid_signature_tampered:PASS test/test_crypto/main.cpp:61:test_ed25519_sign_verify_invalid_signature_wrong_key:PASS test/test_crypto/main.cpp:62:test_ed25519_sign_verify_empty_message:PASS test/test_crypto/main.cpp:63:test_ed25519_sign_verify_long_message:PASS test/test_crypto/main.cpp:64:test_ed25519_keypair_known_vector:PASS test/test_crypto/main.cpp:65:test_ed25519_signature_known_vector:PASS Ed25519 create_keypair: 503 ms for 100 iterations (5.03 ms avg) test/test_crypto/main.cpp:66:test_ed25519_benchmark_create_keypair:PASS Ed25519 key_exchange: 7114 ms for 500 iterations (14.23 ms avg) test/test_crypto/main.cpp:67:test_ed25519_benchmark_key_exchange:PASS Ed25519 sign: 1299 ms for 200 iterations (6.50 ms avg) test/test_crypto/main.cpp:68:test_ed25519_benchmark_sign:PASS Ed25519 verify: 3167 ms for 200 iterations (15.84 ms avg) test/test_crypto/main.cpp:69:test_ed25519_benchmark_verify:PASS test/test_crypto/main.cpp:72:test_encryptThenMAC_basic_small_payload:PASS test/test_crypto/main.cpp:73:test_encryptThenMAC_various_sizes:PASS test/test_crypto/main.cpp:74:test_MACThenDecrypt_valid_mac:PASS test/test_crypto/main.cpp:75:test_MACThenDecrypt_invalid_mac_tampered:PASS test/test_crypto/main.cpp:76:test_MACThenDecrypt_invalid_mac_ciphertext_tampered:PASS test/test_crypto/main.cpp:77:test_MACThenDecrypt_wrong_shared_secret:PASS test/test_crypto/main.cpp:78:test_MACThenDecrypt_max_payload:PASS test/test_crypto/main.cpp:79:test_encryptThenMAC_MACThenDecrypt_roundtrip_max_payload:PASS test/test_crypto/main.cpp:80:test_encryptThenMAC_MACThenDecrypt_empty_payload:PASS test/test_crypto/main.cpp:81:test_MACThenDecrypt_invalid_length_too_short:PASS test/test_crypto/main.cpp:82:test_encryptThenMAC_different_keys_different_output:PASS test/test_crypto/main.cpp:83:test_encryptThenMAC_deterministic:PASS encryptThenMAC produced 34 bytes of output test/test_crypto/main.cpp:84:test_encryptThenMAC_known_vector:PASS MACThenDecrypt produced 32 bytes of output test/test_crypto/main.cpp:85:test_MACThenDecrypt_known_vector:PASS encryptThenMAC (184 bytes): 56 ms for 100 iterations (0.56 ms avg) test/test_crypto/main.cpp:86:test_encryptThenMAC_benchmark:PASS MACThenDecrypt (184 bytes): 56 ms for 100 iterations (0.56 ms avg) test/test_crypto/main.cpp:87:test_MACThenDecrypt_benchmark:PASS encryptThenMAC + MACThenDecrypt roundtrip (184 bytes): 548 ms for 500 iterations (1.10 ms avg) test/test_crypto/main.cpp:88:test_encryptThenMAC_MACThenDecrypt_benchmark_roundtrip:PASS AsconRNG entropy benchmark: 8192 bytes hardware entropy: 45677 us (175.14 KB/s) PRNG feed : 29054 us (275.35 KB/s) PRNG speedup : 1.57x (higher is faster) test/test_crypto/main.cpp:89:test_AsconRNG_benchmark_entropy_vs_prng:PASS test/test_crypto/main.cpp:90:test_AsconRNG_deterministic_for_same_seed:PASS test/test_crypto/main.cpp:91:test_AsconRNG_stream_advances_between_calls:PASS test/test_crypto/main.cpp:92:test_AsconRNG_reseed_changes_stream:PASS test/test_crypto/main.cpp:93:test_AsconRNG_reseed_deterministic_for_equal_inputs:PASS ----------------------- 38 Tests 0 Failures 0 Ignored OK ═══════════════════════════════════════════════════════ TEST_DONE ═══════════════════════════════════════════════════════
I haven't tested it but it looks easy enough
jcjones
left a comment
There was a problem hiding this comment.
Very nice implementation! Should be plenty fast, and a clear improvement without compatibility risks.
| blob.magic = RNG_SEED_MAGIC; | ||
| blob.version = RNG_SEED_VERSION; | ||
|
|
||
| ascon_state_t tmp = _xof; |
There was a problem hiding this comment.
If we consider the PRNG state to be sensitive, then note that this gets left behind on the stack unzeroized
| if (loadSeed(seed, sizeof(seed))) { | ||
| initState(seed, sizeof(seed)); | ||
| reseed(NULL, 0); | ||
| return; |
There was a problem hiding this comment.
seed is also arguably sensitive and should be zeroed before returning this call
| begin(); | ||
| } | ||
|
|
||
| uint8_t carry[32]; |
| if (!ok || blob.magic != RNG_SEED_MAGIC || blob.version != RNG_SEED_VERSION) { | ||
| return false; | ||
| } | ||
|
|
There was a problem hiding this comment.
Thinking about this, if saveSeed fails because the disk is full, then next time through loadSeed, we'll reload the previous seed and get a PRNG replay.
ISTM we should delete dest after having successfully loaded it.
| @@ -0,0 +1,5 @@ | |||
| #define CRYPTO_VERSION "1.3.0" | |||
| #define CRYPTO_BYTES 64 | |||
| #define ASCON_HASH_BYTES 0 /* XOF */ | |||
There was a problem hiding this comment.
If you search the PR for this, we get conflicting definitions. lib/ascon/api.h defines 0, lib/ascon/config.h defines 1, lib/ascon/library.json sets flag =1.
Obviously it's working, but it's brittle. Probably we should let this file be authoritative.
| if ((micros() - start) > 2000) { | ||
| uint32_t m = micros(); | ||
| uint32_t n = millis(); | ||
| r = (m << 16) ^ (n * 2654435761u); | ||
| break; |
There was a problem hiding this comment.
I don't know how much energy anyone wants to put into this fallback code, but it might be worthwhile to add a TODO here that we could also XOR in a chip ID or something.
| if ((micros() - start) > 2000) { | |
| uint32_t m = micros(); | |
| uint32_t n = millis(); | |
| r = (m << 16) ^ (n * 2654435761u); | |
| break; | |
| // TODO: Low entropy fallback — only micros()/millis() boot timing. | |
| // Consider mixing in chip unique ID (STM32 UID, etc.) for per-device | |
| // differentiation, or sampling LSBs from a floating ADC pin for | |
| // additional thermal noise. | |
| if ((micros() - start) > 2000) { | |
| uint32_t m = micros(); | |
| uint32_t n = millis(); | |
| r = (m << 16) ^ (n * 2654435761u); | |
| break; |
| void begin() { AsconRNG::begin(); } | ||
| void begin(long seed) { | ||
| if (!_is_ready) { | ||
| begin(); |
There was a problem hiding this comment.
I think we used to call this name hiding, or something like that. It's not buying us anything to make the compiler optimize the call away, I'd just call directly.
| begin(); | |
| AsconRNG::begin(); |
The current random number generation is slow and potentially biased (modulo arithmetic). It relies pretty much on "shake & pray".
This one will be faster, will make use of hardware RNGs if they are present (ESP32, nrf52, STM32 and RadioLib's !) and will persist entropy across reboots. The idea is to feed a single ascon_xof() instance with some entropy at bootup and just squeeze from it. There is no periodic reseeding (other than at bootup) or anything... that could be added later.
It also introduces on-device crypto tests (like #2171 but without the native parts).