Filter candidates by length and alphabet
A signature is a digest rendered as text, and that constrains its shape. Hex output of HMAC-SHA256 is 64 characters, HMAC-SHA1 is 40, MD5 is 32, and base64 output of a 32-byte digest is 44 characters with padding. A header carrying a long readable string is a payload, an identifier or a serialised object, so it can be set aside for now. Sorting a captured header list by value length and character set usually leaves a handful of candidates.
Three measurements sharpen the list.
- Decode base64 values and count the bytes. 32 bytes fits SHA-256 or HMAC-SHA256, 20 fits SHA-1 or HMAC-SHA1, 16 fits MD5, and 64 fits SHA-512. The byte count does not prove which algorithm produced the value, and it removes the candidates that cannot fit.
- Check whether a decoded value has structure. A base64 header that expands into JSON or a protobuf-looking byte sequence is carrying data rather than a digest.
- Look for companion headers. Most schemes pair the signature with a timestamp, a nonce, a key identifier or a client version, because the server needs those values to recompute the digest.
Collect a set of requests and sort by behaviour
One capture cannot tell a constant from a value that changes per call. Record ten or more successful requests from the app. An interceptor that logs the final request after the whole chain has run gives the exact bytes, and a decrypting proxy gives the same view when interception works for this app. Sort every header by how it behaves across the set.
- A value that never changes is a constant you can copy. Client identifiers, API versions and device identifiers stay the same across requests, so a client can reuse them until they expire.
- A value that increases over time is a timestamp or a counter. Compare one against the device clock to work out its unit, because a seconds value and a milliseconds value look alike at a glance.
- A value that is different on every call is a nonce or a request identifier. The device or the server generates these, and a client has to reproduce them rather than copy them.
- A value that moves with the body, path or query is computed over the request. That behaviour is what a signature produces, and the header you want belongs to this group.
The last group is where the signature sits. When two requests differ in one body field and exactly one header moves with it, that header covers the body.
Confirm coverage with deliberate edits
Coverage is the part most people skip, and it decides how you rebuild the value. Take two captures that differ in one controlled element and compare the candidate headers. Then force the app to produce variants: change a query parameter on a screen that takes input, change a filter, or change a field in a form. The header that moves is derived from the request.
Repeat for each element: the path, the query, the body, and the headers the app itself sets. Each variant tells you whether that element is inside the signed string. Some schemes sign a canonical string built from the method, path, query and body. Others sign a subset and add a timestamp. The set of edits that change the signature is the set of inputs, and you only learn it by varying them.
A rejected request confirms the same thing from the other side. When editing a parameter to a nonsense value still produces the same signature error, the signature covers that parameter and the server checked it before the business logic. When the server returns a business error instead, the check order differs and the parameter is still inside the inputs.
Separate a signature from a credential
A bearer token and a per-request signature can look the same: both are opaque, long and random. They behave differently. A credential is reusable, and a signature is bound to one request.
Send the candidate value to a different endpoint with the same method and the same session. If the server accepts it, the value is a credential. If it refuses, the value is tied to the request it came with, which is consistent with a signature. Run the test against a harmless endpoint so you do not create data you would have to remove afterwards.
Find where the app produces the value
On Android the signing step usually lives in an OkHttp interceptor, a generated request builder, or a native routine reached through JNI. The decompiled interceptor chain shows the order, and the signing interceptor commonly runs last so that it sees the final request. Search the decompiled sources for the header name, because a literal string survives obfuscation even when method names do not. When the name is built at run time or the signing happens in native code, hook the request builder and read the stack that reaches it.
After you confirm the header and its inputs, reproducing the value is a separate job that needs the key and the exact canonical string. Android request signing covers that step, and dynamic header reverse engineering covers headers that change on every call.
Related work
Reviewed 28 September 2026 · SReverse research desk