Skip to content

refactor(remote-config): Trusted-entitlements signing for the binary remote config response#3601

Merged
tonidero merged 2 commits into
mainfrom
support-rc-format-remote-config-request
Jun 19, 2026
Merged

refactor(remote-config): Trusted-entitlements signing for the binary remote config response#3601
tonidero merged 2 commits into
mainfrom
support-rc-format-remote-config-request

Conversation

@tonidero

@tonidero tonidero commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Stacked on #3607 (which adds the binary RC Container request/parsing).

Enables trusted-entitlements signature verification for the binary GET /v2/config response. This hasn't been tested against the actual endpoint yet, but this endpoint is currently unused so should be safe to merge.

What

  • GetRemoteConfig.supportsSignatureVerification = true. The response signature is verified over the config element's 24-byte truncated SHA-256 checksum (no nonce). Two independent checks are both required, else FAILED:
    1. X-Signature (Ed25519) covers the checksum -> backend authenticity.
    2. RCElement.isChecksumValid() ties the checksum to the actual config bytes.
  • SigningManager: signed body is now ByteArray? (binary-safe).
  • HTTPClient.verifyBinaryResponse parses the container, checks integrity, and verifies the signature over the config checksum.
  • The resulting VerificationResult is surfaced through the getRemoteConfig callback.

Note

Medium Risk
Touches signature verification and changes the getRemoteConfig callback shape, but scope is limited to the still-unused remote config endpoint with strong test coverage.

Overview
Enables trusted-entitlements signature verification for GET /v2/config RC Container responses and threads the outcome to callers.

GetRemoteConfig is now in the set of endpoints that require response signing. Verification is not over the raw body: the client parses the container, requires RCElement.isChecksumValid() on the config element, then verifies X-Signature over that element’s 24-byte truncated SHA-256 checksum (no nonce / post-params). Empty 204 responses are treated as VERIFIED without calling the signer.

SigningManager.verifyResponse now takes ByteArray? for the signed payload so JSON bodies and binary checksums use the same API. getRemoteConfig success callbacks are (RCContainer?, VerificationResult) instead of container-only.

Reviewed by Cursor Bugbot for commit de07e2b. Bugbot is set up for automated code reviews on this repo. Configure here.

@tonidero tonidero changed the base branch from poc/rc-container-format-deserializer to graphite-base/3601 June 15, 2026 16:34
@tonidero tonidero force-pushed the support-rc-format-remote-config-request branch from 4193b51 to 17f6f5d Compare June 15, 2026 16:35
@tonidero tonidero changed the base branch from graphite-base/3601 to rc-format-binary-remote-config June 15, 2026 16:35
@tonidero tonidero changed the title Support RC format remote config request Trusted-entitlements signing for the binary remote config response Jun 15, 2026
@codecov

codecov Bot commented Jun 16, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 79.48718% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.31%. Comparing base (5487b43) to head (de07e2b).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
...at/purchases/common/verification/SigningManager.kt 40.00% 6 Missing ⚠️
...tlin/com/revenuecat/purchases/common/HTTPClient.kt 90.90% 0 Missing and 2 partials ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main    #3601   +/-   ##
=======================================
  Coverage   80.30%   80.31%           
=======================================
  Files         378      378           
  Lines       15475    15503   +28     
  Branches     2147     2154    +7     
=======================================
+ Hits        12427    12451   +24     
- Misses       2189     2193    +4     
  Partials      859      859           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@tonidero tonidero changed the title Trusted-entitlements signing for the binary remote config response refactor(remote-config): Trusted-entitlements signing for the binary remote config response Jun 16, 2026
@tonidero tonidero force-pushed the support-rc-format-remote-config-request branch from 17f6f5d to e30cd22 Compare June 16, 2026 11:08
@tonidero tonidero force-pushed the rc-format-binary-remote-config branch from 4cd4c47 to 2538600 Compare June 16, 2026 11:08
@tonidero tonidero force-pushed the support-rc-format-remote-config-request branch from e30cd22 to c161d23 Compare June 16, 2026 14:40
assertThat(parsed.config.data.let { buf -> ByteArray(buf.remaining()).also { buf.duplicate().get(it) } })
.isEqualTo(config)
assertThat(parsed.contentElements).hasSize(1)
assertThat(verification).isEqualTo(VerificationResult.VERIFIED)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I will want to add an integration test for this request once the backend is working and we have tested it... for now, I haven't been able to test this yet.

