feat: add validator to check that macOS app allows unsigned executable memory (#2806)

This commit is contained in:
Bryan Oltman
2025-01-27 10:55:50 -05:00
committed by GitHub
parent c9709a614d
commit 6a45710bf2
3 changed files with 103 additions and 120 deletions
+2 -2
View File
@@ -26,7 +26,7 @@ class Doctor {
/// Validators that verify shorebird will work on macOS.
final List<Validator> macosCommandValidators = [
MacosNetworkEntitlementValidator(),
MacosEntitlementsValidator(),
];
/// Validators that verify shorebird will work on Windows.
@@ -38,7 +38,7 @@ class Doctor {
List<Validator> generalValidators = [
ShorebirdVersionValidator(),
AndroidInternetPermissionValidator(),
MacosNetworkEntitlementValidator(),
MacosEntitlementsValidator(),
ShorebirdYamlAssetValidator(),
TrackedLockFilesValidator(),
];
@@ -9,12 +9,18 @@ import 'package:shorebird_cli/src/validators/validators.dart';
/// Checks that the macOS app has the network client entitlement. Without this
/// entitlement, the app will not be able to make network requests, and
/// Shorebird will not be able to check for patches.
class MacosNetworkEntitlementValidator extends Validator {
/// The plist key for the Outgoing Connections (Client) entitlement, which
/// allows macOS apps to make network requests.
class MacosEntitlementsValidator extends Validator {
/// The entitlements plist key for the Outgoing Connections (Client)
/// entitlement, which allows macOS apps to make network requests.
static const networkClientEntitlementKey =
'com.apple.security.network.client';
/// The entitlements plist key for the Allow Unsigned Executable Memory
/// entitlement, which allows the app to run code not included in the original
/// binary (e.g. patches).
static const allowUnsignedExecutableMemoryKey =
'com.apple.security.cs.allow-unsigned-executable-memory';
Directory? get _macosDirectory {
final projectRoot = shorebirdEnv.getFlutterProjectRoot();
if (projectRoot == null) {
@@ -45,7 +51,7 @@ class MacosNetworkEntitlementValidator extends Validator {
}
@override
String get description => 'macOS app has Outgoing Connections entitlement';
String get description => 'macOS app has correct entitlements';
@override
bool canRunInCurrentContext() => _macosDirectory?.existsSync() ?? false;
@@ -67,18 +73,34 @@ The command you are running must be run within a Flutter app project that suppor
];
}
final issues = <ValidationIssue>[];
if (!hasNetworkClientEntitlement(plistFile: _releaseEntitlementsPlist!)) {
return [
issues.add(
ValidationIssue(
severity: ValidationIssueSeverity.error,
message:
'''${_releaseEntitlementsPlist!.path} is missing the Outgoing Connections ($networkClientEntitlementKey) entitlement.''',
fix: () => addNetworkEntitlementToPlist(_releaseEntitlementsPlist!),
),
];
);
}
return [];
if (!hasAllowUnsignedExecutableMemoryEntitlement(
plistFile: _releaseEntitlementsPlist!,
)) {
issues.add(
ValidationIssue(
severity: ValidationIssueSeverity.error,
message:
'''${_releaseEntitlementsPlist!.path} is missing the Allow Unsigned Executable Memory ($allowUnsignedExecutableMemoryKey) entitlement.''',
fix: () => addAllowUnsignedExecutableMemoryEntitlementToPlist(
_releaseEntitlementsPlist!,
),
),
);
}
return issues;
}
/// Whether the given entitlements plist file has the network client
@@ -94,4 +116,26 @@ The command you are running must be run within a Flutter app project that suppor
plist.properties[networkClientEntitlementKey] = true;
entitlementsPlist.writeAsStringSync(plist.toString());
}
/// Whether the given entitlements plist file has the allow unsigned
/// executable memory entitlement.
@visibleForTesting
static bool hasAllowUnsignedExecutableMemoryEntitlement({
required File plistFile,
}) {
return Plist(file: plistFile)
.properties[allowUnsignedExecutableMemoryKey] ==
true;
}
/// Adds the allow unsigned executable memory entitlement to the given
/// entitlements plist file.
@visibleForTesting
static void addAllowUnsignedExecutableMemoryEntitlementToPlist(
File entitlementsPlist,
) {
final plist = Plist(file: entitlementsPlist);
plist.properties[allowUnsignedExecutableMemoryKey] = true;
entitlementsPlist.writeAsStringSync(plist.toString());
}
}
@@ -10,8 +10,8 @@ import 'package:test/test.dart';
import '../mocks.dart';
void main() {
group(MacosNetworkEntitlementValidator, () {
const entitlementsPlistWithoutEntitlement = '''
group(MacosEntitlementsValidator, () {
const entitlementsPlistWithoutEntitlements = '''
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
@@ -22,13 +22,15 @@ void main() {
</plist>
''';
const entitlementsPlistWithEntitlement = '''
const entitlementsPlistWithAllEntitlements = '''
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
</dict>
@@ -37,7 +39,7 @@ void main() {
late Directory projectRoot;
late ShorebirdEnv shorebirdEnv;
late MacosNetworkEntitlementValidator validator;
late MacosEntitlementsValidator validator;
R runWithOverrides<R>(R Function() body) {
return runScoped(
@@ -70,14 +72,14 @@ void main() {
when(() => shorebirdEnv.getFlutterProjectRoot()).thenReturn(projectRoot);
validator = MacosNetworkEntitlementValidator();
validator = MacosEntitlementsValidator();
});
group('description', () {
test('returns the correct description', () {
expect(
runWithOverrides(() => validator.description),
'macOS app has Outgoing Connections entitlement',
'macOS app has correct entitlements',
);
});
});
@@ -130,34 +132,61 @@ void main() {
});
group('when release entitlements plist exists', () {
group('when network client entitlement is missing', () {
group('when entitlements are missing', () {
setUp(() {
setUpProjectRoot(entitlements: entitlementsPlistWithoutEntitlement);
setUpProjectRoot(
entitlements: entitlementsPlistWithoutEntitlements,
);
});
test('returns a validation issue with a fix', () async {
test('returns validation issues with fixes', () async {
final validationResults = await runWithOverrides(
() => validator.validate(),
);
expect(validationResults, hasLength(1));
final issue = validationResults[0];
expect(issue.severity, ValidationIssueSeverity.error);
expect(validationResults, hasLength(2));
final networkIssue = validationResults[0];
expect(networkIssue.severity, ValidationIssueSeverity.error);
expect(
issue.message,
networkIssue.message,
contains(
'''is missing the Outgoing Connections (com.apple.security.network.client) entitlement.''',
),
);
expect(issue.fix, isNotNull);
expect(networkIssue.fix, isNotNull);
expect(
MacosNetworkEntitlementValidator.hasNetworkClientEntitlement(
MacosEntitlementsValidator.hasNetworkClientEntitlement(
plistFile: releaseEntitlementsFile(),
),
isFalse,
);
runWithOverrides(() => issue.fix!());
runWithOverrides(() => networkIssue.fix!());
expect(
MacosNetworkEntitlementValidator.hasNetworkClientEntitlement(
MacosEntitlementsValidator.hasNetworkClientEntitlement(
plistFile: releaseEntitlementsFile(),
),
isTrue,
);
final unsignedMemoryIssue = validationResults[1];
expect(unsignedMemoryIssue.severity, ValidationIssueSeverity.error);
expect(
unsignedMemoryIssue.message,
contains(
'''is missing the Allow Unsigned Executable Memory (com.apple.security.cs.allow-unsigned-executable-memory) entitlement.''',
),
);
expect(unsignedMemoryIssue.fix, isNotNull);
expect(
MacosEntitlementsValidator
.hasAllowUnsignedExecutableMemoryEntitlement(
plistFile: releaseEntitlementsFile(),
),
isFalse,
);
runWithOverrides(() => unsignedMemoryIssue.fix!());
expect(
MacosEntitlementsValidator
.hasAllowUnsignedExecutableMemoryEntitlement(
plistFile: releaseEntitlementsFile(),
),
isTrue,
@@ -165,9 +194,11 @@ void main() {
});
});
group('when network client entitlement is present', () {
group('when entitlements are present', () {
setUp(() {
setUpProjectRoot(entitlements: entitlementsPlistWithEntitlement);
setUpProjectRoot(
entitlements: entitlementsPlistWithAllEntitlements,
);
});
test('returns an empty list', () async {
@@ -176,97 +207,5 @@ void main() {
});
});
});
group('fix', () {
group('when the network client entitlement is missing', () {
setUp(() {
setUpProjectRoot(entitlements: entitlementsPlistWithoutEntitlement);
});
test('adds the network client entitlement to the entitlements plist',
() {
expect(
MacosNetworkEntitlementValidator.hasNetworkClientEntitlement(
plistFile: releaseEntitlementsFile(),
),
isFalse,
);
MacosNetworkEntitlementValidator.addNetworkEntitlementToPlist(
releaseEntitlementsFile(),
);
expect(
MacosNetworkEntitlementValidator.hasNetworkClientEntitlement(
plistFile: releaseEntitlementsFile(),
),
isTrue,
);
});
});
group('when the network client entitlement is present', () {
setUp(() {
setUpProjectRoot(entitlements: entitlementsPlistWithEntitlement);
});
test('does not modify the entitlements plist', () {
final plistContents = releaseEntitlementsFile().readAsStringSync();
expect(
MacosNetworkEntitlementValidator.hasNetworkClientEntitlement(
plistFile: releaseEntitlementsFile(),
),
isTrue,
);
MacosNetworkEntitlementValidator.addNetworkEntitlementToPlist(
releaseEntitlementsFile(),
);
expect(
MacosNetworkEntitlementValidator.hasNetworkClientEntitlement(
plistFile: releaseEntitlementsFile(),
),
isTrue,
);
final updatedPlistContents =
releaseEntitlementsFile().readAsStringSync();
expect(updatedPlistContents, plistContents);
});
});
});
group('plistHasNetworkClientEntitlement', () {
group('when entitlement is present', () {
setUp(() {
setUpProjectRoot(entitlements: entitlementsPlistWithEntitlement);
});
test('returns true', () {
expect(
MacosNetworkEntitlementValidator.hasNetworkClientEntitlement(
plistFile: releaseEntitlementsFile(),
),
isTrue,
);
});
});
group('when entitlement is not present', () {
setUp(() {
setUpProjectRoot(entitlements: entitlementsPlistWithoutEntitlement);
});
test('returns false', () {
expect(
MacosNetworkEntitlementValidator.hasNetworkClientEntitlement(
plistFile: releaseEntitlementsFile(),
),
isFalse,
);
});
});
});
});
}