diff --git a/include/libssh/config.h b/include/libssh/config.h index 21702391..4f8a9b6f 100644 --- a/include/libssh/config.h +++ b/include/libssh/config.h @@ -65,6 +65,7 @@ enum ssh_config_opcode_e { SOC_IDENTITIESONLY, SOC_CONTROLMASTER, SOC_CONTROLPATH, + SOC_CERTIFICATE, SOC_MAX /* Keep this one last in the list */ }; diff --git a/include/libssh/libssh.h b/include/libssh/libssh.h index 35ce2be5..5348370e 100644 --- a/include/libssh/libssh.h +++ b/include/libssh/libssh.h @@ -416,6 +416,7 @@ enum ssh_options_e { SSH_OPTIONS_IDENTITIES_ONLY, SSH_OPTIONS_CONTROL_MASTER, SSH_OPTIONS_CONTROL_PATH, + SSH_OPTIONS_CERTIFICATE, }; enum { diff --git a/include/libssh/session.h b/include/libssh/session.h index b3b3e4e6..cb55db95 100644 --- a/include/libssh/session.h +++ b/include/libssh/session.h @@ -231,6 +231,8 @@ struct ssh_session_struct { struct { struct ssh_list *identity; struct ssh_list *identity_non_exp; + struct ssh_list *certificate; + struct ssh_list *certificate_non_exp; char *username; char *host; char *bindaddr; /* bind the client to an ip addr */ diff --git a/src/auth.c b/src/auth.c index 068a24f1..820d82fc 100644 --- a/src/auth.c +++ b/src/auth.c @@ -557,6 +557,7 @@ int ssh_userauth_try_publickey(ssh_session session, goto fail; } + SSH_LOG(SSH_LOG_TRACE, "Trying signature type %s", sig_type_c); /* request */ rc = ssh_buffer_pack(session->out_buffer, "bsssbsS", SSH2_MSG_USERAUTH_REQUEST, @@ -690,6 +691,7 @@ int ssh_userauth_publickey(ssh_session session, goto fail; } + SSH_LOG(SSH_LOG_TRACE, "Sending signature type %s", sig_type_c); /* request */ rc = ssh_buffer_pack(session->out_buffer, "bsssbsS", SSH2_MSG_USERAUTH_REQUEST, @@ -859,6 +861,7 @@ fail: enum ssh_agent_state_e { SSH_AGENT_STATE_NONE = 0, SSH_AGENT_STATE_PUBKEY, + SSH_AGENT_STATE_CERT, SSH_AGENT_STATE_AUTH }; @@ -908,7 +911,9 @@ int ssh_userauth_agent(ssh_session session, int rc = SSH_AUTH_ERROR; struct ssh_agent_state_struct *state = NULL; ssh_key *configKeys = NULL; + ssh_key *configCerts = NULL; size_t configKeysCount = 0; + size_t configCertsCount = 0; size_t i; if (session == NULL) { @@ -944,17 +949,24 @@ int ssh_userauth_agent(ssh_session session, * is in there. */ size_t identityLen = ssh_list_count(session->opts.identity); + size_t certsLen = ssh_list_count(session->opts.certificate); struct ssh_iterator *it = ssh_list_get_iterator(session->opts.identity); - configKeys = malloc(identityLen * sizeof(configKeys[0])); - if (!configKeys) { + configKeys = malloc(identityLen * sizeof(ssh_key)); + configCerts = malloc((certsLen + identityLen) * sizeof(ssh_key)); + if (configKeys == NULL || configCerts == NULL) { + free(configKeys); + free(configCerts); ssh_set_error_oom(session); return SSH_AUTH_ERROR; } while (it != NULL && configKeysCount < identityLen) { const char *privkeyFile = it->data; + size_t certPathLen; + char *certFile = NULL; ssh_key pubkey = NULL; + ssh_key cert = NULL; /* * Read the private key file listed in the config, but we're only @@ -967,9 +979,7 @@ int ssh_userauth_agent(ssh_session session, char *pubkeyFile = NULL; size_t pubkeyPathLen = strlen(privkeyFile) + sizeof(".pub"); - if (pubkey) { - SSH_KEY_FREE(pubkey); - } + SSH_KEY_FREE(pubkey); /* * If we couldn't get the public key from the private key file, @@ -983,13 +993,47 @@ int ssh_userauth_agent(ssh_session session, } snprintf(pubkeyFile, pubkeyPathLen, "%s.pub", privkeyFile); rc = ssh_pki_import_pubkey_file(pubkeyFile, &pubkey); + free(pubkeyFile); if (rc == SSH_OK) { configKeys[configKeysCount++] = pubkey; } else if (pubkey) { SSH_KEY_FREE(pubkey); } - free(pubkeyFile); } + /* Now try to see if there is a certificate with default name + * do not merge it yet with the key as we need to try first the + * non-certified key */ + certPathLen = strlen(privkeyFile) + sizeof("-cert.pub"); + certFile = malloc(certPathLen); + if (!certFile) { + ssh_set_error_oom(session); + rc = SSH_AUTH_ERROR; + goto done; + } + snprintf(certFile, certPathLen, "%s-cert.pub", privkeyFile); + rc = ssh_pki_import_cert_file(certFile, &cert); + free(certFile); + if (rc == SSH_OK) { + configCerts[configCertsCount++] = cert; + } else if (cert) { + SSH_KEY_FREE(cert); + } + + it = it->next; + } + /* And now load separately-listed certificates. */ + it = ssh_list_get_iterator(session->opts.certificate); + while (it != NULL && configCertsCount < certsLen + identityLen) { + const char *certFile = it->data; + ssh_key cert = NULL; + + rc = ssh_pki_import_cert_file(certFile, &cert); + if (rc == SSH_OK) { + configCerts[configCertsCount++] = cert; + } else if (cert) { + SSH_KEY_FREE(cert); + } + it = it->next; } } @@ -1011,6 +1055,16 @@ int ssh_userauth_agent(ssh_session session, break; } } + /* or in separate certificates */ + for (i = 0; i < configCertsCount; i++) { + int cmp = ssh_key_cmp(state->pubkey, + configCerts[i], + SSH_KEY_CMP_PUBLIC); + if (cmp == 0) { + found_key = true; + break; + } + } if (!found_key) { SSH_LOG(SSH_LOG_DEBUG, @@ -1031,19 +1085,36 @@ int ssh_userauth_agent(ssh_session session, } } if (state->state == SSH_AGENT_STATE_NONE || - state->state == SSH_AGENT_STATE_PUBKEY) { + state->state == SSH_AGENT_STATE_PUBKEY || + state->state == SSH_AGENT_STATE_CERT) { rc = ssh_userauth_try_publickey(session, username, state->pubkey); if (rc == SSH_AUTH_ERROR) { ssh_agent_state_free(state); session->agent_state = NULL; goto done; } else if (rc == SSH_AUTH_AGAIN) { - state->state = SSH_AGENT_STATE_PUBKEY; + state->state = (state->state == SSH_AGENT_STATE_NONE ? + SSH_AGENT_STATE_PUBKEY : state->state); goto done; } else if (rc != SSH_AUTH_SUCCESS) { SSH_LOG(SSH_LOG_DEBUG, "Public key of %s refused by server", state->comment); + if (state->state == SSH_AGENT_STATE_PUBKEY) { + for (i = 0; i < configCertsCount; i++) { + int cmp = ssh_key_cmp(state->pubkey, + configCerts[i], + SSH_KEY_CMP_PUBLIC); + if (cmp == 0) { + SSH_LOG(SSH_LOG_DEBUG, + "Retry with matching certificate"); + SSH_KEY_FREE(state->pubkey); + state->pubkey = ssh_key_dup(configCerts[i]); + state->state = SSH_AGENT_STATE_CERT; + continue; + } + } + } SSH_STRING_FREE_CHAR(state->comment); state->comment = NULL; SSH_KEY_FREE(state->pubkey); @@ -1092,6 +1163,10 @@ done: ssh_key_free(configKeys[i]); } free(configKeys); + for (i = 0; i < configCertsCount; i++) { + ssh_key_free(configCerts[i]); + } + free(configCerts); return rc; } @@ -1099,6 +1174,8 @@ enum ssh_auth_auto_state_e { SSH_AUTH_AUTO_STATE_NONE = 0, SSH_AUTH_AUTO_STATE_PUBKEY, SSH_AUTH_AUTO_STATE_KEY_IMPORTED, + SSH_AUTH_AUTO_STATE_CERTIFICATE_FILE, + SSH_AUTH_AUTO_STATE_CERTIFICATE_OPTION, SSH_AUTH_AUTO_STATE_PUBKEY_ACCEPTED }; @@ -1107,6 +1184,8 @@ struct ssh_auth_auto_state_struct { struct ssh_iterator *it; ssh_key privkey; ssh_key pubkey; + ssh_key cert; + struct ssh_iterator *cert_it; }; /** @@ -1186,6 +1265,9 @@ int ssh_userauth_publickey_auto_get_current_identity(ssh_session session, * @note Most server implementations do not permit changing the username during * authentication. The username should only be set with ssh_options_set() only * before you connect to the server. + * + * The OpenSSH iterates over the identities and first try the plain public key + * and then the certificate if it is in place. */ int ssh_userauth_publickey_auto(ssh_session session, const char *username, @@ -1241,6 +1323,7 @@ int ssh_userauth_publickey_auto(ssh_session session, SSH_LOG(SSH_LOG_DEBUG, "Trying to authenticate with %s", privkey_file); + state->cert = NULL; state->privkey = NULL; state->pubkey = NULL; @@ -1302,7 +1385,7 @@ int ssh_userauth_publickey_auto(ssh_session session, rc = ssh_pki_export_privkey_to_pubkey(state->privkey, &state->pubkey); if (rc == SSH_ERROR) { - ssh_key_free(state->privkey); + SSH_KEY_FREE(state->privkey); SAFE_FREE(session->auth.auto_state); return SSH_AUTH_ERROR; } @@ -1316,28 +1399,101 @@ int ssh_userauth_publickey_auto(ssh_session session, } state->state = SSH_AUTH_AUTO_STATE_KEY_IMPORTED; } - if (state->state == SSH_AUTH_AUTO_STATE_KEY_IMPORTED) { - rc = ssh_userauth_try_publickey(session, username, state->pubkey); + if (state->state == SSH_AUTH_AUTO_STATE_KEY_IMPORTED || + state->state == SSH_AUTH_AUTO_STATE_CERTIFICATE_FILE || + state->state == SSH_AUTH_AUTO_STATE_CERTIFICATE_OPTION) { + ssh_key k = state->pubkey; + if (state->state != SSH_AUTH_AUTO_STATE_KEY_IMPORTED) { + k = state->cert; + } + rc = ssh_userauth_try_publickey(session, username, k); if (rc == SSH_AUTH_ERROR) { SSH_LOG(SSH_LOG_TRACE, "Public key authentication error for %s", privkey_file); - ssh_key_free(state->privkey); - state->privkey = NULL; - ssh_key_free(state->pubkey); - state->pubkey = NULL; + SSH_KEY_FREE(state->cert); + SSH_KEY_FREE(state->privkey); + SSH_KEY_FREE(state->pubkey); SAFE_FREE(session->auth.auto_state); return rc; } else if (rc == SSH_AUTH_AGAIN) { return rc; } else if (rc != SSH_AUTH_SUCCESS) { + int r; /* do not reuse `rc` as it is used to return from here */ + SSH_KEY_FREE(state->cert); SSH_LOG(SSH_LOG_DEBUG, - "Public key for %s refused by server", - privkey_file); - ssh_key_free(state->privkey); - state->privkey = NULL; - ssh_key_free(state->pubkey); - state->pubkey = NULL; + "Public key for %s%s refused by server", + privkey_file, + (state->state != SSH_AUTH_AUTO_STATE_KEY_IMPORTED + ? " (with certificate)" : "")); + /* Try certificate file by appending -cert.pub (if present) */ + if (state->state == SSH_AUTH_AUTO_STATE_KEY_IMPORTED) { + char cert_file[PATH_MAX] = {0}; + ssh_key cert = NULL; + + snprintf(cert_file, + sizeof(cert_file), + "%s-cert.pub", + privkey_file); + SSH_LOG(SSH_LOG_TRACE, + "Trying to load the certificate %s (default path)", + cert_file); + r = ssh_pki_import_cert_file(cert_file, &cert); + if (r == SSH_OK) { + /* TODO check the pubkey and certs match */ + SSH_LOG(SSH_LOG_TRACE, + "Certificate loaded %s. Retry the authentication.", + cert_file); + state->state = SSH_AUTH_AUTO_STATE_CERTIFICATE_FILE; + SSH_KEY_FREE(state->cert); + state->cert = cert; + /* try to authenticate with this certificate */ + continue; + } + /* if the file does not exists, try configuration options */ + state->state = SSH_AUTH_AUTO_STATE_CERTIFICATE_OPTION; + } + /* Try certificate files loaded through options */ + if (state->state == SSH_AUTH_AUTO_STATE_CERTIFICATE_OPTION) { + SSH_KEY_FREE(state->cert); + if (state->cert_it == NULL) { + state->cert_it = ssh_list_get_iterator(session->opts.certificate); + } + while (state->cert_it != NULL) { + const char *cert_file = state->cert_it->data; + ssh_key cert = NULL; + + SSH_LOG(SSH_LOG_TRACE, + "Trying to load the certificate %s (options)", + cert_file); + r = ssh_pki_import_cert_file(cert_file, &cert); + if (r == SSH_OK) { + int cmp = ssh_key_cmp(cert, + state->pubkey, + SSH_KEY_CMP_PUBLIC); + if (cmp != 0) { + state->cert_it = state->cert_it->next; + SSH_KEY_FREE(cert); + continue; /* with next cert */ + } + SSH_LOG(SSH_LOG_TRACE, + "Found matching certificate %s in options. Retry the authentication.", + cert_file); + state->cert = cert; + cert = NULL; + state->state = SSH_AUTH_AUTO_STATE_CERTIFICATE_OPTION; + /* try to authenticate with this identity */ + break; /* try this cert */ + } + /* continue with next identity */ + } + if (state->cert != NULL) { + continue; /* retry with the certificate */ + } + } + SSH_KEY_FREE(state->cert); + SSH_KEY_FREE(state->privkey); + SSH_KEY_FREE(state->pubkey); state->it = state->it->next; state->state = SSH_AUTH_AUTO_STATE_PUBKEY; continue; @@ -1353,8 +1509,8 @@ int ssh_userauth_publickey_auto(ssh_session session, auth_data, &state->privkey); if (rc == SSH_ERROR) { - ssh_key_free(state->pubkey); - state->pubkey = NULL; + SSH_KEY_FREE(state->cert); + SSH_KEY_FREE(state->pubkey); ssh_set_error(session, SSH_FATAL, "Failed to read private key: %s", @@ -1364,8 +1520,8 @@ int ssh_userauth_publickey_auto(ssh_session session, continue; } else if (rc == SSH_EOF) { /* If the file doesn't exist, continue */ - ssh_key_free(state->pubkey); - state->pubkey = NULL; + SSH_KEY_FREE(state->cert); + SSH_KEY_FREE(state->pubkey); SSH_LOG(SSH_LOG_DEBUG, "Private key %s doesn't exist.", privkey_file); @@ -1374,16 +1530,33 @@ int ssh_userauth_publickey_auto(ssh_session session, continue; } } + if (state->cert != NULL && !is_cert_type(state->privkey->cert_type)) { + rc = ssh_pki_copy_cert_to_privkey(state->cert, state->privkey); + if (rc != SSH_OK) { + SSH_KEY_FREE(state->cert); + SSH_KEY_FREE(state->privkey); + SSH_KEY_FREE(state->pubkey); + ssh_set_error(session, + SSH_FATAL, + "Failed to copy cert to private key"); + state->it = state->it->next; + state->state = SSH_AUTH_AUTO_STATE_PUBKEY; + continue; + } + } rc = ssh_userauth_publickey(session, username, state->privkey); if (rc != SSH_AUTH_AGAIN && rc != SSH_AUTH_DENIED) { - ssh_key_free(state->privkey); - ssh_key_free(state->pubkey); + bool cert_used = (state->cert != NULL); + SSH_KEY_FREE(state->cert); + SSH_KEY_FREE(state->privkey); + SSH_KEY_FREE(state->pubkey); SAFE_FREE(session->auth.auto_state); if (rc == SSH_AUTH_SUCCESS) { SSH_LOG(SSH_LOG_DEBUG, - "Successfully authenticated using %s", - privkey_file); + "Successfully authenticated using %s%s", + privkey_file, + (cert_used ? " and certificate" : "")); } return rc; } @@ -1391,8 +1564,9 @@ int ssh_userauth_publickey_auto(ssh_session session, return rc; } - ssh_key_free(state->privkey); - ssh_key_free(state->pubkey); + SSH_KEY_FREE(state->cert); + SSH_KEY_FREE(state->privkey); + SSH_KEY_FREE(state->pubkey); SSH_LOG(SSH_LOG_DEBUG, "The server accepted the public key but refused the signature"); diff --git a/src/config.c b/src/config.c index 156b146e..5eedbce9 100644 --- a/src/config.c +++ b/src/config.c @@ -92,7 +92,7 @@ static struct ssh_config_keyword_table_s ssh_config_keyword_table[] = { { "canonicalizehostname", SOC_UNSUPPORTED}, { "canonicalizemaxdots", SOC_UNSUPPORTED}, { "canonicalizepermittedcnames", SOC_UNSUPPORTED}, - { "certificatefile", SOC_UNSUPPORTED}, + { "certificatefile", SOC_CERTIFICATE}, { "kbdinteractiveauthentication", SOC_UNSUPPORTED}, { "checkhostip", SOC_UNSUPPORTED}, { "connectionattempts", SOC_UNSUPPORTED}, @@ -623,6 +623,7 @@ ssh_config_parse_line(ssh_session session, opcode != SOC_MATCH && opcode != SOC_INCLUDE && opcode != SOC_IDENTITY && + opcode != SOC_CERTIFICATE && opcode > SOC_UNSUPPORTED && opcode < SOC_MAX) { /* Ignore all unknown types here */ /* Skip all the options that were already applied */ @@ -1218,6 +1219,12 @@ ssh_config_parse_line(ssh_session session, ssh_options_set(session, SSH_OPTIONS_CONTROL_PATH, p); } break; + case SOC_CERTIFICATE: + p = ssh_config_get_str_tok(&s, NULL); + if (p && *parsing) { + ssh_options_set(session, SSH_OPTIONS_CERTIFICATE, p); + } + break; default: ssh_set_error(session, SSH_FATAL, "ERROR - unimplemented opcode: %d", opcode); diff --git a/src/options.c b/src/options.c index f0bb476a..f144bbb4 100644 --- a/src/options.c +++ b/src/options.c @@ -118,7 +118,7 @@ int ssh_options_copy(ssh_session src, ssh_session *dest) while (it) { int rc; - id = strdup((char *) it->data); + id = strdup((char *)it->data); if (id == NULL) { ssh_free(new); return -1; @@ -138,6 +138,32 @@ int ssh_options_copy(ssh_session src, ssh_session *dest) it = ssh_list_get_iterator(src->opts.identity); } + list = new->opts.certificate_non_exp; + it = ssh_list_get_iterator(src->opts.certificate_non_exp); + for (i = 0; i < 2; i++) { + while (it) { + int rc; + + id = strdup((char *)it->data); + if (id == NULL) { + ssh_free(new); + return -1; + } + + rc = ssh_list_append(list, id); + if (rc < 0) { + free(id); + ssh_free(new); + return -1; + } + it = it->next; + } + + /* copy the certificate list if there is any already */ + list = new->opts.certificate; + it = ssh_list_get_iterator(src->opts.certificate); + } + if (src->opts.sshdir != NULL) { new->opts.sshdir = strdup(src->opts.sshdir); if (new->opts.sshdir == NULL) { @@ -353,6 +379,21 @@ int ssh_options_set_algo(ssh_session session, * It may include "%s" which will be replaced by the * user home directory. * + * - SSH_OPTIONS_CERTIFICATE: + * Add a new certificate file (const char *, format string) to + * the certificate list.\n + * \n + * By default id_rsa-cert.pub, id_ecdsa-cert.pub and + * id_ed25519-cert.pub files are used, when the underlying + * private key is present.\n + * \n + * The certificate itself can not be used to authenticate to + * remote server so it needs to be paired with private key + * (aka identity file) provided with separate option, from agent + * or from PKCS#11 token. + * It may include "%s" which will be replaced by the + * user home directory. + * * - SSH_OPTIONS_TIMEOUT: * Set a timeout for the connection in seconds (long). * @@ -753,6 +794,22 @@ int ssh_options_set(ssh_session session, enum ssh_options_e type, return -1; } break; + case SSH_OPTIONS_CERTIFICATE: + v = value; + if (v == NULL || v[0] == '\0') { + ssh_set_error_invalid(session); + return -1; + } + q = strdup(v); + if (q == NULL) { + return -1; + } + rc = ssh_list_append(session->opts.certificate_non_exp, q); + if (rc < 0) { + free(q); + return -1; + } + break; case SSH_OPTIONS_KNOWNHOSTS: v = value; SAFE_FREE(session->opts.knownhosts); @@ -1753,6 +1810,23 @@ int ssh_options_apply(ssh_session session) } session->opts.exp_flags |= SSH_OPT_EXP_FLAG_IDENTITY; + for (tmp = ssh_list_pop_head(char *, session->opts.certificate_non_exp); + tmp != NULL; + tmp = ssh_list_pop_head(char *, session->opts.certificate_non_exp)) { + char *id = tmp; + + tmp = ssh_path_expand_escape(session, id); + if (tmp == NULL) { + return -1; + } + free(id); + + rc = ssh_list_append(session->opts.certificate, tmp); + if (rc != SSH_OK) { + return -1; + } + } + return 0; } diff --git a/src/session.c b/src/session.c index 098f94a0..c3aaf32f 100644 --- a/src/session.c +++ b/src/session.c @@ -127,6 +127,17 @@ ssh_session ssh_new(void) goto err; } + session->opts.certificate = ssh_list_new(); + if (session->opts.certificate == NULL) { + goto err; + } + session->opts.certificate_non_exp = ssh_list_new(); + if (session->opts.certificate_non_exp == NULL) { + goto err; + } + /* the default certificates are loaded automatically from the default + * identities later */ + id = strdup("%d/id_ed25519"); if (id == NULL) { goto err; @@ -288,6 +299,28 @@ void ssh_free(ssh_session session) ssh_list_free(session->opts.identity_non_exp); } + if (session->opts.certificate) { + char *cert = NULL; + + for (cert = ssh_list_pop_head(char *, session->opts.certificate); + cert != NULL; + cert = ssh_list_pop_head(char *, session->opts.certificate)) { + SAFE_FREE(cert); + } + ssh_list_free(session->opts.certificate); + } + + if (session->opts.certificate_non_exp) { + char *cert = NULL; + + for (cert = ssh_list_pop_head(char *, session->opts.certificate_non_exp); + cert != NULL; + cert = ssh_list_pop_head(char *, session->opts.certificate_non_exp)) { + SAFE_FREE(cert); + } + ssh_list_free(session->opts.certificate_non_exp); + } + while ((b = ssh_list_pop_head(struct ssh_buffer_struct *, session->out_queue)) != NULL) { SSH_BUFFER_FREE(b);