Skip to content

Implement a PRNG based on ascon_xof & crypto tests#2280

Open
nextgens wants to merge 3 commits intomeshcore-dev:devfrom
nextgens:betterRNG
Open

Implement a PRNG based on ascon_xof & crypto tests#2280
nextgens wants to merge 3 commits intomeshcore-dev:devfrom
nextgens:betterRNG

Conversation

@nextgens
Copy link
Copy Markdown
Contributor

@nextgens nextgens commented Apr 8, 2026

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).

$ pio test -e heltec_v4_companion_radio_ble ... press RST at the end
$ while true; do cat < /dev/ttyACM0 ; done
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

═══════════════════════════════════════════════════════

nextgens added 2 commits April 7, 2026 19:37
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

═══════════════════════════════════════════════════════
@nextgens nextgens changed the title Implement a PRNG based on ascon_xof Implement a PRNG based on ascon_xof & crypto tests Apr 8, 2026
I haven't tested it but it looks easy enough
Copy link
Copy Markdown

@jcjones jcjones left a comment

Choose a reason for hiding this comment

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

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;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

If we consider the PRNG state to be sensitive, then note that this gets left behind on the stack unzeroized

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

RNGSeedBlob blob, too.

if (loadSeed(seed, sizeof(seed))) {
initState(seed, sizeof(seed));
reseed(NULL, 0);
return;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

seed is also arguably sensitive and should be zeroed before returning this call

begin();
}

uint8_t carry[32];
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

same for the carried seed

if (!ok || blob.magic != RNG_SEED_MAGIC || blob.version != RNG_SEED_VERSION) {
return false;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 */
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Comment on lines +88 to +92
if ((micros() - start) > 2000) {
uint32_t m = micros();
uint32_t n = millis();
r = (m << 16) ^ (n * 2654435761u);
break;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Suggested change
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();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Suggested change
begin();
AsconRNG::begin();

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants