Skip to content

fix(kas): well-known missing kas → default KAS URL + Stage-1 xtest CI - #48

Merged
arkavo-com merged 8 commits into
mainfrom
fix/kas-wellknown-fallback-and-xtest
Jul 12, 2026
Merged

fix(kas): well-known missing kas → default KAS URL + Stage-1 xtest CI#48
arkavo-com merged 8 commits into
mainfrom
fix/kas-wellknown-fallback-and-xtest

Conversation

@arkavo-com

@arkavo-com arkavo-com commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Bugfix: When /.well-known/opentdf-configuration returns 200 without a usable kas block, fall back to Connect endpoints on the default KAS base (PLATFORMURL / stripped KASURL) instead of failing with well-known configuration is missing a 'kas' block.
  • CI: New X-Test Stage-1 workflow pulls floating latest release of arkavo-org/opentdf-tests (falls back to community-xtest-stage1 if no release) and runs swift × go Base TDF stage1 on macOS with the native platform stack.

Cross-repo coordination

Direction Pin
OpenTDFKit → tests tests-ref: latestcommunity-xtest-stage1-v0.1.0 (created)
tests → OpenTDFKit community-xtest floats swift-ref: latest; until a post-fix OpenTDFKit release, use swift-ref: main / this PR SHA for green Stage-1

After merge, cut an OpenTDFKit release (or keep community-xtest on main) so floating latest includes this fallback.

Test plan

  • swift test --filter KASDiscoveryTests (24 tests, 0 failures)
  • CI X-Test Stage-1 on this PR
  • community-xtest swift job with swift-ref = this SHA or main after merge

Summary by Gitar

  • TDF Crypto:
    • Updated policyBinding to encode HMAC-SHA256 of the base64-encoded policy, fixing interoperability with the Go SDK.
    • Set wrapSymmetricKeyWithRSA to use RSA-OAEP-SHA1 for alignment with existing KAS unwrap expectations.
    • Standardized segment integrity (GMAC) to use the last 16 bytes of the encrypted segment (AES-GCM tag).
    • Updated TDFEncryptor to use new hexless 4.3.0 root signature logic (base64(HMAC-SHA256(DEK, concat(raw segment sigs)))).
  • CLI Improvements:
    • Improved OAuth token requests by normalizing localhost to 127.0.0.1 and applying strict form-urlencoding.
    • Added XT_WITH_ATTRIBUTES support for injecting FQN-based attribute objects during CLI TDF policy generation.
  • KAS Metadata:
    • Introduced RewrapMetadataValue enum to handle heterogeneous metadata (strings, arrays, objects) in rewrap responses.
  • KAS Discovery:
    • Implemented withKasFallback to automatically synthesize platform Connect endpoints when well-known configuration is incomplete.
  • CI/CD:
    • Added xtest.yml for Stage-1 cross-testing between the Swift SDK and the Go platform peer.

This will update automatically on new commits.

Local platforms often serve /.well-known/opentdf-configuration with IdP
metadata but no kas block. resolveConfiguration treated a successful
fetch as complete, then KasEndpoints.from failed with "missing a 'kas'
block".

- OpenTDFConfiguration.withKasFallback synthesizes Connect endpoints
  while preserving IdP fields
- CLI default base: PLATFORMURL, else KASURL with /kas stripped
- Unit tests for missing/incomplete kas and preserve-usable-well-known
- CI: X-Test Stage-1 pulls floating latest arkavo-org/opentdf-tests
  release (branch fallback) and runs swift × go Base TDF stage1
Comment thread OpenTDFKit/KASDiscovery.swift
withKasFallback no longer uses try? KasEndpoints.from, which swallowed
SSRF and scheme validation errors and rewrote hostile well-known kas
blocks to the fallback base (changing kas.uri / identity).

Fallback only when kas is nil, uri is empty, or neither Connect nor REST
endpoint pairs are advertised. Present endpoint URLs keep their identity;
KasEndpoints.from still enforces validateKasURL.
@gitar-bot

gitar-bot Bot commented Jul 12, 2026

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

Implements a fallback to default KAS URLs when the well-known configuration is missing endpoints, resolving the identified error-handling issue. CI coverage is now extended with a new Stage-1 test suite.

✅ 1 resolved
Edge Case: withKasFallback swallows SSRF/validation errors, silently replaces KAS

📄 OpenTDFKit/KASDiscovery.swift:77-87 📄 OpenTDFKit/KASDiscovery.swift:298-312
withKasFallback treats any thrown error from KasEndpoints.from(self) as "unusable" via (try? ...) != nil. This includes not just the intended "missing kas block" case, but also validateKasURL failures (SSRF: private/link-local IP, non-HTTPS non-loopback, unsupported scheme) and the "empty uri" / "neither Connect nor REST URLs" errors.

Consequence: a well-known document that does advertise a real kas block whose endpoints fail validation (e.g. a genuine internal deployment behind a private IP, or a temporarily malformed URL) is silently discarded and replaced with synthesized Connect endpoints on fallbackBase. Because the synthesized config also overrides kas.uri with fallbackBase, this can change the KAS identity used for manifest key-access matching (see the matchesKasURL note at lines 302-306), causing the client to talk to a different KAS than the manifest intends rather than surfacing the real error.

For the Stage-1/local use case this is likely acceptable, but consider only falling back when config.kas == nil (or the specific "missing/empty kas" errors) and letting genuine validation failures propagate, so SSRF protections aren't quietly bypassed.

Was this helpful? React with 👍 / 👎 | Gitar

Normalize localhost → 127.0.0.1 in the token endpoint URL so Keycloak
issues JWTs whose iss matches server.auth.issuer (CI sets 127.0.0.1).
Use strict form-urlencoded encoding for client credentials.

