Skip to content

Implement TDF JSON Envelope Support for JSON-RPC Protocols #27

@superninja-app

Description

@superninja-app

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 (.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:

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 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

Related PRs

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions