java-topology/defects/contiki-0001/test/ContikiLwm2mSecretKeyLogTest.java

138 lines
6.6 KiB
Java

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);
}
}
}