mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 14:15:24 +00:00
feat: support FIDO2 security keys through ssh agent
This commit is contained in:
1 parent
901a235de4
commit
3b3fd7f135
6 files changed
+324
-1
No files matched your search
@@ -0,0 +1,149 @@
|
||||
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,
|
||||
};
|
||||
Reference in new issue
Block a user