Why the Java view is incomplete
An app that signs in Java leaves a trail: a method that joins strings, a call into a hash or MAC routine, and a header that receives the result. Instrumentation at the HTTP client sees all of it, and the client view shows clearly which parts of the request were involved.
Move the same work into native code and the trail narrows to a call with an argument and a return value. The signed material, the key and the algorithm are all on the far side. Hooking the HTTP client still shows the finished header, which is why the first step is to find the native function that produced it.
Locate the boundary
Work backward from the header. The value that appears in the request came from somewhere, and following it in the Java layer ends at a native declaration or at a return value from a bridge call. That declaration is the boundary you need.
Static work on the library answers the next question. Exported names, a registration table and string references all provide entry points. A library that only signs tends to link against hash and cipher code, which narrows the search further. A library that also builds the request links against networking code as well, and its signing function usually sits near the code that writes the final body.
Capture the signed material
The canonical string is the part most often guessed wrong. It is rarely the full request. Common ingredients include the method, the path, the query string, a digest of the body, a timestamp, a nonce and one or two static values such as an application identifier.
Recording what crosses the boundary enumerates the ingredients instead of guessing them. Each argument maps to a candidate field, and the order in which the arguments are passed is a strong hint about the order used in the canonical form. A separator and a trailing byte sequence are also visible at this point, and both matter to the result.
A quick experiment shows whether an argument contributes to the result.
- Hold the call site fixed and change one argument. If the returned signature changes, that argument is in the material.
- Change a value that you believe is excluded, such as a header that a proxy added. If the signature stays the same, the exclusion is confirmed.
- Change only the order of two arguments. Some schemes sort their fields, and a reordered input with an identical signature reveals that.
Identify the algorithm
Several properties of the output narrow the algorithm. Length is the first: a fixed length binary value points at a message authentication code or a digest, while a longer value in a text alphabet points at an asymmetric signature or at an encoding step applied to a digest. Two calls with identical inputs producing identical output rules out a random component. A small change to the input producing an entirely different output is consistent with a cryptographic hash.
With those properties known, check the common constructions in order: a keyed hash over a canonical string, a digest signed with a private key, and a digest folded into an encoded blob that also carries a key identifier or a timestamp. Reimplementing the likely one and comparing against the app settles it.
Key handling decides the deliverable
Where the key comes from changes what you can hand over. A static key read from the library or from configuration is straightforward to reproduce, and the client can carry it. A key derived from device state or from a keystore operation is not, because your client cannot reproduce the hardware value.
In that situation, look for what the server actually validates. Some flows accept a weaker check on certain endpoints, use a token issued earlier, or tolerate a device identifier that can be registered through a supported path. Data from other endpoints sometimes supplies the identifier that the signature needs.
Confirm the whole chain
A signature that matches on one input proves little. Run your implementation against many inputs covering a changed body, a changed path and a changed timestamp, and compare byte for byte with the app before sending anything. Then send a request to the live service and check the response, because the server may validate a header that the client code only appears to set.
The finished piece is a small function that produces valid signatures from the same inputs the app uses. That function plus the request shapes is what turns the app into an API your code can call.
Related work
Reviewed 28 September 2026 · SReverse research desk