Skip to content

Add Firebase App Check attestation and APNs push support#1

Merged
dingwen07 merged 6 commits into
mainfrom
feature/universal-integrity-and-apns
Jun 26, 2026
Merged

Add Firebase App Check attestation and APNs push support#1
dingwen07 merged 6 commits into
mainfrom
feature/universal-integrity-and-apns

Conversation

@dingwen07

Copy link
Copy Markdown
Owner

Summary

This adds Firebase App Check as the new client attestation path, keeps legacy Play Integrity available during migration, adds attestation diagnostics/metrics, and refactors push delivery to support APNs alongside FCM.

Changes

  • Replaced Android direct Play Integrity calls with Firebase App Check, including debug/release providers and generic cached attestation auth tokens.
  • Generalized /v2/integrity/verify around typed attestation methods and advertises accepted methods plus PoW difficulty from /v2/status.
  • Added server-side App Check JWT verification against JWKS with issuer/audience/app-id/expiry/freshness checks.
  • Added Basic-auth protected /v2/integrity/metrics with per-method accept/reject buckets, recent events, Play Integrity verdict tallies, and App Check app-id counts.
  • Refactored push delivery behind FCM/APNs transports and added APNs HTTP/2 background wake support with shared ES256 signing helpers.
  • Preserved signed route environment and inline payload limits when storing/loading routes, and refined broker push outcome classification.
  • Updated v2/test service configuration for App Check and bumped Android to 1.2.0-rc.1.

Coverage

Added/updated tests for App Check verification, JWKS cache behavior, attestation metrics, App Check broker flow, metrics auth gating, and APNs push edge cases.

APNs is covered with unit tests/mocked provider responses, but has not yet been verified with a real Apple push key or device token. Real APNs validation is deferred until the iOS app is built.

The changes have not yet been verified because the iOS app has not been built.

Introduce APNs push transport and composite push routing. Key changes:

- Add APNs config flags to ServerConfig (apnsEnabled, apnsTeamId, apnsKeyId, apnsPrivateKeyPath, apnsTopic).
- Implement ApnsPushTransport with token JWT provider, PEM private-key loader, DER->JOSE conversion, and HTTP/2 sends; map APNs responses to PushOutcome.
- Add CompositePushTransport to unify APNS and FCM delegates; Application now creates CompositePushTransport and logs apnsEnabled/apnsEnabled state.
- Update Broker to store and use route.environment, prefer APNS over FCM, try multiple candidate routes, and aggregate outcomes (delivered, stale, invalid, missing).
- Rename buildFcmData -> buildPushData and adapt Broker wake calls to pass StoredRoute.
- Persist and decode route environment in RouteStore (StoredRoute updated) using ProtocolCodec fallback to PRODUCTION.
- Update FcmPushTransport to accept StoredRoute and bail if transport mismatch; minor logging/behaviour tweaks.
- Add test adjustments and a new integration test ensuring an APNS route queues the relay when APNS/FCM are disabled.

These changes enable iOS/Apple push support while preserving FCM behavior and improve route selection and error handling across transports.
Introduce Firebase App Check as a first-class attestation method and refactor attestation into a pluggable system. Protocol v2 types replace PlayIntegrity* request/response with generic IntegrityVerificationRequest/Response and an AttestationType enum; server now routes to verifiers via AttestationService. Client-side: remove PlayIntegrityAttestor, add AppCheckAttestor and variant-specific provider factories (src/debug and src/release), update BrokerClient to use the new request shape, support clearing the cached broker token, and fetch PoW difficulty. Server-side: add App Check JWKS fetcher and AppCheckVerifier to verify App Check JWTs locally, wire attestation config options, and advertise powDifficulty / acceptedAttestationMethods on status. Dependencies updated to include firebase-appcheck libs and remove the play-integrity client; deployment service units and UI strings updated accordingly. Overall this enables a migration from legacy Play Integrity to Firebase App Check while keeping Play Integrity supported as a verifier.
Introduce in-memory attestation metrics and a /v2/integrity/metrics HTTP endpoint for diagnostics. Add AttestationMetrics (bucketed aggregates + recent event ring), new serializable snapshot models, and record method-specific VerificationDetail data (Play Integrity verdicts, App Check appId). Wire metrics into Application (route guarded by HTTP Basic; disabled when NOTISYNC_METRICS_PASSWORD is blank) and add constant-time credential check in ServerAuth.metricsAuthorized. Extend Play Integrity decisions to carry detail payloads and add tests covering metrics aggregation and endpoint auth.
Introduce a shared Es256 implementation for ES256 signing, DER<->JOSE conversions and PEM PKCS#8 loading and replace duplicated code in Jwt and APNs providers. Harden HttpAppCheckJwks by making the HTTP fetch injectable for tests, throttling by last fetch attempt, and only replacing the cache on successful 2xx responses with usable RSA keys (preventing cache poisoning); add unit tests for these behaviors. Revise APNs push logic: add a PERMANENT_FAILURE outcome for non-retryable 4xx errors, invalidate the cached provider token on 403 so it will be re-minted, use Es256 for JWT signing, and improve logging. Update route storage to decode signed route claims once in RouteStore, persist the client-advertised inlinePayloadLimitBytes, skip undecodable claims (with a warning), and use the stored limit when computing inline budgets. Update/adjust tests accordingly.
Introduce a dedicated APNs HTTP/2 push transport (ApnsPushTransport) with a JavaNetApnsClient, ApnsTokenProvider/ApnsJwtProvider for ES256 JWT auth and token caching/invalidation, request building, payload escaping, and response -> PushOutcome mapping. Extract common push types and composition into a new Push.kt (PushOutcome, PushTransport, DisabledPushTransport, CompositePushTransport) and wire CompositePushTransport to construct FCM and APNs delegates. Remove the duplicated APNs/push code from Fcm.kt and simplify imports; provide defensive creation via createOrNull based on ServerConfig.
Copilot AI review requested due to automatic review settings June 26, 2026 03:33
@dingwen07
dingwen07 merged commit 3e267ec into main Jun 26, 2026
1 check passed

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

