diff mbox series

[2/2] ssh: implement private key authentication

Message ID 20190726140954.31921-3-ptoscano@redhat.com (mailing list archive)
State New, archived
Headers show
Series ssh: add password and privkey auth methods | expand

Commit Message

Pino Toscano July 26, 2019, 2:09 p.m. UTC
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 <ptoscano@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(-)

Comments

Eric Blake July 26, 2019, 2:24 p.m. UTC | #1
On 7/26/19 9:09 AM, Pino Toscano wrote:
> 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 <ptoscano@redhat.com>

> +++ 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)

Is password-secret intended to be mutually-exclusive with
private-key/private-key-secret?  If so, this should probably utilize an
enum for a discriminator
{ 'enum': 'SshAuth', 'data': ['ssh-agent', 'password', 'private'key'] }

then update BlockdevOptionsSsh to be a union type with an optional
discriminator (defaulting to ssh-agent) for back-compat, where
'auth':'ssh-agent' needs no further fields, 'auth':'password' adds in a
'secret' field for use as password, or where 'auth':'private-key' adds
in both 'key-file' and 'secret' for use as the two pieces needed for
private key use.

Markus may have other suggestions on how best to represent this sort of
union type in QAPI.

> +#
>  # 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' } }
>  
>  
>  ##
> 

On a different topic, how much of this work overlaps with the nbdkit ssh
plugin? Should we be duplicating efforts with both projects supporting
ssh natively, or is it worth considering getting qemu out of the ssh
business and instead connecting to an nbd device provided by nbdkit
connecting to ssh?  (For comparison, we've already decided that nbdkit
does not plan on writing a qcow2 plugin, because it defers to qemu to be
the expert there; or in the other direction, qemu-nbd has deprecated its
partial support for exposing only a partition of a disk in favor of
qemu-nbd having much more partition support through its filters)
Richard W.M. Jones July 26, 2019, 2:29 p.m. UTC | #2
On Fri, Jul 26, 2019 at 09:24:34AM -0500, Eric Blake wrote:
> On a different topic, how much of this work overlaps with the nbdkit ssh
> plugin? Should we be duplicating efforts with both projects supporting
> ssh natively, or is it worth considering getting qemu out of the ssh
> business and instead connecting to an nbd device provided by nbdkit
> connecting to ssh?  (For comparison, we've already decided that nbdkit
> does not plan on writing a qcow2 plugin, because it defers to qemu to be
> the expert there; or in the other direction, qemu-nbd has deprecated its
> partial support for exposing only a partition of a disk in favor of
> qemu-nbd having much more partition support through its filters)

I think it would be good if libvirt could handle this usage, so it
would set up the nbdkit process, set up seccomp or SELinux to confine
it, and kill nbdkit afterwards.

See also:

https://rwmj.wordpress.com/2018/10/30/split-block-drivers-from-qemu-with-nbdkit/

Rich.
Pino Toscano July 29, 2019, 8 a.m. UTC | #3
On Friday, 26 July 2019 16:24:34 CEST Eric Blake wrote:
> On 7/26/19 9:09 AM, Pino Toscano wrote:
> > 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 <ptoscano@redhat.com>
> 
> > +++ 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)
> 
> Is password-secret intended to be mutually-exclusive with
> private-key/private-key-secret?

My initial thought was to allow users to specify data for all the
authentication methods possible.  Either ways (all of them, or a single
one) are fine for me.
Markus Armbruster July 29, 2019, 10:57 a.m. UTC | #4
Pino Toscano <ptoscano@redhat.com> writes:

> On Friday, 26 July 2019 16:24:34 CEST Eric Blake wrote:
>> On 7/26/19 9:09 AM, Pino Toscano wrote:
>> > 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 <ptoscano@redhat.com>
>> 
>> > +++ 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)
>> 
>> Is password-secret intended to be mutually-exclusive with
>> private-key/private-key-secret?
>
> My initial thought was to allow users to specify data for all the
> authentication methods possible.  Either ways (all of them, or a single
> one) are fine for me.

