Skip to content

Implement basic unit tests to prevent regressions for crypto stuff#2171

Closed
nextgens wants to merge 1 commit intomeshcore-dev:devfrom
nextgens:unit_tests
Closed

Implement basic unit tests to prevent regressions for crypto stuff#2171
nextgens wants to merge 1 commit intomeshcore-dev:devfrom
nextgens:unit_tests

Conversation

@nextgens
Copy link
Copy Markdown
Contributor

You can run them either natively or on device; they don't run by default.

Some of the tests are fairly slow (benchmarks), we may want to avoid them/optimise in the future.

I had to fight with the linker and vendor-in part of the crypto library to make it run natively.

Hopefully this will enable the merge of things like #1632 and inform any argument about performance.

$pio test -e native -vv
Testing...

╔════════════════════════════════════════════════════╗
║   MeshCore Crypto Test Suite                       ║
╚════════════════════════════════════════════════════╝

test/test_crypto/main.cpp:50:test_ed25519_create_keypair_deterministic:PASS
test/test_crypto/main.cpp:51:test_ed25519_create_keypair_different_seeds:PASS
test/test_crypto/main.cpp:52:test_ed25519_key_exchange_commutative:PASS
test/test_crypto/main.cpp:53:test_ed25519_key_exchange_different_peers:PASS
test/test_crypto/main.cpp:54:test_ed25519_sign_verify_valid_signature:PASS
test/test_crypto/main.cpp:55:test_ed25519_sign_verify_invalid_signature_wrong_message:PASS
test/test_crypto/main.cpp:56:test_ed25519_sign_verify_invalid_signature_tampered:PASS
test/test_crypto/main.cpp:57:test_ed25519_sign_verify_invalid_signature_wrong_key:PASS
test/test_crypto/main.cpp:58:test_ed25519_sign_verify_empty_message:PASS
test/test_crypto/main.cpp:59:test_ed25519_sign_verify_long_message:PASS
Ed25519 create_keypair: 6 ms for 100 iterations (0.06 ms avg)
test/test_crypto/main.cpp:62:test_ed25519_benchmark_create_keypair:PASS
Ed25519 key_exchange: 67 ms for 500 iterations (0.13 ms avg)
test/test_crypto/main.cpp:63:test_ed25519_benchmark_key_exchange:PASS
Ed25519 sign: 13 ms for 200 iterations (0.07 ms avg)
test/test_crypto/main.cpp:64:test_ed25519_benchmark_sign:PASS
Ed25519 verify: 28 ms for 200 iterations (0.14 ms avg)
test/test_crypto/main.cpp:65:test_ed25519_benchmark_verify:PASS
test/test_crypto/main.cpp:68:test_encryptThenMAC_basic_small_payload:PASS
test/test_crypto/main.cpp:69:test_encryptThenMAC_various_sizes:PASS
test/test_crypto/main.cpp:70:test_MACThenDecrypt_valid_mac:PASS
test/test_crypto/main.cpp:71:test_MACThenDecrypt_invalid_mac_tampered:PASS
test/test_crypto/main.cpp:72:test_MACThenDecrypt_invalid_mac_ciphertext_tampered:PASS
test/test_crypto/main.cpp:73:test_MACThenDecrypt_wrong_shared_secret:PASS
test/test_crypto/main.cpp:74:test_MACThenDecrypt_max_payload:PASS
test/test_crypto/main.cpp:75:test_encryptThenMAC_MACThenDecrypt_roundtrip_max_payload:PASS
test/test_crypto/main.cpp:76:test_encryptThenMAC_MACThenDecrypt_empty_payload:PASS
test/test_crypto/main.cpp:77:test_MACThenDecrypt_invalid_length_too_short:PASS
test/test_crypto/main.cpp:78:test_encryptThenMAC_different_keys_different_output:PASS
test/test_crypto/main.cpp:79:test_encryptThenMAC_deterministic:PASS
encryptThenMAC produced 34 bytes of output
test/test_crypto/main.cpp:80:test_encryptThenMAC_known_vector:PASS
MACThenDecrypt produced 32 bytes of output
test/test_crypto/main.cpp:81:test_MACThenDecrypt_known_vector:PASS
encryptThenMAC (184 bytes): 1 ms for 100 iterations (0.01 ms avg)
test/test_crypto/main.cpp:82:test_encryptThenMAC_benchmark:PASS
MACThenDecrypt (184 bytes): 0 ms for 100 iterations (0.00 ms avg)
test/test_crypto/main.cpp:83:test_MACThenDecrypt_benchmark:PASS
encryptThenMAC + MACThenDecrypt roundtrip (184 bytes): 13 ms for 500 iterations (0.03 ms avg)
test/test_crypto/main.cpp:84:test_encryptThenMAC_MACThenDecrypt_benchmark_roundtrip:PASS
Benchmarking AES128 with 184 bytes of plaintext:
AES128 (184 bytes): 10 ms for 500 iterations (0.02 ms avg)
Benchmarking AESTiny128 with 184 bytes of plaintext:
AESTiny128 (184 bytes): 4 ms for 500 iterations (0.01 ms avg)
Benchmarking AESSmall128 with 184 bytes of plaintext:
AESSmall128 (184 bytes): 11 ms for 500 iterations (0.02 ms avg)
test/test_crypto/main.cpp:85:test_benchmark_AESImpl:PASS

