Skip to content

Sign the device assertion timestamp once, not twice - #48

Merged
Alex Arguello (alex-arguello) merged 1 commit into
mainfrom
alexarguello/pla-2483-android-close-out-the-tap-to-pay-live-tier-and-its-quality
Aug 14, 2026
Merged

Sign the device assertion timestamp once, not twice#48
Alex Arguello (alex-arguello) merged 1 commit into
mainfrom
alexarguello/pla-2483-android-close-out-the-tap-to-pay-live-tier-and-its-quality

Conversation

@alex-arguello

Copy link
Copy Markdown
Collaborator

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 to SHA256withECDSA, 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

  • DeviceAssertionSigner signs UTF8(timestamp) instead of SHA256(UTF8(timestamp)). The private clientDataHash helper is deleted; nothing else called it.
  • Its KDoc previously justified the double hash as mirroring the sibling platform. That claim is deleted with the code: the sibling's second hash is applied by a platform attestation API, on a different layer, over an object this key does not produce.
  • Three assertions in DeviceAssertionSignerTest pinned the old shape and now pin the new one, including one that fails if a digest is ever passed to the signer again.

Verification

:taptopay unit tier is 210 tests, all passing, nothing skipped. assembleDebug, ktlintCheck and lint are 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:

Test Pixel 7a, API 36 SM-S908U1, API 36
challenge and register PASS PASS
unattested device is refused with the expected wording PASS PASS
cold sequence completes through attestation PASS PASS
pending device activates with a code minted out of band PASS PASS

A third handset, an SM-A136U1, cannot run any of them and is a stated skip rather than a silent one: ro.product.cpu.abilist reports armeabi-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 main is 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

`/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 AI balanced review requested due to automatic review settings August 14, 2026 19:40

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@sonarqubecloud

Copy link
Copy Markdown

@alex-arguello
Alex Arguello (alex-arguello) merged commit 86269b6 into main Aug 14, 2026
5 checks passed
@alex-arguello
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants