feat: kmpworkmanager 3.5.0 + opt-in TLS certificate pinning (v1.8.0) - #70
Merged
Conversation
## kmpworkmanager engine bump 3.4.1 -> 3.5.0 "Hardening" release, 20 bug fixes, 0 public API changes — kmpworker.api (the JVM ABI) is byte-identical between the two tags, so KMPSchedulerBridge. swift (FROZEN) needed no changes. Both of 3.5.0's own "breaking changes" verified not applicable to this plugin (FGS_MEDIA_PROCESSING constant fix, FakeBackgroundTaskScheduler — neither referenced here). Fixes that reach real behavior transparently: ExistingPolicy.KEEP input-file data loss, chain-step 8KB+8KB merge-budget bug, retry backoff jitter, exact-alarm AlarmStore desync, Windowed-trigger constraint loss on retry, iOS chain poison-pill + cancellation-during-prologue, dot-prefixed iOS task-id listing gap, both platforms' event-store low-disk silent-data-loss path. xcframework rebuilt from the kmpworkmanager v3.5.0 git TAG (not HEAD). ## Opt-in TLS certificate pinning New `certificatePinning` parameter (CertificatePin/CertificatePinning) on every HTTP-ish worker: HttpRequestWorker, HttpDownloadWorker, HttpUploadWorker, HttpSyncWorker, ParallelHttpDownloadWorker, ParallelHttpUploadWorker, WebSocketWorker (Android). Per-request, additional to platform chain validation, never a replacement. Closes a real gap: both native bridges already had a certificatePinning config field on 2 of 8 workers each, entirely unreachable from Dart (lib/ had zero references). Wired into the remaining workers on both platforms, plus 2 real bugs found and fixed: - iOS: the pin comparison hashed the RAW public key instead of the full SubjectPublicKeyInfo a sha256/ pin is computed over — confirmed empirically against a live TLS handshake (the two hashes differ; only the SPKI one matches an openssl-verified reference). Any pin generated by a standard tool would never have matched. Fixed with an SPKI header table transcribed byte-for-byte from kmpworkmanager's own verified TlsPinning.ios.kt, rejecting unsupported key types outright. - iOS: a pinned URLSession could serve a cached response from an earlier, differently-pinned request to the same URL via the process-wide URLCache.shared, without the pin ever being checked on that call. Found by the device test itself (isolated pass, failed when run right after a same-URL positive case). Fixed by disabling caching on every pinned session. iOS's trust-evaluation shape also switched to check-pin-then- performDefaultHandling (rather than manual SecTrustEvaluateWithError + supplied credential) per kmpworkmanager's field report of that pattern rejecting valid chains on their hardware — not reproduced on this project's own hardware, recorded honestly as a defensive adoption, not an independently-confirmed bug. Device-verified end-to-end on a real Pixel 6 Pro and an iOS simulator: a correct pin lets a real HTTPS request through, a wrong pin genuinely rejects the connection, an unconfigured worker is unaffected. See "TLS Certificate Pinning" in device_integration_test.dart. ## Also fixed: sanitizedURL credential leak (Android + iOS) SecurityValidator.sanitizedURL() redacted query params but not RFC 3986 UserInfo, so a URL carrying Basic-auth credentials in its authority (https://user:pass@host/...) printed the password verbatim into logs and persisted WorkerResult messages, on every HTTP worker on both platforms. Same bug shape kmpworkmanager's own (unrelated, separate) SecurityValidator had just fixed — found by comparison, not shared code. ## Verification - pana: 160/160 on both native_workmanager and native_workmanager_gen - flutter analyze: 0 issues, all 3 packages - Full host suite (unit/integration/security/performance): 1397/1397 green - Android build + iOS build (flutter build ios --simulator --no-codesign): both clean - Device-run: Cancellation group (issue #66/#69, 4/4) unaffected; All Workers group (16/16) unaffected; new TLS Certificate Pinning group (3/3) on both a real Pixel 6 Pro and iOS simulator Version: 1.7.0 -> 1.8.0 (native_workmanager, native_workmanager_gen, example app, podspec).
Kotlin unit test for the UserInfo-redaction fix (7 cases: credentials redacted, redacted+query together, port/path preserved, no-credential URL unchanged, query-only unchanged, "@" in path left alone, empty string doesn't crash). Needed @RunWith(RobolectricTestRunner::class) — sanitizedURL calls android.net.Uri.parse() internally, which throws against the SDK stub jar without it (same reason NativeWorkManagerInitializerTest needs it); 6/7 cases failed until added, confirming the annotation was load-bearing, not decorative. Also caught a wrong assumption in the first draft: expected sanitizedURL("") to return "[invalid URL]", but Uri.parse("") does not throw (verified against the real Robolectric-shadowed implementation, not assumed) — it's a valid empty Uri, so the pre-existing behavior is to return "" unchanged. Fixed the test to assert the real behavior. BUILD SUCCESSFUL, 7/7, verified with ./android/gradlew -p android :native_workmanager:testDebugUnitTest via the example app (no standalone gradlew at the plugin root).
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.
kmpworkmanager engine bump 3.4.1 → 3.5.0
"Hardening" release, 20 bug fixes, 0 public API changes —
kmpworker.api(the JVM ABI) is byte-identical between the two tags, so
KMPSchedulerBridge.swift(FROZEN) needed no changes this bump.Both of 3.5.0's own "breaking changes" verified not applicable to this
plugin (checked by grep, not assumed):
KmpHeavyWorker.FGS_MEDIA_PROCESSINGconstant fix — this plugin already reads the platform constant, never
hardcoded the old wrong value;
FakeBackgroundTaskScheduler— not used inthis plugin's own tests.
Fixes that reach real behavior transparently, no Dart-side change needed:
ExistingPolicy.KEEPinput-file data loss, chain-step 8KB+8KB merge-budgetbug, retry backoff jitter (intentional timing change), exact-alarm
AlarmStoredesync,Windowedtrigger constraint loss on retry, iOS chainpoison-pill + cancellation-during-prologue, dot-prefixed iOS task-id
listing gap, both platforms' event-store low-disk silent-data-loss path.
Full list in CHANGELOG.
KMPWorkManager.xcframeworkrebuilt from the kmpworkmanagerv3.5.0gittag (not
HEAD— that repo had an active working copy mid-edit at thetime).
Opt-in TLS certificate pinning
New
certificatePinningparameter (CertificatePin/CertificatePinning)on every HTTP-ish worker:
HttpRequestWorker,HttpDownloadWorker,HttpUploadWorker,HttpSyncWorker,ParallelHttpDownloadWorker,ParallelHttpUploadWorker,WebSocketWorker(Android). Per-request, notprocess-wide; additional to the platform's own chain validation, never a
replacement.
This closes a real gap, not a green-field feature. Both native bridges
already had a
certificatePinningconfig field on 2 of 8 workers each(added at some earlier point, evidently for exactly this) — entirely
unreachable from the public Dart API,
lib/had zero references to it.Wired into every worker on both platforms, and 2 real bugs found and
fixed along the way:
iOS: the pin comparison hashed the wrong bytes.
SecKeyCopyExternalRepresentationreturns the raw public key, but asha256/…pin — the form every pin tool (OkHttp, openssl, TrustKit)emits, and the form Android's own
HttpSecurityHelperalready expected— is the hash of the full SubjectPublicKeyInfo, which prefixes the key
with an ASN.1 AlgorithmIdentifier. Confirmed empirically against a
live TLS handshake to www.example.com: the raw-key hash and the correct
SPKI hash are different values, and only the SPKI one matches an
openssl-verified reference. Any pin generated by a standard tool would
never have matched on iOS. Fixed with an SPKI header table (RSA-2048/
4096, EC P-256/P-384) transcribed byte-for-byte from kmpworkmanager's
own verified
TlsPinning.ios.ktrather than re-derived, and rejectionof unsupported key types instead of silently waving them through.
iOS: a pinned session could serve a cached response from an earlier,
differently-pinned request to the same URL, without a new TLS
handshake and without the pin ever being checked on that call —
URLCache.sharedis process-wide, independent of whichURLSessionserves a request. Found by the device test itself: "wrong pin rejects
the connection" passed in isolation but failed when run right after
"correct pin lets the request through" against the same URL. Fixed by
disabling caching on every pinned session.
iOS's trust-evaluation shape was also changed defensively — check the pin,
then always defer to
performDefaultHandling, rather than manually callingSecTrustEvaluateWithErrorand supplying a credential — on kmpworkmanager'sown field report that the manual-evaluation pattern rejected valid chains
on their test hardware. Not reproduced on this project's own hardware
(
SecTrustEvaluateWithErrorreturnedok=truecleanly here) — recordedhonestly in the CHANGELOG as a defensive simplification adopted from a peer
implementation's experience, not a bug this project independently confirmed.
Device-verified end-to-end on a real Pixel 6 Pro and an iOS simulator:
a correct pin lets a real HTTPS request through, a wrong pin genuinely
rejects the connection (not silently ignored), a worker with no
certificatePinningconfigured is byte-for-byte unaffected. SeeTLS Certificate Pinningindevice_integration_test.dart.Also fixed:
sanitizedURLcredential leak (Android + iOS)SecurityValidator.sanitizedURL()redacted query params but never touchedRFC 3986 UserInfo, so a URL carrying Basic-auth credentials in its
authority (
https://user:pass@host/...) printed the password verbatiminto logs and persisted
WorkerResultfailure messages, on every HTTPworker on both platforms. Same bug shape kmpworkmanager's own (separate,
unrelated — no shared code)
SecurityValidator.sanitizedURLhad justfixed — found by comparison while reviewing that fix.
Verification
native_workmanagerandnative_workmanager_genflutter analyze: 0 issues, all 3 packagesandroid/build.gradlebuild +flutter build ios --simulator --no-codesign: both cleanCancellationgroup (issue How to stop Dart execution when cancelling a running task? #66/iOS: cancelling a background-session HTTP download/upload never actually stops the transfer #69, 4/4) unaffected by thischange;
All Workersgroup (16/16) unaffected; newTLS Certificate Pinninggroup (3/3) on both a real Pixel 6 Pro and an iOS simulatorVersion
1.7.0→1.8.0acrossnative_workmanager,native_workmanager_gen,the example app, and the podspec.
Not done here
KMPWorkManager.xcframework.zipasset forv1.8.0— needed before SwiftPM consumers resolving this tag can build;the CocoaPods
prepare_commandand the git-trackedios/Frameworks/copy are unaffected either way. Publish is a separate,explicit step per this repo's standing rule.