// Checks a Parallex release file against Parallex's release key, the same // key compiled into the app (UpdateSignature.publicKey in // Sources/ParallexCore/Updates.swift). Published at // parallex.mandip.dev/verify-release.swift. // // swift verify-release.swift Parallex-3.7.0.zip // swift verify-release.swift Parallex-3.7.0.cdx.json // // Download the file with its companions into one folder first: // the app archive: Parallex-.zip, .zip.sig, .zip.manifest, .zip.manifest.sig // the SBOM: Parallex-.cdx.json, .cdx.json.manifest, .cdx.json.manifest.sig // It checks each signature with the key, and that the file's size and // SHA-256 are the ones its signed manifest names. Nothing is sent anywhere. import CryptoKit import Foundation let releaseKey = "Rr6I27BgnXlmopmP1pjqCrIYuKgHJMC5+1gowUaVVEc=" func fail(_ message: String) -> Never { FileHandle.standardError.write(Data("✗ \(message)\n".utf8)) exit(1) } func read(_ path: String) -> Data { guard let data = FileManager.default.contents(atPath: path) else { fail("\(path) isn't there.") } return data } func signed(_ data: Data, by signaturePath: String) -> Bool { let text = String(decoding: read(signaturePath), as: UTF8.self).trimmingCharacters(in: .whitespacesAndNewlines) guard let signature = Data(base64Encoded: text), let key = try? Curve25519.Signing.PublicKey(rawRepresentation: Data(base64Encoded: releaseKey)!) else { return false } return key.isValidSignature(signature, for: data) } let arguments = CommandLine.arguments.dropFirst() guard let path = arguments.first, arguments.count == 1 else { fail("Usage: swift verify-release.swift .zip or .cdx.json>") } let file = read(path) let name = (path as NSString).lastPathComponent let label = path.hasSuffix(".cdx.json") ? "parallex-sbom-v1" : "parallex-release-v1" if path.hasSuffix(".zip") { guard signed(file, by: path + ".sig") else { fail("\(name).sig isn't the release key's signature of \(name).") } print("✓ \(name) is signed with Parallex's release key.") } let manifestData = read(path + ".manifest") guard signed(manifestData, by: path + ".manifest.sig") else { fail("\(name).manifest isn't signed with Parallex's release key.") } var lines = String(decoding: manifestData, as: UTF8.self).components(separatedBy: "\n") if lines.last == "" { lines.removeLast() } var fields: [String: String] = [:] for line in lines.dropFirst() { let parts = line.split(separator: "=", maxSplits: 1).map(String.init) if parts.count == 2 { fields[parts[0]] = parts[1] } } guard lines.first == label, let version = fields["version"], let sha256 = fields["sha256"], let size = fields["size"] else { fail("\(name).manifest isn't a \(label) manifest.") } let digest = SHA256.hash(data: file).map { String(format: "%02x", $0) }.joined() guard digest == sha256.lowercased(), String(file.count) == size else { fail("\(name) isn't the file its signed manifest names (version \(version)).") } print("✓ \(name) is the file Parallex's signed manifest names: version \(version), \(size) bytes, SHA-256 \(sha256).")