Skip to content

Fix audit bugs and remove v13 NanoTDF support#43

Merged
arkavo-com merged 3 commits into
mainfrom
bugfix/audit-fixes
Jun 22, 2026
Merged

Fix audit bugs and remove v13 NanoTDF support#43
arkavo-com merged 3 commits into
mainfrom
bugfix/audit-fixes

Conversation

@arkavo-com

Copy link
Copy Markdown
Contributor

This PR addresses the bugs found in the recent codebase audit and removes v13 NanoTDF support.

Highlights

  • Remove v13 NanoTDF creation; all NanoTDFs now use v12 (L1L) headers and v12 HKDF salt.
  • Fix NanoTDF Collection nonce padding to match the spec ([IV][zeros]).
  • Fix KASRewrapClient sending an empty clientPublicKey by converting the compressed EC point to PEM.
  • Fix TDF-JSON/CBOR encrypt/decrypt mismatch: EC wrap now pairs with EC unwrap and sets keyAccess.type = .ecWrapped.
  • Fix TDF-CBOR algorithm and key-access-type fidelity.
  • Fix KASService.rewrapKey endpoint path (/kas/v2/rewrap).
  • Reject zero-length embedded policy bodies in BinaryParser.
  • Use constant-time comparison for policy binding verification.
  • Use SecRandomCopyBytes for nonce/key generation and check return status.
  • Remove stray print(...) statements from library code.

Verification

  • swift test: 216 tests, 0 failures
  • swiftformat --lint --swiftversion 6.2 .: 0 files require formatting

Branch

bugfix/audit-fixes

- Remove v13 NanoTDF creation; all NanoTDFs use v12 headers and v12 HKDF salt.
- Fix NanoTDF Collection nonce padding to [IV][zeros].
- Fix KASRewrapClient clientPublicKey PEM conversion.
- Fix TDF-JSON/CBOR EC wrap/unwrap mismatch and set .ecWrapped type.
- Fix TDF-CBOR algorithm and key-access-type fidelity.
- Fix KASService rewrap endpoint path to /kas/v2/rewrap.
- Reject zero-length embedded policy bodies.
- Use constant-time policy binding comparison.
- Use SecRandomCopyBytes for nonce/key generation.
- Remove stray print statements and other low-severity cleanup.

@superninja-app superninja-app Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review — PR #43: Fix audit bugs and remove v13 NanoTDF support

Reviewed as a senior cryptography / Swift / OpenTDF engineer. I read the full diff (24 files, +523/−322) and the surrounding code for context (CryptoHelper.adjustNonce, getPayloadPlaintext, TDFCrypto EC wrap/unwrap, KASService.verifyPolicyBinding, TDFFormatDetector, the ParsingError/KASRewrapError enums and all updated throws call-sites). Overall this is a high-quality, security-positive PR that fixes several real correctness and hardening bugs. Detailed findings below.


1. Cryptographic Correctness ✅ (strong)

  • Nonce padding fix is correct and now internally consistent. CryptoHelper.adjustNonce(_:to:) pads a short nonce by appending zeros, yielding [b1,b2,b3,0,0,0,0,0,0,0,0,0]. The create path (createNanoTDF, NanoTDF.swift:259–261) and decrypt path (getPayloadPlaintext, NanoTDF.swift:67) both go through adjustNonce. The previous NanoTDFCollection/Decryptor layout [0×9, b1,b2,b3] did not match that convention. The change in NanoTDFCollection.swift (toGCMNonce() and nonceBuffer[0..2]) and NanoTDFCollectionDecryptor.swift now aligns all four paths on IV-first. This is the correct fix and the round-trip is covered by testPrivateKeyDecryptor and the updated NanoTDFCollectionTests.

  • CSPRNG hardening is a genuine improvement. TDFCrypto.randomBytes(count:) previously used SystemRandomNumberGenerator element-by-element; it now uses SecRandomCopyBytes with an OSStatus check and throws on failure. Same pattern applied to CryptoHelper.generateNonce(...), which now throws instead of silently ignoring the SecRandomCopyBytes result via _ =. This removes a class of silent-failure / weak-randomness bugs for IV and key generation.

  • EC wrap/unwrap symmetry fixed. TDF-JSON/CBOR encrypt sets type = .ecWrapped and the decrypt path now calls unwrapSymmetricKeyWithEC (ECDH → HKDF-SHA256(empty salt/info) → AES-GCM) instead of the mismatched unwrapSymmetricKeyWithRSA. The unwrap (TDFCrypto.swift:341+) mirrors wrapWithP256 exactly (same HKDF parameters, AES-GCM combined format nonce[12]+ct+tag[16]), guards wrappedData.count > 28, and secureZero()s the decrypted key via defer. Correct ECIES construction. Round-trips proven by testTDFJSONRoundTripWithECPrivateKey / testTDFCBORRoundTripWithECPrivateKey.

  • HKDF salt is now consistently v12 across createNanoTDF, NanoTDFCollectionDecryptor, and KASService.rewrapKeyInternal. No salt mismatch remains.

  • clientPublicKey PEM bug fixed (high impact). KASRewrapClient.swift:355 previously did String(data: clientKeyPair.publicKey, encoding: .utf8) ?? "" on a compressed EC point — that is not valid UTF-8, so it silently sent an empty clientPublicKey, which would break the rewrap response unwrap. The new pemRepresentation(compressedPublicKey:) properly reconstructs SPKI/PEM from the 33-byte point. Good, and it round-trips through extractCompressedKeyFromPEM in testClientPublicKeyPEMConversion.

