import java.util.*; import java.util.regex.*; /** * Unit test for Contiki-NG CWE-312 defect: lwm2m-security.c logs LwM2M * PSK secret key and PKI public key verbatim at LOG_DBG level. * * CWE-312: Cleartext Storage of Sensitive Information * MOAD-0004: The Logged Secret * * Defect location: * os/services/lwm2m/lwm2m-security.c write_security_object() * LWM2M_SECURITY_CLIENT_PKI_ID case: logs public_key bytes via LOG_DBG_COAP_STRING * LWM2M_SECURITY_KEY_ID case: logs secret_key bytes via LOG_DBG_COAP_STRING * * Risk: LOG_CONF_LEVEL_LWM2M defaults to LOG_LEVEL_NONE in production builds * but is runtime-configurable and example project-conf.h files set it to * LOG_LEVEL_DBG. When debug logging is enabled, the raw PSK secret key and * TLS client PKI material are written to serial/UART output in cleartext. * On constrained IoT devices these logs are often captured by gateway nodes, * stored in cloud backends, and may appear in support tickets or monitoring * dashboards, exposing the device's network authentication credentials. * * Fix: remove LOG_DBG_COAP_STRING calls for credential fields; keep only * the length metadata which is safe to log. */ public class ContikiLwm2mSecretKeyLogTest { /** Simulate the DEFECTIVE log output: includes raw key bytes */ static String defectiveLogOutput(byte[] secretKey, int keyLen) { StringBuilder sb = new StringBuilder(); sb.append("Writing secret key: len: ").append(keyLen).append(" '"); // LOG_DBG_COAP_STRING dumps raw bytes for (int i = 0; i < keyLen; i++) { sb.append((char) secretKey[i]); } sb.append("'\n"); return sb.toString(); } /** Simulate the FIXED log output: only length, no key bytes */ static String fixedLogOutput(byte[] secretKey, int keyLen) { return "Writing secret key: len: " + keyLen + "\n"; } /** Check whether log output contains credential material */ static boolean containsCredentialMaterial(String logLine, byte[] key) { // Check if any 4+ byte subsequence of the key appears in the log if (key.length < 4) return false; String keyStr = new String(key); // Look for 4-char windows of the key in the log for (int i = 0; i <= key.length - 4; i++) { String window = new String(Arrays.copyOfRange(key, i, i + 4)); if (logLine.contains(window)) { return true; } } return false; } public static void main(String[] args) { System.out.println("Contiki-NG CWE-312: lwm2m-security.c PSK/PKI key logged at LOG_DBG"); System.out.println("============================================================"); boolean allPass = true; // Test 1: defective output contains secret key bytes byte[] psk = "my-iot-preshared-key-1234".getBytes(); String defLog = defectiveLogOutput(psk, psk.length); boolean defectLeaks = containsCredentialMaterial(defLog, psk); System.out.printf("DEFECT leaks PSK in log: %s (expected: true)%n", defectLeaks); if (!defectLeaks) { System.out.println("FAIL: defective log should contain credential material"); allPass = false; } // Test 2: fixed output does NOT contain secret key bytes String fixLog = fixedLogOutput(psk, psk.length); boolean fixLeaks = containsCredentialMaterial(fixLog, psk); System.out.printf("FIXED leaks PSK in log: %s (expected: false)%n", fixLeaks); if (fixLeaks) { System.out.println("FAIL: fixed log must not contain credential material"); allPass = false; } // Test 3: fixed output still contains diagnostic length info boolean fixHasLen = fixLog.contains("len: " + psk.length); System.out.printf("FIXED retains length info: %s (expected: true)%n", fixHasLen); if (!fixHasLen) { System.out.println("FAIL: fixed log should retain length for diagnostics"); allPass = false; } // Test 4: PKI public_key scenario (same pattern, same fix) // Use a key value that does not overlap with the fixed log message text byte[] pki = "XZ99-pki-auth-secret-0xDEAD".getBytes(); String defPkiLog = "Writing client PKI: len: " + pki.length + " '" + new String(pki) + "'\n"; String fixPkiLog = "Writing client PKI: len: " + pki.length + "\n"; boolean defPkiLeaks = containsCredentialMaterial(defPkiLog, pki); boolean fixPkiLeaks = containsCredentialMaterial(fixPkiLog, pki); System.out.printf("DEFECT PKI leaks: %s FIXED PKI leaks: %s (expected: true, false)%n", defPkiLeaks, fixPkiLeaks); if (!defPkiLeaks || fixPkiLeaks) { System.out.println("FAIL: PKI credential logging assertions failed"); allPass = false; } // Test 5: binary key material (PSK is often binary, not just ASCII) byte[] binPsk = {0x4b, 0x3a, (byte)0xff, 0x12, 0x7e, 0x01, 0x5c, (byte)0x88}; String defBinLog = defectiveLogOutput(binPsk, binPsk.length); String fixBinLog = fixedLogOutput(binPsk, binPsk.length); // For binary keys, check the log length difference boolean defBinLonger = defBinLog.length() > fixBinLog.length(); System.out.printf("DEFECT binary PSK log longer than fixed: %s (expected: true)%n", defBinLonger); if (!defBinLonger) { System.out.println("FAIL: defective binary-PSK log should be longer"); allPass = false; } System.out.println("------------------------------------------------------------"); System.out.println("Severity: MEDIUM — CWE-312 credential exposure in debug log"); System.out.println(" Trigger: LOG_CONF_LEVEL_LWM2M >= LOG_LEVEL_DBG (set in"); System.out.println(" examples/libs/logging/project-conf.h and common debugging"); System.out.println(" workflows; runtime-configurable via Contiki log module)"); System.out.println(" Impact: PSK secret and PKI material written to UART/serial"); System.out.println(" in cleartext; gateway capture -> cloud storage -> exposure"); System.out.println("Fix: Remove LOG_DBG_COAP_STRING for secret_key and public_key;"); System.out.println(" retain length-only log for diagnostic value without exposure."); System.out.println("------------------------------------------------------------"); if (allPass) { System.out.println("ALL PASS"); } else { System.out.println("SOME TESTS FAILED"); System.exit(1); } } }