A deep dive into how a single missing == 1 comparison opened the door to unauthorized device access

What is ADB and Why Does it Exist?

Imagine a factory. The factory manager (the developer) needs to directly connect to the machines on the production line (Android devices) and issue commands. For this purpose, a dedicated service door is opened: ADB (Android Debug Bridge).

Normally, this door opens via a USB cable physical access is required. But starting with Android 11, it can also be opened over Wi-Fi: Wireless Debugging.

Under the hood, ADB is a client-server protocol:

When wireless debugging is enabled, adbd binds to a TCP port and advertises itself via mDNS (_adb-tls-connect._tcp and _adb-tls-pairing._tcp service types).

2. Two Different Protocol Paths

There are two distinct “door models” here:

Legacy PathModern Path (Android 11+)adb tcpip fixed port 5555Wireless Debugging random portRSA challenge-response (AUTH)Mutual TLS 1.3 (STLS)Plaintext transportEncrypted transportTrust-on-first-use promptPairing code + certificate exchange

Legacy flow: The guard at the door gives you a puzzle (a random nonce), you sign it with your RSA private key, the device verifies it against the stored public key fingerprint, and if it matches you’re in.

Modern flow (TLS 1.3): Both parties authenticate each other with X.509 certificates derived from their ADB key pairs. Mutual authentication (mTLS) means the device verifies the client and the client verifies the device. Theoretically more secure.

The pairing step uses SPAKE2 (a Password-Authenticated Key Exchange) the 6-digit code you see on screen bootstraps a shared secret without ever sending the code over the wire.

3. Where the Vulnerability is Born: EVP_PKEY_cmp()

Now to the heart of the matter. This is a universal OpenSSL function whose behavior has been documented for years:

EVP_PKEY_cmp(key_a, key_b);
// Return values:
// 1 → keys match
// 0 → keys do NOT match
// -1 → key TYPES are different (e.g., RSA vs EC)
// -2 → operation not supported

This function does not return a true/false for "matched / not matched" it returns four different integers. This is a common pattern in OpenSSL (and many C libraries): a single function encodes both result and error state in its return value.

From the OpenSSL manpage:
“The functions return 1 if the keys match, 0 if they don’t match, -1 if the key types are different and -2 if the operation is not supported.”

4. The Dangerous Semantics of C/C++

Now the dramatic part of the story. In C/C++, an if condition works like this:

You’re telling the guard at the door: “If there’s no match, reject.” But the guard only understands zero as “no match.” When -1 arrives, they think "this is a non-zero number, sure, come in."

// Vulnerable code pattern
if (EVP_PKEY_cmp(stored_key, client_key)) {
authorized = true; // -1 also lands here!
}

The correct form should have been:

if (EVP_PKEY_cmp(stored_key, client_key) == 1) {
authorized = true; // Only an exact match
}

A single-character difference. The missing == 1.

This is the kind of bug that:

5. The Logic of the Attack Scenario (Conceptual)

If the device has an RSA key stored and you try to connect with an EC (Elliptic Curve) key, the function returns -1 → the device interprets this as "matched."

In real-world terms: the vault’s lock checks “Is it a gold key? Come in.” but when you say “It’s not gold, but it is metal,” the door opens anyway.

Attack preconditions:

  1. Target device must have Wireless Debugging enabled (active developer mode)
  2. Attacker must be on the same network segment (or able to reach the mDNS-advertised port)
  3. At least one previously paired RSA key fingerprint exists on the device

Post-exploitation impact:

6. Where Has This Class of Bug Appeared Before?

This is not an isolated mistake. Similar “return value misinterpreted” bugs sit at the root of historically catastrophic vulnerabilities:

Apple’s goto fail CVE-2014-1266

A single duplicated goto fail; line caused the TLS signature verification function to short-circuit successfully before the actual cryptographic check ran. Every SSL/TLS connection on iOS and macOS skipped server authentication for months.

if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) != 0)
goto fail;
goto fail; // ← the infamous extra line
if ((err = SSLHashSHA1.final(&hashCtx, &hashOut)) != 0)
goto fail;

Debian OpenSSL RNG CVE-2008–0166

A Debian maintainer removed what looked like “uninitialized memory usage” (flagged by Valgrind) from OpenSSL’s PRNG seeding code. The result: only the PID (max 32,768 values) was used as entropy. Every SSH/SSL/OpenVPN key generated on Debian-based systems for nearly two years was predictable.

Heartbleed CVE-2014–0160

A missing bounds check on a length field allowed memcpy() to leak up to 64KB of process memory per request including private keys.

The common thread: Security-critical code was not reviewed differently from regular code. Cryptographic primitives have non-obvious contracts, and treating them like ordinary library calls invites disaster.

7. Technical Deep Dive: The Patch

The fix in the May 2026 Android Security Bulletin is essentially:

- if (EVP_PKEY_cmp(stored_pubkey, peer_pubkey)) {
+ if (EVP_PKEY_cmp(stored_pubkey, peer_pubkey) == 1) {
// authenticate peer
}

But the defensive engineering lesson goes further. A more robust pattern explicitly handles each return value:

int cmp_result = EVP_PKEY_cmp(stored_pubkey, peer_pubkey);
switch (cmp_result) {
case 1:
// exact match → authenticate
authorized = true;
break;
case 0:
// mismatch → reject
LOG(WARN) << "Key mismatch";
return AUTH_FAILED;
case -1:
// type mismatch → reject AND log (possible attack)
LOG(ERROR) << "Key type mismatch — possible attack";
return AUTH_FAILED;
case -2:
default:
// unsupported → fail closed
LOG(ERROR) << "Unsupported key comparison";
return AUTH_FAILED;
}

8. Lessons Learned and Defense

For developers:

For users:

The bigger picture:
A single missing == 1 shipped to billions of devices. The lesson isn't "be more careful" humans will always make these mistakes. The lesson is that APIs, tooling, and review processes must make this class of mistake impossible to ship.

A safer API design would use a typed enum:

typedef enum {
PKEY_CMP_MATCH = 1,
PKEY_CMP_NO_MATCH = 0,
PKEY_CMP_TYPE_MISMATCH = -1,
PKEY_CMP_UNSUPPORTED = -2
} pkey_cmp_result_t;

…making if (cmp(...)) a compile-time error, forcing explicit handling of each case.


Android Wireless ADB and TLS Auth Architecture and the Root of the Vulnerability was originally published in System Weakness on Medium, where people are continuing the conversation by highlighting and responding to this story.