Checking remote authorized_keys without entering key passphrase, done more neatly — and against .ssh/config
Tags: ssh, libssh, devops, util
Published: 2024-02-27 21:50:33

You have a bunch of client machines with private keys set up to access a bunch of destination hosts. How do you check which of them has them to access which? *Maybe you wish for `ssh-copy-id` to not ask for key password when you can first check that quicker with `ssh` and `^C`?*
How does ssh act out
```
Enter passphrase for key '/path/to/key': 
```
as opposed to
```
Permission denied (publickey).
```

While `-vvv` (`debug3`) shows more, let's see just `debug1` (`-v`):
```
debug1: Authentications that can continue: publickey,password,keyboard-interactive
debug1: Next authentication method: publickey
debug1: Will attempt key: /path/to/key ED25519 SHA256:... explicit
debug1: Offering public key: /path/to/key ED25519 SHA256:... explicit
debug1: Server accepts key: /path/to/key ED25519 SHA256:... explicit
Enter passphrase for key '/path/to/key': 
```

So we can clearly just scrape that stderr, seek the debug1 accept or the passphrase prompt, regex the path to key out of the thing.

Or we can do it neater.
Cockpit Project happened to make a contribution enabling us to use libssh for that: https://gitlab.com/libssh/libssh-mirror/-/merge_requests/134
— they happened to want to be able to prompt the user for key passphrase only when needed. We've only been having that available in libssh for two years.

The `ssh_userauth_publickey_auto_get_current_identity` function has to be called in an `auth_function` callback, and the value it obtains is the key file path.

**Besides the `auto` mode, even without that it was already possible to check a single key — with the result of the `ssh_userauth_try_publickey` function.**

Attached is the quick-and-dirty source code for such a utility:
```c
// LGPL (c) 2024-02-27 Mika Feiler <m@mikf.pl>
// BUILD: cc -lssh

/*
      This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Lesser General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>
*/

#include <libssh/libssh.h>
#include <libssh/callbacks.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>

ssh_session s;

int leave = -1;

int callback(const char *_prompt, char *_buf, size_t _len,
	     int _echo, int _verify, void* _) {
  int rc;
  char *value[500];
  rc = ssh_userauth_publickey_auto_get_current_identity(s, value);
  // Thanks, Cockpit Project!
  printf("%s\n", *value);
  leave = 0;
}

int usage(char* a0) {
    printf("Usage: %s hostname [port [keypath|'' [keyuser [disregardunknownhost]]]]\n\n%s\n%s\n%s\n\n%s\n",
	   a0,
	   "This little program uses ssh_userauth_publickey_auto_get_current_identity,",
	   "thanks to Cockpit, to offer keys (per config) to host and print out path",
	   "of one that gets accepted by the host, even if it has a passphrase.",
	   "This program is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.");
    return 0;
}

void handle_host_verification(int pass_unknown) {
  enum ssh_known_hosts_e state = ssh_session_is_known_server(s);
  switch(state) {
  case SSH_KNOWN_HOSTS_UNKNOWN: if(pass_unknown) exit(5);
  case SSH_KNOWN_HOSTS_OK: break;
  default:
    exit(3);
  }
}

int main(int argc, char* argv[]) {
  if(argc < 2)
    return usage(argv[0]);

  s = ssh_new();
  if (s == NULL)
    exit(-1);

  int verbosity = SSH_LOG_WARNING;
  ssh_options_set(s, SSH_OPTIONS_LOG_VERBOSITY, &verbosity);
  
  ssh_options_set(s, SSH_OPTIONS_HOST, argv[1]);
  ssh_options_set(s, SSH_OPTIONS_PORT_STR, argc > 2 ? argv[2] : "22");

  int rc;
  rc = ssh_connect(s);
  if (rc != SSH_OK)
    exit(rc);

  handle_host_verification(argc < 6); // will allow unknown host with one more arg

  struct ssh_callbacks_struct callbacks = {
    .auth_function = callback
  };
  // As per test example for ssh_userauth_publickey_auto_get_current_identity !
  
  ssh_set_blocking(s, 1);

  // "User SHOULD be NULL" lol
  char* user = argc > 4 && strlen(argv[4]) > 0 ? argv[4] : NULL;
  
  if (argc > 3 && strlen(argv[3]) > 0) { // if key path specified
    ssh_key key = ssh_key_new();
    rc = ssh_pki_import_pubkey_file(argv[3], &key);
    if (rc != SSH_OK)
      exit(rc);
    rc = ssh_userauth_try_publickey(s, user, key);
    leave = rc;
  } else { // the cool behavior doing .ssh/config :3
    ssh_callbacks_init(&callbacks);
    ssh_set_callbacks(s, &callbacks);
    
    rc = ssh_userauth_publickey_auto(s, user, NULL);
  }
  leave = leave == -1 ? rc : leave;
  
  ssh_disconnect(s); ssh_free(s);
  exit(leave);
}
```