diff --git a/Autoupdate/AppInstaller.m b/Autoupdate/AppInstaller.m index 49a76a9fd..d135fe209 100644 --- a/Autoupdate/AppInstaller.m +++ b/Autoupdate/AppInstaller.m @@ -176,10 +176,10 @@ - (void)extractAndInstallUpdate SPU_OBJC_DIRECT id unarchiver = [SUUnarchiver unarchiverForPath:archivePath extractionDirectory:_extractionDirectory updatingHostBundlePath:_host.bundlePath decryptionPassword:_decryptionPassword expectingInstallationType:_installationType]; - NSError *unarchiverError = nil; + NSError *prevalidationError = nil; BOOL success = NO; if (!unarchiver) { - unarchiverError = [NSError errorWithDomain:SUSparkleErrorDomain code:SUUnarchivingError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"No valid unarchiver was found for %@", archivePath] }]; + prevalidationError = [NSError errorWithDomain:SUSparkleErrorDomain code:SUUnarchivingError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"No valid unarchiver was found for %@", archivePath] }]; success = NO; } else { @@ -192,20 +192,38 @@ - (void)extractAndInstallUpdate SPU_OBJC_DIRECT } _updateValidator = [[SUUpdateValidator alloc] initWithDownloadPath:archivePath signatures:_signatures host:_host verifierInformation:_verifierInformation]; - - // Delta, package updates, and .aar/.yaa archives will require validation before extraction - // Normal application updates are a bit more lenient allowing developers to change one of apple dev ID or EdDSA keys - BOOL needsPrevalidation = [[unarchiver class] mustValidateBeforeExtractionWithArchivePath:archivePath] || ![_installationType isEqualToString:SPUInstallationTypeApplication]; - - if (needsPrevalidation) { - success = [_updateValidator validateDownloadPathWithError:&unarchiverError]; + + // More uncommon archives types (.aar, .yaa) need SUVerifyUpdateBeforeExtraction + BOOL verifyBeforeExtraction = [_host boolForInfoDictionaryKey:SUVerifyUpdateBeforeExtractionKey]; + if (!verifyBeforeExtraction && unarchiver.needsVerifyBeforeExtractionKey) { + prevalidationError = [NSError errorWithDomain:SUSparkleErrorDomain code:SUValidationError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Extracting %@ archives require setting %@ to YES in the old app. Please visit https://sparkle-project.org/documentation/customization/ for more information.", archivePath.pathExtension, SUVerifyUpdateBeforeExtractionKey] }]; + + success = NO; } else { - success = YES; + // Delta, package updates, and apps with SUVerifyUpdateBeforeExtraction will require validation before extraction + // Otherwise normal application updates are a bit more lenient allowing developers to change one of apple dev ID or EdDSA keys after extraction + BOOL archiveTypeMustValidateBeforeExtraction = [[unarchiver class] mustValidateBeforeExtraction]; + BOOL needsPrevalidation = verifyBeforeExtraction || archiveTypeMustValidateBeforeExtraction || ![_installationType isEqualToString:SPUInstallationTypeApplication]; + + if (needsPrevalidation) { + // EdDSA signing is required, so host must have public keys + if (![_updateValidator validateHostHasPublicKeys:&prevalidationError]) { + success = NO; + } else { + // Falling back on code signing for prevalidation requires SUVerifyUpdateBeforeExtraction + // and that update is a regular app update, and not a delta update + BOOL fallbackOnCodeSigning = (verifyBeforeExtraction && !archiveTypeMustValidateBeforeExtraction && [_installationType isEqualToString:SPUInstallationTypeApplication]); + + success = [_updateValidator validateDownloadPathWithFallbackOnCodeSigning:fallbackOnCodeSigning error:&prevalidationError]; + } + } else { + success = YES; + } } } if (!success) { - [self unarchiverDidFailWithError:unarchiverError]; + [self unarchiverDidFailWithError:prevalidationError]; } else { [unarchiver unarchiveWithCompletionBlock:^(NSError * _Nullable error) { diff --git a/Autoupdate/SUBinaryDeltaUnarchiver.m b/Autoupdate/SUBinaryDeltaUnarchiver.m index cb3336d37..ca6b701a1 100644 --- a/Autoupdate/SUBinaryDeltaUnarchiver.m +++ b/Autoupdate/SUBinaryDeltaUnarchiver.m @@ -28,7 +28,7 @@ + (BOOL)canUnarchivePath:(NSString *)path return [[path pathExtension] isEqualToString:@"delta"]; } -+ (BOOL)mustValidateBeforeExtractionWithArchivePath:(NSString *)archivePath ++ (BOOL)mustValidateBeforeExtraction { return YES; } @@ -83,6 +83,11 @@ - (instancetype)initWithArchivePath:(NSString *)archivePath extractionDirectory: return self; } +- (BOOL)needsVerifyBeforeExtractionKey +{ + return NO; +} + - (void)unarchiveWithCompletionBlock:(void (^)(NSError * _Nullable))completionBlock progressBlock:(void (^ _Nullable)(double))progressBlock waitForCleanup:(BOOL)__unused waitForCleanup { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ diff --git a/Autoupdate/SUCodeSigningVerifier.h b/Autoupdate/SUCodeSigningVerifier.h index 285fe6014..7605c202f 100644 --- a/Autoupdate/SUCodeSigningVerifier.h +++ b/Autoupdate/SUCodeSigningVerifier.h @@ -29,6 +29,8 @@ SUCodeSigningVerifierDefinitionAttribute // Same as above except does not check for nested code. This method should be used by the framework. + (BOOL)codeSignatureIsValidAtBundleURL:(NSURL *)bundleURL error:(NSError *__autoreleasing *)error; ++ (BOOL)codeSignatureIsValidAtDownloadURL:(NSURL *)downloadURL andMatchesDeveloperIDTeamFromOldBundleURL:(NSURL *)oldBundleURL error:(NSError * __autoreleasing *)error; + + (BOOL)bundleAtURLIsCodeSigned:(NSURL *)bundleURL; + (NSString * _Nullable)teamIdentifierAtURL:(NSURL *)url; diff --git a/Autoupdate/SUCodeSigningVerifier.m b/Autoupdate/SUCodeSigningVerifier.m index 62ad53cec..45b2b58c0 100644 --- a/Autoupdate/SUCodeSigningVerifier.m +++ b/Autoupdate/SUCodeSigningVerifier.m @@ -26,12 +26,17 @@ + (BOOL)codeSignatureIsValidAtBundleURL:(NSURL *)newBundleURL andMatchesSignatur CFErrorRef cfError = NULL; result = SecStaticCodeCreateWithPath((__bridge CFURLRef)oldBundleURL, kSecCSDefaultFlags, &oldCode); - if (result == errSecCSUnsigned) { + if (result != noErr) { if (error != NULL) { - *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUInsufficientSigningError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Bundle is not code signed: %@", newBundleURL] }]; + NSString *errorMessage = + (result == errSecCSUnsigned) ? + [NSString stringWithFormat:@"Bundle is not code signed: %@", oldBundleURL.path] : + [NSString stringWithFormat:@"Failed to get static code (%d): %@", result, oldBundleURL.path]; + + *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUInsufficientSigningError userInfo:@{ NSLocalizedDescriptionKey: errorMessage }]; } - return NO; + goto finally; } result = SecCodeCopyDesignatedRequirement(oldCode, kSecCSDefaultFlags, &requirement); @@ -258,15 +263,8 @@ + (BOOL)bundleAtURLIsCodeSigned:(NSURL *)bundleURL return (result == 0); } -+ (NSString * _Nullable)teamIdentifierAtURL:(NSURL *)url +static NSString * _Nullable SUTeamIdentifierFromStaticCode(SecStaticCodeRef staticCode) { - SecStaticCodeRef staticCode = NULL; - OSStatus staticCodeResult = SecStaticCodeCreateWithPath((__bridge CFURLRef)url, kSecCSDefaultFlags, &staticCode); - if (staticCodeResult != noErr) { - SULog(SULogLevelError, @"Failed to get static code for retrieving team identifier: %d", staticCodeResult); - return nil; - } - CFDictionaryRef cfSigningInformation = NULL; OSStatus copySigningInfoCode = SecCodeCopySigningInformation(staticCode, kSecCSSigningInformation, &cfSigningInformation); @@ -282,4 +280,145 @@ + (NSString * _Nullable)teamIdentifierAtURL:(NSURL *)url return signingInformation[(NSString *)kSecCodeInfoTeamIdentifier]; } ++ (NSString * _Nullable)teamIdentifierAtURL:(NSURL *)url +{ + SecStaticCodeRef staticCode = NULL; + OSStatus staticCodeResult = SecStaticCodeCreateWithPath((__bridge CFURLRef)url, kSecCSDefaultFlags, &staticCode); + if (staticCodeResult != noErr) { + SULog(SULogLevelError, @"Failed to get static code for retrieving team identifier: %d", staticCodeResult); + return nil; + } + + NSString *teamIdentifier = SUTeamIdentifierFromStaticCode(staticCode); + + if (staticCode != NULL) { + CFRelease(staticCode); + } + + return teamIdentifier; +} + ++ (BOOL)codeSignatureIsValidAtDownloadURL:(NSURL *)downloadURL andMatchesDeveloperIDTeamFromOldBundleURL:(NSURL *)oldBundleURL error:(NSError * __autoreleasing *)error +{ + NSString *teamIdentifier = nil; + NSString *requirementString = nil; + SecRequirementRef requirement = NULL; + SecStaticCodeRef oldStaticCode = NULL; + SecStaticCodeRef downloadStaticCode = NULL; + OSStatus result; + + NSError *resultError = nil; + + NSString *commonErrorMessage = @"The download archive cannot be validated with Apple Developer ID code signing as fallback (after (Ed)DSA verification has failed)"; + + result = SecStaticCodeCreateWithPath((__bridge CFURLRef)oldBundleURL, kSecCSDefaultFlags, &oldStaticCode); + if (result != errSecSuccess) { + NSString *errorMessage = + (result == errSecCSUnsigned) ? + [NSString stringWithFormat:@"%@. The original app is not code signed: %@", commonErrorMessage, oldBundleURL.path] : + [NSString stringWithFormat:@"%@. The static code could not be retrieved from the original app (%d): %@", commonErrorMessage, result, oldBundleURL.path]; + + resultError = [NSError errorWithDomain:SUSparkleErrorDomain code:SUInsufficientSigningError userInfo:@{ NSLocalizedDescriptionKey: errorMessage }]; + + goto finally; + } + + teamIdentifier = SUTeamIdentifierFromStaticCode(oldStaticCode); + if (teamIdentifier == nil) { + resultError = [NSError errorWithDomain:SUSparkleErrorDomain code:SUInsufficientSigningError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"%@. The team identifier could not be retrieved from the original app: %@", commonErrorMessage, oldBundleURL.path] }]; + + goto finally; + } + + // Create a designated requirement with developer ID signing with this team ID + // Validate it against code signing check of this archive + // CertificateIssuedByApple = anchor apple generic + // IssuerIsDeveloperID = certificate 1[field.1.2.840.113635.100.6.2.6] + // LeafIsDeveloperIDApp = certificate leaf[field.1.2.840.113635.100.6.1.13] + // DeveloperIDTeamID = certificate leaf[subject.OU] + // https://developer.apple.com/documentation/technotes/tn3127-inside-code-signing-requirements#Xcode-designated-requirement-for-Developer-ID-code + requirementString = [NSString stringWithFormat:@"anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] and certificate leaf[field.1.2.840.113635.100.6.1.13] and certificate leaf[subject.OU] = %@", teamIdentifier]; + + result = SecRequirementCreateWithString((__bridge CFStringRef)requirementString, kSecCSDefaultFlags, &requirement); + if (result != errSecSuccess) { + resultError = [NSError errorWithDomain:SUSparkleErrorDomain code:SUInsufficientSigningError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"%@. The designated requirement string with a Developer ID requirement with team identifier '%@' could not be created with error %d", commonErrorMessage, teamIdentifier, result] }]; + + goto finally; + } + + result = SecStaticCodeCreateWithPath((__bridge CFURLRef)downloadURL, kSecCSDefaultFlags, &downloadStaticCode); + if (result != errSecSuccess) { + NSString *message = [NSString stringWithFormat:@"%@. The static code could not be retrieved from the download archive with error %d. The download archive may not be Apple code signed.", commonErrorMessage, result]; + + SULog(SULogLevelError, @"%@", message); + + resultError = [NSError errorWithDomain:SUSparkleErrorDomain code:SUInsufficientSigningError userInfo:@{ NSLocalizedDescriptionKey: message }]; + + goto finally; + } + + SecCSFlags flags = (SecCSFlags)kSecCSDefaultFlags; + CFErrorRef cfError = NULL; + result = SecStaticCodeCheckValidityWithErrors(downloadStaticCode, flags, requirement, &cfError); + if (result != errSecSuccess) { + NSError *underlyingError; + if (cfError != NULL) { + NSError *tmpError = CFBridgingRelease(cfError); + underlyingError = tmpError; + } else { + underlyingError = nil; + } + + NSMutableDictionary *userInfo = [NSMutableDictionary dictionary]; + if (underlyingError != nil) { + userInfo[NSUnderlyingErrorKey] = underlyingError; + } + + if (result == errSecCSUnsigned) { + NSString *message = [NSString stringWithFormat:@"%@. The download archive is not Apple code signed.", commonErrorMessage]; + + SULog(SULogLevelError, @"%@", message); + + userInfo[NSLocalizedDescriptionKey] = message; + + resultError = [NSError errorWithDomain:SUSparkleErrorDomain code:SUInsufficientSigningError userInfo:[userInfo copy]]; + } else if (result == errSecCSReqFailed) { + NSString *initialMessage = [NSString stringWithFormat:@"%@. The Apple code signature of new downloaded archive is either not Developer ID code signed, or doesn't have a Team ID that matches the old app version (%@). Please ensure that the archive and app are signed using the same Developer ID certificate.", commonErrorMessage, teamIdentifier]; + + NSDictionary *oldInfo = [self logSigningInfoForCode:oldStaticCode label:@"old info"]; + NSDictionary *newInfo = [self logSigningInfoForCode:downloadStaticCode label:@"new info"]; + + userInfo[NSLocalizedDescriptionKey] = [NSString stringWithFormat:@"%@ old info: %@. new info: %@", initialMessage, oldInfo, newInfo]; + + resultError = [NSError errorWithDomain:SUSparkleErrorDomain code:SUInsufficientSigningError userInfo:[userInfo copy]]; + } else { + userInfo[NSLocalizedDescriptionKey] = [NSString stringWithFormat:@"%@. The downloaded archive code signing signature failed to validate with an unknown error (%d).", commonErrorMessage, result]; + + resultError = [NSError errorWithDomain:SUSparkleErrorDomain code:SUInsufficientSigningError userInfo:[userInfo copy]]; + } + + goto finally; + } + +finally: + + if (oldStaticCode != NULL) { + CFRelease(oldStaticCode); + } + + if (requirement != NULL) { + CFRelease(requirement); + } + + if (downloadStaticCode != NULL) { + CFRelease(downloadStaticCode); + } + + if (resultError != nil && error != NULL) { + *error = resultError; + } + + return (resultError == nil); +} + @end diff --git a/Autoupdate/SUDiskImageUnarchiver.m b/Autoupdate/SUDiskImageUnarchiver.m index f05e72b74..57b8a5b99 100644 --- a/Autoupdate/SUDiskImageUnarchiver.m +++ b/Autoupdate/SUDiskImageUnarchiver.m @@ -35,7 +35,7 @@ + (BOOL)canUnarchivePath:(NSString *)path return [[path pathExtension] isEqualToString:@"dmg"]; } -+ (BOOL)mustValidateBeforeExtractionWithArchivePath:(NSString *)archivePath ++ (BOOL)mustValidateBeforeExtraction { return NO; } @@ -51,6 +51,11 @@ - (instancetype)initWithArchivePath:(NSString *)archivePath extractionDirectory: return self; } +- (BOOL)needsVerifyBeforeExtractionKey +{ + return NO; +} + - (void)unarchiveWithCompletionBlock:(void (^)(NSError * _Nullable))completionBlock progressBlock:(void (^ _Nullable)(double))progressBlock waitForCleanup:(BOOL)waitForCleanup { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ diff --git a/Autoupdate/SUFlatPackageUnarchiver.m b/Autoupdate/SUFlatPackageUnarchiver.m index ba99c4ae8..3e35d995a 100644 --- a/Autoupdate/SUFlatPackageUnarchiver.m +++ b/Autoupdate/SUFlatPackageUnarchiver.m @@ -28,7 +28,7 @@ + (BOOL)canUnarchivePath:(NSString *)path return [path.pathExtension isEqualToString:@"pkg"] || [path.pathExtension isEqualToString:@"mpkg"]; } -+ (BOOL)mustValidateBeforeExtractionWithArchivePath:(NSString *)archivePath ++ (BOOL)mustValidateBeforeExtraction { return YES; } @@ -44,6 +44,11 @@ - (instancetype)initWithFlatPackagePath:(NSString *)flatPackagePath extractionDi return self; } +- (BOOL)needsVerifyBeforeExtractionKey +{ + return NO; +} + - (void)unarchiveWithCompletionBlock:(void (^)(NSError * _Nullable))completionBlock progressBlock:(void (^ _Nullable)(double))progressBlock waitForCleanup:(BOOL)__unused waitForCleanup { SUUnarchiverNotifier *notifier = [[SUUnarchiverNotifier alloc] initWithCompletionBlock:completionBlock progressBlock:progressBlock]; diff --git a/Autoupdate/SUPipedUnarchiver.m b/Autoupdate/SUPipedUnarchiver.m index 22bdec4d4..3b70a4b0d 100644 --- a/Autoupdate/SUPipedUnarchiver.m +++ b/Autoupdate/SUPipedUnarchiver.m @@ -81,9 +81,9 @@ + (BOOL)canUnarchivePath:(NSString *)path return _argumentsConformingToTypeOfPath(path, YES, NULL) != nil; } -+ (BOOL)mustValidateBeforeExtractionWithArchivePath:(NSString *)archivePath ++ (BOOL)mustValidateBeforeExtraction { - return ([archivePath hasSuffix:@".aar"] || [archivePath hasSuffix:@".yaa"]); + return NO; } - (instancetype)initWithArchivePath:(NSString *)archivePath extractionDirectory:(NSString *)extractionDirectory @@ -96,6 +96,11 @@ - (instancetype)initWithArchivePath:(NSString *)archivePath extractionDirectory: return self; } +- (BOOL)needsVerifyBeforeExtractionKey +{ + return ([_archivePath hasSuffix:@".aar"] || [_archivePath hasSuffix:@".yaa"]); +} + - (void)unarchiveWithCompletionBlock:(void (^)(NSError * _Nullable))completionBlock progressBlock:(void (^ _Nullable)(double))progressBlock waitForCleanup:(BOOL)__unused waitForCleanup { NSString *command = nil; diff --git a/Autoupdate/SUUnarchiverProtocol.h b/Autoupdate/SUUnarchiverProtocol.h index 1c3ce6c56..839f3e77f 100644 --- a/Autoupdate/SUUnarchiverProtocol.h +++ b/Autoupdate/SUUnarchiverProtocol.h @@ -12,10 +12,12 @@ NS_ASSUME_NONNULL_BEGIN @protocol SUUnarchiverProtocol -+ (BOOL)mustValidateBeforeExtractionWithArchivePath:(NSString *)archivePath; ++ (BOOL)mustValidateBeforeExtraction; - (void)unarchiveWithCompletionBlock:(void (^)(NSError * _Nullable))completionBlock progressBlock:(void (^ _Nullable)(double))progressBlock waitForCleanup:(BOOL)waitForCleanup; +@property (nonatomic, readonly) BOOL needsVerifyBeforeExtractionKey; + - (NSString *)description; @end diff --git a/Configurations/ConfigCommon.xcconfig b/Configurations/ConfigCommon.xcconfig index 620161cc0..5974236ce 100644 --- a/Configurations/ConfigCommon.xcconfig +++ b/Configurations/ConfigCommon.xcconfig @@ -104,7 +104,7 @@ SPARKLE_VERSION_PATCH = 0 // This should be in SemVer format or empty, ie. "-beta.1" // These variables must have a space after the '=' too SPARKLE_VERSION_SUFFIX = -beta.1 -CURRENT_PROJECT_VERSION = 2041 +CURRENT_PROJECT_VERSION = 2042 MARKETING_VERSION = $(SPARKLE_VERSION_MAJOR).$(SPARKLE_VERSION_MINOR).$(SPARKLE_VERSION_PATCH)$(SPARKLE_VERSION_SUFFIX) ALWAYS_SEARCH_USER_PATHS = NO diff --git a/Sparkle.xcodeproj/project.pbxproj b/Sparkle.xcodeproj/project.pbxproj index d2a7b5c65..e4090dd66 100644 --- a/Sparkle.xcodeproj/project.pbxproj +++ b/Sparkle.xcodeproj/project.pbxproj @@ -223,7 +223,7 @@ 724BB3AA1D3347C2005D534A /* SUInstallerStatus.m in Sources */ = {isa = PBXBuildFile; fileRef = 724BB3971D333832005D534A /* SUInstallerStatus.m */; }; 724BB3B71D35ABA8005D534A /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 61B5F8F609C4CEB300B25A18 /* Security.framework */; }; 724F76F91D6EAD0D00ECD062 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 525A278F133D6AE900FD8D70 /* Cocoa.framework */; }; - 725453552C04DB3700362123 /* SparkleTestCodeSign_apfs_lzma_aux_files.dmg in Resources */ = {isa = PBXBuildFile; fileRef = 725453542C04DB3700362123 /* SparkleTestCodeSign_apfs_lzma_aux_files.dmg */; }; + 725453552C04DB3700362123 /* SparkleTestCodeSign_apfs_lzma_aux_files_adhoc.dmg in Resources */ = {isa = PBXBuildFile; fileRef = 725453542C04DB3700362123 /* SparkleTestCodeSign_apfs_lzma_aux_files_adhoc.dmg */; }; 725602D51C83551C00DAA70E /* SUApplicationInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 725602D31C83551C00DAA70E /* SUApplicationInfo.h */; }; 725602D61C83551C00DAA70E /* SUApplicationInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 725602D41C83551C00DAA70E /* SUApplicationInfo.m */; }; 725B3A82263FBF0C0041AB8E /* testappcast_minimumAutoupdateVersion.xml in Resources */ = {isa = PBXBuildFile; fileRef = 725B3A81263FBF0C0041AB8E /* testappcast_minimumAutoupdateVersion.xml */; }; @@ -314,6 +314,7 @@ 7269E496264798200088C213 /* SPUSkippedUpdate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7269E493264798200088C213 /* SPUSkippedUpdate.m */; }; 7269E4982648D3460088C213 /* SPUSkippedUpdate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7269E493264798200088C213 /* SPUSkippedUpdate.m */; }; 7269E49A2648F7C00088C213 /* SPUUserUpdateState.m in Sources */ = {isa = PBXBuildFile; fileRef = 7269E4992648F7C00088C213 /* SPUUserUpdateState.m */; }; + 726B20612CF4F1D300E6F7DB /* DevSignedAppVersion2.dmg in Resources */ = {isa = PBXBuildFile; fileRef = 726B20602CF4F1D300E6F7DB /* DevSignedAppVersion2.dmg */; }; 726DF88E1C84277600188804 /* SPUUserUpdateState.h in Headers */ = {isa = PBXBuildFile; fileRef = 726DF88D1C84277500188804 /* SPUUserUpdateState.h */; settings = {ATTRIBUTES = (Public, ); }; }; 726E075C1CA3A6D6001A286B /* SPUSecureCoding.h in Headers */ = {isa = PBXBuildFile; fileRef = 726E075A1CA3A6D6001A286B /* SPUSecureCoding.h */; }; 726E075D1CA3A6D6001A286B /* SPUSecureCoding.m in Sources */ = {isa = PBXBuildFile; fileRef = 726E075B1CA3A6D6001A286B /* SPUSecureCoding.m */; }; @@ -1230,7 +1231,7 @@ 724BB3A61D33461B005D534A /* SUXPCInstallerStatus.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SUXPCInstallerStatus.h; sourceTree = ""; }; 724BB3A71D33461B005D534A /* SUXPCInstallerStatus.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SUXPCInstallerStatus.m; sourceTree = ""; }; 724BB3B51D35AAC3005D534A /* ServiceManagement.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ServiceManagement.framework; path = System/Library/Frameworks/ServiceManagement.framework; sourceTree = SDKROOT; }; - 725453542C04DB3700362123 /* SparkleTestCodeSign_apfs_lzma_aux_files.dmg */ = {isa = PBXFileReference; lastKnownFileType = file; path = SparkleTestCodeSign_apfs_lzma_aux_files.dmg; sourceTree = ""; }; + 725453542C04DB3700362123 /* SparkleTestCodeSign_apfs_lzma_aux_files_adhoc.dmg */ = {isa = PBXFileReference; lastKnownFileType = file; path = SparkleTestCodeSign_apfs_lzma_aux_files_adhoc.dmg; sourceTree = ""; }; 725602D31C83551C00DAA70E /* SUApplicationInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SUApplicationInfo.h; sourceTree = ""; }; 725602D41C83551C00DAA70E /* SUApplicationInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SUApplicationInfo.m; sourceTree = ""; }; 72563CA9272E1C5400AF39F0 /* .github */ = {isa = PBXFileReference; lastKnownFileType = folder; path = .github; sourceTree = ""; }; @@ -1306,6 +1307,7 @@ 7269E493264798200088C213 /* SPUSkippedUpdate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SPUSkippedUpdate.m; sourceTree = ""; }; 7269E4992648F7C00088C213 /* SPUUserUpdateState.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SPUUserUpdateState.m; sourceTree = ""; }; 7269E49C2648FC6C0088C213 /* SPUUserUpdateState+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SPUUserUpdateState+Private.h"; sourceTree = ""; }; + 726B20602CF4F1D300E6F7DB /* DevSignedAppVersion2.dmg */ = {isa = PBXFileReference; lastKnownFileType = file; path = DevSignedAppVersion2.dmg; sourceTree = ""; }; 726B2B5D1C645FC900388755 /* UI Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "UI Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 726DF88D1C84277500188804 /* SPUUserUpdateState.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPUUserUpdateState.h; sourceTree = ""; }; 726E075A1CA3A6D6001A286B /* SPUSecureCoding.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SPUSecureCoding.h; path = Sparkle/SPUSecureCoding.h; sourceTree = SOURCE_ROOT; }; @@ -1867,7 +1869,7 @@ 72E6D9722C0526DC005496E4 /* SparkleTestCodeSignApp.enc.nolicense.dmg */, 72AC6B271B9AAD6700F62325 /* SparkleTestCodeSignApp.tar */, 72BC6C3C275027BF0083F14B /* SparkleTestCodeSign_apfs.dmg */, - 725453542C04DB3700362123 /* SparkleTestCodeSign_apfs_lzma_aux_files.dmg */, + 725453542C04DB3700362123 /* SparkleTestCodeSign_apfs_lzma_aux_files_adhoc.dmg */, 72E6D9702C04DE19005496E4 /* SparkleTestCodeSign_pkg.dmg */, 72AC6B291B9AAF3A00F62325 /* SparkleTestCodeSignApp.tar.bz2 */, 72AC6B251B9AAC8800F62325 /* SparkleTestCodeSignApp.tar.gz */, @@ -1879,6 +1881,7 @@ 726FC0372C1E96AA00177986 /* SparkleTestCodeSignApp.enc.aar */, 72EB735E29BE981300FBCEE7 /* DevSignedApp.zip */, 72EB736029BEB36100FBCEE7 /* DevSignedAppVersion2.zip */, + 726B20602CF4F1D300E6F7DB /* DevSignedAppVersion2.dmg */, 14958C6C19AEBC610061B14F /* test-pubkey.pem */, 5AF6C74E1AEA46D10014A3AB /* test.pkg */, 5AD0FA7E1C73F2E2004BCEFF /* testappcast.xml */, @@ -3192,7 +3195,7 @@ 72AC6B261B9AAC8800F62325 /* SparkleTestCodeSignApp.tar.gz in Resources */, 72AC6B2C1B9AB0EE00F62325 /* SparkleTestCodeSignApp.tar.xz in Resources */, 729F7ECE27409077004592DC /* SparkleTestCodeSignApp_bad_extraneous.zip in Resources */, - 725453552C04DB3700362123 /* SparkleTestCodeSign_apfs_lzma_aux_files.dmg in Resources */, + 725453552C04DB3700362123 /* SparkleTestCodeSign_apfs_lzma_aux_files_adhoc.dmg in Resources */, 5A5DD41D249F116E0045EB3E /* test-relative-urls.xml in Resources */, F8761EB31ADC50EB000C9034 /* SparkleTestCodeSignApp.zip in Resources */, 5A5DD40424958B000045EB3E /* SUUpdateValidatorTest in Resources */, @@ -3201,6 +3204,7 @@ 72EB735F29BE981300FBCEE7 /* DevSignedApp.zip in Resources */, 72BC6C3D275027BF0083F14B /* SparkleTestCodeSign_apfs.dmg in Resources */, 726FC0382C1E96AA00177986 /* SparkleTestCodeSignApp.enc.aar in Resources */, + 726B20612CF4F1D300E6F7DB /* DevSignedAppVersion2.dmg in Resources */, 720DC50627A62CDC00DFF3EC /* testappcast_minimumAutoupdateVersionSkipping2.xml in Resources */, 5AD0FA7F1C73F2E2004BCEFF /* testappcast.xml in Resources */, FA30773D24CBC295007BA37D /* testlocalizedreleasenotesappcast.xml in Resources */, diff --git a/Sparkle/SPUUpdater.m b/Sparkle/SPUUpdater.m index 6d99d3697..305aa8f76 100644 --- a/Sparkle/SPUUpdater.m +++ b/Sparkle/SPUUpdater.m @@ -342,11 +342,18 @@ - (BOOL)checkIfConfiguredProperlyAndRequireFeedURL:(BOOL)requireFeedURL validate if (!hasAnyPublicKey) { if ((feedURL != nil && !servingOverHttps) || ![SUCodeSigningVerifier bundleAtURLIsCodeSigned:[[self hostBundle] bundleURL]]) { if (error != NULL) { - *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUNoPublicDSAFoundError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"For security reasons, updates need to be signed with an EdDSA key for %@. See Sparkle's documentation for more information.", hostName] }]; + *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUNoPublicDSAFoundError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"For security reasons, updates need to be signed with an EdDSA key for %@. Visit Sparkle's documentation for more information.", hostName] }]; } return NO; } else { - if (_updatingMainBundle && !_loggedNoSecureKeyWarning) { + BOOL verifyBeforeExtraction = [_host boolForInfoDictionaryKey:SUVerifyUpdateBeforeExtractionKey]; + + if (verifyBeforeExtraction) { + if (error != NULL) { + *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUNoPublicDSAFoundError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"For security reasons, updates need to be signed with an EdDSA key because %@ is specified for %@. Visit Sparkle's documentation for more information.", SUVerifyUpdateBeforeExtractionKey, hostName] }]; + } + return NO; + } else if (_updatingMainBundle && !_loggedNoSecureKeyWarning) { SULog(SULogLevelError, @"Error: Serving updates without an EdDSA key and only using Apple Code Signing is deprecated and may be unsupported in a future release. Visit Sparkle's documentation for more information: https://sparkle-project.org/documentation/#3-segue-for-security-concerns"); _loggedNoSecureKeyWarning = YES; diff --git a/Sparkle/SUConstants.h b/Sparkle/SUConstants.h index ab4cef98d..ebb0a9cdb 100644 --- a/Sparkle/SUConstants.h +++ b/Sparkle/SUConstants.h @@ -48,6 +48,7 @@ extern NSString *const SULastCheckTimeKey; extern NSString *const SUPublicDSAKeyKey; extern NSString *const SUPublicDSAKeyFileKey; extern NSString *const SUPublicEDKeyKey; +extern NSString *const SUVerifyUpdateBeforeExtractionKey; extern NSString *const SUAutomaticallyUpdateKey; extern NSString *const SUAllowsAutomaticUpdatesKey; extern NSString *const SUEnableAutomaticChecksKey; diff --git a/Sparkle/SUConstants.m b/Sparkle/SUConstants.m index 20a2309f1..21bd78bb7 100644 --- a/Sparkle/SUConstants.m +++ b/Sparkle/SUConstants.m @@ -44,6 +44,7 @@ NSString *const SUPublicDSAKeyKey = @"SUPublicDSAKey"; NSString *const SUPublicDSAKeyFileKey = @"SUPublicDSAKeyFile"; NSString *const SUPublicEDKeyKey = @"SUPublicEDKey"; +NSString *const SUVerifyUpdateBeforeExtractionKey = @"SUVerifyUpdateBeforeExtraction"; NSString *const SUAutomaticallyUpdateKey = @"SUAutomaticallyUpdate"; NSString *const SUAllowsAutomaticUpdatesKey = @"SUAllowsAutomaticUpdates"; NSString *const SUEnableSystemProfilingKey = @"SUEnableSystemProfiling"; diff --git a/Sparkle/SULog+NSError.m b/Sparkle/SULog+NSError.m index f435b026e..390f5e174 100644 --- a/Sparkle/SULog+NSError.m +++ b/Sparkle/SULog+NSError.m @@ -11,12 +11,37 @@ #include "AppKitPrevention.h" +static void _SULogErrors(NSArray *errors, int recursionLimit) +{ + if (recursionLimit == 0) { + return; + } + + for (NSError *error in errors) { + SULog(SULogLevelError, @"Error: %@ %@ (URL %@)", error.localizedDescription, error.localizedFailureReason, error.userInfo[NSURLErrorFailingURLErrorKey]); + + NSDictionary *userInfo = error.userInfo; + + if (@available(macOS 11.3, *)) { + NSArray *underlyingErrors = userInfo[NSMultipleUnderlyingErrorsKey]; + if (underlyingErrors != nil) { + _SULogErrors(underlyingErrors, recursionLimit - 1); + continue; + } + } + + NSError *underlyingError = userInfo[NSUnderlyingErrorKey]; + if (underlyingError != nil) { + _SULogErrors(@[underlyingError], recursionLimit - 1); + } + } +} + void SULogError(NSError *error) { - NSError *errorToDisplay = error; - int finiteRecursion = 5; - do { - SULog(SULogLevelError, @"Error: %@ %@ (URL %@)", errorToDisplay.localizedDescription, errorToDisplay.localizedFailureReason, errorToDisplay.userInfo[NSURLErrorFailingURLErrorKey]); - errorToDisplay = errorToDisplay.userInfo[NSUnderlyingErrorKey]; - } while(--finiteRecursion && errorToDisplay); + if (error == nil) { + return; + } + + _SULogErrors(@[error], 7); } diff --git a/Sparkle/SUUpdateValidator.h b/Sparkle/SUUpdateValidator.h index 03ca5f57b..f8ee63495 100644 --- a/Sparkle/SUUpdateValidator.h +++ b/Sparkle/SUUpdateValidator.h @@ -23,8 +23,10 @@ SPU_OBJC_DIRECT_MEMBERS - (instancetype)initWithDownloadPath:(NSString *)downloadPath signatures:(SUSignatures *)signatures host:(SUHost *)host verifierInformation:(SPUVerifierInformation * _Nullable)verifierInformation; +- (BOOL)validateHostHasPublicKeys:(NSError **)error; + // This is "pre" validation, before the archive has been extracted -- (BOOL)validateDownloadPathWithError:(NSError **)error; +- (BOOL)validateDownloadPathWithFallbackOnCodeSigning:(BOOL)fallbackOnCodeSigning error:(NSError **)error; // This is "post" validation, after an archive has been extracted - (BOOL)validateWithUpdateDirectory:(NSString *)updateDirectory error:(NSError **)error; diff --git a/Sparkle/SUUpdateValidator.m b/Sparkle/SUUpdateValidator.m index 0a68c71c0..07d45e12a 100644 --- a/Sparkle/SUUpdateValidator.m +++ b/Sparkle/SUUpdateValidator.m @@ -27,6 +27,7 @@ @implementation SUUpdateValidator SPUVerifierInformation *_verifierInformation; BOOL _prevalidatedSignature; + BOOL _validatedDownloadUsingCodeSigning; } - (instancetype)initWithDownloadPath:(NSString *)downloadPath signatures:(SUSignatures *)signatures host:(SUHost *)host verifierInformation:(SPUVerifierInformation * _Nullable)verifierInformation @@ -41,25 +42,76 @@ - (instancetype)initWithDownloadPath:(NSString *)downloadPath signatures:(SUSign return self; } -- (BOOL)validateDownloadPathWithError:(NSError * __autoreleasing *)error +- (BOOL)validateHostHasPublicKeys:(NSError * __autoreleasing *)error { SUPublicKeys *publicKeys = _host.publicKeys; - SUSignatures *signatures = _signatures; - + if (!publicKeys.hasAnyKeys) { if (error != NULL) { - *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUInstallationError userInfo:@{ NSLocalizedDescriptionKey: @"Failed to validate update before unarchiving because no (Ed)DSA public key was found in the old app" }]; + *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUInsufficientSigningError userInfo:@{ NSLocalizedDescriptionKey: @"Failed to validate update before unarchiving because no (Ed)DSA public key was found in the old app" }]; } - } else { - NSError *innerError = nil; - if ([SUSignatureVerifier validatePath:_downloadPath withSignatures:signatures withPublicKeys:publicKeys verifierInformation:_verifierInformation error:&innerError]) { + + return NO; + } + + return YES; +} + +- (BOOL)validateDownloadPathWithFallbackOnCodeSigning:(BOOL)fallbackOnCodeSigning error:(NSError * __autoreleasing *)error +{ + SUPublicKeys *publicKeys = _host.publicKeys; + SUSignatures *signatures = _signatures; + + NSError *dsaVerificationError = nil; + if ([SUSignatureVerifier validatePath:_downloadPath withSignatures:signatures withPublicKeys:publicKeys verifierInformation:_verifierInformation error:&dsaVerificationError]) { + _prevalidatedSignature = YES; + return YES; + } + + NSMutableArray *underlyingErrors = [[NSMutableArray alloc] init]; + if (dsaVerificationError != nil) { + [underlyingErrors addObject:dsaVerificationError]; + } + + if (fallbackOnCodeSigning) { + SULog(SULogLevelError, @"Failed to validate update archive with (Ed)DSA signing. Trying fallback with Apple Developer ID code signing verification: %@", dsaVerificationError); + + // (Ed)DSA validation failed + signed archives are required + regular app update + // As fallback for key rotation, check if the archive is Developer ID signed with a team ID that matches the host + NSError *codeSignError = nil; + NSURL *downloadURL = [NSURL fileURLWithPath:_downloadPath isDirectory:NO]; + + if (![SUCodeSigningVerifier codeSignatureIsValidAtDownloadURL:downloadURL andMatchesDeveloperIDTeamFromOldBundleURL:_host.bundle.bundleURL error:&codeSignError]) { + SULog(SULogLevelError, @"Failed to validate update archive with Developer ID code signing fallback: %@", codeSignError); + + if (codeSignError != nil) { + [underlyingErrors addObject:codeSignError]; + } + } else { _prevalidatedSignature = YES; + _validatedDownloadUsingCodeSigning = YES; return YES; } - if (error != NULL) { - *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUInstallationError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"(Ed)DSA signature validation before unarchiving failed for update %@", _downloadPath], NSUnderlyingErrorKey: innerError }]; + } + + if (error != NULL) { + NSMutableDictionary *userInfo = [[NSMutableDictionary alloc] init]; + userInfo[NSLocalizedDescriptionKey] = [NSString stringWithFormat:@"(Ed)DSA signature validation before unarchiving failed for update %@", _downloadPath]; + + if (dsaVerificationError != nil) { + // This is the primary error + userInfo[NSUnderlyingErrorKey] = dsaVerificationError; } + + if (underlyingErrors.count > 1) { + if (@available(macOS 11.3, *)) { + userInfo[NSMultipleUnderlyingErrorsKey] = [underlyingErrors copy]; + } + } + + *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUInstallationError userInfo:[userInfo copy]]; } + return NO; } @@ -119,9 +171,9 @@ - (BOOL)validateWithUpdateDirectory:(NSString *)updateDirectory error:(NSError * #endif else { - // We already validated the EdDSA signature + // We already validated the download archive // Let's check if the update passes Sparkle's basic update policy and that the update is properly signed - // Currently, this case gets hit only for binary delta updates and .aar/.yaa archives + // Currently, this case gets hit for binary delta updates and updates requiring SUVerifyUpdateBeforeExtraction NSBundle *newBundle = [NSBundle bundleWithURL:installSourceURL]; SUHost *newHost = [[SUHost alloc] initWithBundle:newBundle]; @@ -140,15 +192,47 @@ - (BOOL)validateWithUpdateDirectory:(NSString *)updateDirectory error:(NSError * return NO; } - NSError *innerError = nil; - if (updateIsCodeSigned && ![SUCodeSigningVerifier codeSignatureIsValidAtBundleURL:installSourceURL error:&innerError]) { + NSError *codeSigningInnerError = nil; + if (updateIsCodeSigned && ![SUCodeSigningVerifier codeSignatureIsValidAtBundleURL:installSourceURL error:&codeSigningInnerError]) { if (error != NULL) { - *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUValidationError userInfo:@{ NSLocalizedDescriptionKey: @"Failed to validate apple code sign signature on bundle after archive validation", NSUnderlyingErrorKey: innerError }]; + NSMutableDictionary *userInfo = [NSMutableDictionary dictionary]; + + userInfo[NSLocalizedDescriptionKey] = @"The update archive is validly signed, but the app's Apple code signing signature is corrupted. The update will be rejected."; + + if (codeSigningInnerError != nil) { + userInfo[NSUnderlyingErrorKey] = codeSigningInnerError; + } + + *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUValidationError userInfo:userInfo]; } return NO; } + if (_validatedDownloadUsingCodeSigning) { + // Old EdDSA key failed on download archive, and Apple Code signing validation was used as a fallback (with SUVerifyUpdateBeforeExtraction set to YES), + // which means the developer may be rotating keys. + // So we must validate new EdDSA key with the new download. + // This is a policy to ensure the next update can be updatable with the new EdDSA key (not a security measure). + NSError *validateInnerError = nil; + BOOL validationCheckSuccess = [SUSignatureVerifier validatePath:downloadPath withSignatures:signatures withPublicKeys:newPublicKeys verifierInformation:_verifierInformation error:&validateInnerError]; + if (!validationCheckSuccess) { + if (error != NULL) { + NSMutableDictionary *userInfo = [NSMutableDictionary dictionary]; + + userInfo[NSLocalizedDescriptionKey] = @"(Ed)DSA signature validation failed after using Apple code signing to validate the update archive. The update has a public (Ed)DSA key, but the public key shipped with the update doesn't match the signature. To prevent future problems, the update will be rejected."; + + if (validateInnerError != nil) { + userInfo[NSUnderlyingErrorKey] = validateInnerError; + } + + *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUValidationError userInfo:userInfo]; + } + + return NO; + } + } + return YES; } } @@ -262,10 +346,10 @@ - (BOOL)validateUpdateForHost:(SUHost *)host downloadedToPath:(NSString *)downlo if (hostIsCodeSigned) { passedCodeSigning = [SUCodeSigningVerifier codeSignatureIsValidAtBundleURL:newHost.bundle.bundleURL andMatchesSignatureAtBundleURL:host.bundle.bundleURL error:&codeSignedError]; } - // End of security-critical part - - // If the new DSA key differs from the old, then this check is not a security measure, because the new key is not trusted. - // In that case, the check ensures that the app author has correctly used DSA keys, so that the app will be updateable in the next version. + + // If code signing passes, and the new DSA key differs from the old, the check ensures that the app author has correctly used DSA keys for the new update, so the app will be updateable in the next version. + // Code signing passing ensures the new DSA key can also be trusted for validating the archive. + // If code signing doesn't pass, DSA validation failing will be an error either way. if (!passedDSACheck && newHasAnyDSAKey) { NSError *innerError = nil; if (![SUSignatureVerifier validatePath:downloadedPath withSignatures:signatures withPublicKeys:newPublicKeys verifierInformation:_verifierInformation error:&innerError]) { @@ -275,6 +359,7 @@ - (BOOL)validateUpdateForHost:(SUHost *)host downloadedToPath:(NSString *)downlo return NO; } } + // End of security-critical part // If the new update is code signed but it's not validly code signed, we reject it NSError *innerError = nil; diff --git a/Tests/Resources/DevSignedAppVersion2.dmg b/Tests/Resources/DevSignedAppVersion2.dmg new file mode 100644 index 000000000..c5073f489 Binary files /dev/null and b/Tests/Resources/DevSignedAppVersion2.dmg differ diff --git a/Tests/Resources/SparkleTestCodeSign_apfs_lzma_aux_files.dmg b/Tests/Resources/SparkleTestCodeSign_apfs_lzma_aux_files_adhoc.dmg similarity index 100% rename from Tests/Resources/SparkleTestCodeSign_apfs_lzma_aux_files.dmg rename to Tests/Resources/SparkleTestCodeSign_apfs_lzma_aux_files_adhoc.dmg diff --git a/Tests/SUCodeSigningVerifierTest.m b/Tests/SUCodeSigningVerifierTest.m index 302dc47e7..5eb1f0389 100644 --- a/Tests/SUCodeSigningVerifierTest.m +++ b/Tests/SUCodeSigningVerifierTest.m @@ -23,6 +23,9 @@ @implementation SUCodeSigningVerifierTest NSURL *_devSignedAppURL; NSURL *_devSignedVersion2AppURL; NSURL *_devInvalidSignedAppURL; + NSURL *_devSignedDiskImageURL; + NSURL *_unsignedDiskImageURL; + NSURL *_adhocSignedDiskImageURL; } - (void)setUp @@ -30,6 +33,11 @@ - (void)setUp [super setUp]; NSBundle *unitTestBundle = [NSBundle bundleForClass:[self class]]; + + _devSignedDiskImageURL = [unitTestBundle URLForResource:@"DevSignedAppVersion2" withExtension:@"dmg"]; + _unsignedDiskImageURL = [unitTestBundle URLForResource:@"SparkleTestCodeSign_apfs" withExtension:@"dmg"]; + _adhocSignedDiskImageURL = [unitTestBundle URLForResource:@"SparkleTestCodeSign_apfs_lzma_aux_files_adhoc" withExtension:@"dmg"]; + NSString *zippedAppURL = [unitTestBundle pathForResource:@"SparkleTestCodeSignApp" ofType:@"zip"]; SUFileManager *fileManager = [[SUFileManager alloc] init]; @@ -248,6 +256,41 @@ - (void)testValidMatchingDevIdApp } } +- (void)testValidMatchingDevIdDiskImage +{ + NSError *error = nil; + XCTAssertTrue([SUCodeSigningVerifier codeSignatureIsValidAtDownloadURL:_devSignedDiskImageURL andMatchesDeveloperIDTeamFromOldBundleURL:_devSignedAppURL error:&error]); + XCTAssertNil(error); +} + +- (void)testInvalidMatchingDevIdDiskImageWithAppNoSigning +{ + NSError *error = nil; + XCTAssertFalse([SUCodeSigningVerifier codeSignatureIsValidAtDownloadURL:_devSignedDiskImageURL andMatchesDeveloperIDTeamFromOldBundleURL:_notSignedAppURL error:&error]); + XCTAssertNotNil(error); +} + +- (void)testInvalidMatchingDevIdDiskImageWithAppAdhocSigning +{ + NSError *error = nil; + XCTAssertFalse([SUCodeSigningVerifier codeSignatureIsValidAtDownloadURL:_devSignedDiskImageURL andMatchesDeveloperIDTeamFromOldBundleURL:_validSignedAppURL error:&error]); + XCTAssertNotNil(error); +} + +- (void)testInvalidMatchWithNoDiskImageSigning +{ + NSError *error = nil; + XCTAssertFalse([SUCodeSigningVerifier codeSignatureIsValidAtDownloadURL:_unsignedDiskImageURL andMatchesDeveloperIDTeamFromOldBundleURL:_validSignedAppURL error:&error]); + XCTAssertNotNil(error); +} + +- (void)testInvalidMatchWithAdhocSignedDiskImage +{ + NSError *error = nil; + XCTAssertFalse([SUCodeSigningVerifier codeSignatureIsValidAtDownloadURL:_adhocSignedDiskImageURL andMatchesDeveloperIDTeamFromOldBundleURL:_devSignedAppURL error:&error]); + XCTAssertNotNil(error); +} + - (void)testInvalidMatchingWithBrokenBundle { // We can't test our own app because matching with ad-hoc signed apps understandably does not succeed diff --git a/Tests/SUUnarchiverTest.swift b/Tests/SUUnarchiverTest.swift index 2db7aa969..552ba16ba 100644 --- a/Tests/SUUnarchiverTest.swift +++ b/Tests/SUUnarchiverTest.swift @@ -165,7 +165,7 @@ class SUUnarchiverTest: XCTestCase func testUnarchivingAPFSAdhocSignedDMGWithAuxFiles() { - self.unarchiveTestAppWithExtension("dmg", resourceName: "SparkleTestCodeSign_apfs_lzma_aux_files") + self.unarchiveTestAppWithExtension("dmg", resourceName: "SparkleTestCodeSign_apfs_lzma_aux_files_adhoc") } func testUnarchivingAPFSDMGWithPackage() diff --git a/Tests/SUUpdateValidatorTest.swift b/Tests/SUUpdateValidatorTest.swift index 4d9842205..914e3868f 100644 --- a/Tests/SUUpdateValidatorTest.swift +++ b/Tests/SUUpdateValidatorTest.swift @@ -117,7 +117,7 @@ class SUUpdateValidatorTest: XCTestCase { let signatures = self.signatures(signatureConfig) let validator = SUUpdateValidator(downloadPath: self.signedTestFilePath, signatures: signatures, host: host, verifierInformation: nil) - let result = (try? validator.validateDownloadPath()) != nil + let result = (try? validator.validateHostHasPublicKeys()) != nil && (try? validator.validateDownloadPathWithFallback(onCodeSigning: false)) != nil XCTAssertEqual(result, expectedResult, "bundle: \(bundleConfig), signatures: \(signatureConfig)", line: line) }