mirror of
https://github.com/morluto/rea.git
synced 2026-10-21 05:19:56 -05:00
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:
1 parent
f1f5582ce8
commit
ec3db37457
3 files changed
+100
-43
No files matched your search
@@ -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",
|
||||
|
||||
Reference in new issue
Block a user