fix(native): project decoded entitlement scalars through plist owners
Consume shared value and exact-number projections before JSON validation so decoded plist scalars cannot leak as ordinary dictionaries. Carry projection limitations into signature evidence with entitlement context. Related: #4906. Controlled command-runner regression passed. Real codesign refused the authored non-finite entitlement; this does not claim real entitlement support. Existing real signature CLI controls passed.
This commit is contained in:
1 parent
6c98708793
commit
69324b0f86
2 files changed
+80
-4
No files matched your search
@@ -61,6 +61,14 @@ import {
|
||||
resolvedAnalysisError,
|
||||
} from "../domain/analysisErrorBase.js";
|
||||
import { jsonValueSchema, type JsonValue } from "../domain/jsonValue.js";
|
||||
import {
|
||||
projectPlistValue,
|
||||
nonFiniteRealLimitation,
|
||||
unknownRealLimitation,
|
||||
negativeZeroRealLimitation,
|
||||
unknownDateLimitation,
|
||||
} from "../domain/apple/plistValue.js";
|
||||
import { createPlistNumberProjection } from "../domain/apple/plistNumbers.js";
|
||||
import type { NativeUiObservationResult } from "../domain/native/nativeUiObservation.js";
|
||||
import {
|
||||
omittedPrototypeKeysLimitation,
|
||||
@@ -648,6 +656,7 @@ class NativeMacOSClient implements AnalysisClient {
|
||||
const captures = [display.value, requirements.value, entitlements.value];
|
||||
const limitations = [
|
||||
...parsed.limitations,
|
||||
...entitlementValue.limitations.map((note) => `Entitlements: ${note}`),
|
||||
"Inspected a digest-verified Mach-O byte snapshot; filesystem-bound signing context and trust validation are not examined.",
|
||||
...(entitlementValue.omittedPrototypeKeys === 0
|
||||
? []
|
||||
@@ -1199,14 +1208,40 @@ const invocation = (
|
||||
|
||||
const parseEntitlements = (
|
||||
output: string,
|
||||
): { readonly value: JsonValue; readonly omittedPrototypeKeys: number } => {
|
||||
): {
|
||||
readonly value: JsonValue;
|
||||
readonly omittedPrototypeKeys: number;
|
||||
readonly limitations: readonly string[];
|
||||
} => {
|
||||
const start = output.indexOf("<?xml");
|
||||
const end = output.lastIndexOf("</plist>");
|
||||
if (start < 0 || end < start) return { value: null, omittedPrototypeKeys: 0 };
|
||||
const { value, omittedPrototypeKeys } = parseXmlPropertyList(
|
||||
if (start < 0 || end < start)
|
||||
return { value: null, omittedPrototypeKeys: 0, limitations: [] };
|
||||
const decoded = parseXmlPropertyList(
|
||||
output.slice(start, end + "</plist>".length),
|
||||
);
|
||||
return { value: jsonValueSchema.parse(value), omittedPrototypeKeys };
|
||||
const projected = projectPlistValue(decoded.value);
|
||||
const numbers = createPlistNumberProjection(decoded.numberLiterals);
|
||||
const value = numbers.project(projected.value);
|
||||
return {
|
||||
value,
|
||||
omittedPrototypeKeys: decoded.omittedPrototypeKeys,
|
||||
limitations: [
|
||||
...numbers.limitations(),
|
||||
...(projected.nonFiniteRealCount === 0
|
||||
? []
|
||||
: [nonFiniteRealLimitation(projected.nonFiniteRealCount)]),
|
||||
...(projected.unknownRealCount === 0
|
||||
? []
|
||||
: [unknownRealLimitation(projected.unknownRealCount)]),
|
||||
...(projected.negativeZeroRealCount === 0
|
||||
? []
|
||||
: [negativeZeroRealLimitation(projected.negativeZeroRealCount)]),
|
||||
...(projected.unknownDateCount === 0
|
||||
? []
|
||||
: [unknownDateLimitation(projected.unknownDateCount)]),
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
/** plutil prefixes its diagnostic with the path the message already names. */
|
||||
|
||||
@@ -74,6 +74,47 @@ describe("native signature target identity", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("projects typed plist scalars from a controlled entitlement response", async () => {
|
||||
const { path, target } = await fixture();
|
||||
const replay = new NativeFixtureRunner();
|
||||
const xml = `<?xml version="1.0"?><plist><dict>
|
||||
<key>positive</key><real>+infinity</real>
|
||||
<key>negative</key><real>-inf</real>
|
||||
<key>nan</key><real>nan</real>
|
||||
<key>integer</key><integer>9007199254740993</integer>
|
||||
<key>date</key><date>2001-01-01T00:00:00Z</date>
|
||||
<key>data</key><data>AQID</data><key>control</key><true/>
|
||||
</dict></plist>`;
|
||||
const runner: NativeCommandRunner = {
|
||||
run(tool, args) {
|
||||
return args.includes("--entitlements")
|
||||
? new NativeFixtureRunner({ codesign: xml }).run(tool, args)
|
||||
: replay.run(tool, args);
|
||||
},
|
||||
};
|
||||
const result = await new NativeMacOSProvider({}, runner, "darwin")
|
||||
.createClient(target)
|
||||
.execute("inspect_signature", {});
|
||||
if (!result.ok) throw result.error;
|
||||
const signature = inspectSignatureSchema.parse(result.value.result);
|
||||
expect(signature.entitlements).toEqual({
|
||||
positive: { $plist_type: "real", value: "+infinity" },
|
||||
negative: { $plist_type: "real", value: "-infinity" },
|
||||
nan: { $plist_type: "real", value: "nan" },
|
||||
integer: { $plist_type: "integer", decimal: "9007199254740993" },
|
||||
date: { $plist_type: "date", iso8601: "2001-01-01T00:00:00.000Z" },
|
||||
data: { $plist_type: "data", base64: "AQID" },
|
||||
control: true,
|
||||
});
|
||||
expect(signature.limitations).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.stringContaining("Entitlements: 3 non-finite real value(s)"),
|
||||
expect.stringContaining("unambiguous integer value(s)"),
|
||||
]),
|
||||
);
|
||||
expect(await readFile(path, "utf8")).toBe(CONTENT);
|
||||
});
|
||||
|
||||
it.each(["changed", "missing", "directory", "symlink"] as const)(
|
||||
"rejects a %s target before commands run",
|
||||
async (replacement) => {
|
||||
|
||||
Reference in new issue
Block a user