Add a 'private-key' option which represents the path of a private key to use for authentication, and 'private-key-secret' as the name of an object with its passphrase.
Signed-off-by: Pino Toscano <ptosc...@redhat.com> --- block/ssh.c | 98 ++++++++++++++++++++++++++++++++++++ block/trace-events | 1 + docs/qemu-block-drivers.texi | 12 ++++- qapi/block-core.json | 9 +++- 4 files changed, 117 insertions(+), 3 deletions(-) diff --git a/block/ssh.c b/block/ssh.c index 04ae223282..1b7c1f4108 100644 --- a/block/ssh.c +++ b/block/ssh.c @@ -500,6 +500,89 @@ static int check_host_key(BDRVSSHState *s, SshHostKeyCheck *hkc, Error **errp) return -EINVAL; } +static int authenticate_privkey(BDRVSSHState *s, BlockdevOptionsSsh *opts, + Error **errp) +{ + int err; + int ret; + char *pubkey_file = NULL; + ssh_key public_key = NULL; + ssh_key private_key = NULL; + char *passphrase; + + pubkey_file = g_strdup_printf("%s.pub", opts->private_key); + + /* load the private key */ + trace_ssh_auth_key_passphrase(opts->private_key_secret, opts->private_key); + passphrase = qcrypto_secret_lookup_as_utf8(opts->private_key_secret, errp); + if (!passphrase) { + err = SSH_AUTH_ERROR; + goto error; + } + ret = ssh_pki_import_privkey_file(opts->private_key, passphrase, + NULL, NULL, &private_key); + g_free(passphrase); + if (ret == SSH_EOF) { + error_setg(errp, "Cannot read private key '%s'", opts->private_key); + err = SSH_AUTH_ERROR; + goto error; + } else if (ret == SSH_ERROR) { + error_setg(errp, + "Cannot open private key '%s', maybe the passphrase is " + "wrong", + opts->private_key); + err = SSH_AUTH_ERROR; + goto error; + } + + /* try to open the public part of the private key */ + ret = ssh_pki_import_pubkey_file(pubkey_file, &public_key); + if (ret == SSH_ERROR) { + error_setg(errp, "Cannot read public key '%s'", pubkey_file); + err = SSH_AUTH_ERROR; + goto error; + } else if (ret == SSH_EOF) { + /* create the public key from the private key */ + ret = ssh_pki_export_privkey_to_pubkey(private_key, &public_key); + if (ret == SSH_ERROR) { + error_setg(errp, + "Cannot export the public key from the private key " + "'%s'", + opts->private_key); + err = SSH_AUTH_ERROR; + goto error; + } + } + + ret = ssh_userauth_try_publickey(s->session, NULL, public_key); + if (ret != SSH_AUTH_SUCCESS) { + err = SSH_AUTH_DENIED; + goto error; + } + + ret = ssh_userauth_publickey(s->session, NULL, private_key); + if (ret != SSH_AUTH_SUCCESS) { + err = SSH_AUTH_DENIED; + goto error; + } + + ssh_key_free(private_key); + ssh_key_free(public_key); + g_free(pubkey_file); + + return SSH_AUTH_SUCCESS; + + error: + if (private_key) { + ssh_key_free(private_key); + } + if (public_key) { + ssh_key_free(public_key); + } + g_free(pubkey_file); + return err; +} + static int authenticate(BDRVSSHState *s, BlockdevOptionsSsh *opts, Error **errp) { @@ -538,6 +621,21 @@ static int authenticate(BDRVSSHState *s, BlockdevOptionsSsh *opts, ret = 0; goto out; } + + /* + * Try to authenticate with private key, if available. + */ + if (opts->has_private_key && opts->has_private_key_secret) { + r = authenticate_privkey(s, opts, errp); + if (r == SSH_AUTH_ERROR) { + ret = -EINVAL; + goto out; + } else if (r == SSH_AUTH_SUCCESS) { + /* Authenticated! */ + ret = 0; + goto out; + } + } } /* diff --git a/block/trace-events b/block/trace-events index 391aae03e6..ccb51b9992 100644 --- a/block/trace-events +++ b/block/trace-events @@ -187,6 +187,7 @@ ssh_seek(int64_t offset) "seeking to offset=%" PRIi64 ssh_auth_methods(int methods) "auth methods=0x%x" ssh_server_status(int status) "server status=%d" ssh_option_secret_object(const char *path) "using password from object %s" +ssh_auth_key_passphrase(const char *path, const char *key) "using passphrase from object %s for private key %s" # curl.c curl_timer_cb(long timeout_ms) "timer callback timeout_ms %ld" diff --git a/docs/qemu-block-drivers.texi b/docs/qemu-block-drivers.texi index c77ef2dd69..5513bf261c 100644 --- a/docs/qemu-block-drivers.texi +++ b/docs/qemu-block-drivers.texi @@ -774,8 +774,16 @@ tools only use MD5 to print fingerprints). The optional @var{password-secret} parameter provides the ID of a @code{secret} object that contains the password for authenticating. -Currently authentication must be done using ssh-agent, or providing a -password. Other authentication methods may be supported in future. +The optional @var{private-key} parameter provides the path to the +private key for authenticating. + +The optional @var{private-key-secret} parameter provides the ID of a +@code{secret} object that contains the passphrase of the private key +specified as @var{private-key} for authenticating. + +Currently authentication must be done using ssh-agent, providing a +private key with its passphrase, or providing a password. +Other authentication methods may be supported in future. Note: Many ssh servers do not support an @code{fsync}-style operation. The ssh driver cannot guarantee that disk flush requests are diff --git a/qapi/block-core.json b/qapi/block-core.json index 1244562c7b..e873f8934d 100644 --- a/qapi/block-core.json +++ b/qapi/block-core.json @@ -3226,6 +3226,11 @@ # @password-secret: ID of a QCryptoSecret object providing a password # for authentication (since 4.2) # +# @private-key: path to the private key (since 4.2) +# +# @private-key-secret: ID of a QCryptoSecret object providing the passphrase +# for 'private-key' (since 4.2) +# # Since: 2.9 ## { 'struct': 'BlockdevOptionsSsh', @@ -3233,7 +3238,9 @@ 'path': 'str', '*user': 'str', '*host-key-check': 'SshHostKeyCheck', - '*password-secret': 'str' } } + '*password-secret': 'str', + '*private-key': 'str', + '*private-key-secret': 'str' } } ## -- 2.21.0