Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Don't allow removal of signing keys more strictly #2647

Merged
merged 4 commits into from
Oct 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 90 additions & 21 deletions Sparkle/SUUpdateValidator.m
Original file line number Diff line number Diff line change
Expand Up @@ -119,23 +119,98 @@ - (BOOL)validateWithUpdateDirectory:(NSString *)updateDirectory error:(NSError *
#endif
else
{
// Because we already validated the EdDSA signature, this is just a consistency check to see
// if the developer signed their application properly with their Apple ID
// Currently, this case gets hit for binary delta updates and .aar/.yaa archives
// We already validated the EdDSA signature
// 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

NSBundle *newBundle = [NSBundle bundleWithURL:installSourceURL];
SUHost *newHost = [[SUHost alloc] initWithBundle:newBundle];

SUPublicKeys *publicKeys = host.publicKeys;
SUPublicKeys *newPublicKeys = newHost.publicKeys;

BOOL oldHasAnyDSAKey = NO;
BOOL newHasAnyDSAKey = NO;
BOOL hostIsCodeSigned = NO;
BOOL updateIsCodeSigned = NO;

[self getHostIsCodeSigned:&hostIsCodeSigned updateIsCodeSigned:&updateIsCodeSigned hostHasAnyDSAKey:&oldHasAnyDSAKey updateHasAnyDSAKey:&newHasAnyDSAKey migratesDSAKeys:NULL hostPublicKeys:publicKeys updatePublicKeys:newPublicKeys hostBundleURL:host.bundle.bundleURL updateBundleURL:installSourceURL];

if (![self passesBasicUpdatePolicyWithHostIsCodeSigned:hostIsCodeSigned updateIsCodeSigned:updateIsCodeSigned hostHasAnyDSAKey:oldHasAnyDSAKey updateHasAnyDSAKey:newHasAnyDSAKey error:error]) {
return NO;
}

NSError *innerError = nil;
if ([SUCodeSigningVerifier bundleAtURLIsCodeSigned:installSourceURL] && ![SUCodeSigningVerifier codeSignatureIsValidAtBundleURL:installSourceURL error:&innerError]) {
if (updateIsCodeSigned && ![SUCodeSigningVerifier codeSignatureIsValidAtBundleURL:installSourceURL error:&innerError]) {
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 }];
}

return NO;
} else {
return YES;
}

return YES;
}
}

- (void)getHostIsCodeSigned:(BOOL *)outHostIsCodeSigned updateIsCodeSigned:(BOOL *)outUpdateIsCodeSigned hostHasAnyDSAKey:(BOOL *)outHostHasAnyDSAKey updateHasAnyDSAKey:(BOOL *)outUpdateHasAnyDSAKey migratesDSAKeys:(BOOL *)outMigratesDSAKeys hostPublicKeys:(SUPublicKeys *)hostPublicKeys updatePublicKeys:(SUPublicKeys *)updatePublicKeys hostBundleURL:(NSURL *)hostBundleURL updateBundleURL:(NSURL *)updateBundleURL SPU_OBJC_DIRECT
{
BOOL oldHasLegacyDSAKey = hostPublicKeys.dsaPubKeyStatus != SUSigningInputStatusAbsent;
BOOL oldHasEdDSAKey = hostPublicKeys.ed25519PubKeyStatus != SUSigningInputStatusAbsent;
BOOL oldHasAnyDSAKey = oldHasLegacyDSAKey || oldHasEdDSAKey;
if (outHostHasAnyDSAKey != NULL) {
*outHostHasAnyDSAKey = oldHasAnyDSAKey;
}

BOOL newHasLegacyDSAKey = updatePublicKeys.dsaPubKeyStatus != SUSigningInputStatusAbsent;
BOOL newHasEdDSAKey = updatePublicKeys.ed25519PubKeyStatus != SUSigningInputStatusAbsent;
BOOL newHasAnyDSAKey = newHasLegacyDSAKey || newHasEdDSAKey;
if (outUpdateHasAnyDSAKey != NULL) {
*outUpdateHasAnyDSAKey = newHasAnyDSAKey;
}

BOOL migratesDSAKeys = oldHasLegacyDSAKey && !oldHasEdDSAKey && newHasEdDSAKey && !newHasLegacyDSAKey;
if (outMigratesDSAKeys != NULL) {
*outMigratesDSAKeys = migratesDSAKeys;
}

BOOL hostIsCodeSigned = [SUCodeSigningVerifier bundleAtURLIsCodeSigned:hostBundleURL];
if (outHostIsCodeSigned != NULL) {
*outHostIsCodeSigned = hostIsCodeSigned;
}

BOOL updateIsCodeSigned = [SUCodeSigningVerifier bundleAtURLIsCodeSigned:updateBundleURL];
if (outUpdateIsCodeSigned != NULL) {
*outUpdateIsCodeSigned = updateIsCodeSigned;
}
}

// This is not essential for security, only a policy
- (BOOL)passesBasicUpdatePolicyWithHostIsCodeSigned:(BOOL)hostIsCodeSigned updateIsCodeSigned:(BOOL)updateIsCodeSigned hostHasAnyDSAKey:(BOOL)hostHasAnyDSAKey updateHasAnyDSAKey:(BOOL)updateHasAnyDSAKey error:(NSError * __autoreleasing *)error SPU_OBJC_DIRECT
{
// Don't allow removal of (Ed)DSA keys
if (hostHasAnyDSAKey && !updateHasAnyDSAKey) {
if (error != NULL) {
*error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUValidationError userInfo:@{ NSLocalizedDescriptionKey: @"A public (Ed)DSA key was found in the old bundle but no public (Ed)DSA key was found in the new update. Sparkle only supports rotation, but not removal of (Ed)DSA keys. Please add an EdDSA key to the new app." }];
}
return NO;
}

