From 1ca60d5ab65b034b68d5307c0bcc044f9139ca77 Mon Sep 17 00:00:00 2001 From: Zorg Date: Sun, 8 Dec 2024 16:31:26 -0800 Subject: [PATCH] Add option to verify updates before extraction (#2667) Adds an opt-in option (SUVerifyUpdateBeforeExtraction) to enforce verifying updates before extracting them for stronger security. EdDSA signing is required to use this option. As fallback in case EdDSA keys are lost, disk image archives's code signatures are validated assuming it's Developer ID signed. Key rotation is still possible. Apple Archives (aar, yaa) now require using this option. --- Autoupdate/AppInstaller.m | 40 +++-- Autoupdate/SUBinaryDeltaUnarchiver.m | 7 +- Autoupdate/SUCodeSigningVerifier.h | 2 + Autoupdate/SUCodeSigningVerifier.m | 161 ++++++++++++++++-- Autoupdate/SUDiskImageUnarchiver.m | 7 +- Autoupdate/SUFlatPackageUnarchiver.m | 7 +- Autoupdate/SUPipedUnarchiver.m | 9 +- Autoupdate/SUUnarchiverProtocol.h | 4 +- Configurations/ConfigCommon.xcconfig | 2 +- Sparkle.xcodeproj/project.pbxproj | 12 +- Sparkle/SPUUpdater.m | 11 +- Sparkle/SUConstants.h | 1 + Sparkle/SUConstants.m | 1 + Sparkle/SULog+NSError.m | 37 +++- Sparkle/SUUpdateValidator.h | 4 +- Sparkle/SUUpdateValidator.m | 121 +++++++++++-- Tests/Resources/DevSignedAppVersion2.dmg | Bin 0 -> 47898 bytes ...estCodeSign_apfs_lzma_aux_files_adhoc.dmg} | Bin Tests/SUCodeSigningVerifierTest.m | 43 +++++ Tests/SUUnarchiverTest.swift | 2 +- Tests/SUUpdateValidatorTest.swift | 2 +- 21 files changed, 411 insertions(+), 62 deletions(-) create mode 100644 Tests/Resources/DevSignedAppVersion2.dmg rename Tests/Resources/{SparkleTestCodeSign_apfs_lzma_aux_files.dmg => SparkleTestCodeSign_apfs_lzma_aux_files_adhoc.dmg} (100%) diff --git a/Autoupdate/AppInstaller.m b/Autoupdate/AppInstaller.m index 49a76a9fd5..d135fe2090 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 cb3336d374..ca6b701a12 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 285fe60146..7605c202f6 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 62ad53cec7..45b2b58c03 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 f05e72b74e..57b8a5b994 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 ba99c4ae87..3e35d995a1 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 22bdec4d4d..3b70a4b0d2 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 1c3ce6c564..839f3e77fa 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 620161cc09..5974236ce8 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 d2a7b5c65c..e4090dd66e 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 6d99d36970..305aa8f763 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 ab4cef98db..ebb0a9cdb7 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 20a2309f13..21bd78bb71 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 f435b026e9..390f5e1743 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 03ca5f57bb..f8ee63495b 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 0a68c71c0d..07d45e12a0 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 0000000000000000000000000000000000000000..c5073f4898c647bc8d19b779bde7e11c649e18df GIT binary patch literal 47898 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%0D1{Fk8x zhJed2m&I|2yh7hO!Clu^Q^_hNt{4C?sG!52l*TSbUI(Fhu3_Jcy&RuIhH$aJk1fP% z9tc9yi=ICyZmW1z6plTLpnXV)RqOgU+Q%26by@Y`vi2W(8XC+C{j!UIpieAc_nh>d z5LDjR%FXj{*gX2e{1BW^fDh3OKHdxFCxF=F68aK_8bjrHX%J533%Miy*{&};vEC4^ zE9$)8*&D`xeuJv3Vy~UoqvpnOI8>?UnuF<-N1yrvw$(9q(^?1`7T7*l5%t z`&i4|wq5c6Lg6!Bt{1I%&g8EMomTDkWp})W>2BG)D{s5^CW^0)5|}%ISIFU=^U3%5 zHx8*@l-sv+vgrKPrd%eMZ*4g<@67h2rFZX!J2{xveXzW>n&;{1GRs|)wsoJlW^nhp z>b1o)%`D$ukE+*uTlf4*a`386e`nbXJX^5AZ+@6tVBgeVyq9mZ2b&iZ9ABv}a%`8d z!vEKom*uJ}sVGScKjAB3JX5=G)s~aOux{%?VEbqrR4SyjKY_sm-@7DlN3`1wfl z-Y1#KKrP@QB~TlS703bBTmRsNE~pRs!9WAnumCnrWUw|)pk2^m(ip?usNyg%1~s$l~t-)?1s(GtTl zc*h^sZ8Ci$i_!V~57|Dg50=mDtKM^X#v-A!r8aI?=69~W&dpe!vyx-|;{%)Srq;dE zmdbzE6VRo4p*HTarovwXhaCP1Q{=uVzq@?jHm@z0Z^;hp&r!8%w#jqr_x$nl)!6Xt zvTx&}_iO#HymV@Aoi#gL>$&gEJ3ovQWY0d2IC|t8$M)OPe(Y!4c7NOJNx;+A7vGMt zDU+D{@POD4j?56w)qh^CdpV=sSg+?v{3(C$73!sFdowmYX=!6{X?-qkb@hpeLhpr1 z-$i{*ex+;WOkK*hTi}hSLgif-*G2hllTAe)x$UfFD2RSA_u95kzieL0CrsvC?j5}F z`wfYQKDv5YH*8)%1@?T|f!&Yy&|xn=MUS_i$VGg;+q(lf`;-T{|TH6g%keJ#r)9sQQV2J4^}|@Cw`P z6rO$RUDv$3)zSYKzr47(vF_jhXFZ-z zths&)EGshJt#iaHs))-npyaMb9&qRV@)aWI!#0QIEPeUm$hx1e6#68NYm_|V@aThO6s`T4f-oWn)V*KC~vr|()T+c)V_Y4_pSIbTor+{wzay8Ak$_N_?v z=WBM8PvlmG#D8+`6SzX)+9Pa3=CpPV7LU{<}a;N*vy|KE7Ke)c=1 z#20>DiT$b4miCu(F1w!2vwzyRddEKRS*-sK)UPo0#l@Eg5U@=upSNwicK;;QdKNk9R?1kAW1tXAB%0;zj8J6Br#* zM(AWQ1kRAJ@T+hZ%6=HT|L2cKnod1u*vFAAD0xwQ1EDPs+cKGVc*;c8$ zYW#I4!nLSFI;^z5nJ(dTu^*&MeInn9kx@{M6vyq8INLzwSF%;C_Ee zq}IAqM;3&vo6DHT{oYpRR{8Y<6QiRv4z2T_>n;|akhZT=Zt9Xf0x!3uZ`>AbGrxzq zb^|l6dO`dZj$cWw_Ok;Y-MiHNH&egN;QQkjX<{>fLRQXX=jT)cSu70T1{$dY#TO75Kqx2)0F;{@Bme*a literal 0 HcmV?d00001 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 302dc47e78..5eb1f0389f 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 2db7aa9690..552ba16ba7 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 4d98422051..914e3868f1 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) }