@tonidero tonidero marked this pull request as ready for review June 16, 2026 15:20
@tonidero tonidero requested a review from a team as a code owner June 16, 2026 15:20
@tonidero tonidero force-pushed the rc-format-binary-remote-config branch from 7ea7a07 to 678860e Compare June 17, 2026 13:05
@tonidero tonidero force-pushed the support-rc-format-remote-config-request branch 2 times, most recently from e730be7 to 7df7393 Compare June 18, 2026 10:02
@tonidero tonidero force-pushed the rc-format-binary-remote-config branch from 42d3db7 to 84b96ec Compare June 18, 2026 10:02
@tonidero tonidero force-pushed the support-rc-format-remote-config-request branch from 7df7393 to 18e1888 Compare June 18, 2026 10:27
@tonidero tonidero force-pushed the support-rc-format-remote-config-request branch 2 times, most recently from 3799228 to aca57d7 Compare June 18, 2026 13:24

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit aca57d7. Configure here.

Comment thread purchases/src/main/kotlin/com/revenuecat/purchases/common/HTTPClient.kt Outdated
@tonidero tonidero force-pushed the support-rc-format-remote-config-request branch from aca57d7 to c07f7b1 Compare June 18, 2026 13:46
@emerge-tools

emerge-tools Bot commented Jun 18, 2026

Copy link
Copy Markdown

📸 Snapshot Test

593 unchanged

Name Added Removed Modified Renamed Unchanged Errored Approval
TestPurchasesUIAndroidCompatibility
com.revenuecat.testpurchasesuiandroidcompatibility
0 0 0 0 335 0 N/A
TestPurchasesUIAndroidCompatibility Paparazzi
com.revenuecat.testpurchasesuiandroidcompatibility.paparazzi
0 0 0 0 258 0 N/A

🛸 Powered by Emerge Tools

@rickvdl rickvdl left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Awesome work! As far as I can see this is in line with what we discussed. Just two questions :)

nonce = null,
bodyBytes = config.checksumBytes(),
requestTime = getRequestTimeHeader(connection),
eTag = getETagHeader(connection),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

How is this handled, since we don't have etag support for these?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Right, I left this a bit as a failsafe TBH... We are indeed not storing etags for this endpoint. The idea here is that, if the backend does send an etag, even if we don't store it, as long as it uses the same signing mechanism, it should be part of the signature, so we get it from the response headers to calculate the signature. This might not happen if the backend does not provide an etag, but this would also handle it.

So yeah, basically a guard just in case the backend does send etags.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Makes sense, thanks for clarifying!

@@ -393,7 +394,11 @@ internal class HTTPClient(
val verificationResult = if (shouldSignResponse &&
RCHTTPStatusCodes.isSuccessful(responseCode)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

How do we handle 204's here? Should we early exit and not do any verification since we don't have any content?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That's a valid question!! And yeah, right now I wasn't handling it specially or anything, but if we get a 204 (and no content), I don't think there is a need to verify anything (no chance of MiTM attack I would say, so we can just assume verified. Let me know if you think that's ok!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I added a check for this case 🙏 de07e2b

Base automatically changed from rc-format-binary-remote-config to main June 18, 2026 15:47
Enables Ed25519 signature verification for GetRemoteConfig: the backend signs
the config element's 24-byte truncated SHA-256 checksum, and the SDK both
verifies that signature and independently confirms the config bytes hash to it
(SigningManager now accepts a raw-bytes body; HTTPClient.verifyBinaryResponse
ties the two checks together). The resulting VerificationResult is surfaced
through the getRemoteConfig callback and stored on RemoteConfigManager's cache
entry for the follow-up that consumes blobs/entitlements.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@tonidero tonidero force-pushed the support-rc-format-remote-config-request branch from c07f7b1 to 86e78b3 Compare June 18, 2026 15:57
@tonidero tonidero added this pull request to the merge queue Jun 19, 2026
Merged via the queue into main with commit 17541f4 Jun 19, 2026
38 checks passed
@tonidero tonidero deleted the support-rc-format-remote-config-request branch June 19, 2026 09:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants