Files
Sparkle/Tests/SUUpdateValidatorTest.swift
T
Zorg 1e419c8c16 Improve signing error message to developers (#2471)
If we detect the wrong archive is being served (i.e, expected content length differs from archive length) we log this out to developers. If the app version in the archive (if available) also differs, we report this discrepancy as well. If the update archive looks the same but signing validation fails, we tell the developer the update may have not been signed correctly.

I'm not changing the generic error that is reported to users about the update being improperly signed (that would involve propagating this information there and updating bunch of localizations). The extra info is more for the developer than it is for the user.

Fixes #2468
2023-11-18 16:04:44 -08:00

248 lines
12 KiB
Swift

//
// SUUpdateValidatorTest.swift
// Sparkle
//
// Created by Jordan Rose on 2020-06-13.
// Copyright © 2020 Sparkle Project. All rights reserved.
//
import Foundation
import XCTest
class SUUpdateValidatorTest: XCTestCase {
enum BundleConfig: String, CaseIterable, Equatable {
case none = "None"
case dsaOnly = "DSAOnly"
case edOnly = "EDOnly"
case both = "Both"
case codeSignedOnly = "CodeSignedOnly"
case codeSignedBoth = "CodeSignedBoth"
case codeSignedOnlyNew = "CodeSignedOnlyNew"
case codeSignedBothNew = "CodeSignedBothNew"
case codeSignedOldED = "CodeSignedOldED"
case codeSignedInvalidOnly = "CodeSignedInvalidOnly"
case codeSignedInvalid = "CodeSignedInvalid"
var hasAnyKeys: Bool {
switch self {
case .none, .codeSignedOnly, .codeSignedOnlyNew, .codeSignedInvalidOnly:
return false
case .edOnly, .both, .codeSignedBoth, .codeSignedBothNew, .codeSignedOldED, .codeSignedInvalid:
return true
case .dsaOnly:
return true
}
}
}
struct SignatureConfig: CaseIterable, Equatable, CustomDebugStringConvertible {
enum State: CaseIterable, Equatable {
case none, invalid, invalidFormat, valid
}
var ed: State
#if SPARKLE_BUILD_LEGACY_DSA_SUPPORT
var dsa: State
#endif
static let allCases: [SignatureConfig] = State.allCases.flatMap { dsaState in
State.allCases.map { edState in
#if SPARKLE_BUILD_LEGACY_DSA_SUPPORT
SignatureConfig(ed: edState, dsa: dsaState)
#else
SignatureConfig(ed: edState)
#endif
}
}
var debugDescription: String {
#if SPARKLE_BUILD_LEGACY_DSA_SUPPORT
return "(ed: \(self.ed), dsa: \(self.dsa))"
#else
return "(ed: \(self.ed))"
#endif
}
}
func bundle(_ config: BundleConfig) -> Bundle {
let testBundle = Bundle(for: SUUpdateValidatorTest.self)
let configBundleURL = testBundle.url(forResource: config.rawValue, withExtension: "bundle", subdirectory: "SUUpdateValidatorTest")!
return Bundle(url: configBundleURL)!
}
func signatures(_ config: SignatureConfig) -> SUSignatures {
#if SPARKLE_BUILD_LEGACY_DSA_SUPPORT
let dsaSig: String?
switch config.dsa {
case .none: dsaSig = nil
case .invalid: dsaSig = "ABwCFCIHCIYYkfZavNzTitTW5tlRp/k5AhQ40poFytqcVhIYdCxQznaXeJPJDQ=="
// Use some invalid base64 strings
case .invalidFormat: dsaSig = "%%wCFCIHCIYYkfZavNzTitTW5tlRp/k5AhQ40poFytqcVhIYdCxQznaXeJPJDQ=="
case .valid: dsaSig = "MCwCFCIHCIYYkfZavNzTitTW5tlRp/k5AhQ40poFytqcVhIYdCxQznaXeJPJDQ=="
}
#endif
let edSig: String?
switch config.ed {
case .none: edSig = nil
case .invalid: edSig = "wTcpXCgWoa4NrJpsfzS61FXJIbv963//12U2ef9xstzVOLPHYK2N4/ojgpDV5N1/NGG1uWMBgK+kEWp0Z5zMDQ=="
// Use some invalid base64 strings
case .invalidFormat: edSig = "%%cpXCgWoa4NrJpsfzS61FXJIbv963//12U2ef9xstzVOLPHYK2N4/ojgpDV5N1/NGG1uWMBgK+kEWp0Z5zMDQ=="
case .valid: edSig = "EIawm2YkDZ2gBfkEMF2+1VuuTeXnCGZOdnMdVgPPvDZioq7bvDayXqKkIIzSjKMmeFdcFJOHdnba5ZV60+gPBw=="
}
#if SPARKLE_BUILD_LEGACY_DSA_SUPPORT
return SUSignatures(ed: edSig, dsa: dsaSig)
#else
return SUSignatures(ed: edSig)
#endif
}
var signedTestFilePath: String {
let testBundle = Bundle(for: SUUpdateValidatorTest.self)
return testBundle.path(forResource: "signed-test-file", ofType: "txt")!
}
func testPrevalidation(bundle bundleConfig: BundleConfig, signatures signatureConfig: SignatureConfig, expectedResult: Bool, line: UInt = #line) {
let host = SUHost(bundle: self.bundle(bundleConfig))
let signatures = self.signatures(signatureConfig)
let validator = SUUpdateValidator(downloadPath: self.signedTestFilePath, signatures: signatures, host: host, verifierInformation: nil)
let result = (try? validator.validateDownloadPath()) != nil
XCTAssertEqual(result, expectedResult, "bundle: \(bundleConfig), signatures: \(signatureConfig)", line: line)
}
func testPrevalidation() {
for signatureConfig in SignatureConfig.allCases {
testPrevalidation(bundle: .none, signatures: signatureConfig, expectedResult: false)
#if SPARKLE_BUILD_LEGACY_DSA_SUPPORT
testPrevalidation(bundle: .dsaOnly, signatures: signatureConfig, expectedResult: signatureConfig.dsa == .valid)
#endif
#if SPARKLE_BUILD_LEGACY_DSA_SUPPORT
testPrevalidation(bundle: .edOnly, signatures: signatureConfig, expectedResult: signatureConfig.ed == .valid && signatureConfig.dsa != .invalidFormat)
#else
testPrevalidation(bundle: .edOnly, signatures: signatureConfig, expectedResult: signatureConfig.ed == .valid)
#endif
#if SPARKLE_BUILD_LEGACY_DSA_SUPPORT
testPrevalidation(bundle: .both, signatures: signatureConfig, expectedResult: signatureConfig.ed == .valid && signatureConfig.dsa != .invalidFormat)
#else
testPrevalidation(bundle: .both, signatures: signatureConfig, expectedResult: signatureConfig.ed == .valid)
#endif
}
}
func testPostValidation(oldBundle oldBundleConfig: BundleConfig, newBundle newBundleConfig: BundleConfig, signatures signatureConfig: SignatureConfig, expectedResult: Bool, line: UInt = #line) {
let oldBundle = self.bundle(oldBundleConfig)
let host = SUHost(bundle: oldBundle)
let signatures = self.signatures(signatureConfig)
let validator = SUUpdateValidator(downloadPath: self.signedTestFilePath, signatures: signatures, host: host, verifierInformation: nil)
let updateDirectory = temporaryDirectory("SUUpdateValidatorTest")!
defer { try! FileManager.default.removeItem(atPath: updateDirectory) }
let newBundle = self.bundle(newBundleConfig)
try! FileManager.default.copyItem(at: newBundle.bundleURL, to: URL(fileURLWithPath: updateDirectory).appendingPathComponent(oldBundle.bundleURL.lastPathComponent))
let result = (try? validator.validate(withUpdateDirectory: updateDirectory)) != nil
XCTAssertEqual(result, expectedResult, "oldBundle: \(oldBundleConfig), newBundle: \(newBundleConfig), signatures: \(signatureConfig)", line: line)
}
func testPostValidation(bundle bundleConfig: BundleConfig, signatures signatureConfig: SignatureConfig, expectedResult: Bool, line: UInt = #line) {
testPostValidation(oldBundle: bundleConfig, newBundle: bundleConfig, signatures: signatureConfig, expectedResult: expectedResult, line: line)
}
func testPostValidationWithoutCodeSigning() {
for signatureConfig in SignatureConfig.allCases {
testPostValidation(bundle: .none, signatures: signatureConfig, expectedResult: false)
#if SPARKLE_BUILD_LEGACY_DSA_SUPPORT
testPostValidation(bundle: .dsaOnly, signatures: signatureConfig, expectedResult: signatureConfig.dsa == .valid)
#endif
#if SPARKLE_BUILD_LEGACY_DSA_SUPPORT
testPostValidation(bundle: .edOnly, signatures: signatureConfig, expectedResult: signatureConfig.ed == .valid && signatureConfig.dsa != .invalidFormat)
#else
testPostValidation(bundle: .edOnly, signatures: signatureConfig, expectedResult: signatureConfig.ed == .valid)
#endif
#if SPARKLE_BUILD_LEGACY_DSA_SUPPORT
testPostValidation(bundle: .both, signatures: signatureConfig, expectedResult: signatureConfig.ed == .valid && signatureConfig.dsa != .invalidFormat)
#else
testPostValidation(bundle: .both, signatures: signatureConfig, expectedResult: signatureConfig.ed == .valid)
#endif
}
}
func testPostValidationWithCodeSigning() {
for signatureConfig in SignatureConfig.allCases {
testPostValidation(bundle: .codeSignedOnly, signatures: signatureConfig, expectedResult: true)
#if SPARKLE_BUILD_LEGACY_DSA_SUPPORT
testPostValidation(bundle: .codeSignedBoth, signatures: signatureConfig, expectedResult: signatureConfig.ed == .valid && signatureConfig.dsa != .invalidFormat)
#else
testPostValidation(bundle: .codeSignedBoth, signatures: signatureConfig, expectedResult: signatureConfig.ed == .valid)
#endif
testPostValidation(bundle: .codeSignedInvalidOnly, signatures: signatureConfig, expectedResult: false)
testPostValidation(bundle: .codeSignedInvalid, signatures: signatureConfig, expectedResult: false)
}
}
func testPostValidationWithKeyRemoval() {
for bundleConfig in BundleConfig.allCases {
#if SPARKLE_BUILD_LEGACY_DSA_SUPPORT
testPostValidation(oldBundle: .dsaOnly, newBundle: bundleConfig, signatures: SignatureConfig(ed: .valid, dsa: .valid), expectedResult: bundleConfig.hasAnyKeys && bundleConfig != .codeSignedInvalid)
#else
testPostValidation(oldBundle: .dsaOnly, newBundle: bundleConfig, signatures: SignatureConfig(ed: .valid), expectedResult: false)
#endif
do {
#if SPARKLE_BUILD_LEGACY_DSA_SUPPORT
let signatureConfig = SignatureConfig(ed: .valid, dsa: .valid)
#else
let signatureConfig = SignatureConfig(ed: .valid)
#endif
testPostValidation(oldBundle: .edOnly, newBundle: bundleConfig, signatures: signatureConfig, expectedResult: bundleConfig.hasAnyKeys && bundleConfig != .codeSignedInvalid)
}
#if SPARKLE_BUILD_LEGACY_DSA_SUPPORT
testPostValidation(oldBundle: .both, newBundle: bundleConfig, signatures: SignatureConfig(ed: .valid, dsa: .valid), expectedResult: bundleConfig.hasAnyKeys && bundleConfig != .codeSignedInvalid)
#else
testPostValidation(oldBundle: .both, newBundle: bundleConfig, signatures: SignatureConfig(ed: .valid), expectedResult: bundleConfig.hasAnyKeys && bundleConfig != .codeSignedInvalid)
#endif
do {
#if SPARKLE_BUILD_LEGACY_DSA_SUPPORT
let signatureConfig = SignatureConfig(ed: .valid, dsa: .valid)
#else
let signatureConfig = SignatureConfig(ed: .valid)
#endif
testPostValidation(oldBundle: .codeSignedBoth, newBundle: bundleConfig, signatures: signatureConfig, expectedResult: bundleConfig.hasAnyKeys && bundleConfig != .codeSignedInvalid)
}
}
}
func testPostValidationWithKeyRotation() {
for signatureConfig in SignatureConfig.allCases {
#if SPARKLE_BUILD_LEGACY_DSA_SUPPORT
let signatureIsValid = (signatureConfig.ed == .valid && signatureConfig.dsa != .invalidFormat)
#else
let signatureIsValid = (signatureConfig.ed == .valid)
#endif
// It's okay to add DSA keys or add code signing.
testPostValidation(oldBundle: .codeSignedOnly, newBundle: .codeSignedBoth, signatures: signatureConfig, expectedResult: signatureIsValid)
testPostValidation(oldBundle: .both, newBundle: .codeSignedBoth, signatures: signatureConfig, expectedResult: signatureIsValid)
// If you want to change your code signing, you have to be using both forms of auth.
testPostValidation(oldBundle: .codeSignedOnly, newBundle: .codeSignedOnlyNew, signatures: signatureConfig, expectedResult: false)
testPostValidation(oldBundle: .codeSignedBoth, newBundle: .codeSignedOnlyNew, signatures: signatureConfig, expectedResult: false)
testPostValidation(oldBundle: .codeSignedOnly, newBundle: .codeSignedBothNew, signatures: signatureConfig, expectedResult: false)
testPostValidation(oldBundle: .codeSignedBoth, newBundle: .codeSignedBothNew, signatures: signatureConfig, expectedResult: signatureIsValid)
// If you want to change your keys, you have to be using both forms of auth.
testPostValidation(oldBundle: .codeSignedOldED, newBundle: .codeSignedOnly, signatures: signatureConfig, expectedResult: false)
testPostValidation(oldBundle: .codeSignedOldED, newBundle: .codeSignedBoth, signatures: signatureConfig, expectedResult: signatureIsValid)
// You can't change two things at once.
testPostValidation(oldBundle: .codeSignedOldED, newBundle: .codeSignedBothNew, signatures: signatureConfig, expectedResult: false)
// It's permitted to remove code signing too.
testPostValidation(oldBundle: .codeSignedBoth, newBundle: .both, signatures: signatureConfig, expectedResult: signatureIsValid)
}
}
}