-
Notifications
You must be signed in to change notification settings - Fork 679
Client: Allow explicitely specifying a publicKey #808
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
This is to support SSH certificates. As before the privateKey will be used for the publicKey (i.e. the derived publicKey) if nothing is given. The given publicKey is checked to match the given privateKey.
180e91e
to
db08164
Compare
Can this get merged in? |
confirm this is worked for me with Vault module Signed SSH Certificates |
Tested this out with the latest mscdex/ssh2:master and two sets of user certificates (
Removed the check
Upon closer examination, |
I did not realize that mscdex/ssh2-streams#137 was still open. Merged those changes into my local workspace and everything works perfectly. Please disregard the last comment. |
Ping: Any chance we can get this merged soon? |
I haven't forgotten about this feature, however the PR will need to be updated to match the internal changes in v1.0.0. |
Whoops. Appears I missed the email notification about this comment. @mscdex Will you handle this, please? You know the internals better than I do and it's almost 2 years since I last touched it myself. |
Is there any chance this to be merged soon? I'm considering using this module, but it's lacking authentication with signed public key. |
Hi all, We also need this feature urgently. Email me via [email protected] if you are interested. Thanks! |
@david-autonoma This PR is way outdated and would likely need to be adapted to the 1.0 release of the library. This PR was developed on company time, where we currently don't have a need for this feature (as we've worked around it internally). I'm afraid the costs of relearning how the current version of this library works after 4 years alone would likely exceed the offered compensation. |
I don't remember the details after 4 years. I believe we used a regular public / private key pair, because while using certificates would have been nicer, the cost of maintaining a fork of this library would not have been worth it. |
Disclaimer: I have no idea what I am doing. I just hit this stuff until it started working. I would like to use OpenSSH certificates. Thus I looked at the state of this PR and mscdex/ssh2-streams#137 and tried to get something to work on latest master (commit dd5510c). For testing, I used the "uptime" example from the readme (and added Hopefully this motivates someone with more knowledge to work again on this and make it work properly. It seems to me that someone with some knowledge about the SSH protocol should be able to get this to work relatively easily. The resulting patchdiff --git a/lib/client.js b/lib/client.js
index aa94ace..e0678f3 100644
--- a/lib/client.js
+++ b/lib/client.js
@@ -84,6 +84,7 @@ class Client extends EventEmitter {
username: undefined,
password: undefined,
privateKey: undefined,
+ publicKey: undefined,
tryKeyboard: undefined,
agent: undefined,
allowAgentFwd: undefined,
@@ -209,6 +210,10 @@ class Client extends EventEmitter {
|| Buffer.isBuffer(cfg.privateKey)
? cfg.privateKey
: undefined);
+ this.config.publicKey = (typeof cfg.publicKey === 'string'
+ || Buffer.isBuffer(cfg.publicKey)
+ ? cfg.publicKey
+ : undefined);
this.config.localHostname = (typeof cfg.localHostname === 'string'
? cfg.localHostname
: undefined);
@@ -253,7 +258,7 @@ class Client extends EventEmitter {
this._agentFwdEnabled = false;
this._agent = (this.config.agent ? this.config.agent : undefined);
this._remoteVer = undefined;
- let privateKey;
+ let privateKey, publicKey;
if (this.config.privateKey) {
privateKey = parseKey(this.config.privateKey, cfg.passphrase);
@@ -270,6 +275,22 @@ class Client extends EventEmitter {
}
}
+ if (this.config.publicKey) {
+ publicKey = parseKey(this.config.publicKey);
+ if (publicKey instanceof Error)
+ throw new Error('Cannot parse publicKey: ' + publicKey.message);
+ if (Array.isArray(publicKey))
+ publicKey = publicKey[0]; // OpenSSH's newer format only stores 1 key for now
+ if (publicKey.getPublicSSH() === null)
+ throw new Error('publicKey value does not contain a (valid) public key');
+ if (publicKey.getPublicPEM() !== privateKey.getPublicPEM()) {
+ throw new Error('publicKey does not belong to the private key');
+ }
+ }
+ else {
+ publicKey = privateKey;
+ }
+
let hostVerifier;
if (typeof cfg.hostVerifier === 'function') {
const hashCb = cfg.hostVerifier;
@@ -462,7 +483,7 @@ class Client extends EventEmitter {
});
} else if (curAuth.type === 'publickey') {
proto.authPK(curAuth.username, curAuth.key, keyAlgo, (buf, cb) => {
- const signature = curAuth.key.sign(buf, hashAlgo);
+ const signature = privateKey.sign(buf, hashAlgo);
if (signature instanceof Error) {
signature.message =
`Error signing data with key: ${signature.message}`;
@@ -836,7 +857,7 @@ class Client extends EventEmitter {
const authsAllowed = ['none'];
if (this.config.password !== undefined)
authsAllowed.push('password');
- if (privateKey !== undefined)
+ if (privateKey !== undefined && publicKey !== undefined)
authsAllowed.push('publickey');
if (this._agent !== undefined)
authsAllowed.push('agent');
@@ -882,13 +903,13 @@ class Client extends EventEmitter {
nextAuth = { type, username, password: this.config.password };
break;
case 'publickey':
- nextAuth = { type, username, key: privateKey };
+ nextAuth = { type, username, key: publicKey };
break;
case 'hostbased':
nextAuth = {
type,
username,
- key: privateKey,
+ key: publicKey,
localHostname: this.config.localHostname,
localUsername: this.config.localUsername,
};
diff --git a/lib/protocol/Protocol.js b/lib/protocol/Protocol.js
index 7302488..654cceb 100644
--- a/lib/protocol/Protocol.js
+++ b/lib/protocol/Protocol.js
@@ -697,15 +697,18 @@ class Protocol {
}
cbSign(packet, (signature) => {
- signature = convertSignature(signature, keyType);
- if (signature === false)
+ var converted = convertSignature(signature, keyType);
+ if (converted === false)
throw new Error('Error while converting handshake signature');
+ signature = converted.signature;
+ var signatureType = converted.keyType;
+ var signatureTypeLen = Buffer.byteLength(signatureType);
const sigLen = signature.length;
p = this._packetRW.write.allocStart;
packet = this._packetRW.write.alloc(
1 + 4 + userLen + 4 + 14 + 4 + 9 + 1 + 4 + algoLen + 4 + pubKeyLen + 4
- + 4 + algoLen + 4 + sigLen
+ + 4 + signatureTypeLen + 4 + sigLen
);
// TODO: simply copy from original "packet" to new `packet` to avoid
@@ -729,12 +732,12 @@ class Protocol {
writeUInt32BE(packet, pubKeyLen, p += algoLen);
packet.set(pubKey, p += 4);
- writeUInt32BE(packet, 4 + algoLen + 4 + sigLen, p += pubKeyLen);
+ writeUInt32BE(packet, 4 + signatureTypeLen + 4 + sigLen, p += pubKeyLen);
- writeUInt32BE(packet, algoLen, p += 4);
- packet.utf8Write(keyAlgo, p += 4, algoLen);
+ writeUInt32BE(packet, signatureTypeLen, p += 4);
+ packet.utf8Write(signatureType, p += 4, signatureTypeLen);
- writeUInt32BE(packet, sigLen, p += algoLen);
+ writeUInt32BE(packet, sigLen, p += signatureTypeLen);
packet.set(signature, p += 4);
// Servers shouldn't send packet type 60 in response to signed publickey
@@ -806,9 +809,10 @@ class Protocol {
data.utf8Write(userlocal, p += 4, userlocalLen);
cbSign(data, (signature) => {
- signature = convertSignature(signature, keyType);
- if (!signature)
+ var converted = convertSignature(signature, keyType);
+ if (!converted)
throw new Error('Error while converting handshake signature');
+ signature = converted.signature;
const sigLen = signature.length;
const reqDataLen = (data.length - sesLen - 4);
diff --git a/lib/protocol/keyParser.js b/lib/protocol/keyParser.js
index a276c1a..085ff99 100644
--- a/lib/protocol/keyParser.js
+++ b/lib/protocol/keyParser.js
@@ -1210,6 +1210,12 @@ OpenSSH_Public.prototype = BaseKey;
if (type === undefined || type.indexOf(baseType) !== 0)
return new Error('Malformed OpenSSH public key');
+ if (/-cert-v0[01]@openssh.com/.test(type)) {
+ var nonce = readString(data, data._pos);
+ if (nonce === false) {
+ return new Error('Malformed OpenSSH certificate');
+ }
+ }
return parseDER(data, baseType, comment, fullType);
};
}
@@ -1327,7 +1333,7 @@ function parseDER(data, baseType, comment, fullType) {
if (n === undefined)
return new Error('Malformed OpenSSH public key');
pubPEM = genOpenSSLRSAPub(n, e);
- pubSSH = genOpenSSHRSAPub(n, e);
+ pubSSH = data;
algo = 'sha1';
break;
}
@@ -1345,7 +1351,7 @@ function parseDER(data, baseType, comment, fullType) {
if (y === undefined)
return new Error('Malformed OpenSSH public key');
pubPEM = genOpenSSLDSAPub(p, q, g, y);
- pubSSH = genOpenSSHDSAPub(p, q, g, y);
+ pubSSH = data;
algo = 'sha1';
break;
}
@@ -1354,7 +1360,7 @@ function parseDER(data, baseType, comment, fullType) {
if (edpub === undefined || edpub.length !== 32)
return new Error('Malformed OpenSSH public key');
pubPEM = genOpenSSLEdPub(edpub);
- pubSSH = genOpenSSHEdPub(edpub);
+ pubSSH = data;
algo = null;
break;
}
@@ -1380,7 +1386,7 @@ function parseDER(data, baseType, comment, fullType) {
if (ecpub === undefined)
return new Error('Malformed OpenSSH public key');
pubPEM = genOpenSSLECDSAPub(oid, ecpub);
- pubSSH = genOpenSSHECDSAPub(oid, ecpub);
+ pubSSH = data;
break;
}
default:
diff --git a/lib/protocol/utils.js b/lib/protocol/utils.js
index 26f4cab..e7e0928 100644
--- a/lib/protocol/utils.js
+++ b/lib/protocol/utils.js
@@ -282,10 +282,20 @@ module.exports = {
}
},
convertSignature: (signature, keyType) => {
+ switch (keyType) {
+ case '[email protected]':
+ case '[email protected]':
+ case 'ecdsa-sha2-nistp256':
+ case '[email protected]':
+ case '[email protected]':
+ case '[email protected]':
+ keyType = keyType.replace(/[email protected]$/, '');
+ }
switch (keyType) {
case 'ssh-dss': {
if (signature.length <= 40)
return signature;
+ else {
// This is a quick and dirty way to get from BER encoded r and s that
// OpenSSL gives us, to just the bare values back to back (40 bytes
// total) like OpenSSH (and possibly others) are expecting
@@ -315,13 +325,15 @@ module.exports = {
Buffer.allocUnsafe((r.length - rOffset) + (s.length - sOffset));
bufferCopy(r, newSig, rOffset, r.length, 0);
bufferCopy(s, newSig, sOffset, s.length, r.length - rOffset);
- return newSig;
+ }
+ signature = newSig;
+ break;
}
case 'ecdsa-sha2-nistp256':
case 'ecdsa-sha2-nistp384':
case 'ecdsa-sha2-nistp521': {
- if (signature[0] === 0)
- return signature;
+ if (signature[0] === 0) {
+ } else {
// Convert SSH signature parameters to ASN.1 BER values for OpenSSL
const asnReader = new Ber.Reader(signature);
asnReader.readSequence();
@@ -334,11 +346,16 @@ module.exports = {
newSig.set(r, 4);
writeUInt32BE(newSig, s.length, 4 + r.length);
newSig.set(s, 4 + 4 + r.length);
- return newSig;
+ signature = newSig;
+ }
}
}
+ if (signature === false) { return false; }
- return signature;
+ return {
+ signature: signature,
+ keyType: keyType
+ }
},
sendPacket: (proto, packet, bypass) => {
if (!bypass && proto._kexinit !== undefined) { |
Hi All. I took some time to address this issue in the current version without needing a patch. class CustomAgent extends ssh2.OpenSSHAgent {
constructor(private readonly publicKey, sockerPath: string) {
super(sockerPath);
}
override getIdentities(cb) {
super.getIdentities((err, keys) => {
if (this.publicKey) {
const key = keys?.find((key) => {
if ("equals" in key) {
return key.equals(this.publicKey);
}
return false;
});
cb(err, key ? [key] : []);
return;
}
cb(err, keys);
});
}
}
const publicKey = "ssh-ed25519 AAAAC3NzaC2lZDI1NTE5AAAAIORt2rxtrPfh7ea8NQy3ABMZPwLkePmmh8enxOKizmUD";
const conn = new Client();
conn.connect({
host,
port,
username,
agent: new CustomAgent(publicKey, process.env.SSH_AUTH_SOCK);
}); |
(see mscdex/ssh2-streams#137)
This is to support SSH certificates. As before the privateKey will
be used for the publicKey (i.e. the derived publicKey) if nothing
is given.
The given publicKey is checked to match the given privateKey.
Closes #551
Example: