diff --git a/OpenTDFKit/CryptoHelper.swift b/OpenTDFKit/CryptoHelper.swift index b7dde25..8044443 100644 --- a/OpenTDFKit/CryptoHelper.swift +++ b/OpenTDFKit/CryptoHelper.swift @@ -219,7 +219,13 @@ public actor CryptoHelper { /// - Throws: `CryptoKitError` if the AES-GCM seal operation fails. func createGMACBinding(policyBody: Data, symmetricKey: SymmetricKey) throws -> Data { // Seal empty data, authenticating the policyBody. The tag is the GMAC binding. - let sealedBox = try CryptoKit.AES.GCM.seal(Data(), using: symmetricKey, authenticating: policyBody) + // Use a deterministic zero nonce so the binding can be recomputed and verified later. + // Security note: this is safe only because every NanoTDF derives a unique symmetric key + // from a fresh ephemeral ECDH key. Reusing a symmetric key with a fixed zero nonce while + // also encrypting payloads under the same key would create a catastrophic AES-GCM nonce-reuse + // condition. Do not cache or reuse symmetric keys across NanoTDFs. + let nonce = try CryptoKit.AES.GCM.Nonce(data: Data(count: CryptoConstants.aesGcmNonceSize)) + let sealedBox = try CryptoKit.AES.GCM.seal(Data(), using: symmetricKey, nonce: nonce, authenticating: policyBody) // Truncate to 64 bits (8 bytes) per spec section 3.3.1.3 return Data(sealedBox.tag.prefix(8)) } diff --git a/OpenTDFKit/KASService.swift b/OpenTDFKit/KASService.swift index c746085..0c10957 100644 --- a/OpenTDFKit/KASService.swift +++ b/OpenTDFKit/KASService.swift @@ -344,8 +344,10 @@ public actor KASService { policyData: Data, symmetricKey: SymmetricKey, ) async throws -> Bool { - // For GMAC binding verification, create a tag with empty ciphertext and the policy data as authenticated data - let fullTag = try AES.GCM.seal(Data(), using: symmetricKey, authenticating: policyData).tag + // For GMAC binding verification, create a tag with empty ciphertext and the policy data as authenticated data. + // Use the same deterministic zero nonce that createGMACBinding uses so the tag can be reproduced. + let nonce = try AES.GCM.Nonce(data: Data(count: 12)) + let fullTag = try AES.GCM.seal(Data(), using: symmetricKey, nonce: nonce, authenticating: policyData).tag guard policyBinding.count == 8 || policyBinding.count == fullTag.count else { return false } diff --git a/OpenTDFKitTests/BinaryParserTests.swift b/OpenTDFKitTests/BinaryParserTests.swift new file mode 100644 index 0000000..29e63b3 --- /dev/null +++ b/OpenTDFKitTests/BinaryParserTests.swift @@ -0,0 +1,91 @@ +import Foundation +@testable import OpenTDFKit +import XCTest + +final class BinaryParserTests: XCTestCase { + func testInvalidMagicNumberThrows() { + let data = Data([0x00, 0x00, 0x4C]) + let parser = BinaryParser(data: data) + + XCTAssertThrowsError(try parser.parseHeader()) { error in + guard let parsingError = error as? ParsingError, + case .invalidMagicNumber = parsingError + else { + XCTFail("Expected ParsingError.invalidMagicNumber, got \(error)") + return + } + } + } + + func testInvalidVersionThrows() { + // Valid magic number ("L1") followed by unsupported v13 (0x4D) + let data = Data([0x4C, 0x31, 0x4D]) + let parser = BinaryParser(data: data) + + XCTAssertThrowsError(try parser.parseHeader()) { error in + guard let parsingError = error as? ParsingError, + case .invalidVersion = parsingError + else { + XCTFail("Expected ParsingError.invalidVersion, got \(error)") + return + } + } + } + + func testTruncatedHeaderThrowsInvalidFormat() { + // Magic + version only; missing KAS locator, ECC mode, payload config, policy, and ephemeral key + let data = Data([0x4C, 0x31, 0x4C]) + let parser = BinaryParser(data: data) + + XCTAssertThrowsError(try parser.parseHeader()) { error in + guard let parsingError = error as? ParsingError, + case .invalidFormat = parsingError + else { + XCTFail("Expected ParsingError.invalidFormat, got \(error)") + return + } + } + } + + func testMissingEphemeralKeyThrowsInvalidFormat() { + // Valid magic + version + valid KAS locator + valid ECC mode + valid payload config + remote policy + var data = Data() + data.append(Header.magicNumber) + data.append(Header.versionV12) + data.append(ResourceLocator(protocolEnum: .http, body: "kas.example.com")!.toData()) + data.append(PolicyBindingConfig(ecdsaBinding: false, curve: .secp256r1).toData()) + data.append(SignatureAndPayloadConfig(signed: false, signatureCurve: nil, payloadCipher: .aes256GCM128).toData()) + data.append(Policy.PolicyType.remote.rawValue) + data.append(ResourceLocator(protocolEnum: .http, body: "kas.example.com/policy")!.toData()) + data.append(Data(count: 8)) // placeholder GMAC binding + // Intentionally omit the ephemeral public key + + let parser = BinaryParser(data: data) + XCTAssertThrowsError(try parser.parseHeader()) { error in + guard let parsingError = error as? ParsingError, + case .invalidFormat = parsingError + else { + XCTFail("Expected ParsingError.invalidFormat, got \(error)") + return + } + } + } + + func testMalformedResourceLocatorThrows() { + var data = Data() + data.append(Header.magicNumber) + data.append(Header.versionV12) + // Protocol byte with invalid protocol value (0xFF) and zero-length body + data.append(contentsOf: [0xFF, 0x00]) + + let parser = BinaryParser(data: data) + XCTAssertThrowsError(try parser.parseHeader()) { error in + guard let parsingError = error as? ParsingError, + case .invalidFormat = parsingError + else { + XCTFail("Expected ParsingError.invalidFormat, got \(error)") + return + } + } + } +} diff --git a/OpenTDFKitTests/CryptoHelperTests.swift b/OpenTDFKitTests/CryptoHelperTests.swift index 0ff9268..98e82cb 100644 --- a/OpenTDFKitTests/CryptoHelperTests.swift +++ b/OpenTDFKitTests/CryptoHelperTests.swift @@ -1,4 +1,3 @@ -import CryptoKit import Foundation import XCTest @@ -53,92 +52,3 @@ final class CryptoHelperTests: XCTestCase { ) } } - -/// Add these to CryptoHelper.swift: -extension CryptoHelper { - /// Combined operation that keeps sensitive types within the actor - struct EncryptionResult { - let gmacTag: Data - let nonce: Data - let ciphertext: Data - let tag: Data - } - - func deriveKeysAndEncrypt( - keyPair: EphemeralKeyPair, - recipientPublicKey: Data, - plaintext: Data, - policyBody: Data, - ) throws -> EncryptionResult { - // Derive shared secret - guard let sharedSecret = try deriveSharedSecret( - keyPair: keyPair, - recipientPublicKey: recipientPublicKey, - ) else { - throw CryptoHelperError.keyDerivationFailed - } - - // Derive symmetric key - let symmetricKey = deriveSymmetricKey( - sharedSecret: sharedSecret, - salt: CryptoConstants.hkdfSalt, - info: CryptoConstants.hkdfInfoEncryption, - outputByteCount: CryptoConstants.symmetricKeyByteCount, - ) - - // Create GMAC binding - let gmacTag = try createGMACBinding( - policyBody: policyBody, - symmetricKey: symmetricKey, - ) - - // Generate nonce - let nonce = try generateNonce() - - // Encrypt payload - let (ciphertext, tag) = try encryptPayload( - plaintext: plaintext, - symmetricKey: symmetricKey, - nonce: nonce, - ) - - return EncryptionResult( - gmacTag: gmacTag, - nonce: nonce, - ciphertext: ciphertext, - tag: tag, - ) - } - - func decryptWithDerivedKeys( - keyPair: EphemeralKeyPair, - recipientPublicKey: Data, - ciphertext: Data, - nonce: Data, - tag: Data, - ) throws -> Data { - // Derive shared secret - guard let sharedSecret = try deriveSharedSecret( - keyPair: keyPair, - recipientPublicKey: recipientPublicKey, - ) else { - throw CryptoHelperError.keyDerivationFailed - } - - // Derive symmetric key - let symmetricKey = deriveSymmetricKey( - sharedSecret: sharedSecret, - salt: CryptoConstants.hkdfSalt, - info: CryptoConstants.hkdfInfoEncryption, - outputByteCount: CryptoConstants.symmetricKeyByteCount, - ) - - // Decrypt - return try decryptPayload( - ciphertext: ciphertext, - symmetricKey: symmetricKey, - nonce: nonce, - tag: tag, - ) - } -} diff --git a/OpenTDFKitTests/Helpers/CryptoHelper+TestHelpers.swift b/OpenTDFKitTests/Helpers/CryptoHelper+TestHelpers.swift new file mode 100644 index 0000000..33b30e8 --- /dev/null +++ b/OpenTDFKitTests/Helpers/CryptoHelper+TestHelpers.swift @@ -0,0 +1,91 @@ +import CryptoKit +import Foundation +@testable import OpenTDFKit + +extension CryptoHelper { + /// Combined operation that keeps sensitive types within the actor + struct EncryptionResult { + let gmacTag: Data + let nonce: Data + let ciphertext: Data + let tag: Data + } + + func deriveKeysAndEncrypt( + keyPair: EphemeralKeyPair, + recipientPublicKey: Data, + plaintext: Data, + policyBody: Data, + ) throws -> EncryptionResult { + // Derive shared secret + guard let sharedSecret = try deriveSharedSecret( + keyPair: keyPair, + recipientPublicKey: recipientPublicKey, + ) else { + throw CryptoHelperError.keyDerivationFailed + } + + // Derive symmetric key + let symmetricKey = deriveSymmetricKey( + sharedSecret: sharedSecret, + salt: CryptoConstants.hkdfSalt, + info: CryptoConstants.hkdfInfoEncryption, + outputByteCount: CryptoConstants.symmetricKeyByteCount, + ) + + // Create GMAC binding + let gmacTag = try createGMACBinding( + policyBody: policyBody, + symmetricKey: symmetricKey, + ) + + // Generate nonce + let nonce = try generateNonce() + + // Encrypt payload + let (ciphertext, tag) = try encryptPayload( + plaintext: plaintext, + symmetricKey: symmetricKey, + nonce: nonce, + ) + + return EncryptionResult( + gmacTag: gmacTag, + nonce: nonce, + ciphertext: ciphertext, + tag: tag, + ) + } + + func decryptWithDerivedKeys( + keyPair: EphemeralKeyPair, + recipientPublicKey: Data, + ciphertext: Data, + nonce: Data, + tag: Data, + ) throws -> Data { + // Derive shared secret + guard let sharedSecret = try deriveSharedSecret( + keyPair: keyPair, + recipientPublicKey: recipientPublicKey, + ) else { + throw CryptoHelperError.keyDerivationFailed + } + + // Derive symmetric key + let symmetricKey = deriveSymmetricKey( + sharedSecret: sharedSecret, + salt: CryptoConstants.hkdfSalt, + info: CryptoConstants.hkdfInfoEncryption, + outputByteCount: CryptoConstants.symmetricKeyByteCount, + ) + + // Decrypt + return try decryptPayload( + ciphertext: ciphertext, + symmetricKey: symmetricKey, + nonce: nonce, + tag: tag, + ) + } +} diff --git a/OpenTDFKitTests/InitializationTests.swift b/OpenTDFKitTests/InitializationTests.swift index f1e2f16..2fb51bc 100644 --- a/OpenTDFKitTests/InitializationTests.swift +++ b/OpenTDFKitTests/InitializationTests.swift @@ -88,7 +88,6 @@ final class InitializationTests: XCTestCase { XCTAssertNotNil(locator) let nanoTDF = initializeSmallNanoTDF(kasResourceLocator: locator!) let data = nanoTDF.toData() - print("data.count", data.count) XCTAssertLessThan(data.count, 240) } diff --git a/OpenTDFKitTests/KASServiceBenchmarkTests.swift b/OpenTDFKitTests/KASServiceBenchmarkTests.swift index d10bab0..c4f7c4f 100644 --- a/OpenTDFKitTests/KASServiceBenchmarkTests.swift +++ b/OpenTDFKitTests/KASServiceBenchmarkTests.swift @@ -129,8 +129,17 @@ final class KASServiceBenchmarkTests: XCTestCase { let policyData = Data(repeating: 0x42, count: size) let symmetricKey = SymmetricKey(size: .bits256) - // Create a GMAC binding - let policyBinding = try AES.GCM.seal(Data(), using: symmetricKey, authenticating: policyData).tag + // Create a deterministic GMAC binding using the same zero nonce as production. + let nonce = try AES.GCM.Nonce(data: Data(count: 12)) + let policyBinding = try AES.GCM.seal(Data(), using: symmetricKey, nonce: nonce, authenticating: policyData).tag + + // Sanity-check that verification succeeds before benchmarking. + let isValid = try await kasService.verifyPolicyBinding( + policyBinding: policyBinding, + policyData: policyData, + symmetricKey: symmetricKey, + ) + XCTAssertTrue(isValid, "Benchmark binding should be valid") // Start benchmark let startTime = DispatchTime.now() diff --git a/OpenTDFKitTests/KASServiceTests.swift b/OpenTDFKitTests/KASServiceTests.swift index 91c2a15..a52cfd2 100644 --- a/OpenTDFKitTests/KASServiceTests.swift +++ b/OpenTDFKitTests/KASServiceTests.swift @@ -186,78 +186,77 @@ final class KASServiceTests: XCTestCase { XCTAssertEqual(decryptedData, plaintext, "Decrypted data should match original plaintext") } - // This test was removed because it was failing - // TODO: Fix policy binding verification test - /* - func testVerifyPolicyBinding() async throws { - // Create a KAS service - let baseURL = URL(string: "https://kas.example.com")! - let kasService = KASService(keyStore: keyStore, baseURL: baseURL) - - // Generate KAS metadata - let kasMetadata = try await kasService.generateKasMetadata() - - // Create test plaintext - let plaintext = "Policy binding test data".data(using: .utf8)! - - // Create a policy for the NanoTDF - let policyData = "classification:secret".data(using: .utf8)! - let embeddedPolicyBody = EmbeddedPolicyBody(body: policyData, keyAccess: nil) - var policy = Policy(type: .embeddedPlaintext, body: embeddedPolicyBody, remote: nil, binding: nil) - - // Create a NanoTDF - this will generate a policy binding during creation - let nanoTDF = try await createNanoTDF( - kas: kasMetadata, - policy: &policy, - plaintext: plaintext - ) - - // Extract the policy binding from the created NanoTDF + func testVerifyPolicyBinding() async throws { + // Create a KAS service + let baseURL = URL(string: "https://kas.example.com")! + let kasService = KASService(keyStore: keyStore, baseURL: baseURL) + + // Generate KAS metadata + let kasMetadata = try await kasService.generateKasMetadata() + + // Create test plaintext + let plaintext = "Policy binding test data".data(using: .utf8)! + + // Create a policy for the NanoTDF + let policyData = "classification:secret".data(using: .utf8)! + let embeddedPolicyBody = EmbeddedPolicyBody(body: policyData, keyAccess: nil) + var policy = Policy(type: .embeddedPlaintext, body: embeddedPolicyBody, remote: nil, binding: nil) + + // Create a NanoTDF - this will generate a policy binding during creation + let nanoTDF = try await createNanoTDF( + kas: kasMetadata, + policy: &policy, + plaintext: plaintext, + ) + + // Extract the policy binding from the created NanoTDF let policyBinding = nanoTDF.header.policy.binding XCTAssertNotNil(policyBinding, "Policy binding should be created during NanoTDF creation") XCTAssertEqual(policyBinding?.count, 8, "Policy binding should be 8 bytes (64 bits)") - // Get the KAS public key and derive the same symmetric key that was used for binding - let kasPublicKey = try kasMetadata.getPublicKey() - let kasPrivateKeyData = await keyStore.getPrivateKey(forPublicKey: kasPublicKey) - XCTAssertNotNil(kasPrivateKeyData, "KAS private key should be found in keystore") - - // Derive the same symmetric key that was used to create the binding - let privateKey = try P256.KeyAgreement.PrivateKey(rawRepresentation: kasPrivateKeyData!) - let clientPublicKey = try P256.KeyAgreement.PublicKey(compressedRepresentation: nanoTDF.header.ephemeralPublicKey) - let sharedSecret = try privateKey.sharedSecretFromKeyAgreement(with: clientPublicKey) - - // Compute salt as SHA256(MAGIC_NUMBER + VERSION) per spec - // Using v12 (L1L) since createNanoTDF uses v12 only - let salt = CryptoHelper.computeHKDFSalt(version: Header.versionV12) - - let symmetricKey = sharedSecret.hkdfDerivedSymmetricKey( - using: SHA256.self, - salt: salt, - sharedInfo: Data(), // Empty per spec section 4 - outputByteCount: 32 - ) - - // Verify the binding - let isValid = try await kasService.verifyPolicyBinding( - policyBinding: policyBinding!, - policyData: policyData, - symmetricKey: symmetricKey - ) - - XCTAssertTrue(isValid, "Policy binding should be valid") - - // Test with invalid binding - let invalidBinding = Data([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]) - let isInvalid = try await kasService.verifyPolicyBinding( - policyBinding: invalidBinding, - policyData: policyData, - symmetricKey: symmetricKey - ) - - XCTAssertFalse(isInvalid, "Invalid policy binding should fail verification") - } - */ + // Get the KAS public key and derive the same symmetric key that was used for binding + let kasPublicKey = try kasMetadata.getPublicKey() + let kasPrivateKeyData = await keyStore.getPrivateKey(forPublicKey: kasPublicKey) + XCTAssertNotNil(kasPrivateKeyData, "KAS private key should be found in keystore") + + // Derive the same symmetric key that was used to create the binding + let privateKey = try P256.KeyAgreement.PrivateKey(rawRepresentation: kasPrivateKeyData!) + let clientPublicKey = try P256.KeyAgreement.PublicKey(compressedRepresentation: nanoTDF.header.ephemeralPublicKey) + let sharedSecret = try privateKey.sharedSecretFromKeyAgreement(with: clientPublicKey) + + // Compute salt as SHA256(MAGIC_NUMBER + VERSION) per spec + // Using v12 (L1L) since createNanoTDF uses v12 only + let salt = CryptoHelper.computeHKDFSalt(version: Header.versionV12) + + let symmetricKey = sharedSecret.hkdfDerivedSymmetricKey( + using: SHA256.self, + salt: salt, + sharedInfo: Data(), // Empty per spec section 4 + outputByteCount: 32, + ) + + // The binding was computed over the serialized embedded policy body, not the raw policy bytes. + let serializedPolicyBody = EmbeddedPolicyBody(body: policyData, keyAccess: nil).toData() + + // Verify the binding + let isValid = try await kasService.verifyPolicyBinding( + policyBinding: policyBinding!, + policyData: serializedPolicyBody, + symmetricKey: symmetricKey, + ) + + XCTAssertTrue(isValid, "Policy binding should be valid") + + // Test with invalid binding + let invalidBinding = Data([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]) + let isInvalid = try await kasService.verifyPolicyBinding( + policyBinding: invalidBinding, + policyData: serializedPolicyBody, + symmetricKey: symmetricKey, + ) + + XCTAssertFalse(isInvalid, "Invalid policy binding should fail verification") + } func testRewrapKeyEndpointPath() async throws { // Use a mock URLSession to capture the request URL without network I/O @@ -337,6 +336,42 @@ final class KASServiceTests: XCTestCase { XCTAssertFalse(newKeyPair.publicKey.isEmpty) XCTAssertFalse(newKeyPair.privateKey.isEmpty) } + + func testRewrapKeyMockResponse() async throws { + // Known rewrapped key payload returned by the mock KAS server + let expectedKey = Data("rewrapped-session-key".utf8) + let expectedBase64 = expectedKey.base64EncodedString() + + var capturedRequest: URLRequest? + MockURLProtocol.handler = { request in + capturedRequest = request + let response = HTTPURLResponse( + url: request.url!, + statusCode: 200, + httpVersion: nil, + headerFields: ["Content-Type": "application/json"], + )! + let body = Data("{\"rewrapped_key\":\"\(expectedBase64)\"}".utf8) + return (response, body) + } + + let session = MockURLProtocol.makeSession() + let baseURL = URL(string: "https://kas.example.com")! + let kasService = KASService(keyStore: keyStore, baseURL: baseURL, urlSession: session) + + let ephemeralPublicKey = Data(count: 33) + let encryptedSessionKey = Data(count: 44) + + let rewrappedKey = try await kasService.rewrapKey( + ephemeralPublicKey: ephemeralPublicKey, + encryptedSessionKey: encryptedSessionKey, + ) + + XCTAssertEqual(rewrappedKey, expectedKey, "Rewrapped key should match the mocked response") + + XCTAssertNotNil(capturedRequest) + XCTAssertEqual(capturedRequest?.url?.path, "/kas/v2/rewrap", "Rewrap request should target /kas/v2/rewrap") + } } // MARK: - Mock URLProtocol for rewrap endpoint verification diff --git a/OpenTDFKitTests/KeyStoreTests.swift b/OpenTDFKitTests/KeyStoreTests.swift index ec4b5a8..b9b7411 100644 --- a/OpenTDFKitTests/KeyStoreTests.swift +++ b/OpenTDFKitTests/KeyStoreTests.swift @@ -26,7 +26,7 @@ final class KeyStoreTests: XCTestCase { // Serialize all keys let serializedData = await keyStore.serialize() - print("Total serialized size: \(serializedData.count) bytes") + XCTAssertGreaterThan(serializedData.count, 0, "Serialized data should not be empty") // Create new store and deserialize let restoredStore = KeyStore(curve: .secp256r1) diff --git a/OpenTDFKitTests/NanoTDFCreationTests.swift b/OpenTDFKitTests/NanoTDFCreationTests.swift index 2850c3e..394a5ad 100644 --- a/OpenTDFKitTests/NanoTDFCreationTests.swift +++ b/OpenTDFKitTests/NanoTDFCreationTests.swift @@ -27,40 +27,15 @@ class NanoTDFCreationTests: XCTestCase { XCTAssertNotNil(nanoTDF.payload.iv, "Payload nonce should not be nil") XCTAssertNotNil(nanoTDF.payload.ciphertext, "Payload ciphertext should not be nil") XCTAssertEqual(nanoTDF.payload.length, 43) - print(nanoTDF) // round trip - serialize let serializedData = nanoTDF.toData() - var counter = 0 - let serializedHexString = serializedData.map { byte -> String in - counter += 1 - let newline = counter % 20 == 0 ? "\n" : " " - return String(format: "%02x", byte) + newline - }.joined() - print("Created:") - print(serializedHexString) // round trip - parse let parser = BinaryParser(data: serializedData) let header = try parser.parseHeader() - print("Parsed Header:", header) - let pheader = header.toData() - counter = 0 - let pheaderHexString = pheader.map { byte -> String in - counter += 1 - let newline = counter % 20 == 0 ? "\n" : " " - return String(format: "%02x", byte) + newline - }.joined() - print("Parsed Header:") - print(pheaderHexString) - // Policy - let policyHexString = header.policy.toData().map { String(format: "%02x", $0) }.joined(separator: " ") - print("Policy:", policyHexString) - // Ephemeral Key - let ephemeralKeyHexString = header.ephemeralPublicKey.map { String(format: "%02x", $0) }.joined(separator: " ") - print("Ephemeral Key:", ephemeralKeyHexString) + XCTAssertEqual(header.toData(), nanoTDF.header.toData(), "Header should serialize consistently") let payload = try parser.parsePayload(config: header.payloadSignatureConfig) let snanoTDF = NanoTDF(header: header, payload: payload, signature: nil) - // Print final the signature NanoTDF - print(snanoTDF) + XCTAssertEqual(snanoTDF.toData(), serializedData, "Round-tripped NanoTDF should equal original") XCTAssertEqual(payload.length, 43) } } diff --git a/OpenTDFKitTests/NanoTDFTests.swift b/OpenTDFKitTests/NanoTDFTests.swift index 9588b99..e96e6c3 100644 --- a/OpenTDFKitTests/NanoTDFTests.swift +++ b/OpenTDFKitTests/NanoTDFTests.swift @@ -32,7 +32,6 @@ final class NanoTDFTests: XCTestCase { let parser = BinaryParser(data: binaryData!) do { let header = try parser.parseHeader() - print("Parsed Header:", header) // Serialize the parsed header back to Data let serializedHeaderData = header.toData() @@ -48,22 +47,15 @@ final class NanoTDFTests: XCTestCase { XCTAssertEqual(serializedHeaderData, originalHeaderData, "Serialized header content should match original header content.") // KAS - print("KAS:", header.kas.body) - if header.kas.body != "kas.virtru.com" { - XCTFail("KAS body does not match expected value.") - } + XCTAssertEqual(header.kas.body, "kas.virtru.com", "KAS body should match expected value") + // Ephemeral Key let ephemeralKeyHexString = header.ephemeralPublicKey.map { String(format: "%02x", $0) }.joined(separator: " ") - print("Ephemeral Key:", ephemeralKeyHexString) let compareHexString = """ 02 f7 7f ba e5 26 09 da c5 e8 eb f7 86 e1 1b 7a ed d7 0f 89 80 f9 48 0c 7e 67 1c ba ab 8e 24 50 92 """.replacingOccurrences(of: "\n", with: " ") - if ephemeralKeyHexString == compareHexString { - print("Ephemeral Key equals comparison string.") - } else { - XCTFail("Ephemeral Key does not equal comparison string. Actual: \(ephemeralKeyHexString), Expected: \(compareHexString)") - } + XCTAssertEqual(ephemeralKeyHexString, compareHexString, "Ephemeral Key should match comparison string") } catch { XCTFail("Failed to parse data: \(error)") } @@ -93,42 +85,17 @@ final class NanoTDFTests: XCTestCase { // EccMode let serializedEccMode = header.policyBindingConfig.toData() let serializedEccModeHexString = serializedEccMode.map { String(format: "%02x", $0) }.joined(separator: " ") - var compareHexString = """ - 80 - """.replacingOccurrences(of: "\n", with: " ") - if serializedEccModeHexString == compareHexString { - print("EccMode equals comparison string.") - } else { - print(serializedEccModeHexString) - XCTFail("EccMode does not equal comparison string.") - } + XCTAssertEqual(serializedEccModeHexString, "80", "EccMode should equal comparison string") let payload = try parser.parsePayload(config: header.payloadSignatureConfig) let serializedPayloadMacHexString = payload.mac.map { String(format: "%02x", $0) }.joined(separator: " ") - compareHexString = """ - f9 fd 80 14 af 7c cb 06 - """.replacingOccurrences(of: "\n", with: " ") - if serializedPayloadMacHexString == compareHexString { - print("MAC equals comparison string.") - } else { - print(serializedPayloadMacHexString) - XCTFail("MAC does not equal comparison string.") - } + XCTAssertEqual(serializedPayloadMacHexString, "f9 fd 80 14 af 7c cb 06", "MAC should equal comparison string") let signature = try parser.parseSignature(config: header.payloadSignatureConfig) let nano = NanoTDF(header: header, payload: payload, signature: signature) let serializedNanoTDF = nano.toData() - var counter = 0 - let serializedNanoTDFHexString = serializedNanoTDF.map { byte -> String in - counter += 1 - let newline = counter % 20 == 0 ? "\n" : " " - return String(format: "%02x", byte) + newline - }.joined() - print("Actual:") - print(serializedNanoTDFHexString) // NanoTDF creation now only generates v12 format (4c 31 4c). // Just check that the serialized data starts with the magic number and version. XCTAssertEqual(serializedNanoTDF.prefix(2), Data([0x4C, 0x31]), "Magic number should match") XCTAssertEqual(serializedNanoTDF[2], 0x4C, "Version should be 0x4C (v12)") - print("NanoTDF has correct magic number and version.") // back again let bparser = BinaryParser(data: serializedNanoTDF) let bheader = try bparser.parseHeader() @@ -160,31 +127,11 @@ final class NanoTDFTests: XCTestCase { let parser = BinaryParser(data: binaryData!) do { let header = try parser.parseHeader() - // Ephemeral Key - let ephemeralKeyHexString = header.ephemeralPublicKey.map { String(format: "%02x", $0) }.joined(separator: " ") - print("Ephemeral Key:", ephemeralKeyHexString) // Parse payload let payload = try parser.parsePayload(config: header.payloadSignatureConfig) - let payloadIvHexString = payload.iv.map { String(format: "%02x", $0) }.joined(separator: " ") - print("Payload IV:", payloadIvHexString) - var compareHexString = """ - 9e bd 09 - """.replacingOccurrences(of: "\n", with: " ") - if payloadIvHexString == compareHexString { - print("Payload IV equals comparison string.") - } else { - XCTFail("Payload IV does not equal comparison string.") - } - let payloadCiphertextHexString = payload.ciphertext.map { String(format: "%02x", $0) }.joined(separator: " ") - print("Payload Ciphertext:", payloadCiphertextHexString) - compareHexString = """ - 17 52 26 8e 03 - """.replacingOccurrences(of: "\n", with: " ") - if payloadCiphertextHexString == compareHexString { - print("Payload Ciphertext equals comparison string.") - } else { - XCTFail("Payload Ciphertext does not equal comparison string.") - } + XCTAssertEqual(payload.iv.map { String(format: "%02x", $0) }.joined(separator: " "), "9e bd 09", "Payload IV should match comparison string") + XCTAssertEqual(payload.ciphertext.map { String(format: "%02x", $0) }.joined(separator: " "), "17 52 26 8e 03", "Payload ciphertext should match comparison string") + // Create the symmetric key _ = SymmetricKey(size: .bits256) @@ -221,40 +168,24 @@ final class NanoTDFTests: XCTestCase { let parser = BinaryParser(data: binaryData!) do { let header = try parser.parseHeader() - print("Parsed Header:", header) - // KAS - print("KAS:", header.payloadKeyAccess.kasLocator.body) - if header.payloadKeyAccess.kasLocator.body != "kas.example.com" { - XCTFail("KAS incorrect") - } - if header.policy.remote?.body != "kas.example.com/policy/abcdef" { - XCTFail("Policy Body incorrect") - } + XCTAssertEqual(header.payloadKeyAccess.kasLocator.body, "kas.example.com", "KAS body should match expected value") + XCTAssertEqual(header.policy.remote?.body, "kas.example.com/policy/abcdef", "Policy body should match expected value") + // Ephemeral Key let ephemeralKeyHexString = header.ephemeralPublicKey.map { String(format: "%02x", $0) }.joined(separator: " ") - print("Ephemeral Key:", ephemeralKeyHexString) let compareHexString = """ 03 e8 b3 3f 44 9a 73 92 77 13 d4 a4 a2 b4 e5 e9 45 2e 2f 05 34 33 9d 35 91 1b df a1 5e e1 8b 3a db """.replacingOccurrences(of: "\n", with: " ") - print("Compare Key:", compareHexString) - if ephemeralKeyHexString == compareHexString { - print("Ephemeral Key equals comparison string.") - } else { - XCTFail("Ephemeral Key does not equal comparison string.") - } + XCTAssertEqual(ephemeralKeyHexString, compareHexString, "Ephemeral Key should match comparison string") + // ECC and Binding Mode - if header.policyBindingConfig.toData() == Data([0x80]) { - print("EccMode equals comparison.") - } else { - XCTFail("EccMode does not equal comparison.") - } + XCTAssertEqual(header.policyBindingConfig.toData(), Data([0x80]), "EccMode should equal comparison") + // Symmetric and Payload Config - if header.payloadSignatureConfig.toData() == Data([0x05]) { // 0x35 should be the test but secp256k1 is not supported - print("SigMode equals comparison.") - } else { - XCTFail("SigMode does not equal comparison. \(header.payloadSignatureConfig.toData().hexString)") - } + // 0x35 should be the test but secp256k1 is not supported + XCTAssertEqual(header.payloadSignatureConfig.toData(), Data([0x05]), "SigMode should equal comparison") + // Signature XCTAssertFalse(header.payloadSignatureConfig.signed) } catch { @@ -306,9 +237,8 @@ final class NanoTDFTests: XCTestCase { let parser = BinaryParser(data: binaryData!) do { let header = try parser.parseHeader() - print("Parsed Header:", header) - print("Policy type:", header.policy.type) - print("Policy body:", header.policy.body as Any) + XCTAssertEqual(header.policy.type, .embeddedEncrypted) + XCTAssertNotNil(header.policy.body) } catch { XCTFail("Failed to parse data: \(error)") } @@ -337,46 +267,18 @@ final class NanoTDFTests: XCTestCase { // Generate an ECDSA private key let privateKey = P256.Signing.PrivateKey() // Add the signature to the NanoTDF - print("Adding signature") try await addSignatureToNanoTDF(nanoTDF: &nanoTDF, privateKey: privateKey, config: header.payloadSignatureConfig) - // Print the updated NanoTDF - print(nanoTDF) - var serializedSignature = nanoTDF.signature?.toData() - print("Added signature length:", serializedSignature?.count as Any) + XCTAssertNotNil(nanoTDF.signature, "NanoTDF should have a signature after signing") // round trip - serialize let serializedWithSignature = nanoTDF.toData() - var counter = 0 - let serializedNanoTDFHexString = serializedWithSignature.map { byte -> String in - counter += 1 - let newline = counter % 20 == 0 ? "\n" : " " - return String(format: "%02x", byte) + newline - }.joined() - print("Added:") - print(serializedNanoTDFHexString) // round trip - parse let sparser = BinaryParser(data: serializedWithSignature) let sheader = try sparser.parseHeader() let spayload = try sparser.parsePayload(config: sheader.payloadSignatureConfig) let ssignature = try sparser.parseSignature(config: sheader.payloadSignatureConfig) let snanoTDF = NanoTDF(header: sheader, payload: spayload, signature: ssignature) - // Print final the signature NanoTDF - print(snanoTDF) - serializedSignature = snanoTDF.signature?.toData() - print("Added signature length:", serializedSignature?.count as Any) let sserializedWithSignature = snanoTDF.toData() - counter = 0 - let sserializedNanoTDFHexString = sserializedWithSignature.map { byte -> String in - counter += 1 - let newline = counter % 20 == 0 ? "\n" : " " - return String(format: "%02x", byte) + newline - }.joined() - print("Parsed:") - print(sserializedNanoTDFHexString) - if serializedWithSignature == sserializedWithSignature { - print("NanoTDF equals comparison bytes.") - } else { - XCTFail("NanoTDF does not equal comparison bytes.") - } + XCTAssertEqual(serializedWithSignature, sserializedWithSignature, "Round-tripped NanoTDF should equal original") } catch { XCTFail("Failed to parse data: \(error)") } @@ -600,15 +502,17 @@ final class NanoTDFTests: XCTestCase { outputByteCount: 32, ) - // Create GMAC tag for verification - do it locally instead of using the actor method - let fullTag = try AES.GCM.seal(Data(), using: symmetricKey, authenticating: policyData).tag + // Create GMAC tag for verification - do it locally instead of using the actor method. + // createGMACBinding uses a deterministic zero nonce so the binding is reproducible, + // and binds over the serialized embedded policy body. + let serializedPolicyBody = EmbeddedPolicyBody(body: policyData, keyAccess: nil).toData() + let nonce = try AES.GCM.Nonce(data: Data(count: 12)) + let fullTag = try AES.GCM.seal(Data(), using: symmetricKey, nonce: nonce, authenticating: serializedPolicyBody).tag // Per spec, GMAC binding should be truncated to 8 bytes (64 bits) let expectedTag = Data(fullTag.prefix(8)) - // Just verify that the binding is not empty - we can't predict the exact value - // in a test but we can make sure it was generated with a valid size (8 bytes per spec) XCTAssertEqual(policyBinding!.count, 8, "Policy binding should be 8 bytes (64 bits) per spec") - XCTAssertEqual(policyBinding!.count, expectedTag.count, "Policy binding should have the same length as truncated GMAC tag") + XCTAssertEqual(policyBinding!, expectedTag, "Policy binding should match the calculated GMAC tag") // Test with invalid binding let invalidBinding = Data([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]) @@ -751,8 +655,7 @@ class PayloadTests: XCTestCase { let parser = BinaryParser(data: binaryData!) do { let header = try parser.parseHeader() - let payload = try parser.parsePayload(config: header.payloadSignatureConfig) - print(payload) + _ = try parser.parsePayload(config: header.payloadSignatureConfig) XCTFail("Negative should have failed") } catch { // pass