fix(native): preserve literal line breaks in signing identifiers (#2107)

* fix(native): preserve literal line breaks in signing identifiers

* fix(native): infer codesign framing without a matching path
This commit is contained in:
Yijie Lu authored and GitHub committed 2026-10-13 18:22:34 +08:00
1 parent f1f5582ce8
commit ec3db37457
3 files changed
+100 -43

No files matched your search

+26 -12
View File
@@ -16,14 +16,11 @@ export const parseCodeSignature = (
unsigned: boolean,
executablePaths: readonly string[] = [],
): Omit<InspectSignature, "provenance"> => {
const { identifier, fields } = splitEchoedValues(
output.replaceAll("\r\n", "\n"),
executablePaths,
);
const { identifier, fields } = splitEchoedValues(output, executablePaths);
const values = new Map<string, string>();
const authorities: string[] = [];
const cdhashes: string[] = [];
for (const line of fields.split("\n")) {
for (const line of fields.split(/\r?\n/u)) {
if (line.startsWith("CodeDirectory ")) {
values.set("CodeDirectory", line.slice("CodeDirectory ".length));
continue;
@@ -65,12 +62,20 @@ const splitEchoedValues = (
text: string,
executablePaths: readonly string[],
): { readonly identifier: string | undefined; readonly fields: string } => {
const executable = executablePaths
.map((path) => `Executable=${path}\n`)
.find((line) => text.startsWith(line));
let rest = text;
if (executable !== undefined) rest = text.slice(executable.length);
else if (text.startsWith("Executable=")) {
// Frame the exact source path before interpreting record separators. A CRLF
// inside that path or the signing identifier is payload, not a line ending.
let newline: string | undefined;
for (const path of executablePaths) {
const prefix = `Executable=${path}`;
if (!text.startsWith(prefix)) continue;
const separator = /^\r?\n/u.exec(text.slice(prefix.length))?.[0];
if (separator === undefined) continue;
newline = separator;
rest = text.slice(prefix.length + separator.length);
break;
}
if (rest === text && text.startsWith("Executable=")) {
const identifierLine = text.indexOf("\nIdentifier=");
rest = identifierLine < 0 ? text : text.slice(identifierLine + 1);
}
@@ -79,8 +84,17 @@ const splitEchoedValues = (
// Later fields come from the signature itself, so the last `Format=` line
// is the one codesign printed after the identifier.
const format = rest.lastIndexOf("\nFormat=");
const lineEnd = rest.indexOf("\n");
const end = format >= 0 ? format : lineEnd >= 0 ? lineEnd : rest.length;
// Without an exact path, use the producer-owned Format record's terminator.
// Looking inside Identifier instead would confuse literal CRLF payload with
// record framing, or strip a literal trailing CR from an LF-delimited value.
if (newline === undefined && format >= 0)
newline = /\r?\n/u.exec(rest.slice(format + 1))?.[0];
const boundary = format >= 0 ? format : rest.indexOf("\n");
const crlf = newline === undefined || newline === "\r\n";
const end =
boundary < 0
? rest.length
: boundary - (crlf && rest[boundary - 1] === "\r" ? 1 : 0);
return {
identifier: rest.slice("Identifier=".length, end),
fields: rest.slice(end),
@@ -21,14 +21,14 @@ const adHocProgram = async (directory: string, name: string) => {
describe.skipIf(process.platform !== "darwin")(
"native signature CLI with echoed path text",
() => {
cliTest(
"does not read signature fields from the file name codesign echoes",
async ({ cli }) => {
cliTest.for(["\n", "\r\n"])(
"does not read signature fields from the file name codesign echoes (%j)",
async (newline, { cli }) => {
const directory = await createTestTempDirectory("rea-signature-");
const forged = "Developer ID Application: Example (ABCDE12345)";
const program = await adHocProgram(
directory,
`tool\nAuthority=${forged}\nTeamIdentifier=ABCDE12345`,
`tool${newline}Authority=${forged}${newline}Identifier=forged${newline}TeamIdentifier=ABCDE12345`,
);
const unsignedName = await adHocProgram(
directory,
@@ -75,7 +75,7 @@ describe.skipIf(process.platform !== "darwin")(
normalized_result: {
identifier: expect.stringMatching(
new RegExp(
`^tool\\nAuthority=${forged.replace(/[()]/gu, "\\$&")}\\nTeamIdentifier=ABCDE12345-`,
`^tool${newline}Authority=${forged.replace(/[()]/gu, "\\$&")}${newline}Identifier=forged${newline}TeamIdentifier=ABCDE12345-`,
"u",
),
),
@@ -61,7 +61,13 @@ afterEach(async () => {
});
describe("native macOS provider discovery", () => {
it.each(["ordinary.fixture", " spaced.fixture ", " leading", "trailing "])(
it.each([
"ordinary.fixture",
" spaced.fixture ",
" leading",
"trailing ",
"literal\r\nidentifier\r",
])(
"preserves the exact signing identifier %j reported by codesign",
(identifier) => {
const parsed = parseCodeSignature(
@@ -74,31 +80,68 @@ describe("native macOS provider discovery", () => {
},
);
it("separates echoed path and identifier text from signature fields", () => {
const path = "/apps/x\nAuthority=Forged\nIdentifier=forged";
const identifier = "x\nFormat=forged\nAuthority=Forged-5555";
const parsed = parseCodeSignature(
[
`Executable=${path}`,
`Identifier=${identifier}`,
"Format=Mach-O thin (arm64)",
"CodeDirectory v=20400 size=342 flags=0x2(adhoc) hashes=2+2",
"CDHash=6aee90e6",
"Signature=adhoc",
"TeamIdentifier=not set",
"",
].join("\r\n"),
false,
["/elsewhere", path],
);
expect(parsed).toMatchObject({
identifier,
format: "Mach-O thin (arm64)",
authorities: [],
cdhashes: ["6aee90e6"],
team_identifier: null,
});
});
it.each(["\n", "\r\n"])(
"separates echoed path and identifier text from signature fields (%j)",
(newline) => {
const path = "/apps/x\r\nAuthority=Forged\nIdentifier=forged";
const identifier = "x\r\nFormat=forged\nAuthority=Forged-5555\r";
const parsed = parseCodeSignature(
[
`Executable=${path}`,
`Identifier=${identifier}`,
"Format=Mach-O thin (arm64)",
"CodeDirectory v=20400 size=342 flags=0x2(adhoc) hashes=2+2",
"CDHash=6aee90e6",
"Signature=adhoc",
"TeamIdentifier=not set",
"",
].join(newline),
false,
["/elsewhere", path],
);
expect(parsed).toMatchObject({
identifier,
format: "Mach-O thin (arm64)",
authorities: [],
cdhashes: ["6aee90e6"],
team_identifier: null,
});
},
);
it.each([
{ executable: "", paths: [], newline: "\r\n", identifier: "fixture" },
{
executable: "Executable=/apps/fixture\r\n",
paths: [],
newline: "\r\n",
identifier: "fixture\nFormat=payload\r",
},
{
executable: "Executable=/apps/fixture\n",
paths: ["/elsewhere"],
newline: "\n",
identifier: "fixture\r\nFormat=payload\r",
},
{
executable: "Executable=/apps/fixture\r\n",
paths: ["/elsewhere"],
newline: "\r\n",
identifier: "fixture\r",
},
])(
"frames the identifier without a matching executable path: %j",
({ executable, paths, newline, identifier }) => {
const parsed = parseCodeSignature(
`${executable}Identifier=${identifier}${newline}Format=Mach-O thin (arm64)${newline}TeamIdentifier=not set${newline}`,
false,
paths,
);
expect(parsed.identifier).toBe(identifier);
expect(parsed.format).toBe("Mach-O thin (arm64)");
expect(parsed.team_identifier).toBeNull();
},
);
it.each(["timeout", "output-limit"] as const)(
"preserves %s resolution failures with their captured output and cleanup status",