RadrootsVerifiedArtifactAccessTests.swift (5967B)
1 import Darwin 2 import Foundation 3 import Testing 4 5 @testable import RadrootsKit 6 7 private let verifiedArtifactPublicKey = 8 "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" 9 10 @Test func verifiedArtifactAccessRechecksExactProtectedBytes() throws { 11 let fixture = try VerifiedArtifactFixture() 12 defer { fixture.remove() } 13 let data = Data("verified inbound media".utf8) 14 let digest = try fixture.install(data, mediaType: "image/png") 15 let descriptor = try RadrootsVerifiedArtifactDescriptor( 16 artifactID: digest, 17 byteSize: UInt64(data.count), 18 mediaType: "image/png", 19 width: 12, 20 height: 8 21 ) 22 23 let artifact = try fixture.access.open(descriptor) 24 25 #expect(artifact.data == data) 26 #expect(try fixture.access.revalidate(artifact)) 27 #expect(!artifact.debugDescription.contains(String(decoding: data, as: UTF8.self))) 28 } 29 30 @Test func verifiedArtifactAccessFailsClosedForLockMissingTamperAndSymlink() throws { 31 let fixture = try VerifiedArtifactFixture() 32 defer { fixture.remove() } 33 let data = Data("immutable".utf8) 34 let digest = try fixture.install(data, mediaType: "image/jpeg") 35 let descriptor = try RadrootsVerifiedArtifactDescriptor( 36 artifactID: digest, 37 byteSize: UInt64(data.count), 38 mediaType: "image/jpeg", 39 width: 4, 40 height: 4 41 ) 42 43 fixture.protectedData.state = .locked 44 #expect(throws: RadrootsVerifiedArtifactAccessError.protectedDataUnavailable) { 45 _ = try fixture.access.open(descriptor) 46 } 47 fixture.protectedData.state = .available 48 49 try Data("tampered".utf8).write(to: fixture.artifactURL(for: descriptor), options: .atomic) 50 #expect(throws: RadrootsVerifiedArtifactAccessError.artifactCorrupt) { 51 _ = try fixture.access.open(descriptor) 52 } 53 try FileManager.default.removeItem(at: fixture.artifactURL(for: descriptor)) 54 #expect(throws: RadrootsVerifiedArtifactAccessError.artifactUnavailable) { 55 _ = try fixture.access.open(descriptor) 56 } 57 58 let outside = fixture.root.appendingPathComponent("outside.jpg") 59 try data.write(to: outside) 60 try FileManager.default.createSymbolicLink( 61 at: fixture.artifactURL(for: descriptor), withDestinationURL: outside) 62 #expect(throws: RadrootsVerifiedArtifactAccessError.artifactCorrupt) { 63 _ = try fixture.access.open(descriptor) 64 } 65 } 66 67 @Test func verifiedArtifactDescriptorRejectsUngovernedMetadata() throws { 68 #expect(throws: RadrootsVerifiedArtifactAccessError.invalidDescriptor) { 69 _ = try RadrootsVerifiedArtifactDescriptor( 70 artifactID: String(repeating: "A", count: 64), byteSize: 1, mediaType: "image/png", width: 1, 71 height: 1 72 ) 73 } 74 #expect(throws: RadrootsVerifiedArtifactAccessError.invalidDescriptor) { 75 _ = try RadrootsVerifiedArtifactDescriptor( 76 artifactID: String(repeating: "a", count: 64), byteSize: 1, mediaType: "image/svg+xml", 77 width: 1, height: 1 78 ) 79 } 80 #expect(throws: RadrootsVerifiedArtifactAccessError.invalidDescriptor) { 81 _ = try RadrootsVerifiedArtifactDescriptor( 82 artifactID: String(repeating: "a", count: 64), byteSize: 1, mediaType: "image/png", 83 width: 16_384, height: 16_384 84 ) 85 } 86 } 87 88 private final class VerifiedArtifactProtectedData: @unchecked Sendable { 89 private let lock = NSLock() 90 private var value = RadrootsProtectedDataState.available 91 92 var state: RadrootsProtectedDataState { 93 get { 94 lock.lock() 95 defer { lock.unlock() } 96 return value 97 } 98 set { 99 lock.lock() 100 value = newValue 101 lock.unlock() 102 } 103 } 104 } 105 106 private struct VerifiedArtifactFixture { 107 let root: URL 108 let store: RadrootsAppleMobileStoreConfiguration 109 let protectedData: VerifiedArtifactProtectedData 110 let access: RadrootsAppleVerifiedArtifactAccess 111 112 init() throws { 113 let unresolvedRoot = FileManager.default.temporaryDirectory 114 .appendingPathComponent("radroots-verified-artifact-\(UUID().uuidString)", isDirectory: true) 115 try FileManager.default.createDirectory(at: unresolvedRoot, withIntermediateDirectories: false) 116 guard let resolvedPointer = unresolvedRoot.path.withCString({ Darwin.realpath($0, nil) }) else { 117 throw RadrootsVerifiedArtifactAccessError.fileSystemFailure 118 } 119 defer { Darwin.free(resolvedPointer) } 120 root = URL(fileURLWithPath: String(cString: resolvedPointer), isDirectory: true) 121 let roots = try RadrootsAppleFileRoots( 122 appIdentifier: "org.radroots.tests", 123 dataRoot: root.appendingPathComponent("data", isDirectory: true), 124 cacheRoot: root.appendingPathComponent("cache", isDirectory: true), 125 temporaryRoot: root.appendingPathComponent("tmp", isDirectory: true) 126 ) 127 store = try RadrootsAppleMobileStore.prepare( 128 roots: roots, 129 publicKeyHex: verifiedArtifactPublicKey, 130 protectedDataAvailability: .available 131 ) 132 let protectedData = VerifiedArtifactProtectedData() 133 self.protectedData = protectedData 134 access = RadrootsAppleVerifiedArtifactAccess( 135 mobileStore: store, 136 protectedData: RadrootsProtectedDataProvider { protectedData.state } 137 ) 138 } 139 140 func install(_ data: Data, mediaType: String) throws -> String { 141 let temporary = root.appendingPathComponent("digest-source") 142 try data.write(to: temporary) 143 let digest = try RadrootsAppleFileDigest.sha256(at: temporary) 144 let fileExtension = mediaType == "image/jpeg" ? "jpg" : "png" 145 let directory = store.ownerDirectory.appendingPathComponent( 146 "inbound_media.v1", isDirectory: true) 147 try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: false) 148 try data.write(to: directory.appendingPathComponent("\(digest).\(fileExtension)")) 149 return digest 150 } 151 152 func artifactURL(for descriptor: RadrootsVerifiedArtifactDescriptor) -> URL { 153 store.ownerDirectory 154 .appendingPathComponent("inbound_media.v1", isDirectory: true) 155 .appendingPathComponent("\(descriptor.artifactID).\(descriptor.fileExtension)") 156 } 157 158 func remove() { 159 try? FileManager.default.removeItem(at: root) 160 } 161 }