-----------------------
32 Tests 0 Failures 0 Ignored 
OK

---------------- native:test_crypto [PASSED] Took 0.84 seconds ----------------

=================================== SUMMARY ===================================
Environment                                                   Test         Status    Duration
------------------------------------------------------------  -----------  --------  ------------
native                                                        test_crypto  PASSED    00:00:00.843
================= 32 test cases: 32 succeeded in 00:00:00.843 =================

$pio run -e heltec_v4_companion_radio_usb -vv
TEST_START

╔════════════════════════════════════════════════════╗
║   MeshCore Crypto Test Suite (Arduino)             ║
╚════════════════════════════════════════════════════╝

test/test_crypto/arduino_main.cpp:50:test_ed25519_create_keypair_deterministic:PASS
test/test_crypto/arduino_main.cpp:51:test_ed25519_create_keypair_different_seeds:PASS
test/test_crypto/arduino_main.cpp:52:test_ed25519_key_exchange_commutative:PASS
test/test_crypto/arduino_main.cpp:53:test_ed25519_key_exchange_different_peers:PASS
test/test_crypto/arduino_main.cpp:54:test_ed25519_sign_verify_valid_signature:PASS
test/test_crypto/arduino_main.cpp:55:test_ed25519_sign_verify_invalid_signature_wrong_message:PASS
test/test_crypto/arduino_main.cpp:56:test_ed25519_sign_verify_invalid_signature_tampered:PASS
test/test_crypto/arduino_main.cpp:57:test_ed25519_sign_verify_invalid_signature_wrong_key:PASS
test/test_crypto/arduino_main.cpp:58:test_ed25519_sign_verify_empty_message:PASS
test/test_crypto/arduino_main.cpp:59:test_ed25519_sign_verify_long_message:PASS
Ed25519 create_keypair: 471 ms for 100 iterations (4.71 ms avg)
test/test_crypto/arduino_main.cpp:62:test_ed25519_benchmark_create_keypair:PASS
Ed25519 key_exchange: 7094 ms for 500 iterations (14.19 ms avg)
test/test_crypto/arduino_main.cpp:63:test_ed25519_benchmark_key_exchange:PASS
Ed25519 sign: 1294 ms for 200 iterations (6.47 ms avg)
test/test_crypto/arduino_main.cpp:64:test_ed25519_benchmark_sign:PASS
Ed25519 verify: 3162 ms for 200 iterations (15.81 ms avg)
test/test_crypto/arduino_main.cpp:65:test_ed25519_benchmark_verify:PASS
test/test_crypto/arduino_main.cpp:68:test_encryptThenMAC_basic_small_payload:PASS
test/test_crypto/arduino_main.cpp:69:test_encryptThenMAC_various_sizes:PASS
test/test_crypto/arduino_main.cpp:70:test_MACThenDecrypt_valid_mac:PASS
test/test_crypto/arduino_main.cpp:71:test_MACThenDecrypt_invalid_mac_tampered:PASS
test/test_crypto/arduino_main.cpp:72:test_MACThenDecrypt_invalid_mac_ciphertext_tampered:PASS
test/test_crypto/arduino_main.cpp:73:test_MACThenDecrypt_wrong_shared_secret:PASS
test/test_crypto/arduino_main.cpp:74:test_MACThenDecrypt_max_payload:PASS
test/test_crypto/arduino_main.cpp:75:test_encryptThenMAC_MACThenDecrypt_roundtrip_max_payload:PASS
test/test_crypto/arduino_main.cpp:76:test_encryptThenMAC_MACThenDecrypt_empty_payload:PASS
test/test_crypto/arduino_main.cpp:77:test_MACThenDecrypt_invalid_length_too_short:PASS
test/test_crypto/arduino_main.cpp:78:test_encryptThenMAC_different_keys_different_output:PASS
test/test_crypto/arduino_main.cpp:79:test_encryptThenMAC_deterministic:PASS
encryptThenMAC produced 34 bytes of output
Output (hex):
0x5F, 0x49, 0x07, 0xFE, 0xEF, 0x74, 0xE1, 0xD5, 
0x03, 0x6E, 0x90, 0x0E, 0xEE, 0x11, 0x8E, 0x94, 
0x92, 0x93, 0x5B, 0xE8, 0x7E, 0x2E, 0x5B, 0x44, 
0x7C, 0x94, 0x4B, 0x21, 0xC9, 0xAF, 0x77, 0x56, 
0xC0, 0xD8
test/test_crypto/arduino_main.cpp:80:test_encryptThenMAC_known_vector:PASS
MACThenDecrypt produced 32 bytes of output
Decrypted output (hex):
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 
0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 
0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 
0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F

