-
Notifications
You must be signed in to change notification settings - Fork 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) and should be ported to OpenTDFKit for Swift/Apple platforms.
Background
The current OpenTDFKit implementation supports:
- TDF Archive Envelope (
.archivecase) - 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: TDF-JSON for JSON-RPC AI protocols opentdf-rs#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
/// 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
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
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
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
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
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
// 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
// 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
// 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
TDFJSONContainertype - New
encryptToJSON()method onTDFEncryptor - New
decrypt(container: TDFJSONContainer, ...)methods onTDFDecryptor
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
- Developer Experience Improvements for StandardTDF Encryption/Decryption #24 - StandardTDF Developer Experience (foundational work)
- Improve NanoTDF Developer Experience #25 - NanoTDF Developer Experience (parallel effort)
- DX #26 - StandardTDF → TDF Rename (sets up naming for JSON envelope)
Related PRs
- Rust implementation: TDF-JSON for JSON-RPC AI protocols opentdf-rs#22
- Original proposal: ZTDF-JSON Integration: Native TDF Support in MCP and A2A Protocols arkavo-edge#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
- Should we support streaming encryption/decryption for JSON envelope, or is in-memory only acceptable?
- What should be the maximum payload size limit for JSON envelope?
- Should we provide convenience methods for common JSON-RPC protocol integrations (A2A, MCP)?
- Should we add validation for JSON envelope size to warn about performance implications?
- 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