feat: add validator to check that macOS app allows unsigned executable memory (#2806)
This commit is contained in:
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
+50
-111
@@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user