mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-17 21:10:54 +00:00
fix ssh sandbox key cp (#48924)
Signed-off-by: sallyom <somalley@redhat.com>
This commit is contained in:
@@ -39,10 +39,52 @@ describe("sandbox ssh helpers", () => {
|
||||
expect(config).toContain("UpdateHostKeys no");
|
||||
|
||||
const configDir = session.configPath.slice(0, session.configPath.lastIndexOf("/"));
|
||||
expect(await fs.readFile(`${configDir}/identity`, "utf8")).toBe("PRIVATE KEY");
|
||||
expect(await fs.readFile(`${configDir}/certificate.pub`, "utf8")).toBe("SSH CERT");
|
||||
expect(await fs.readFile(`${configDir}/identity`, "utf8")).toBe("PRIVATE KEY\n");
|
||||
expect(await fs.readFile(`${configDir}/certificate.pub`, "utf8")).toBe("SSH CERT\n");
|
||||
expect(await fs.readFile(`${configDir}/known_hosts`, "utf8")).toBe(
|
||||
"example.com ssh-ed25519 AAAATEST",
|
||||
"example.com ssh-ed25519 AAAATEST\n",
|
||||
);
|
||||
});
|
||||
|
||||
it("normalizes CRLF and escaped-newline private keys before writing temp files", async () => {
|
||||
const session = await createSshSandboxSessionFromSettings({
|
||||
command: "ssh",
|
||||
target: "peter@example.com:2222",
|
||||
strictHostKeyChecking: true,
|
||||
updateHostKeys: false,
|
||||
identityData:
|
||||
"-----BEGIN OPENSSH PRIVATE KEY-----\\nbGluZTE=\\r\\nbGluZTI=\\r\\n-----END OPENSSH PRIVATE KEY-----",
|
||||
knownHostsData: "example.com ssh-ed25519 AAAATEST",
|
||||
});
|
||||
sessions.push(session);
|
||||
|
||||
const configDir = session.configPath.slice(0, session.configPath.lastIndexOf("/"));
|
||||
expect(await fs.readFile(`${configDir}/identity`, "utf8")).toBe(
|
||||
"-----BEGIN OPENSSH PRIVATE KEY-----\n" +
|
||||
"bGluZTE=\n" +
|
||||
"bGluZTI=\n" +
|
||||
"-----END OPENSSH PRIVATE KEY-----\n",
|
||||
);
|
||||
});
|
||||
|
||||
it("normalizes mixed real and escaped newlines in private keys", async () => {
|
||||
const session = await createSshSandboxSessionFromSettings({
|
||||
command: "ssh",
|
||||
target: "peter@example.com:2222",
|
||||
strictHostKeyChecking: true,
|
||||
updateHostKeys: false,
|
||||
identityData:
|
||||
"-----BEGIN OPENSSH PRIVATE KEY-----\nline-1\\nline-2\n-----END OPENSSH PRIVATE KEY-----",
|
||||
knownHostsData: "example.com ssh-ed25519 AAAATEST",
|
||||
});
|
||||
sessions.push(session);
|
||||
|
||||
const configDir = session.configPath.slice(0, session.configPath.lastIndexOf("/"));
|
||||
expect(await fs.readFile(`${configDir}/identity`, "utf8")).toBe(
|
||||
"-----BEGIN OPENSSH PRIVATE KEY-----\n" +
|
||||
"line-1\n" +
|
||||
"line-2\n" +
|
||||
"-----END OPENSSH PRIVATE KEY-----\n",
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -35,6 +35,35 @@ export type RunSshSandboxCommandParams = {
|
||||
tty?: boolean;
|
||||
};
|
||||
|
||||
function normalizeInlineSshMaterial(contents: string, filename: string): string {
|
||||
const withoutBom = contents.replace(/^\uFEFF/, "");
|
||||
const normalizedNewlines = withoutBom.replace(/\r\n?/g, "\n");
|
||||
const normalizedEscapedNewlines = normalizedNewlines
|
||||
.replace(/\\r\\n/g, "\\n")
|
||||
.replace(/\\r/g, "\\n");
|
||||
const expanded =
|
||||
filename === "identity" || filename === "certificate.pub"
|
||||
? normalizedEscapedNewlines.replace(/\\n/g, "\n")
|
||||
: normalizedEscapedNewlines;
|
||||
return expanded.endsWith("\n") ? expanded : `${expanded}\n`;
|
||||
}
|
||||
|
||||
function buildSshFailureMessage(stderr: string, exitCode?: number): string {
|
||||
const trimmed = stderr.trim();
|
||||
if (
|
||||
trimmed.includes("error in libcrypto") &&
|
||||
(trimmed.includes('Load key "') || trimmed.includes("Permission denied (publickey)"))
|
||||
) {
|
||||
return `${trimmed}\nSSH sandbox failed to load the configured identity. The private key contents may be malformed (for example CRLF or escaped newlines). Prefer identityFile when possible.`;
|
||||
}
|
||||
return (
|
||||
trimmed ||
|
||||
(exitCode !== undefined
|
||||
? `ssh exited with code ${exitCode}`
|
||||
: "ssh exited with a non-zero status")
|
||||
);
|
||||
}
|
||||
|
||||
export function shellEscape(value: string): string {
|
||||
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
||||
}
|
||||
@@ -201,14 +230,11 @@ export async function runSshSandboxCommand(
|
||||
const exitCode = code ?? 0;
|
||||
if (exitCode !== 0 && !params.allowFailure) {
|
||||
reject(
|
||||
Object.assign(
|
||||
new Error(stderr.toString("utf8").trim() || `ssh exited with code ${exitCode}`),
|
||||
{
|
||||
code: exitCode,
|
||||
stdout,
|
||||
stderr,
|
||||
},
|
||||
),
|
||||
Object.assign(new Error(buildSshFailureMessage(stderr.toString("utf8"), exitCode)), {
|
||||
code: exitCode,
|
||||
stdout,
|
||||
stderr,
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -328,7 +354,10 @@ async function writeSecretMaterial(
|
||||
contents: string,
|
||||
): Promise<string> {
|
||||
const pathname = path.join(dir, filename);
|
||||
await fs.writeFile(pathname, contents, { encoding: "utf8", mode: 0o600 });
|
||||
await fs.writeFile(pathname, normalizeInlineSshMaterial(contents, filename), {
|
||||
encoding: "utf8",
|
||||
mode: 0o600,
|
||||
});
|
||||
await fs.chmod(pathname, 0o600);
|
||||
return pathname;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user