Skip to content

feat(kas)!: ConnectRPC transport + well-known discovery (CWT)#39

Merged
arkavo-com merged 24 commits into
mainfrom
feat/connectrpc-kas-migration
Jun 2, 2026
Merged

feat(kas)!: ConnectRPC transport + well-known discovery (CWT)#39
arkavo-com merged 24 commits into
mainfrom
feat/connectrpc-kas-migration

Conversation

@arkavo-com

@arkavo-com arkavo-com commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Summary

Ports opentdf-rs #86 to OpenTDFKit: migrates the native KAS client transport from REST /kas/v2/* to ConnectRPC unary-JSON at /kas.AccessService/*, driven by /.well-known/opentdf-configuration discovery. Bearer tokens are now opaque passthrough — a JWT or a base64url-encoded CWT (the platform decides validation).

Breaking: KASRewrapClient.init now requires an OpenTDFConfiguration instead of a base kasURL.

What's included

  • New OpenTDFKit/KASDiscovery.swiftOpenTDFConfiguration/KasConfig/IdpConfig Codable types (incl. access_token_format: application/cwt); KasEndpoints.from(_:) resolution (Connect-preferred, REST-fallback) that validates both resolved URLs; validateKasURL with an SSRF guard (IPv4/IPv6 private, link-local, unspecified, and IPv4-mapped-IPv6 literals via inet_pton); ConnectError/parseConnectError; fetchWellKnown; and forKasConnect/forKasLegacyRest builders.
  • KASRewrapClient — breaking init(configuration:oauthToken:) throws; a NoRedirectDelegate (Swift equivalent of redirect::Policy::none()) applied to all bearer-carrying calls; rewrap posts to the resolved Connect URL with Connect error-envelope parsing; authenticationFailed(String?) surfaces the Connect reason; transport-branched EC public-key fetch; NanoTDF request-body KAS identity derived from the parsed header with a config.kas.uri fallback.
  • CLI resolves config via well-known discovery (synthesized-Connect fallback), using the platform root as the discovery base.
  • TestsKASDiscoveryTests (config decode, endpoint resolution, full SSRF matrix, Connect error parsing, well-known fetch via a MockURLProtocol); KASConnectTransportTests (Connect public-key POST + rewrap 401-envelope surfacing); opt-in PlatformConnectIntegrationTests (3 live tests, gated on KAS_INTEGRATION_TESTS=1).

Corrections vs. a literal Rust copy (both verified)

  • NanoTDF request-body KAS url is reconstructed from parsedHeader.kas (config fallback), mirroring Rust's rewrap_nanotdf(header, kas_url).
  • Connect EC public-key fetch POSTs {"algorithm":"ec:secp256r1"} — confirmed against the Go opentdf/platform PublicKeyRequest { algorithm, fmt, v } proto, since Rust's empty {} returns RSA.
  • Connect-Protocol-Version: 1 is sent only on the .connect transport.

Test Plan

  • swift build — clean
  • swift test — 205 tests, 11 skipped, 1 failure (testKeyUnwrappingWithValidKeys, pre-existing on base 77920b3, an unrelated bug in static unwrapKey)
  • Live KAS_INTEGRATION_TESTS=1 swift test --filter PlatformConnectIntegrationTests3/3 passed against https://platform.arkavo.net (well-known discovery, Connect PublicKey PEM, Connect rewrap → 401)

🤖 Generated with Claude Code