test/test_crypto/arduino_main.cpp:81:test_MACThenDecrypt_known_vector:PASS
encryptThenMAC (184 bytes): 57 ms for 100 iterations (0.57 ms avg)
test/test_crypto/arduino_main.cpp:82:test_encryptThenMAC_benchmark:PASS
MACThenDecrypt (184 bytes): 56 ms for 100 iterations (0.56 ms avg)
test/test_crypto/arduino_main.cpp:83:test_MACThenDecrypt_benchmark:PASS
encryptThenMAC + MACThenDecrypt roundtrip (184 bytes): 548 ms for 500 iterations (1.10 ms avg)
test/test_crypto/arduino_main.cpp:84:test_encryptThenMAC_MACThenDecrypt_benchmark_roundtrip:PASS
AES128 (184 bytes): 371 ms for 500 iterations (0.74 ms avg)
AESTiny128 (184 bytes): 361 ms for 500 iterations (0.72 ms avg)
AESSmall128 (184 bytes): 370 ms for 500 iterations (0.74 ms avg)
AES128_ESP (184 bytes): 370 ms for 500 iterations (0.74 ms avg)
test/test_crypto/arduino_main.cpp:85:test_benchmark_AESImpl:PASS

-----------------------
32 Tests 0 Failures 0 Ignored 
OK



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

TEST_DONE

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

@ripplebiz
Copy link
Copy Markdown
Collaborator

Not sure I understand why the AES impl needs to be injected into the Utils::encrypt() / ::decrypt() ?
And, why the crypto libs needs to be copied to the test /test source set? Is it because these libs are different for native target vs Arduino? And if so, aren't all the tests basically testing whether the native impls are coherent/pass?

I've always been skeptical of testing strategies that have to modify frameworks, and that they don't necessarily verify real environment, and are basically a LOT of work with little ROI

@nextgens
Copy link
Copy Markdown
Contributor Author

nextgens commented Apr 2, 2026

Not sure I understand why the AES impl needs to be injected into the Utils::encrypt() / ::decrypt() ?

That enables us to use the same test code path for both native & on device tests. We could test at a lower level but what we really care about not breaking against known test vectors is that.

And, why the crypto libs needs to be copied to the test /test source set? Is it because these libs are different for native target vs Arduino?

The only difference in between this vendored in library and upstream is the library.json metadata where upstream restricts it to arduino (preventing us from using it on native) and the bits we don't use being stripped out. See
https://github.com/rweather/arduinolibs/blob/37a76b8f7516568e1c575b6dc9268da1ccaac6b6/libraries/Crypto/library.json#L20

I have not found a way to tell PIO to build/link it anyway without vendoring it in. If you know the magic invocation to override 'framework' of libraries I am all hears :) It would indeed be much cleaner.

And if so, aren't all the tests basically testing whether the native impls are coherent/pass?

My understanding is that they are not: native_crypto is only added to the "native" target in pio:
https://github.com/meshcore-dev/MeshCore/pull/2171/changes#diff-4446afd728a4f34cbcddc306a9cb6be845d1a61c216076a295683bcc9c106724R165

native links against the vendored-in stuff, platform keep using upstream's version

I've always been skeptical of testing strategies that have to modify frameworks, and that they don't necessarily verify real environment, and are basically a LOT of work with little ROI

I share your concerns, I do think that the long term solution is to have a single, minimal, vendored in copy of the library in lib/something that implements just the bits we need. I didn't want to do it in this PR because the whole idea is to introduce tests before any potential regression.

This is pretty much step2 and what #1632 (not authored by me) proposes.

@nextgens
Copy link
Copy Markdown
Contributor Author

nextgens commented Apr 3, 2026

Lliam pointed out that lib_compat_mode = off may be doing what I was looking for, I will test, simplify and submit another PR for this.

@nextgens nextgens closed this Apr 3, 2026
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