A name-based certificate-pinning script may work on a normal APK and stop matching after obfuscation. The policy still runs; names such as okhttp3.CertificatePinner are what disappeared. Parameter types, input data, and object-construction timing offer a different route to the same code.
Jeroen Beckers' OkHttp experiment uses Android 7.1.2, Objection 1.4.3, and Apktool 2.3.4 to compare a normal build with default ProGuard obfuscation. The conclusions are specific to that environment. The focus is identifying a construction method through its type descriptor, rather than assuming stable class names across versions.
Separate the three request paths
The test app retrieves the same service's robots.txt through three clients: the platform client, OkHttp without an explicit pinner, and OkHttp with CertificatePinner. Its network_security_config.xml also configures a SHA-256 pin for the target domain and its subdomains.
No OkHttp pinner does not mean no pinning anywhere in the connection. Platform configuration and library policy can overlap. Likewise, trusting the proxy CA at the system level does not establish that it matches an additional app pin.
| Condition | Recorded value | Why it matters |
|---|---|---|
| Device | Android 7.1.2 in the terminal capture | Fixes the platform verification implementation |
| Tools | Objection 1.4.3; Apktool 2.3.4 | Keeps historical behavior separate from current versions |
| Proxy CA | Installed in the system trust store | Helps exclude ordinary chain-trust failures |
| Configuration expiry | expiration="2022-01-01" |
Preserves the timing condition of the 2019 experiment |
| Comparison variable | Normal build versus default ProGuard obfuscation | Helps distinguish failed name lookup from changed request logic |
A tool match is not a successful request
Objection's output shows that it found OkHttp and TrustManagerImpl. The excerpt omits repeated calls, session identifiers, the app package name, and local paths:
$ android sslpinning disable
Custom, Empty TrustManager ready
OkHTTP 3.x Found
TrustManagerImplFinding a class establishes that lookup succeeded. It does not establish coverage of every verification entry point. The app's three result labels provide a request-level comparison:
| UI label | Normal build: Objection defaults only | Obfuscated build: platform and Builder handling combined |
|---|---|---|
| SecurityPolicy | ERROR | OK |
| OKHTTP | OK | OK |
| Pinned OKHTTP | OK | OK |
These columns are not a strict A/B test that changes one hook on the same build. They represent different stages: the platform request first fails under the default handling, extra platform handling is added, obfuscation is introduced, and the obfuscated Builder is handled last. The table shows the two endpoint states, not a complete record of every intermediate stage.
On this device, the tool's default entry points did not cover the entire platform check. That observation is narrower than a claim about every Android 7 minor release.
Parameter descriptors outlast short names
After obfuscation, the name-based script stops identifying OkHttp and the request with additional pins fails again. Inspect the lookup chain before assuming that the app introduced a new cryptographic mechanism.
The target is CertificatePinner.Builder.add(), a policy-construction method. It receives a hostname pattern and an array of pins, adds rules to the Builder's collection, and returns the Builder for chaining. Its first argument is a hostname or pattern, not an arbitrary full URL.
At the bytecode level, String... is String[]. The descriptor shape used to filter candidates is:
(Ljava/lang/String;[Ljava/lang/String;)Lreturn/type;Here, Lreturn/type; is an illustrative object return type. The [ denotes an array. Standard-library type descriptors often survive default name obfuscation, but ordinary business methods may share the same signature. Treat it as a candidate filter, not proof of identity.
Confirm the construction point from search results
After Apktool 2.3.4 decodes obfuscated.apk, the search results include call sites and method declarations. The candidate's declaration is:
.method public varargs a(Ljava/lang/String;[Ljava/lang/String;)Lokhttp3/g$a;A fixed-string search avoids interpreting [ as regular-expression syntax. The following are verification commands, not an execution log:
apktool d obfuscated.apk -o application
rg -n -F 'Ljava/lang/String;[Ljava/lang/String;)L' application/smali*Search every smali directory rather than only the first DEX. Deduplicate declarations and call sites so that one method is not counted as several candidates.
- 1
Record its identity
Save the class, method, full parameter descriptor, return type, and DEX. This APK uses
okhttp3.g$aanda; those names are specific to that build. - 2
Observe arguments
Check that the first argument is the expected hostname pattern and the second array contains the expected pins, rather than unrelated business strings.
- 3
Inspect the callers
Use callers and stack context to establish that certificate-pinning policy is being constructed. Check the writes to the Builder's collection.
- 4
Check timing
Confirm that instrumentation is active before the client and policy objects are built, then record request outcomes.
Why returning the Builder changes policy
In this sample, the method both adds rules and returns itself. Returning the object without performing that addition preserves chaining but omits those pins. The following script retains the historical APK's obfuscated names:
Java.perform(function () {
const Builder = Java.use('okhttp3.g$a');
const add = Builder.a.overload(
'java.lang.String', '[Ljava.lang.String;'
);
add.implementation = function (hostPattern, pins) {
console.log('Observed pin configuration: ' + hostPattern);
return this;
};
});- Select both argument types to avoid a different overload with the same name.
- Preserve the chained return value without calling the original method. This skips one rule addition; it does not remove rules from existing clients.
Timing is essential. Instrumenting a Builder after the client is built does not retroactively change the old instance. Early static initialization, another class loader, different overloads, direct policy construction, and Builder inlining can all prevent this path from covering the policy.
The final result depends on the existing platform handling and the added Builder handling together. It connects the configuration entry point to the later requests in this sample. It does not establish a universal result for every OkHttp build.
Keep the conclusion reproducible
Obfuscation removes convenient names, but type shapes, argument meaning, and construction relationships can still establish identity. Start with per-request baselines, confirm tool matches, introduce obfuscation, narrow candidates by descriptor, and verify them through arguments and construction timing.
Client-side observation and behavior changes also do not replace server-side authentication or data authorization checks. Use a separate debug configuration during development, and record platform trust, library pinning, and server permissions independently. “Traffic became visible” is not a complete assessment.