How does this work at the libssh level?  Can you configure multiple
authentication methods, and let negotiation pick the one to be used?
Kevin Wolf July 29, 2019, 11:08 a.m. UTC | #5
Am 26.07.2019 um 16:24 hat Eric Blake geschrieben:
> On 7/26/19 9:09 AM, Pino Toscano wrote:
> > 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 <ptoscano@redhat.com>
> 
> > +++ 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)
> 
> Is password-secret intended to be mutually-exclusive with
> private-key/private-key-secret?  If so, this should probably utilize an
> enum for a discriminator
> { 'enum': 'SshAuth', 'data': ['ssh-agent', 'password', 'private'key'] }
> 
> then update BlockdevOptionsSsh to be a union type with an optional
> discriminator (defaulting to ssh-agent) for back-compat, where
> 'auth':'ssh-agent' needs no further fields, 'auth':'password' adds in a
> 'secret' field for use as password, or where 'auth':'private-key' adds
> in both 'key-file' and 'secret' for use as the two pieces needed for
> private key use.

Can we actually support optional discriminators when we don't have
defaults in the QAPI schema yet?

> On a different topic, how much of this work overlaps with the nbdkit ssh
> plugin? Should we be duplicating efforts with both projects supporting
> ssh natively, or is it worth considering getting qemu out of the ssh
> business and instead connecting to an nbd device provided by nbdkit
> connecting to ssh?

ssh behaves essentially like a filesystem whereas NBD behaves like a
block device. This is especially relevant for everything related to the
file size. As far as I know, using an image format like qcow2 that wants
to grow the image file isn't possible over NBD, whereas I expect it to
work with the ssh block driver.

Kevin
Pino Toscano July 29, 2019, 11:21 a.m. UTC | #6
On Monday, 29 July 2019 12:57:40 CEST Markus Armbruster wrote:
> Pino Toscano <ptoscano@redhat.com> writes:
> 
> > On Friday, 26 July 2019 16:24:34 CEST Eric Blake wrote:
> >> On 7/26/19 9:09 AM, Pino Toscano wrote:
> >> > 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 <ptoscano@redhat.com>
> >> 
> >> > +++ 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)
> >> 
> >> Is password-secret intended to be mutually-exclusive with
> >> private-key/private-key-secret?
> >
> > My initial thought was to allow users to specify data for all the
> > authentication methods possible.  Either ways (all of them, or a single
> > one) are fine for me.
> 
> How does this work at the libssh level?  Can you configure multiple
> authentication methods, and let negotiation pick the one to be used?

The remote servers sends all the authentication methods supported, and
the user of libssh can decide which one(s) to attempt.  See for example
the small tutorial in the libssh documentation:
http://api.libssh.org/stable/libssh_tutor_authentication.html
Markus Armbruster July 29, 2019, 3:10 p.m. UTC | #7
Pino Toscano <ptoscano@redhat.com> writes:

> On Monday, 29 July 2019 12:57:40 CEST Markus Armbruster wrote:
>> Pino Toscano <ptoscano@redhat.com> writes:
>> 
>> > On Friday, 26 July 2019 16:24:34 CEST Eric Blake wrote:
>> >> On 7/26/19 9:09 AM, Pino Toscano wrote:
>> >> > 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 <ptoscano@redhat.com>
>> >> 
>> >> > +++ 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)
>> >> 
>> >> Is password-secret intended to be mutually-exclusive with
>> >> private-key/private-key-secret?
>> >
>> > My initial thought was to allow users to specify data for all the
>> > authentication methods possible.  Either ways (all of them, or a single
>> > one) are fine for me.
>> 
>> How does this work at the libssh level?  Can you configure multiple
>> authentication methods, and let negotiation pick the one to be used?
>
> The remote servers sends all the authentication methods supported, and
> the user of libssh can decide which one(s) to attempt.  See for example
> the small tutorial in the libssh documentation:
> http://api.libssh.org/stable/libssh_tutor_authentication.html

SSH server and client negotiate: the server offers methods, the client
tries offered methods it likes one after the other.

This means we want QMP to let us configure which methods we like, along
with whatever data is necessary to actually try them.

In short, we don't want mutually exclusive here.

Correct?
Max Reitz Aug. 12, 2019, 9:08 p.m. UTC | #8
On 29.07.19 13:08, Kevin Wolf wrote:
> Am 26.07.2019 um 16:24 hat Eric Blake geschrieben:
>> On 7/26/19 9:09 AM, Pino Toscano wrote:
>>> 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 <ptoscano@redhat.com>
>>
>>> +++ 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)
>>
>> Is password-secret intended to be mutually-exclusive with
>> private-key/private-key-secret?  If so, this should probably utilize an
>> enum for a discriminator
>> { 'enum': 'SshAuth', 'data': ['ssh-agent', 'password', 'private'key'] }
>>
>> then update BlockdevOptionsSsh to be a union type with an optional
>> discriminator (defaulting to ssh-agent) for back-compat, where
>> 'auth':'ssh-agent' needs no further fields, 'auth':'password' adds in a
>> 'secret' field for use as password, or where 'auth':'private-key' adds
>> in both 'key-file' and 'secret' for use as the two pieces needed for
>> private key use.
> 
> Can we actually support optional discriminators when we don't have
> defaults in the QAPI schema yet?

Just chiming in here, because I wanted to throw in that v4 of my “block:
Try to create well-typed json:{} filenames​” series adds that.

>> On a different topic, how much of this work overlaps with the nbdkit ssh
>> plugin? Should we be duplicating efforts with both projects supporting
>> ssh natively, or is it worth considering getting qemu out of the ssh
>> business and instead connecting to an nbd device provided by nbdkit
>> connecting to ssh?
> 
> ssh behaves essentially like a filesystem whereas NBD behaves like a
> block device. This is especially relevant for everything related to the
> file size. As far as I know, using an image format like qcow2 that wants
> to grow the image file isn't possible over NBD, whereas I expect it to
> work with the ssh block driver.

Just using sshfs and file-posix would seem simpler to me, by the way.

Max
Eric Blake Aug. 12, 2019, 9:22 p.m. UTC | #9
On 7/29/19 6:08 AM, Kevin Wolf wrote:

>> On a different topic, how much of this work overlaps with the nbdkit ssh
>> plugin? Should we be duplicating efforts with both projects supporting
>> ssh natively, or is it worth considering getting qemu out of the ssh
>> business and instead connecting to an nbd device provided by nbdkit
>> connecting to ssh?
> 
> ssh behaves essentially like a filesystem whereas NBD behaves like a
> block device. This is especially relevant for everything related to the
> file size. As far as I know, using an image format like qcow2 that wants
> to grow the image file isn't possible over NBD, whereas I expect it to
> work with the ssh block driver.

Resizing NBD devices isn't available yet, but it is rapidly moving
higher on my list of TODO items to implement as an extension (the ideas
for how it should work have been tossed around, but having code to back
up those ideas is the next step).
diff mbox series

Patch

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' } }
 
 
 ##