How do you keep third-party code from being swapped under you: Package.resolved, revisions, binary target checksums and what a checksum does not prove?

14.8.3 · L3 · Interview

Predict first

Is the SwiftPM binary checksum just SHA-256 of the file?

import Foundation
import CryptoKit

func sh(_ args: [String], in dir: URL) -> String {
    let p = Process(); p.executableURL = URL(fileURLWithPath: "/usr/bin/env"); p.arguments = args; p.currentDirectoryURL = dir
    let out = Pipe(); p.standardOutput = out; p.standardError = Pipe()
    try! p.run()
    let data = out.fileHandleForReading.readDataToEndOfFile()
    p.waitUntilExit()
    return String(decoding: data, as: UTF8.self).trimmingCharacters(in: .whitespacesAndNewlines)
}
let dir = FileManager.default.temporaryDirectory.appendingPathComponent("sp\(getpid())")
try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
try Data("hello".utf8).write(to: dir.appendingPathComponent("f.txt"))
_ = sh(["zip", "-q", "f.zip", "f.txt"], in: dir)
let checksum = sh(["swift", "package", "compute-checksum", "f.zip"], in: dir)
let mine = SHA256.hash(data: try Data(contentsOf: dir.appendingPathComponent("f.zip"))).map { String(format: "%02x", $0) }.joined()
print("compute-checksum is SHA-256 of the file:", checksum == mine, "| length:", checksum.count)
var bytes = try Data(contentsOf: dir.appendingPathComponent("f.zip")); bytes[10] ^= 1
let other = SHA256.hash(data: bytes).map { String(format: "%02x", $0) }.joined()
print("one changed byte gives another checksum:", other != checksum)
try? FileManager.default.removeItem(at: dir)
  • compute-checksum is SHA-256 of the file: true | length: 64 / one changed byte gives another checksum: true
  • compute-checksum is SHA-256 of the file: false | length: 40
  • one changed byte gives another checksum: false

Mental model. A checksum is the sealed-bottle ring on a medicine: it tells you nobody opened it after you wrote the number down, not that the medicine was safe in the factory.

Your app’s security includes every dependency, so treat them as code you ship. With Swift Package Manager, Package.resolved records the exact version and Git revision that was resolved, which makes builds reproducible and lets a review notice a changed dependency; commit it for applications and have CI build with the resolved file (--disable-automatic-resolution) so a moved tag cannot silently change what you ship. A binary target (an XCFramework zip) has no source to review, so SwiftPM requires a checksum: the SHA-256 of the zip. The check confirms that swift package compute-checksum f.zip printed a 64-character value equal to the SHA-256 I computed with CryptoKit over the same file, and that changing one byte of the archive gives a different value. If the file at the URL changes, resolution fails instead of linking different code (binary frameworks: Q366).

Know what the checksum does and does not prove. It proves the bytes match what you recorded, so a swapped file is detected; it does not prove the original is safe, because if you copied the checksum from the same compromised page you only pinned the compromise. Hence the layers: review new dependencies and their transitive tree, pin to revisions or exact versions, prefer fewer and well-maintained libraries, keep an inventory (a software bill of materials), watch advisories, and remember the privacy side, since an SDK you add must ship its own privacy manifest and declare its required-reason APIs (Q345). Sandbox what you can: an SDK runs with your app’s entitlements and permissions, so isolate risky work in an extension or a separate process where that makes sense. Not run: signature verification of packages (registry signing), vulnerability scanning, and an SBOM tool.

The checksum is a 64-hex-character SHA-256 of the archive, so any change shows.

Common traps

  • Not committing Package.resolved for an app. Each machine may then resolve different versions.
  • Copying a binary checksum from the same place as the binary. A compromised host changes both.
  • Treating a dependency as outside your threat model. It runs with all of your app’s privileges.

They might follow up with

  • Why pin a revision instead of a tag? Tags can be moved; a commit hash cannot.
  • What changes the privacy picture when you add an SDK? Its data collection and required-reason API use become yours to declare.

Related questions

  • 14.7.1 How do you compute a public-key (SPKI) pin, how does the system evaluate a trust object with a custom anchor…
  • 14.3.1 What is a privacy manifest, what are required-reason APIs, and what must third-party SDKs declare?
  • 14.4.2 Why are secrets in the app binary not secret, and what do you do instead?
  • 16.2.2 What is an XCFramework, what do module maps and library evolution have to do with it, and when do you use a…

← Where else besides SQL can injection happen in an iOS app…   What do the file protection classes mean, how do you set them, and… →