Skip to content

Instantly share code, notes, and snippets.

@dhensby
Created March 25, 2021 10:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dhensby/f01c42dc794a3710c0901e723fcaeb56 to your computer and use it in GitHub Desktop.
Save dhensby/f01c42dc794a3710c0901e723fcaeb56 to your computer and use it in GitHub Desktop.
Example of publickey authentication with ssh2-streams and azure key vault
const { CryptographyClient, KeyClient } = require('@azure/keyvault-keys');
const { AzureCliCredential } = require('@azure/identity');
const { SSH2Stream, utils, constants: { ALGORITHMS: { SUPPORTED_SERVER_HOST_KEY } } } = require('ssh2-streams');
const { Socket } = require('net');
const { asn1, pki, md, ssh, jsbn: { BigInteger } } = require('node-forge');
const VAULT_URI = process.env.VAULT_URI;
// this is lifted from node-forge (https://github.com/digitalbazaar/forge/blob/0.10.0/lib/rsa.js#L284-L313)
// unfortunately it's not exported
const emsaPkcs1v15encode = function (messagedigest) {
// get the oid for the algorithm
let oid;
if (messagedigest.algorithm in pki.oids) {
oid = pki.oids[messagedigest.algorithm];
} else {
const error = new Error('Unknown message digest algorithm.');
error.algorithm = messagedigest.algorithm;
throw error;
}
const oidBytes = asn1.oidToDer(oid).getBytes();
// create the digest info
const digestInfo = asn1.create(
asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, []);
const digestAlgorithm = asn1.create(
asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, []);
digestAlgorithm.value.push(asn1.create(
asn1.Class.UNIVERSAL, asn1.Type.OID, false, oidBytes));
digestAlgorithm.value.push(asn1.create(
asn1.Class.UNIVERSAL, asn1.Type.NULL, false, ''));
const digest = asn1.create(
asn1.Class.UNIVERSAL, asn1.Type.OCTETSTRING,
false, messagedigest.digest().getBytes());
digestInfo.value.push(digestAlgorithm);
digestInfo.value.push(digest);
// encode digest info
return Buffer.from(asn1.toDer(digestInfo).getBytes(), 'binary');
};
// load the azureKey into a key that SSH2Stream understands
async function getAzureKey() {
const azCred = new AzureCliCredential();
const keyClient = new KeyClient(VAULT_URI, azCred);
const azKey = await keyClient.getKey('test');
// we need the key in openssh format for the parseKey utility
const publicKey = pki.rsa.setPublicKey(new BigInteger(azKey.key.n), new BigInteger(azKey.key.e));
const sshKey = ssh.publicKeyToOpenSSH(publicKey);
const key = utils.parseKey(sshKey);
// monkey patch the "sign" method to get the Azure KV key to sign the message
key.sign = async (data) => {
const cryptoClient = new CryptographyClient(azKey, azCred);
const message = md.sha1.create();
message.update(data.toString('binary'));
// there is no sha1 signing in Azure KV, so we have to encode the asn1 message and send it
// to azure with 'RSNULL' to get it signed
const digest = emsaPkcs1v15encode(message);
const { result } = await cryptoClient.sign('RSNULL', digest);
return result;
};
return key;
}
// host keys of the server to connect to
const hostKeys = {
'ecdsa-sha2-nistp256': Buffer.from('AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBLH4RhqRIVFAxDidCPbZ2pQV6ZiPpN04xNnXT3O94xjKIobUN7rfQU9cs8B5zhreSJ2JIqHcyVvJpiuSQM5nDzs=', 'base64'),
'ssh-ed25519': Buffer.from('AAAAC3NzaC1lZDI1NTE5AAAAIOV3RJ8FpTf5J2j8yR1VSgq+XRyS4KDL2f65U8hB4ngE', 'base64'),
'ssh-rsa': Buffer.from('AAAAB3NzaC1yc2EAAAADAQABAAABgQDdLTzufyLWbX2iVB7t+JfCXfwRBKQC9Ty6QC511aj2ByxD1Fu2gGo93RjXNB7S9pWKnHQpJ8SinplttIDl5zUlrpRVcM7+mJNjcYw46kOLfxf0BldYRth5KeQ5C5o+U9FyvJ14RsZuuxptti3mt1VRVQlWkiLqUO+Sv83vIykIsba/F0DruxJsKbWeGryD6xea7GSm/sk5wgKBfwV7fmSxaQT4h2DGLzC5jCJX8EsGhhOioekCOo045705LLunxTVV2qEWUB8f6n/e0zgeF8hXhqM8aPvf1HkJUCp70qJA2xr7FAFoHZZjAnPvRwLo2y9IXUr62m0qc15a2cOk4guczydLAvZnRZpbqV5kf3LqsvbfljnccAJlRZq4dUkR26Eers9AyCOWlbZ5BxW14YkmL9kpr+s8aeUTfC0BXSq4jqNjVZ5wFhgB5hL5BAUGJsbZDlq8ZfRjzP0v18rzUE3bK1OZmGhGeKxngTEv91fHjFBNdjk+wJVnMJabqWlsgZE=', 'base64'),
'ssh-dss': Buffer.from('AAAAB3NzaC1kc3MAAACBAPDTltsn+HhW98ZC5q9rZ/FuXptXxEqJET4MqzqVlyZx0qbsJbGe4tcwqbQPQoKsF3us5IevbpDI2nBLtJNgV6VUU1n47URq8Mt2obIMSXNRNlXVhuEFs5ydr3fvV3+h7WH+Xt6cSbnNL1zcqb8GwZJ2kS/o5FNw8RUmo1Zs4T8/AAAAFQDdO2n28t93mJM9nfSjfthUlocqswAAAIBgWyljjSuA+q4iq6Qf6w7iN+zEKwZCXyvXT1eIPfQAFcFrQzx85TFaRLK/Or+Ctn/od7j6YkljG3RBjjxa+TZrUGmQvTCxiO1h7z8ow7itWpMAXn2ntm7wqkhFOFmCNXsJr43l6004Vks7gcbmLINGhz0xqP/VPtGt4SXB2bPTcwAAAIEA52hagD4Jr/5LTlKPxBkodFCK41peVSkkWL763tfEiXBZaL6Xt8LsIF/3s0Yub4X0B7DLqZgc/RQvhoh2bptkrTflCCnAPX99xOwi41GlUfIIjY0ESvd10e0KcYi9PtzcfHXUIDhdm5bmXiZuxi2xkzEWIjbMaYjIQtJs2s7Eze4=', 'base64'),
};
// creates a socket to pass to the SSH2Stream
function connectSocket() {
const sock = new Socket();
sock.connect({
host: '', // pick your server
port: 22,
});
sock.on('connect', () => {
console.log('connected');
});
sock.setNoDelay(true);
sock.setMaxListeners(0);
// sock.setTimeout(typeof cfg.timeout === 'number' ? cfg.timeout : 0);
return sock;
}
// create the stream
const stream = new SSH2Stream({
// debug: console.log,
algorithms: {
serverHostKey: Object.keys(hostKeys).filter((alg) => SUPPORTED_SERVER_HOST_KEY.includes(alg)),
},
});
stream.on('header', (headerInfo) => {
console.log(headerInfo);
});
// this is where we verify the host key
stream.on('fingerprint', (hostKey, cb) => {
console.log('Fingerprint');
cb(Object.entries(hostKeys).some(([alg, known]) => {
return hostKey.equals(known) && (console.log('matched', alg) || true);
}));
});
stream.on('USERAUTH_BANNER', (msg) => {
console.log(msg);
});
stream.on('USERAUTH_INFO_REQUEST', (name, instructions, lang, prompts) => {
console.log('info request', {
name,
instructions,
lang,
prompts,
});
});
stream.on('USERAUTH_SUCCESS', () => {
console.log('user auth successful');
});
stream.on('USERAUTH_PK_OK', () => {
console.log('user pk ok');
});
stream.on('end', () => {
console.log('connection closed');
});
stream.once('ready', () => {
stream.service('ssh-userauth');
stream.once('SERVICE_ACCEPT', async (svcName) => {
if (svcName === 'ssh-userauth') {
try {
const key = await getAzureKey();
// parseKey can return an error or key
if (key instanceof Error) {
throw key;
}
// key auth can happen in 1 or 2 steps
// first check the public key is allowed - you can skip this if you only have one
// candidate key and you just want to attempt authentication
// await new Promise((resolve, reject) => {
// const acceptor = () => {
// stream.removeAllListeners('USERAUTH_FAILURE');
// console.log('key accepted');
// resolve();
// };
// const rejector = () => {
// stream.removeAllListeners('USERAUTH_PK_OK');
// reject(new Error('Key rejected'));
// };
// stream.authPK('droptest', key);
// stream.once('USERAUTH_PK_OK', acceptor);
// stream.once('USERAUTH_FAILURE', rejector);
// });
// second step is use the allowed key to sign some data as proof of possession of
// the private key
await new Promise((resolve, reject) => {
stream.authPK('droptest', key, async (data, cb) => {
key.sign(data).then(cb);
});
stream.once('USERAUTH_SUCCESS', () => {
resolve();
stream.removeAllListeners('USERAUTH_FAILURE');
});
stream.once('USERAUTH_FAILURE', () => {
// reject(new Error('no auth'));
reject(new Error('Unable to auth'));
stream.removeAllListeners('USERAUTH_SUCCESS');
});
});
} catch (err) {
console.error(err);
} finally {
stream.disconnect();
}
}
});
});
const socket = connectSocket();
socket.pipe(stream).pipe(socket);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment