Skip to content

feat: support encrypted CDN resolver state#428

Draft
nicklasl wants to merge 20 commits into
mainfrom
nicklasl/encrypted-cdn-state
Draft

feat: support encrypted CDN resolver state#428
nicklasl wants to merge 20 commits into
mainfrom
nicklasl/encrypted-cdn-state

Conversation

@nicklasl

@nicklasl nicklasl commented May 29, 2026

Copy link
Copy Markdown
Member

Summary

  • Add AES-256-GCM decryption to the WASM guest layer so all providers can consume encrypted CDN state
  • Add encryption key configuration to all 4 WASM-based providers (JS, Java, Go, Python)
  • Detect encrypted responses via x-goog-meta-encrypted CDN header, with a clear error when no key is configured

Changes

WASM layer

  • New proto message SetEncryptedResolverStateRequest (encrypted blob + raw key + SDK metadata)
  • New guest export wasm_msg_guest_set_encrypted_resolver_state — decrypts AES-256-GCM (Tink NO_PREFIX: 12-byte nonce ∥ ciphertext+tag), then parses and sets state
  • aes-gcm crate added (~42K WASM binary increase)

JS provider

  • ProviderOptions.encryptionKey — hex-encoded AES-256 key
  • updateState() checks x-goog-meta-encrypted header and routes to the encrypted or plain path
  • Crash-recovery wrapper (WasmResolver) caches encrypted state for instance reload

Java provider

  • LocalProviderConfig.builder().encryptionKey() — hex-encoded AES-256 key
  • FlagsAdminStateFetcher detects encrypted responses and stores raw CDN bytes
  • setEncryptedResolverState propagated through all resolver layers (Pooled, Recovering, Materializing)

Go provider

  • ProviderConfig.EncryptionKey — hex-encoded AES-256 key
  • FlagsAdminStateFetcher detects encrypted responses and stores raw CDN bytes
  • SetEncryptedResolverState propagated through all resolver layers (Pool, Recovering, Materialization)

Python provider

  • ConfidenceProvider(encryption_key=...) — hex-encoded AES-256 key
  • StateFetcher detects encrypted responses and stores raw CDN bytes
  • set_encrypted_resolver_state propagated through resolver layers

Test plan

  • WASM builds, new export verified via wasm-objdump
  • Clippy clean
  • JS build + 150/150 tests pass
  • Java build passes
  • Go build + local_resolver tests pass
  • Python lint passes (ruff + format)
  • E2E: configure encryption key on a credential, verify providers decrypt and resolve flags

🤖 Generated with Claude Code

@nicklasl nicklasl changed the title feat(wasm,js): support encrypted CDN resolver state feat: support encrypted CDN resolver state Jun 1, 2026
@nicklasl nicklasl force-pushed the nicklasl/encrypted-cdn-state branch 3 times, most recently from c98377a to 99d4ff2 Compare June 1, 2026 06:44
nicklasl and others added 11 commits June 18, 2026 09:18
Add AES-256-GCM decryption support for encrypted CDN state blobs.

WASM layer:
- New `set_encrypted_resolver_state` guest function
- Decrypts AES-256-GCM (Tink NO_PREFIX) and sets resolver state
- Uses `aes-gcm` crate (~42K binary increase)

JS provider:
- New `encryptionKey` option in `ProviderOptions`
- Detects encrypted responses via `x-goog-meta-encrypted` header
- Informative error when state is encrypted but no key is provided

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add `encryptionKey` to `LocalProviderConfig` builder
- Detect encrypted CDN responses via `x-goog-meta-encrypted` header
- Add `setEncryptedResolverState` through all resolver layers
- Informative error when state is encrypted but no key is provided

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add `EncryptionKey` to `ProviderConfig`
- Detect encrypted CDN responses via `x-goog-meta-encrypted` header
- Add `SetEncryptedResolverState` through all resolver layers
- Informative error when state is encrypted but no key is provided

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add `encryption_key` parameter to `ConfidenceProvider`
- Detect encrypted CDN responses via `x-goog-meta-encrypted` header
- Add `set_encrypted_resolver_state` through resolver layers
- Informative error when state is encrypted but no key is provided

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace nil stores into atomic.Value with atomic.Bool flags in both
RecoveringResolver and FlagsAdminStateFetcher. atomic.Value panics
when storing nil after a typed value was previously stored.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use atomic.Bool to track encrypted state instead of storing nil
into rawCdnBytes atomic.Value.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Match the error string expected by TestLocalResolverProvider_Init_EmptyAccountID.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@nicklasl nicklasl force-pushed the nicklasl/encrypted-cdn-state branch from 7285e4f to 280e9aa Compare June 18, 2026 07:21
nicklasl and others added 7 commits June 18, 2026 11:25
The deployer now detects encrypted CDN responses via the
x-goog-meta-encrypted header and decrypts using Node.js crypto
(already available for Wrangler) before embedding in the Worker.

Requires STATE_ENCRYPTION_KEY env var (hex-encoded AES-256 key)
when the CDN state is encrypted.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move AES-256-GCM decryption to decrypt_state.js so it can be
called manually:
  node decrypt_state.js <file> <hex_key> [output_file]

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The CDN serves GCS metadata with x-amz-meta-* prefix, not
x-goog-meta-*. Updated all providers and the deployer.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Check x-amz-meta-encrypted header first, then fall back to
protobuf decode — if parsing fails, treat the state as encrypted.
Covers CDN configurations that don't forward metadata headers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Generated from wasm/resolver_state.pb wrapped as SetResolverStateRequest
with account_id "confidence-test", encrypted with AES-256-GCM.

Key is deterministic (sha256 of "confidence-test-encryption-key")
and stored alongside the fixture — not secret.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Each provider loads the encrypted fixture from data/, decrypts via
the WASM set_encrypted_resolver_state function, and verifies flag
resolution works. Also tests wrong-key rejection.

- JS: 2 tests in WasmResolver.test.ts
- Java: 2 tests in EncryptedStateTest.java
- Go: 2 tests in encrypted_state_test.go
- Python: 3 tests in test_encrypted_state.py

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Rust provider uses the native resolver (no WASM), so decryption
is implemented directly in StateFetcher::fetch().

- Add `encryption_key` to `ProviderOptions` with builder method
- Detect encrypted responses via x-amz-meta-encrypted header
- Fall back to protobuf parse failure detection
- 3 tests: decrypt+resolve, wrong key rejection, missing key

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Comment thread openfeature-provider/rust/src/state.rs Fixed
Derive the wrong key by flipping a bit of the real test key instead
of using a zero-filled byte array, which CodeQL flags as a hardcoded
cryptographic key.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Comment thread openfeature-provider/rust/src/state.rs Fixed
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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