arkavo-org / arkavo-org/OpenTDFKit
Implement TDF JSON Envelope Support for JSON-RPC Protocols
- Dominant language
- Swift
- Stars
- 1
- Forks
- 1
- Avg merge
- 9d 4h
- Merged PRs (30d)
- 1
Description
# Implement TDF JSON Envelope Support for JSON-RPC Protocols
## Summary
Implement native TDF support for JSON envelope format to enable seamless integration with JSON-RPC protocols like A2A (Agent-to-Agent) and MCP (Model Context Protocol). This feature has been successfully implemented in the Rust OpenTDF implementation (see [opentdf-rs#22](https://github.com/arkavo-org/opentdf-rs/pull/22)) and should be ported to OpenTDFKit for Swift/Apple platforms.
## Background
The current OpenTDFKit implementation supports:
- **TDF Archive Envelope** (`.archive` case) - ZIP-based format with separate manifest and payload files
- **NanoTDF** - Compact binary format for constrained environments
The JSON envelope format is needed to support modern JSON-RPC protocols where:
- Payloads need to be embedded inline within JSON messages
- ZIP archive overhead is unnecessary and adds complexity
- Native JSON serialization is preferred for protocol compatibility
- All OpenTDF security properties must be maintained
## Reference Implementation
The Rust implementation provides a complete reference:
- **PR**: https://github.com/arkavo-org/opentdf-rs/pull/22
- **Documentation**: ZTDF_JSON.md in the Rust repository
- **Example**: examples/jsonrpc_example.rs
## Proposed Changes
### 1. Add JSON Envelope Case to TrustedDataFormatKind
**File**: `OpenTDFKit/TDF/TrustedDataFormat.swift`
```swift
/// Supported Trusted Data Format variants.
///
/// TDF supports multiple envelope types:
/// - `archive`: ZIP-based envelope (current implementation)
/// - `json`: JSON-based envelope with inline payload (for JSON-RPC protocols)
/// - `nano`: Compact binary envelope (NanoTDF)
public enum TrustedDataFormatKind: Sendable {
case nano
case archive
case json // NEW
}
```
### 2. Create TDFJSONContainer
**New File**: `OpenTDFKit/TDF/TDFJSONContainer.swift`
```swift
import Foundation
/// TDF container with inline JSON payload for JSON-RPC protocols
public struct TDFJSONContainer: TrustedDataContainer {
public var manifest: TDFManifestInline
public var version: String
public init(manifest: TDFManifestInline, version: String = "3.0.0") {
self.manifest = manifest
self.version = version
}
public var formatKind: TrustedDataFormatKind { .json }
public func serializedData() throws -> Data {
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
return try encoder.encode(self)
}
}
/// TDF manifest with inline payload support
public struct TDFManifestInline: Codable, Sendable {
public let payload: InlinePayload
public let encryptionInformation: TDFEncryptionInformation
public let schemaVersion: String?
public init(
payload: InlinePayload,
encryptionInformation: TDFEncryptionInformation,
schemaVersion: String? = "4.3.0"
) {
self.payload = payload
self.encryptionInformation = encryptionInformation
self.schemaVersion = schemaVersion
}
}
/// Inline payload for JSON-RPC transport
public struct InlinePayload: Codable, Sendable {
public let type: String // Always "inline"
public let mimeType: String
public let protocol: String // Always "base64"
public let value: String // Base64-encoded encrypted data
public let isEncrypted: Bool
public init(
mimeType: String,
value: String,
isEncrypted: Bool = true
) {
self.type = "inline"
self.mimeType = mimeType
self.protocol = "base64"
self.value = value
self.isEncrypted = isEncrypted
}
}
```
### 3. Add JSON Encryption Support to TDFEncryptor
**File**: `OpenTDFKit/TDF/TDFProcessor.swift`
```swift
extension TDFEncryptor {
/// Encrypt data to JSON envelope format
public func encryptToJSON(
plaintext: Data,
configuration: TDFEncryptionConfiguration
) throws -> TDFJSONEncryptionResult {
// Generate symmetric key
let symmetricKey = try TDFCrypto.generateSymmetricKey()
// Encrypt payload
let (iv, ciphertext, tag) = try TDFCrypto.encryptPayload(
plaintext: plaintext,
symmetricKey: symmetricKey
)
// Combine ciphertext and tag
var encryptedPayload = ciphertext
encryptedPayload.append(tag)
// Base64 encode for JSON
let base64Payload = encryptedPayload.base64EncodedString()
// Create inline payload
let inlinePayload = InlinePayload(
mimeType: configuration.mimeType ?? "application/octet-stream",
value: base64Payload,
isEncrypted: true
)
// Wrap symmetric key with KAS public key
let wrappedKey = try TDFCrypto.wrapSymmetricKeyWithRSA(
publicKeyPEM: configuration.kas.publicKeyPEM,
symmetricKey: symmetricKey
)
// Create policy binding
let policyBinding = TDFCrypto.policyBinding(
policy: configuration.policy.json,
symmetricKey: symmetricKey
)
// Build manifest
let builder = TDFManifestBuilder()
let manifest = builder.buildInlineManifest(
wrappedKey: wrappedKey,
kasURL: configuration.kas.url,
policy: configuration.policy.base64String,
iv: iv.base64EncodedString(),
inlinePayload: inlinePayload,
policyBinding: policyBinding
)
// Create container
let container = TDFJSONContainer(
manifest: manifest,
version: configuration.tdfSpecVersion
)
return TDFJSONEncryptionResult(
container: container,
symmetricKey: symmetricKey
)
}
}
public struct TDFJSONEncryptionResult {
public let container: TDFJSONContainer
public let symmetricKey: SymmetricKey
}
```
### 4. Add JSON Decryption Support to TDFDecryptor
**File**: `OpenTDFKit/TDF/TDFProcessor.swift`
```swift
extension TDFDecryptor {
/// Decrypt JSON envelope with symmetric key
public func decrypt(
container: TDFJSONContainer,
symmetricKey: SymmetricKey
) throws -> Data {
// Decode base64 payload
guard let encryptedData = Data(base64Encoded: container.manifest.payload.value) else {
throw TDFDecryptError.invalidPayloadEncoding
}
// Extract IV from manifest
guard let ivData = Data(base64Encoded: container.manifest.encryptionInformation.method.iv) else {
throw TDFDecryptError.invalidIV
}
// Split ciphertext and tag (last 16 bytes)
guard encryptedData.count >= 16 else {
throw TDFDecryptError.invalidCiphertext
}
let ciphertext = encryptedData.prefix(encryptedData.count - 16)
let tag = encryptedData.suffix(16)
// Decrypt
return try TDFCrypto.decryptPayload(
ciphertext: ciphertext,
iv: ivData,
tag: tag,
symmetricKey: symmetricKey
)
}
/// Decrypt JSON envelope with KAS rewrap
public func decrypt(
container: TDFJSONContainer,
kasClient: KASRewrapClient,
clientPublicKeyPEM: String
) async throws -> Data {
// Convert inline manifest to standard manifest for KAS rewrap
let standardManifest = try container.manifest.toStandardManifest()
// Perform KAS rewrap
let rewrapResult = try await kasClient.rewrapTDF(
manifest: standardManifest,
clientPublicKeyPEM: clientPublicKeyPEM
)
// Reconstruct symmetric key from wrapped keys
let symmetricKey = try TDFCrypto.reconstructSymmetricKey(
from: rewrapResult.wrappedKeys
)
// Decrypt with symmetric key
return try decrypt(container: container, symmetricKey: symmetricKey)
}
}
```
### 5. Update TDFManifestBuilder
**File**: `OpenTDFKit/TDF/TDFManifestBuilder.swift`
```swift
extension TDFManifestBuilder {
/// Build manifest with inline payload for JSON envelope
public func buildInlineManifest(
wrappedKey: String,
kasURL: URL,
policy: String,
iv: String,
inlinePayload: InlinePayload,
policyBinding: TDFPolicyBinding,
tdfSpecVersion: String = "4.3.0"
) -> TDFManifestInline {
let keyAccessObject = TDFKeyAccessObject(
type: .wrapped,
url: kasURL.absoluteString,
protocolValue: .kas,
wrappedKey: wrappedKey,
policyBinding: policyBinding,
encryptedMetadata: nil,
kid: nil,
sid: nil,
schemaVersion: "1.0",
ephemeralPublicKey: nil
)
let method = TDFMethodDescriptor(
algorithm: "AES-256-GCM",
iv: iv,
isStreamable: false // JSON envelope doesn't support streaming
)
let encryptionInformation = TDFEncryptionInformation(
type: .split,
keyAccess: [keyAccessObject],
method: method,
integrityInformation: nil, // Optional for JSON envelope
policy: policy
)
return TDFManifestInline(
payload: inlinePayload,
encryptionInformation: encryptionInformation,
schemaVersion: tdfSpecVersion
)
}
}
```
### 6. Add Conversion Utilities
**File**: `OpenTDFKit/TDF/TDFManifest.swift`
```swift
extension TDFManifestInline {
/// Convert inline manifest to standard manifest for KAS operations
public func toStandardManifest() throws -> TDFManifest {
let payloadDescriptor = TDFPayloadDescriptor(
type: .reference,
url: "0.payload",
protocolValue: .zip,
isEncrypted: true,
mimeType: payload.mimeType
)
return TDFManifest(
schemaVersion: schemaVersion ?? "4.3.0",
payload: payloadDescriptor,
encryptionInformation: encryptionInformation,
assertions: nil
)
}
}
```
## Use Cases
### 1. A2A (Agent-to-Agent) Protocol Integration
```swift
// Encrypt sensitive agent message
let policy = try TDFPolicy(json: policyJSON)
let config = TDFEncryptionConfiguration(
kas: kasInfo,
policy: policy,
mimeType: "application/json"
)
let encryptor = TDFEncryptor()
let result = try encryptor.encryptToJSON(
plaintext: messageData,
configuration: config
)
// Serialize for JSON-RPC transmission
let jsonData = try result.container.serializedData()
let jsonString = String(data: jsonData, encoding: .utf8)
// Include in A2A message
let a2aMessage = """
{
"jsonrpc": "2.0",
"id": 123,
"result": {
"message": {
"role": "agent",
"tdf": \(jsonString)
}
}
}
"""
```
### 2. MCP (Model Context Protocol) Integration
```swift
// Encrypt tool response
let result = try encryptor.encryptToJSON(
plaintext: toolResponseData,
configuration: config
)
// Include in MCP response
let mcpResponse = """
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": [
{ "type": "text", "text": "Tool executed" }
],
"tdf": \(try result.container.serializedData())
}
}
"""
```
### 3. Decryption with KAS
```swift
// Parse received JSON envelope
let decoder = JSONDecoder()
let container = try decoder.decode(TDFJSONContainer.self, from: jsonData)
// Decrypt with KAS
let decryptor = TDFDecryptor()
let plaintext = try await decryptor.decrypt(
container: container,
kasClient: kasClient,
clientPublicKeyPEM: clientPublicKey
)
```
## Testing Requirements
### Unit Tests
- [ ] Test JSON envelope creation
- [ ] Test JSON serialization/deserialization round-trip
- [ ] Test encryption with inline payload
- [ ] Test decryption with symmetric key
- [ ] Test decryption with KAS rewrap
- [ ] Test manifest conversion (inline ↔ standard)
- [ ] Test policy binding validation
- [ ] Test error handling for invalid payloads
### Integration Tests
- [ ] Test end-to-end encryption/decryption flow
- [ ] Test KAS integration with JSON envelope
- [ ] Test multi-KAS scenarios
- [ ] Test attribute-based access control
- [ ] Test interoperability with Rust implementation
### Performance Tests
- [ ] Benchmark JSON envelope vs Archive envelope
- [ ] Test memory usage with large payloads
- [ ] Test serialization performance
## Documentation Requirements
- [ ] Add JSON envelope section to README.md
- [ ] Update CLAUDE.md with JSON envelope usage
- [ ] Create MIGRATION_GUIDE.md section for JSON envelope
- [ ] Add inline code documentation with examples
- [ ] Update SECURITY.md with JSON envelope security properties
- [ ] Create example project demonstrating JSON-RPC integration
## Security Considerations
### Maintained Security Properties
- ✅ AES-256-GCM authenticated encryption
- ✅ HMAC-SHA256 policy binding
- ✅ RSA-2048+ key wrapping
- ✅ Zero trust architecture (keys separate from data)
- ✅ KAS-based access control
- ✅ Attribute-based access control (ABAC)
### JSON-Specific Considerations
- Base64 encoding adds ~33% size overhead (acceptable for JSON-RPC)
- No streaming support (entire payload in memory)
- JSON parsing security (use Swift's built-in JSONDecoder)
- Validate payload size limits to prevent DoS
## Breaking Changes
None. This is a purely additive change that introduces new functionality without modifying existing APIs.
## Migration Path
No migration required. Existing code continues to work unchanged. New functionality is opt-in through:
- New `TDFJSONContainer` type
- New `encryptToJSON()` method on `TDFEncryptor`
- New `decrypt(container: TDFJSONContainer, ...)` methods on `TDFDecryptor`
## Success Criteria
- [ ] All unit tests passing
- [ ] All integration tests passing
- [ ] Documentation complete
- [ ] Example code provided
- [ ] Interoperability with Rust implementation verified
- [ ] Performance benchmarks meet expectations
- [ ] Security review completed
- [ ] Code review approved
## Related Issues
- #24 - StandardTDF Developer Experience (foundational work)
- #25 - NanoTDF Developer Experience (parallel effort)
- #26 - StandardTDF → TDF Rename (sets up naming for JSON envelope)
## Related PRs
- Rust implementation: https://github.com/arkavo-org/opentdf-rs/pull/22
- Original proposal: https://github.com/arkavo-org/arkavo-edge/issues/293
## Timeline
**Estimated Effort**: 2-3 weeks
- Week 1: Core implementation (types, encryption, decryption)
- Week 2: Testing, documentation, examples
- Week 3: Integration testing, security review, polish
## Questions for Discussion
1. Should we support streaming encryption/decryption for JSON envelope, or is in-memory only acceptable?
2. What should be the maximum payload size limit for JSON envelope?
3. Should we provide convenience methods for common JSON-RPC protocol integrations (A2A, MCP)?
4. Should we add validation for JSON envelope size to warn about performance implications?
5. How should we handle backward compatibility if the JSON envelope format evolves?
## References
- OpenTDF Specification: https://github.com/opentdf/spec
- A2A Protocol: https://a2a-protocol.org/
- MCP Protocol: https://modelcontextprotocol.io/
- Rust Implementation: https://github.com/arkavo-org/opentdf-rs
Contributor guide
No contributing guide indexed for this repository
Research direction
Start by comparing the referenced Rust implementation with OpenTDFKit/TDF/TrustedDataFormat.swift, TDFProcessor.swift, TDFManifestBuilder.swift, and TDFManifest.swift. Implement the proposed JSON container, inline manifest support, encryption/decryption paths, and conversion utilities, then cover JSON envelope creation and serialization/deserialization round trips in the test suite.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- swift
- Domain
- api, security
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Clearly specified
- Newbie friendliness
- 35/100