Also track community-xtest-stage1 branch tip for floating tests-ref so
Stage-1 infra fixes land without waiting on a new tests release.
Stage-1 failures after auth was fixed:

1. Rewrap responses include X-Required-Obligations as a JSON array;
   metadata was [String:String] and decode aborted after a successful
   permit. Accept heterogeneous RewrapMetadataValue.

2. policyBinding used base64(raw HMAC over policy bytes). Go/KAS expect
   base64(hex(HMAC-SHA256(DEK, base64(policyJSON)))) — wrong hash made
   go rewrap report "tamper detected" on Swift-produced TDFs.
Comment thread OpenTDFKit/KASRewrapClient.swift
- SwiftFormat: preferKeyPath / drop redundant throws in policy binding tests
- RewrapMetadataValue.stringValue: integral doubles without trailing .0
- Standard TDF EC session unwrap salt = SHA256("TDF") (go tdfSalt), not empty
- Honor XT_WITH_ATTRIBUTES when building default encrypt policy (go attributeObject)
Workflow file, display name, job id, step titles, and artifact names drop
stage1/Stage-1. Pytest still filters with -m stage1 (suite marker) and
still pins the community-xtest-stage1 tests branch by ref.
Comment thread OpenTDFKitCLI/main.swift Outdated
OpenTDF platform/go encrypt DEKs with rsa.EncryptOAEP(sha1.New(), …).
Swift used SecKey .rsaEncryptionOAEPSHA256, so KAS could not unwrap the
DEK and returned a non-permit rewrap result (surfaced as "Access denied
by policy"). Match go OAEP-SHA1 for wrap/unwrap.

Also surface full rewrap metadata on deny for easier CI diagnosis.
Gitar: share Config.defaultPolicyData / attributeObjectsFromEnvironment
for XT_WITH_ATTRIBUTES instead of duplicating parse in main+Commands.

Integrity: go hexless GMAC is last 16 bytes of the encrypted segment
(AES-GCM tag), base64-encoded — not a synthetic GMAC. Root is
base64(HMAC-SHA256(DEK, concat(raw tags))). Fixes go decrypt
"failed integrity check on segment hash".

Naming: Base/standard TDF is called tdf; ztdf kept only as a silent
legacy wire alias for xtest shims (not NATO STANAG ZTDF).
@gitar-bot

gitar-bot Bot commented Jul 12, 2026

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

Implements KAS discovery fallback and cross-repo Stage-1 testing while standardizing TDF crypto formats for Go SDK interoperability. Resolved findings include SSRF validation errors, metadata rendering quirks, and logic duplication in attribute parsing.

✅ 3 resolved
Edge Case: withKasFallback swallows SSRF/validation errors, silently replaces KAS

📄 OpenTDFKit/KASDiscovery.swift:77-87 📄 OpenTDFKit/KASDiscovery.swift:298-312
withKasFallback treats any thrown error from KasEndpoints.from(self) as "unusable" via (try? ...) != nil. This includes not just the intended "missing kas block" case, but also validateKasURL failures (SSRF: private/link-local IP, non-HTTPS non-loopback, unsupported scheme) and the "empty uri" / "neither Connect nor REST URLs" errors.

Consequence: a well-known document that does advertise a real kas block whose endpoints fail validation (e.g. a genuine internal deployment behind a private IP, or a temporarily malformed URL) is silently discarded and replaced with synthesized Connect endpoints on fallbackBase. Because the synthesized config also overrides kas.uri with fallbackBase, this can change the KAS identity used for manifest key-access matching (see the matchesKasURL note at lines 302-306), causing the client to talk to a different KAS than the manifest intends rather than surfacing the real error.

For the Stage-1/local use case this is likely acceptable, but consider only falling back when config.kas == nil (or the specific "missing/empty kas" errors) and letting genuine validation failures propagate, so SSRF protections aren't quietly bypassed.

Quality: RewrapMetadataValue.stringValue renders numbers with .0 suffix

📄 OpenTDFKit/KASRewrapClient.swift:281-288 📄 OpenTDFKit/KASRewrapClient.swift:476 📄 OpenTDFKit/KASRewrapClient.swift:607
In RewrapMetadataValue.stringValue, the .number case uses String(n) where n is a Double. JSON integers decoded via decode(Double.self) become e.g. 1.0, so String(n) yields "1.0" rather than "1". This is currently low-impact because metadata["error"] (the only consumer at KASRewrapClient.swift:476 and :607) is expected to be a string, so the number branch is not exercised there. If stringValue is later used to surface numeric metadata to users, the trailing .0 would be surprising. Consider formatting integral doubles without the fractional suffix.

sources: OpenTDFKit/KASRewrapClient.swift:281-288

Quality: Duplicated XT_WITH_ATTRIBUTES policy-parsing logic

📄 OpenTDFKitCLI/main.swift:624-637 📄 OpenTDFKitCLI/Commands.swift:1506-1519
The XT_WITH_ATTRIBUTES env parsing (split on commas, trim whitespace, filter empties, map to ["attribute": fqn]) is duplicated verbatim in main.swift loadPolicyData() and Commands.swift loadPolicy(). If the attribute-object shape or delimiter handling ever needs to change (e.g. to match a go SDK update), both copies must be kept in sync, and it's easy to update only one. Consider extracting a single shared helper (e.g. on CLIConfig or a small utility) that both call. Minor — functionally correct today.

Was this helpful? React with 👍 / 👎 | Gitar

@arkavo-com
arkavo-com merged commit 07b6b97 into main Jul 12, 2026
8 checks passed
@arkavo-com
arkavo-com deleted the fix/kas-wellknown-fallback-and-xtest branch July 12, 2026 20:18
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