Also bundled in this PR (per maintainer request to combine)

  • Stale-test fix (was PR test(kas): fix stale unwrapKey HKDF salt round-trip test #40): testKeyUnwrappingWithValidKeys sealed with an empty HKDF salt but unwrapped with the v12 default salt (since 0ee0797). Split into two explicit, self-consistent round-trips (NanoTDF default salt + Standard TDF empty salt). This turns the test check green for the first time since 2026-03-20.
  • Secret-scan false positive: replaced the fake JWT bearer in the gated live test with a plain "invalid-bearer-token" placeholder.
  • Lint fix: added a pinned .swiftformat config (--swiftversion 6.2 matching Package.swift, --disable noForceUnwrapInTests since its autocorrect breaks non-throwing benchmark functions, --exclude .build) and applied swiftformat across the repo (21 files) — the lint check had been red on main since 2026-01-18. CI now lints via the config.

Known pre-existing issue (NOT addressed here)

StreamingBenchmarkTests.testMemoryUsageWith2MBChunk fails its 70MB memory assertion (peaks ~213MB for a 50MB file) in both debug and release — verified identical on pre-reformat code, so unrelated to this PR. It's a flaky benchmark that runs in the test job; recommend recalibrating the threshold or excluding benchmark suites from the test job in a follow-up.

arkavo-com and others added 19 commits May 30, 2026 11:50
Port of opentdf-rs #86 to OpenTDFKit. ConnectRPC unary-JSON transport at
/kas.AccessService/*, well-known discovery, SSRF-validated endpoints,
no-redirect URLSession, Connect error-envelope parsing, opaque CWT/JWT
bearer passthrough. EC public-key request shape validated against the Go
opentdf/platform PublicKeyRequest proto.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Introduces OpenTDFConfiguration, KasConfig, and IdpConfig structs for
decoding the /.well-known/opentdf-configuration document, plus static
builders forKasConnect() and forKasLegacyRest() for synthesizing
configurations without a well-known endpoint.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds KASDiscoveryError enum and validateKasURL() function that enforces
HTTPS (HTTP allowed only for loopback), rejects private/link-local/unspecified
IPs (IPv4, IPv6 ULA/link-local, and IPv4-mapped IPv6 literals).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…host

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Introduce KasTransport enum and KasEndpoints struct with a from(_:)
resolver that prefers ConnectRPC URLs, falls back to legacy REST, and
validates both resolved URLs (HTTPS/scheme/SSRF) before returning.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Migrate CLI KASRewrapClient instantiation from legacy kasURL-based init
to configuration-based init, resolving platform config via well-known
discovery at the platform root with ConnectRPC fallback.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removes the transitional init(kasURL:) bridge. KASRewrapClient now requires
a resolved OpenTDFConfiguration (well-known discovery or forKasConnect).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add KASConnectTransportTests using MockURLProtocol to verify the Connect
transport path: EC public-key POST request headers/parsing and 401 error
envelope surfacing as authenticationFailed with code+message reason string.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@gitguardian

gitguardian Bot commented Jun 2, 2026

Copy link
Copy Markdown

⚠️ GitGuardian has uncovered 1 secret following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

🔎 Detected hardcoded secret in your pull request
GitGuardian id GitGuardian status Secret Commit Filename
33565676 Triggered Generic High Entropy Secret 328f8a2 OpenTDFKitTests/PlatformConnectIntegrationTests.swift View secret
🛠 Guidelines to remediate hardcoded secrets
  1. Understand the implications of revoking this secret by investigating where it is used in your code.
  2. Replace and store your secret safely. Learn here the best practices.
  3. Revoke and rotate this secret.
  4. If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.

To avoid such incidents in the future consider


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

Comment thread OpenTDFKit/KASDiscovery.swift
Comment thread OpenTDFKit/KASRewrapClient.swift

@gitar-bot gitar-bot Bot 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.

Gitar has auto-approved this PR (configure)

arkavo-com and others added 4 commits June 1, 2026 21:04
testKeyUnwrappingWithValidKeys sealed the wrapped key with an empty HKDF
salt (the Standard TDF convention) but called unwrapKey() with no salt arg,
which since 0ee0797 defaults to CryptoConstants.hkdfSalt (the NanoTDF v12
session-key salt). The salt mismatch produced different AES-GCM keys, so tag
verification failed with authenticationFailure — a stale test, not a product
bug (unwrapKey's default matches the real KAS rewrap_dek derivation).

Replace the single ambiguous test with a DRY helper and two explicit,
self-consistent round-trips:
- testKeyUnwrappingNanoTDFDefaultSalt: seal+unwrap with the v12 default salt
- testKeyUnwrappingStandardTDFEmptySalt: seal+unwrap with empty salt

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…an FP)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a .swiftformat config (--swiftversion 6.2 matching Package.swift,
--disable noForceUnwrapInTests since its autocorrect breaks non-throwing
benchmark functions, --exclude .build) so local 'swiftformat .' and the CI
lint step agree. CI now runs 'swiftformat --lint .' driven by the config.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mechanical reformat to satisfy the (previously red) lint check. No behavior
change: build and the full test suite are unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread OpenTDFKit/NanoTDF.swift Outdated
- NanoTDF.getPayloadPlaintext: move the orphaned doc comment back onto the
  method (it had been separated from its declaration by sharedCryptoHelper,
  which is why swiftformat downgraded it to // — reorder keeps /// and lint green)
- isLoopbackHost: normalize a trailing FQDN dot so 'localhost.' is treated as
  loopback (parity with the Rust loopback detection)
- KasEndpoints.from: reject an empty kas.uri up front (an empty identity would
  make matchesKasURL match nothing and fail every rewrapTDF), with a test

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@gitar-bot

gitar-bot Bot commented Jun 2, 2026

Copy link
Copy Markdown
Code Review ✅ Approved 3 resolved / 3 findings

Migrates KAS client transport to ConnectRPC with well-known discovery and addresses all previously identified SSRF, URL matching, and documentation findings. Includes repository-wide SwiftFormat application and resolved legacy test regressions.

Auto-approved: No blocking issues found.
Please see Auto-approve Docs for details on setting custom approval criteria.

✅ 3 resolved
Security: SSRF loopback check bypassed by localhost. (FQDN dot)

📄 OpenTDFKit/KASDiscovery.swift:188-195
The isLoopbackHost function at line 188-195 checks host == "localhost" as a string literal. An attacker-controlled well-known document could advertise http://localhost./path (with a trailing FQDN dot) which resolves to 127.0.0.1 on most systems but bypasses this check. Similarly, variations like localHost (since only the scheme is lowercased, not the host comparison) would bypass it — though url.host in Foundation does lowercase the host, so that specific case is safe.

While this is defense-in-depth (the attacker would need to control the well-known response), the Rust equivalent uses more robust loopback detection. Consider also checking host.hasPrefix("localhost") or normalizing trailing dots.

Edge Case: Empty kasIdentityURL makes matchesKasURL always return false

📄 OpenTDFKit/KASRewrapClient.swift:309 📄 OpenTDFKit/KASRewrapClient.swift:769-771
kasIdentityURL is assigned configuration.kas?.uri ?? "" (line 309). While in practice KasEndpoints.from ensures kas is non-nil (so uri is always populated), if a KasConfig is constructed with an empty uri string, URL(string: "") returns nil at line 771, causing matchesKasURL to always return false. This would make rewrapTDF throw "No key access entries for KAS" for every manifest.

Consider adding validation that uri is a valid non-empty URL in KasEndpoints.from, or at least documenting the requirement.

Quality: Doc comments downgraded to plain comments on public method

📄 OpenTDFKit/NanoTDF.swift:55-59
Lines 55-59 in NanoTDF.swift convert /// doc comments to // plain comments. This is the opposite direction of every other comment change in this PR (which upgrades // to ///). The comments document the public getPayloadPlaintext(symmetricKey:) method and should remain as doc comments so they appear in Quick Help / generated documentation.

Options

Auto-apply is off → Gitar will not commit updates to this branch.
Display: compact → Showing less information.

Comment with these commands to change:

Auto-apply Compact
gitar auto-apply:on         
gitar display:verbose         

Important

Your trial ends in 4 days — upgrade now to keep code review, CI analysis, auto-apply, custom automations, and more.

Was this helpful? React with 👍 / 👎 | Gitar

@arkavo-com arkavo-com merged commit 6f699f2 into main Jun 2, 2026
6 checks passed
@arkavo-com arkavo-com deleted the feat/connectrpc-kas-migration branch June 2, 2026 01:31
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