apple_kit

Apple-native services for Radroots iOS and macOS apps
git clone https://radroots.dev/git/apple_kit.git
Log | Files | Refs | README | LICENSE

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 }