Skip to content

feat: kmpworkmanager 3.5.0 + opt-in TLS certificate pinning (v1.8.0) - #70

Merged
vietnguyentuan2019 merged 2 commits into
mainfrom
feat/kmpworkmanager-3.5.0
Sep 11, 2026
Merged

feat: kmpworkmanager 3.5.0 + opt-in TLS certificate pinning (v1.8.0)#70
vietnguyentuan2019 merged 2 commits into
mainfrom
feat/kmpworkmanager-3.5.0

Conversation

@vietnguyentuan2019

Copy link
Copy Markdown
Contributor

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_PROCESSING
constant fix — this plugin already reads the platform constant, never
hardcoded the old wrong value; FakeBackgroundTaskScheduler — not used in
this plugin's own tests.

Fixes that reach real behavior transparently, no Dart-side change needed:
ExistingPolicy.KEEP input-file data loss, chain-step 8KB+8KB merge-budget
bug, retry backoff jitter (intentional timing change), 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.
Full list in CHANGELOG.

KMPWorkManager.xcframework rebuilt from the kmpworkmanager v3.5.0 git
tag (not HEAD — that repo had an active working copy mid-edit at the
time).

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, not
process-wide; additional to the platform's own chain validation, never a
replacement.

NativeWorker.httpRequest(
  url: 'https://api.example.com/data',
  certificatePinning: CertificatePinning([
    CertificatePin(
      hostname: 'api.example.com',
      sha256Pins: ['sha256/AAAA…', 'sha256/BBBB…'], // current + backup
    ),
  ]),
)

This closes a real gap, not a green-field feature. Both native bridges
already had a certificatePinning config 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:

  1. iOS: the pin comparison hashed the wrong bytes.
    SecKeyCopyExternalRepresentation returns the raw public key, but a
    sha256/… pin — the form every pin tool (OkHttp, openssl, TrustKit)
    emits, and the form Android's own HttpSecurityHelper already 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.kt rather than re-derived, and rejection
    of unsupported key types instead of silently waving them through.

  2. 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.shared is process-wide, independent of which URLSession
    serves 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 calling
SecTrustEvaluateWithError and supplying a credential — on kmpworkmanager's
own field report that the manual-evaluation pattern rejected valid chains
on their test hardware. Not reproduced on this project's own hardware
(SecTrustEvaluateWithError returned ok=true cleanly here) — recorded
honestly 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
certificatePinning configured is byte-for-byte unaffected. See
TLS Certificate Pinning in device_integration_test.dart.

Also fixed: sanitizedURL credential leak (Android + iOS)

SecurityValidator.sanitizedURL() redacted query params but never touched
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 failure messages, on every HTTP
worker on both platforms. Same bug shape kmpworkmanager's own (separate,
unrelated — no shared code) SecurityValidator.sanitizedURL had just
fixed — found by comparison while reviewing that fix.

Verification

Version

1.7.01.8.0 across native_workmanager, native_workmanager_gen,
the example app, and the podspec.

Not done here

  • The GitHub release + KMPWorkManager.xcframework.zip asset for
    v1.8.0 — needed before SwiftPM consumers resolving this tag can build;
    the CocoaPods prepare_command and the git-tracked
    ios/Frameworks/ copy are unaffected either way. Publish is a separate,
    explicit step per this repo's standing rule.

## 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).
@vietnguyentuan2019
vietnguyentuan2019 merged commit 312a9ec into main Sep 11, 2026
13 checks passed
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.

1 participant