// Don't allow removal of code signing
if (hostIsCodeSigned && !updateIsCodeSigned) {
if (error != NULL) {
if (hostHasAnyDSAKey) {
*error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUValidationError userInfo:@{ NSLocalizedDescriptionKey: @"The old bundle is code signed but the update is not code signed. Sparkle only supports rotation, but not removal of Apple Code Signing identity. Please code sign the new app. If no Apple Code Signing certificate is available, adhoc signing can be used at minimum." }];
} else {
*error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUValidationError userInfo:@{ NSLocalizedDescriptionKey: @"The old bundle is code signed but the update is not code signed. Please code sign the new app with the same signing identity." }];
}
}
return NO;
}

return YES;
}

/**
* If the update is a bundle, then it must meet any one of:
*
Expand All @@ -161,21 +236,15 @@ - (BOOL)validateUpdateForHost:(SUHost *)host downloadedToPath:(NSString *)downlo

_verifierInformation.actualVersion = newHost.version;

BOOL oldHasLegacyDSAKey = publicKeys.dsaPubKeyStatus != SUSigningInputStatusAbsent;
BOOL oldHasEdDSAKey = publicKeys.ed25519PubKeyStatus != SUSigningInputStatusAbsent;
BOOL oldHasAnyDSAKey = oldHasLegacyDSAKey || oldHasEdDSAKey;
BOOL newHasLegacyDSAKey = newPublicKeys.dsaPubKeyStatus != SUSigningInputStatusAbsent;
BOOL newHasEdDSAKey = newPublicKeys.ed25519PubKeyStatus != SUSigningInputStatusAbsent;
BOOL newHasAnyDSAKey = newHasLegacyDSAKey || newHasEdDSAKey;
BOOL migratesDSAKeys = oldHasLegacyDSAKey && !oldHasEdDSAKey && newHasEdDSAKey && !newHasLegacyDSAKey;
BOOL updateIsCodeSigned = [SUCodeSigningVerifier bundleAtURLIsCodeSigned:newHost.bundle.bundleURL];
BOOL hostIsCodeSigned = [SUCodeSigningVerifier bundleAtURLIsCodeSigned:host.bundle.bundleURL];

// This is not essential for security, only a policy
if (oldHasAnyDSAKey && !newHasAnyDSAKey) {
if (error != NULL) {
*error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUValidationError userInfo:@{ NSLocalizedDescriptionKey: @"A public (Ed)DSA key was found in the old bundle but no public (Ed)DSA key was found in the new update. Sparkle only supports rotation, but not removal of (Ed)DSA keys. Please add an EdDSA key to the new app." }];
}
BOOL oldHasAnyDSAKey = NO;
BOOL newHasAnyDSAKey = NO;
BOOL migratesDSAKeys = NO;
BOOL hostIsCodeSigned = NO;
BOOL updateIsCodeSigned = NO;

[self getHostIsCodeSigned:&hostIsCodeSigned updateIsCodeSigned:&updateIsCodeSigned hostHasAnyDSAKey:&oldHasAnyDSAKey updateHasAnyDSAKey:&newHasAnyDSAKey migratesDSAKeys:&migratesDSAKeys hostPublicKeys:publicKeys updatePublicKeys:newPublicKeys hostBundleURL:host.bundle.bundleURL updateBundleURL:newHost.bundle.bundleURL];

if (![self passesBasicUpdatePolicyWithHostIsCodeSigned:hostIsCodeSigned updateIsCodeSigned:updateIsCodeSigned hostHasAnyDSAKey:oldHasAnyDSAKey updateHasAnyDSAKey:newHasAnyDSAKey error:error]) {
return NO;
}

Expand Down
15 changes: 12 additions & 3 deletions Tests/SUUpdateValidatorTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,15 @@ class SUUpdateValidatorTest: XCTestCase {
return true
}
}

var isValidCodeSigned: Bool {
switch self {
case .codeSignedOnly, .codeSignedOnlyNew, .codeSignedBothNew, .codeSignedOldED, .codeSignedBoth:
return true
case .none, .dsaOnly, .edOnly, .both, .codeSignedInvalid, .codeSignedInvalidOnly:
return false
}
}
}

struct SignatureConfig: CaseIterable, Equatable, CustomDebugStringConvertible {
Expand Down Expand Up @@ -210,7 +219,7 @@ class SUUpdateValidatorTest: XCTestCase {
#else
let signatureConfig = SignatureConfig(ed: .valid)
#endif
testPostValidation(oldBundle: .codeSignedBoth, newBundle: bundleConfig, signatures: signatureConfig, expectedResult: bundleConfig.hasAnyKeys && bundleConfig != .codeSignedInvalid)
testPostValidation(oldBundle: .codeSignedBoth, newBundle: bundleConfig, signatures: signatureConfig, expectedResult: bundleConfig.hasAnyKeys && bundleConfig.isValidCodeSigned)
}
}
}
Expand Down Expand Up @@ -240,8 +249,8 @@ class SUUpdateValidatorTest: XCTestCase {
// You can't change two things at once.
testPostValidation(oldBundle: .codeSignedOldED, newBundle: .codeSignedBothNew, signatures: signatureConfig, expectedResult: false)

// It's permitted to remove code signing too.
testPostValidation(oldBundle: .codeSignedBoth, newBundle: .both, signatures: signatureConfig, expectedResult: signatureIsValid)
// You can't remove code signing.
testPostValidation(oldBundle: .codeSignedBoth, newBundle: .both, signatures: signatureConfig, expectedResult: false)
}
}
}
Loading