2. Cybersecurity Concerns ✅

  • Timing-attack mitigation: verifyPolicyBinding now uses constantTimeEquals (XOR-accumulate over zip, result |= a ^ b). Correct constant-time equality for equal-length inputs. The early guard lhs.count == rhs.count leaks only the (public) tag length — standard and acceptable; a one-line comment noting this would be ideal.
  • Input validation hardening: BinaryParser now rejects zero-length embedded policy bodies (contentLength == 0 → throw .invalidPolicy), matching spec §3.4.2.3.2 (min length 1). Previously it fabricated Data([0x00]), which masked malformed input. Covered by testZeroLengthEmbeddedPolicyBodyFails.
  • KAS request hygiene: KASRewrapClient now rejects empty / non-base64 policy bodies before building the rewrap request.
  • Information-leak cleanup: removal of stray print(...) from BinaryParser/NanoTDF is good — those could have leaked structural details about malformed/attacker-supplied inputs into logs.

3. Swift Implementation Quality ✅ (minor nits)

  • KASRewrapClientpublic final class … Sendable: good concurrency-safety tightening.
  • KeyStore.publicKey/privateKey now use safe buffer.prefix/suffix slicing instead of baseAddress! pointer arithmetic — removes force-unwrap crash risk on empty buffers. Nice.
  • Migration of return X-style switches to Swift's expression-switch (PayloadKeyAccess.kasKeyCurve, PolicyKeyAccess.curve, CBOR enum mappings) is idiomatic for Swift 6.2.
  • Nit — redundant rethrow: in BinaryParser.parseHeader (both v12 paths) the
    do { ... policy = p } catch { throw error }
    wrapper adds nothing; try readPolicyField(...) could be called directly. Harmless but worth simplifying.
  • The RewrapURLProtocol test mock using nonisolated(unsafe) static var is acceptable for a single-threaded test but technically a shared-mutable-state hazard; fine for now.

4. OpenTDF Specification Compliance ✅

  • Collection nonce [IV][zeros] now matches the NanoTDF spec/adjustNonce convention.
  • KASService.rewrapKey endpoint corrected to /kas/v2/rewrap (was /rewrap) — aligns with the KAS v2 protocol.
  • v12 (0x4C "L1L") is now the single emitted header form; Header.toData() no longer branches on kasPublicKey emptiness. Consistent with the stated intent to standardize on v12 (and otdfctl interop).
  • CBOR key-access types (wrapped/remote/remoteWrapped/ecWrapped) and symmetric algorithms (AES-128/256-GCM/CBC) now encode/decode with fidelity instead of silently collapsing to wrapped / AES-256-GCM. Hash-alg mapping now throws on unknown values instead of silently defaulting to HS256 — a correctness win that prevents accepting/forging mislabeled bindings.
  • TDFFormatDetector.cborMagic reduced to the 3-byte self-describe tag plus a check that the 4th byte is a CBOR map major type (& 0xE0 == 0xA0). The header[cborMagic.count] access is safe under the existing data.count >= 4 guard — no out-of-bounds — and is more spec-faithful than the hardcoded 0xA5.

5. General Code Quality ✅

  • Error handling moves decisively from print + return nil to typed thrown errors (ParsingError.invalidPolicy, TDFCBORError.*, KASRewrapError.*) — better failure propagation and testability.
  • Strong test coverage added for every behavioral change: PEM conversion, rewrap endpoint path (RewrapURLProtocol mock), EC round-trips, CBOR algorithm/key-access fidelity, zero-length policy rejection, and the new collection nonce layout. Author reports swift test: 216 passed / 0 failed and a clean swiftformat --lint. All updated throws call-sites (generateNonce, enumToHashAlg/hashAlgToEnum) are consistently propagated.

6. Summary & Recommendations

This PR meaningfully improves both correctness and security and is well-tested. Recommendation: Approve. None of the findings below are blocking, but please address them in a follow-up.