This PR migrates client attestation toward Firebase App Check (while keeping Play Integrity during rollout), adds attestation diagnostics/metrics, and refactors server push delivery to support both FCM and APNs transports.

Changes:

  • Introduces typed attestation (IntegrityVerificationRequest/Response) with /v2/status advertising accepted methods and PoW difficulty.
  • Adds server-side App Check verification via JWKS (with caching/poisoning guards) and a Basic-auth gated /v2/integrity/metrics endpoint.
  • Refactors push delivery into transport abstractions and adds APNs HTTP/2 background wake support, plus shared ES256 signing helpers.

Reviewed changes

Copilot reviewed 39 out of 39 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
server/src/test/kotlin/net/extrawdw/notisync/server/HttpAppCheckJwksTest.kt Adds JWKS cache tests (poisoning/throttle behavior).
server/src/test/kotlin/net/extrawdw/notisync/server/BrokerFlowTest.kt Extends broker flow tests for App Check, metrics auth gating, and APNs-disabled routing.
server/src/test/kotlin/net/extrawdw/notisync/server/AttestationMetricsTest.kt Adds unit tests for attestation metrics aggregation/pruning.
server/src/test/kotlin/net/extrawdw/notisync/server/AppCheckVerifierTest.kt Adds unit tests for local App Check JWT verification and attestation dispatch.
server/src/test/kotlin/net/extrawdw/notisync/server/ApnsPushTransportTest.kt Adds unit tests for APNs transport outcome mapping and request headers.
server/src/main/kotlin/net/extrawdw/notisync/server/Stores.kt Extends stored route model to include environment + inline payload limit derived from signed claim.
server/src/main/kotlin/net/extrawdw/notisync/server/Push.kt Introduces common push abstractions + composite transport routing.
server/src/main/kotlin/net/extrawdw/notisync/server/PlayIntegrity.kt Refactors Play Integrity verifier into pluggable attestation verifier + enriches decision detail for metrics.
server/src/main/kotlin/net/extrawdw/notisync/server/Metrics.kt Adds in-memory attestation metrics model and snapshot payload types.
server/src/main/kotlin/net/extrawdw/notisync/server/Jwt.kt Centralizes ES256 signing/conversion via new Es256 helper.
server/src/main/kotlin/net/extrawdw/notisync/server/Fcm.kt Adapts FCM transport to new PushTransport interface and shared PushOutcome.
server/src/main/kotlin/net/extrawdw/notisync/server/Es256.kt Adds shared ES256 JWT signing + DER/JOSE conversion + PEM loading.
server/src/main/kotlin/net/extrawdw/notisync/server/Config.kt Adds APNs, App Check, and metrics endpoint configuration fields.
server/src/main/kotlin/net/extrawdw/notisync/server/Broker.kt Refactors push routing across APNs/FCM and preserves per-route inline budget.
server/src/main/kotlin/net/extrawdw/notisync/server/Auth.kt Adds Basic-auth validation for metrics endpoint.
server/src/main/kotlin/net/extrawdw/notisync/server/Attestation.kt Adds pluggable attestation verifier interface + dispatcher service with metrics recording.
server/src/main/kotlin/net/extrawdw/notisync/server/Application.kt Wires attestation service (PI + optional App Check), adds /v2/integrity/metrics, and extends /v2/status.
server/src/main/kotlin/net/extrawdw/notisync/server/AppCheck.kt Adds App Check JWKS fetch/cache and RS256 JWT verification implementation.
server/src/main/kotlin/net/extrawdw/notisync/server/Apns.kt Adds APNs HTTP/2 background wake transport + provider JWT signing.
README.md Documents APNs-related configuration.
protocol/src/main/kotlin/net/extrawdw/notisync/protocol/ControlPlane.kt Adds new attestation types and replaces legacy verify request/response with typed v2 structures.
gradle/libs.versions.toml Removes Play Integrity client dep and adds Firebase App Check dependencies.
docker-compose.yml Documents APNs secrets/env wiring in compose config.
deploy/notisync-broker.service Removed legacy systemd unit file.
deploy/notisync-broker-v2.service Adds APNs/App Check configuration guidance to v2 unit file.
deploy/notisync-broker-test.service Updates test unit file with APNs/App Check env and master switch semantics.
app/src/release/java/net/extrawdw/apps/notisync/integrity/AppCheckProvider.kt Release App Check provider factory (Play Integrity provider).
app/src/main/res/values/strings.xml Adds diagnostics string for clearing cached auth token and tweaks diagnostics copy.
app/src/main/res/values-zh-rCN/strings.xml Adds diagnostics string for clearing cached auth token (zh-CN).
app/src/main/java/net/extrawdw/apps/notisync/ui/SettingsScreen.kt Wires diagnostics UI action to clear cached broker auth.
app/src/main/java/net/extrawdw/apps/notisync/ui/DiagnosticsCard.kt Adds “Clear local token” button and reorders diagnostics sections.
app/src/main/java/net/extrawdw/apps/notisync/transport/BrokerClient.kt Switches attestation to App Check, updates PoW to use broker-advertised difficulty, and adds clearCachedAuth.
app/src/main/java/net/extrawdw/apps/notisync/transport/AuthTokenStore.kt Updates stored token type to IntegrityVerificationResponse.
app/src/main/java/net/extrawdw/apps/notisync/integrity/PlayIntegrityAttestor.kt Removed direct Play Integrity client attestor.
app/src/main/java/net/extrawdw/apps/notisync/integrity/AppCheckAttestor.kt Adds App Check token attestor based on Firebase SDK.
app/src/main/java/net/extrawdw/apps/notisync/crypto/DeviceCrypto.kt Updates encrypted token storage to new response type.
app/src/main/java/net/extrawdw/apps/notisync/AppGraph.kt Wires AppCheckAttestor into BrokerClient.
app/src/debug/java/net/extrawdw/apps/notisync/integrity/AppCheckProvider.kt Debug App Check provider factory (debug tokens).
app/build.gradle.kts Bumps app version, removes Play Integrity client config, and adds App Check dependencies.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +155 to +158
val header = runCatching { Json.parseToJsonElement(String(b64.decode(parts[0]))).jsonObject }.getOrNull()
?: return@withContext reject("appcheck_bad_header")
if ((header["typ"] as? JsonPrimitive)?.contentOrNull != "JWT") return@withContext reject("appcheck_bad_typ")
if ((header["alg"] as? JsonPrimitive)?.contentOrNull != "RS256") return@withContext reject("appcheck_bad_alg")
Comment on lines +37 to +39
val header = call.request.headers[HttpHeaders.Authorization]?.takeIf { it.startsWith("Basic ") } ?: return false
val decoded = runCatching { String(Base64.getDecoder().decode(header.removePrefix("Basic ").trim()), Charsets.UTF_8) }
.getOrNull() ?: return false
<string name="diag_rotate_failed">Rotation failed — check the broker connection</string>
<string name="diag_channels">Notification channels</string>
<string name="diag_channels_hint">Notification channels (categories) are created be created when new notifications arrive. Notifications originated from Android will mirror the original notification channels, while a single notification channel will be used for iOS apps. Use this option to deletee all mirrored notification channels. This will also discard your customizations for any notification channels (including sound, vibration, pop-ups, app icon badges, etc.).</string>
<string name="diag_channels_hint">Notification channels (categories) are created be created when new notifications arrive. Notifications originated from Android will mirror the original notification channels, while a single notification channel will be used for iOS apps.\n\nUse this option to deletee all mirrored notification channels. This will also discard your customizations for any notification channels (including sound, vibration, pop-ups, app icon badges, etc.).</string>
Comment on lines 131 to 139
log.info(
"NotiSync broker {} starting (db={}, fcm={}, playIntegrity={})",
"NotiSync broker {} starting (db={}, fcm={}, apns={}, playIntegrity={}, appCheck={})",
config.version,
config.dbPath,
push !is DisabledPushTransport,
config.fcmEnabled,
config.apnsEnabled,
config.playIntegrityEnabled,
config.appCheckEnabled,
)
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