Files
Termix/scripts/patch-ssh2-sk.cjs
T

150 lines
4.5 KiB
JavaScript

const fs = require("node:fs");
const SK_ED = "sk-ssh-ed25519@openssh.com";
const SK_EC = "sk-ecdsa-sha2-nistp256@openssh.com";
function replaceOnce(source, from, to, label) {
const first = source.indexOf(from);
if (first === -1 || source.indexOf(from, first + 1) !== -1) {
throw new Error(`ssh2 changed (${label}); review the security key patch`);
}
return source.replace(from, () => to);
}
function patchKeyParserSource(source) {
if (source.includes("// termix-sk")) return source;
let out = replaceOnce(
source,
` case 'ecdsa-sha2-nistp521':
return true;
case 'ssh-ed25519':
if (eddsaSupported)
return true;`,
` case 'ecdsa-sha2-nistp521':
return true;
case '${SK_EC}': // termix-sk
return true;
case '${SK_ED}':
case 'ssh-ed25519':
if (eddsaSupported)
return true;`,
"key types",
);
out = replaceOnce(
out,
` default:
return new Error(\`Unsupported OpenSSH public key type: \${baseType}\`);
}
return new OpenSSH_Public(`,
` case '${SK_ED}':
case '${SK_EC}': {
// Public blob only; the security key signs, so there is no private half.
const rest = data.subarray(data._pos || 0);
const typeLen = Buffer.byteLength(baseType);
pubSSH = Buffer.allocUnsafe(4 + typeLen + rest.length);
writeUInt32BE(pubSSH, typeLen, 0);
pubSSH.utf8Write(baseType, 4, typeLen);
pubSSH.set(rest, 4 + typeLen);
algo = baseType === '${SK_ED}' ? null : 'sha256';
break;
}
default:
return new Error(\`Unsupported OpenSSH public key type: \${baseType}\`);
}
return new OpenSSH_Public(`,
"parseDER",
);
return out;
}
function patchAgentSource(source) {
if (source.includes("skTrailer")) return source;
return replaceOnce(
source,
` binaryParser.init(signature, 0);
binaryParser.readString(true);
signature = binaryParser.readString();
binaryParser.clear();`,
` binaryParser.init(signature, 0);
binaryParser.readString(true);
signature = binaryParser.readString();
// Security key signatures end with flags and a counter.
const skTrailer = signature === undefined
? undefined
: binaryParser.readRaw();
binaryParser.clear();
if (skTrailer && skTrailer.length)
signature.skTrailer = Buffer.from(skTrailer);`,
"agent sign response",
);
}
function patchProtocolSource(source) {
if (source.includes("skTrailer")) return source;
const start = source.indexOf(" authPK(username, pubKey, keyAlgo, cbSign) {");
const end = source.indexOf(" authHostbased(", start);
if (start === -1 || end === -1) {
throw new Error("ssh2 changed (authPK); review the security key patch");
}
let region = source.slice(start, end);
region = replaceOnce(
region,
` signature = convertSignature(signature, keyType);`,
` const skTrailer = signature && signature.skTrailer;
signature = convertSignature(signature, keyType);`,
"authPK sign",
);
region = replaceOnce(
region,
` + 4 + algoLen + 4 + sigLen
);`,
` + 4 + algoLen + 4 + sigLen + (skTrailer ? skTrailer.length : 0)
);`,
"authPK alloc",
);
region = replaceOnce(
region,
` writeUInt32BE(packet, 4 + algoLen + 4 + sigLen, p += pubKeyLen);`,
` writeUInt32BE(
packet,
4 + algoLen + 4 + sigLen + (skTrailer ? skTrailer.length : 0),
p += pubKeyLen
);`,
"authPK length",
);
region = replaceOnce(
region,
` packet.set(signature, p += 4);`,
` packet.set(signature, p += 4);
if (skTrailer)
packet.set(skTrailer, p + sigLen);`,
"authPK copy",
);
return source.slice(0, start) + region + source.slice(end);
}
if (require.main === module) {
const targets = [
["ssh2/lib/protocol/keyParser.js", patchKeyParserSource],
["ssh2/lib/agent.js", patchAgentSource],
["ssh2/lib/protocol/Protocol.js", patchProtocolSource],
];
for (const [id, patch] of targets) {
const target = require.resolve(id);
const source = fs.readFileSync(target, "utf8");
const result = patch(source);
if (result !== source) fs.writeFileSync(target, result);
}
console.log(
"[patch-ssh2-sk] Security key public keys and signatures enabled",
);
}
module.exports = {
patchKeyParserSource,
patchAgentSource,
patchProtocolSource,
};