The integration bug hiding behind a valid algorithm
One of the easiest ways to break a signed integration is to let two services mean different things by “the signature.”
In a B2B fraud-detection platform I worked on, HMAC request signing connected a Laravel application, its SDKs and an ML service. A signature-format mismatch nearly shipped. That is the engineering lesson I want to share, without exposing the employer, its code or its internal API contract.
The question I now ask during review is simple: can two independent implementations produce exactly the same authenticated message and the same transmitted signature representation?
Saying “we both use HMAC” does not answer that question. The algorithm is one part of the agreement. The message bytes, key handling and representation also need an explicit contract.
The examples and review process below are general guidance. They are not a reconstruction of that private implementation or a claim about the exact mismatch we encountered.
Start with a contract someone else can implement
Before reviewing a signing function, I want a short document that another engineer can implement without asking what the original author intended.
I would give that document an owner and review it with the people responsible for each caller. For this kind of integration, that means application engineers, SDK maintainers and the team operating the receiving service. Each group should be able to point to the same agreed input and expected output.
My review questions would include: which fields are authenticated, where those fields come from, how their bytes are constructed, which key identifier is sent, and what representation the receiver accepts. I would also ask how a change becomes a new contract version.
A useful review ends with fewer interpretations. If the document says “sign the request” but two engineers can reasonably produce different inputs, I would consider the interface unfinished. That is a documentation and compatibility problem before it becomes a deployment problem.
Agree on the message bytes
HMAC authenticates a message using a shared secret. It does not understand the business meaning of a request. The same key and message under the same algorithm produce the same authentication value; changing the input changes what is being authenticated. [1]
For an HTTP integration, the design therefore needs to identify the covered request components and define how they form the signature input. RFC 9421 provides a standardized approach to HTTP message signatures, including explicit covered components and signature parameters. I would evaluate a standard before inventing a private format. [2]
In a design discussion, I would put the request the sender actually transmits beside the input the receiver actually verifies. Then I would ask the team to explain every transformation between them. This makes assumptions visible while the system is still easy to change.
I would avoid resolving uncertainty by guessing which transformation is harmless. The decision belongs in the contract, with a fixture that demonstrates it. A reviewer should be able to answer “why is this input correct?” by reading the specification and the test case together.
Treat encoding as part of the public interface
PHP provides a concrete example of why representation needs attention. Its hash_hmac function returns lowercase hexadecimal by default; its binary option returns raw digest bytes instead. Those are different representations of the calculated value. [3]
A caller that encodes raw digest bytes as Base64 will send a different string from a caller that Base64-encodes the hexadecimal text. Neither function name tells the receiving service which representation it is getting. Agreement must be explicit.
I would write down the accepted encoding, any prefix, and how the value is carried. I would give SDK authors one expected transmitted value alongside the original fixture inputs. I would also name the example clearly so nobody mistakes an illustration for a production credential.
During review, I would ask someone outside the original implementation to explain the format back to me. If their explanation requires opening three source files and interpreting defaults, I would move those decisions into the interface documentation. Cross-language integrations deserve a contract that survives a change of library.
Use a shared fixture as the meeting point
My preferred review artifact is a small fixture with a deliberately synthetic payload, a public test-only key and an expected result. It should contain no customer data, internal endpoint or production secret.
I would have each implementation consume that fixture independently. The Laravel side, the SDK and the receiving service should agree on the intermediate signing input as well as the final transmitted representation. That gives reviewers a common reference when they investigate a difference.
I would keep fixtures close to the interface specification and review their changes together. A pull request that changes expected output should explain whether it corrects a test or changes the contract. Those are different decisions, and a reviewer needs enough context to tell which one is happening.
The test suite I would propose also contains intentional disagreements. One case alters a covered field. Another supplies a malformed representation. Another uses a different test key. Each should have an explicit expected outcome. These are recommended tests for this pattern, not results from a test run against the employer's system.
Make failures useful without making them revealing
When investigating a mismatch, I want to identify the stage where agreement ended. Did the request parse correctly? Was the expected key configuration available? Did the representation decode as specified? Did authentication fail after those steps?
I would use internal diagnostic categories for those stages and decide separately what the public response should disclose. That helps an on-call engineer investigate the failure without turning an external error message into a detailed account of the server's internals.
I would review logging alongside the signing code. A debugging change should not copy secrets, sensitive request bodies or reusable authentication material into logs. For a reproduction, I would first build a sanitized fixture that demonstrates the same disagreement using synthetic inputs.
For comparing authentication values in PHP, hash_equals is the documented timing-attack-safe comparison function. Its known value belongs first and the user-supplied value second. The documentation also notes that inputs of different lengths return false immediately. [4]
That detail belongs in a code review, but it does not replace the earlier interface review. A comparison function can only compare the values the implementation actually constructed.
Review freshness and retries as separate requirements
A valid message authentication value does not, by itself, establish that a request is fresh. A previously valid signed message can be presented again. HTTP Message Signatures discusses replay considerations and parameters such as creation time, expiry and nonce values. What an application must enforce depends on its protocol and threat model. [2]
I would put that question on the design agenda before rollout. What should happen if a legitimate caller retries? What should happen if an old authenticated request is replayed? Which component owns the decision? What happens during clock drift or a temporary dependency failure?
I would ask the product and service owners to agree on expected behavior rather than allow each SDK to choose independently. A retry policy is an observable part of the integration, and it needs documentation that the support team can use too.
I would then make those agreed cases part of the release checklist. This article does not claim that the private platform implemented a particular replay window, nonce store or idempotency mechanism; those details are outside the information I can share.
Review the rollout with the same care as the code
For a future change to a signing contract, I would want an inventory of callers, the versions they run and the team responsible for upgrading each one. I would review that inventory before approving a switch in the receiving service.
My rollout plan would state what changes, how compatibility is evaluated and what evidence permits the next step. I would ask for a clear rollback decision and a way to distinguish a contract mismatch from an unrelated service failure.
That plan should be understandable to someone who did not write the signing implementation. If the deployment depends on one engineer remembering an undocumented exception, I would treat that as unfinished work.
The lesson I take from the near miss is to make agreement reviewable. I want a written format, synthetic fixtures and independently checked implementations before an integration is called ready. “Both sides use HMAC” is a starting point; the contract explains whether they actually agree.
What do you use to keep signing behavior consistent across your application, SDKs and downstream services?
References
- RFC 2104: HMAC — message authentication with shared keys.
- RFC 9421: HTTP Message Signatures — covered components, signature parameters and replay considerations. This article does not claim the private integration implements RFC 9421.
- PHP hash_hmac — digest output representation.
- PHP hash_equals — comparison behavior and argument order.

Top comments (0)