diff --git a/src/decoder/src/GcmParser.cpp b/src/decoder/src/GcmParser.cpp index 2920d586..6440e0d3 100644 --- a/src/decoder/src/GcmParser.cpp +++ b/src/decoder/src/GcmParser.cpp @@ -132,7 +132,10 @@ int8_t GCMParser::parse(uint8_t *d, DataParserContext &ctx, bool hastag) { } br_gcm_flip(&gcmCtx); br_gcm_run(&gcmCtx, 0, (void*) (ptr), len - authkeylen - 5); // 5 == security tag and frame counter - if(authkeylen > 0 && br_gcm_check_tag_trunc(&gcmCtx, authentication_tag, authkeylen) != 1) { + // Only enforce the tag when an authentication key is configured, matching the + // ESP32/native paths. With a blank AK we decrypt without verifying integrity, + // which lets meters that don't use standard SC+AK authentication be read. + if(authenticate && br_gcm_check_tag_trunc(&gcmCtx, authentication_tag, authkeylen) != 1) { return GCM_AUTH_FAILED; } #elif defined(ESP32) diff --git a/test/test_decoder/test_encrypted.cpp b/test/test_decoder/test_encrypted.cpp index 26b2f639..401816bf 100644 --- a/test/test_decoder/test_encrypted.cpp +++ b/test/test_decoder/test_encrypted.cpp @@ -179,3 +179,40 @@ void test_encrypted_framing_no_key(void) { TEST_ASSERT_EQUAL_MESSAGE(0, missing_title, "frame reached GCM but extracted no system title"); TEST_ASSERT_GREATER_THAN_MESSAGE(0, reached, "no encrypted frame reached the GCM layer"); } + +// Decoding an authenticated frame (one issued with an AK) using only the encryption +// key — AK omitted — must still decode: a blank authentication key skips GCM tag +// verification rather than failing. This locks the cross-platform contract that the +// ESP8266 BearSSL path now matches (the ESP32/native mbedTLS paths already behaved +// this way). Exercises the native mbedTLS path; the ESP8266 br_gcm path is identical +// in intent but only compiles on-device. +void test_encrypted_decode_without_authkey(void) { +#if !defined(HAVE_MBEDTLS) + TEST_IGNORE_MESSAGE("native mbedTLS not available (install libmbedtls-dev) — skipping"); +#else + int tested = 0, failures = 0; + for (size_t i = 0; i < COUNT(ENC_KEYED); i++) { + const KeyedFixture& k = ENC_KEYED[i]; + if (k.ak_secret == nullptr) continue; // only authenticated fixtures are meaningful here + uint8_t ek[16]; + if (!load_key(k.ek_secret, ek)) continue; // need the encryption key + + static uint8_t buf[4096]; + int len = harness_load_fixture(k.path, buf, sizeof(buf)); + if (len <= 0) { printf(" load FAIL %s\n", k.path); failures++; continue; } + MeterConfig cfg; memset(&cfg, 0, sizeof(cfg)); + // EK only, AK deliberately omitted -> auth must be skipped, frame still decodes + AmsData* d = harness_decode(buf, (uint16_t)len, &cfg, ek, NULL); + tested++; + if (!d || d->getListType() < 1) { printf(" DECODE FAIL (no-AK) %s\n", k.path); failures++; } + if (d) delete d; + } + if (tested == 0) { + TEST_IGNORE_MESSAGE("no authenticated fixtures with keys available"); + } else { + char msg[96]; + snprintf(msg, sizeof(msg), "%d/%d authenticated fixtures failed to decode with AK omitted", failures, tested); + TEST_ASSERT_EQUAL_MESSAGE(0, failures, msg); + } +#endif +} diff --git a/test/test_decoder/test_main.cpp b/test/test_decoder/test_main.cpp index 9691ce1c..953d1f12 100644 --- a/test/test_decoder/test_main.cpp +++ b/test/test_decoder/test_main.cpp @@ -73,6 +73,7 @@ void test_encrypted_landisgyr_501(void); void test_encrypted_kaifa_905(void); void test_encrypted_kamstrup_73(void); void test_encrypted_framing_no_key(void); +void test_encrypted_decode_without_authkey(void); // defined in test_plaintext_with_key.cpp void test_plaintext_dsmr_with_key_does_not_overflow(void); @@ -96,6 +97,7 @@ int main(int argc, char** argv) { RUN_TEST(test_encrypted_kaifa_905); RUN_TEST(test_encrypted_kamstrup_73); RUN_TEST(test_encrypted_framing_no_key); + RUN_TEST(test_encrypted_decode_without_authkey); RUN_TEST(test_plaintext_dsmr_with_key_does_not_overflow); return UNITY_END(); }