Sign the device assertion timestamp once, not twice - #48
Merged
Alex Arguello (alex-arguello) merged 1 commit intoAug 14, 2026
Conversation
`/activate` refused every Android device with `Assertion verification failed: `, on both bench handsets. The signer hashed the timestamp and handed the digest to `SHA256withECDSA`, which hashes again, so the signature was taken over the hash of a hash. The service verifies a signature over `SHA256(UTF8(timestamp))`, one hash. Hand the timestamp bytes to the key unhashed and let the algorithm apply the single hash. `clientDataHash` goes with it, along with its claim that the double hash mirrored the sibling platform: the sibling's second hash is imposed by a platform attestation API on a different layer, over an object this key does not produce. This was unreachable until now. Attestation does not verify an assertion this way, and the unattested case returns at the attestation lookup before verification is reached, so the confirm path is the only one that exercises it. Three unit assertions pinned the old shape and now pin the new one.
Copilot started reviewing on behalf of
Alex Arguello (alex-arguello)
August 14, 2026 19:40
View session
There was a problem hiding this comment.
Pull request overview
Corrects device assertion signing so Android activation signatures match server verification.
Changes:
- Signs timestamp bytes directly, avoiding a double SHA-256 hash.
- Updates assertion contract documentation.
- Revises unit tests to enforce the corrected signature payload.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
DeviceAssertionSigner.kt |
Removes timestamp pre-hashing. |
DeviceAssertion.kt |
Documents the corrected signature contract. |
DeviceAssertionSignerTest.kt |
Verifies direct timestamp signing. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
Alex Arguello (alex-arguello)
deleted the
alexarguello/pla-2483-android-close-out-the-tap-to-pay-live-tier-and-its-quality
branch
August 14, 2026 19:59
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Tickets: PLA-2483 (run the tap-to-pay live tier and close what it finds). It also meets PLA-2389, whose acceptance is a real device completing the attestation sequence and getting a 200: that now happens on both bench handsets, and it could not be demonstrated before this fix because the call after attestation refused every Android device.
Device activation refused every Android device at the confirm step, with the service answering
Assertion verification failed:. The proof-of-possession signer hashed the timestamp itself and then handed that digest toSHA256withECDSA, which hashes whatever it is given, so the signature was taken over the hash of a hash. The service verifies a signature over the timestamp hashed once. This change hands the timestamp bytes to the key unhashed and lets the signing algorithm apply the single hash.Notable changes
DeviceAssertionSignersignsUTF8(timestamp)instead ofSHA256(UTF8(timestamp)). The privateclientDataHashhelper is deleted; nothing else called it.DeviceAssertionSignerTestpinned the old shape and now pin the new one, including one that fails if a digest is ever passed to the signer again.Verification
:taptopayunit tier is 210 tests, all passing, nothing skipped.assembleDebug,ktlintCheckandlintare green.The live tier,
DeviceActivationLiveTest, is four tests run against the real service on wired hardware. Before this change, three passed and the fourth failed on both handsets; after it, all four pass on both:A third handset, an SM-A136U1, cannot run any of them and is a stated skip rather than a silent one:
ro.product.cpu.abilistreportsarmeabi-v7a,armeabi, and the card reader dependency ships no 32-bit native library, so the test APK will not install. An emulator is not a target here either, and the suite fails rather than skips on one, deliberately, so that a run pointed at the wrong device is visible.The last of the four tests is the only one that exercises the confirm path end to end, which is why this defect survived until now: attestation does not verify an assertion this way, and the unattested case returns at the attestation lookup before verification is reached.
Not included
The SonarCloud quality gate on
mainis still failing on one open finding against the hardware identifier read, unrelated to this change. Dispositioning it needs project permissions and is tracked on PLA-2483.🤖 Generated with Claude Code