138 lines
6.6 KiB
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);
|
|
}
|
|
}
|
|
}
|