Should-fix / acknowledge (non-blocking):

  1. Backward compatibility — biggest item. Removing the v13 HKDF-salt fallback in KASService.rewrapKeyInternal (and dropping the version hint) means any NanoTDF previously created with the v13 (0x4D) salt can no longer be rewrapped/decrypted by this service. If no v13 artifacts exist in the wild this is fine, but it should be explicitly stated as a breaking change and ideally noted in the migration doc / release notes.
  2. Silent capability reduction in fetchKasEcPublicKey: it now rejects ecP384/ecP521 (only ecP256 accepted). Reasonable given only P-256 parsing is implemented, but callers relying on P-384/521 will now get unsupportedKeyAlgorithm. Worth a CHANGELOG line.
  3. TDFArchiveWriter default changed from .deflate to .none. This changes on-the-wire output size/format for default callers; confirm this is intentional (e.g., for spec/interop) rather than an incidental change.

Minor / nits:

  • Remove the redundant do { … } catch { throw error } wrappers in BinaryParser.parseHeader.
  • Add a one-line comment in constantTimeEquals noting the length-comparison is intentionally non-secret.
  • TDFCrypto.randomBytes throws TDFCryptoError.keyWrapFailed(nil) on RNG failure — a dedicated randomGenerationFailed case would read better than reusing keyWrapFailed.

Excellent, surgical hardening work overall — particularly the CSPRNG fix, the EC wrap/unwrap pairing, the constant-time binding check, and the clientPublicKey PEM correction. 👍

- Add P-256/P-384/P-521 parsing to KASRewrapClient.validateEcPublicKeyPEM.
- Introduce KasEcPublicKey enum to type parsed EC keys by curve.
- Simplify redundant do/catch wrappers in BinaryParser.parseHeader.
- Add comment noting constant-time compare length check is intentionally public.
- Add TDFCryptoError.randomGenerationFailed and use it for SecRandomCopyBytes failures.
- Add unit tests for P-256/P-384/P-521 PEM validation.
@arkavo-com arkavo-com marked this pull request as ready for review June 22, 2026 01:34
@arkavo-com

Copy link
Copy Markdown
Contributor Author

@gitar-bot review

@gitar-bot

gitar-bot Bot commented Jun 22, 2026

Copy link
Copy Markdown

@gitar-bot review

On it — running a full review now.

@arkavo-com

arkavo-com commented Jun 22, 2026

Copy link
Copy Markdown
Contributor Author

@gitar-bot review

Addressed both findings:

  • Removed the dead hkdfSaltV13 constant and the Header.version/v13 plumbing it depended on.
  • Explicitly rejected v13 (0x4D) parsing in BinaryParser.parseHeader and removed the unused readPayloadKeyAccess(version:) helper.

All 220 tests pass and swiftformat is clean.

@gitar-bot

gitar-bot Bot commented Jun 22, 2026

Copy link
Copy Markdown

@gitar-bot review

Addressed both findings:

  • Removed the dead hkdfSaltV13 constant and the Header.version/v13 plumbing it depended on.
  • Explicitly rejected v13 (0x4D) parsing in BinaryParser.parseHeader and removed the unused readPayloadKeyAccess(version:) helper.

All 220 tests pass and swiftformat is clean.

On it — running a full review now.

@gitar-bot

gitar-bot Bot commented Jun 22, 2026

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

Eliminates v13 NanoTDF support and addresses multiple audit findings, including corrected nonce padding, constant-time policy verification, and secure random number generation. The removal of dead code and resolution of cryptographic implementation inconsistencies have been verified.

✅ 2 resolved
Quality: Dead code: hkdfSaltV13 unused after v13 removal

📄 OpenTDFKit/CryptoHelper.swift:36-37
After removing v13 NanoTDF support, CryptoConstants.hkdfSaltV13 is no longer referenced anywhere in library code — KASService.rewrapKeyInternal (its previous only consumer) now uses hkdfSaltV12 exclusively, and NanoTDFCollectionDecryptor/createNanoTDF switched to v12 salt. The constant (and the Header.version/v13 plumbing it depends on) is now dead. Since the stated goal of this PR is removing v13 creation, consider removing hkdfSaltV13 to avoid confusion about which salts are actually live, or add a comment documenting why it is retained (e.g. legacy decrypt path).

Edge Case: v13 NanoTDFs can be parsed but no longer rewrapped (no v13 salt)

📄 OpenTDFKit/KASService.swift:235-249 📄 OpenTDFKit/BinaryParser.swift:276-290
BinaryParser.parseHeaderV13 is still present so v13 ("L1M") headers can be parsed, but KASService.rewrapKeyInternal now derives the session key with hkdfSaltV12 only — the previous v13 primary-salt + v12-fallback logic was removed. Any NanoTDF previously produced with a v13 header/salt (older versions of this library created v13 via createNanoTDF) will now fail to rewrap/decrypt through this KAS path with no fallback. If this is intentional (audit concluded v12 is the interop-correct salt) it is fine, but the asymmetry between still-parsing v13 headers and only-deriving v12 salt is worth a code comment, or v13 parsing could be rejected explicitly to fail fast with a clear error.

Was this helpful? React with 👍 / 👎 | Gitar

@arkavo-com arkavo-com merged commit b2a9765 into main Jun 22, 2026
2 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