[PATCH 1/2] Add support for openssl engine based keys

James Bottomley James.Bottomley at HansenPartnership.com
Fri Jan 31 02:24:01 AEDT 2020


Engine keys are keys whose file format is understood by a specific
engine rather than by openssl itself.  Since these keys are file
based, the pkcs11 interface isn't appropriate for them because they
don't actually represent tokens.  The current most useful engine for
openssh keys are the TPM engines, which allow all private keys to be
stored in a form only the TPM hardware can decode, making them
impossible to steal.

To convert an existing ssh key to engine form, the ssh private key
usually has to be converted first to a form openssl can understand,
like PKCS8 and then passed into the engine conversion command.  So to
convert a private key stored in file rsa to TPM engine format, you do

ssh-keygen -p -m PKCS8 -f rsa
create_tpm2_key -w rsa rsa.tpm

Then to use the TPM key simply mv rsa.tpm rsa and openssh will be able
to use this key using the -o option to specify the engine:

ssh -o tpm2 -i rsa user at host

Note that engines usually have specific limits on the type of keys
they accept (so TPM 2.0 usually only does 2048 bits for RSA and NIST
elliptic curves) so not all existing openssh keys can be converted to
engine keys.

Signed-off-by: James Bottomley <James.Bottomley at HansenPartnership.com>
---
 Makefile.in  |   2 +-
 authfd.c     |  46 ++++++++++++++++++
 authfd.h     |   7 +++
 ssh-add.c    |  41 ++++++++++++++--
 ssh-agent.c  |  83 ++++++++++++++++++++++++++++++++
 ssh-engine.c | 154 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 ssh-engine.h |  10 ++++
 7 files changed, 338 insertions(+), 5 deletions(-)
 create mode 100644 ssh-engine.c
 create mode 100644 ssh-engine.h

diff --git a/Makefile.in b/Makefile.in
index e7549470c..aa4be9612 100644
--- a/Makefile.in
+++ b/Makefile.in
@@ -136,7 +136,7 @@ SCP_OBJS=	scp.o progressmeter.o
 
 SSHADD_OBJS=	ssh-add.o $(SKOBJS)
 
-SSHAGENT_OBJS=	ssh-agent.o ssh-pkcs11-client.o $(SKOBJS)
+SSHAGENT_OBJS=	ssh-agent.o ssh-pkcs11-client.o ssh-engine.o $(SKOBJS)
 
 SSHKEYGEN_OBJS=	ssh-keygen.o sshsig.o $(SKOBJS)
 
diff --git a/authfd.c b/authfd.c
index 05fd45401..7cd22044e 100644
--- a/authfd.c
+++ b/authfd.c
@@ -569,6 +569,52 @@ ssh_remove_identity(int sock, struct sshkey *key)
 	return r;
 }
 
+/*
+ * Add an engine based identity
+ */
+int
+ssh_add_engine_key(int sock, const char *file, const char *engine,
+		   const char *pin, u_int lifetime, u_int confirm,
+		   u_int maxsign)
+{
+	struct sshbuf *msg;
+	int r, constrained = (lifetime || confirm);
+	u_char type = constrained ? SSH_AGENTC_ADD_ENGINE_KEY_CONSTRAINED :
+				    SSH_AGENTC_ADD_ENGINE_KEY;
+
+	msg = sshbuf_new();
+	if (!msg)
+		return SSH_ERR_ALLOC_FAIL;
+	r = sshbuf_put_u8(msg, type);
+	if (r)
+		goto out;
+	r = sshbuf_put_cstring(msg, engine);
+	if (r)
+		goto out;
+	r = sshbuf_put_cstring(msg, file);
+	if (r)
+		goto out;
+	r = sshbuf_put_cstring(msg, pin);
+	if (r)
+		goto out;
+	if (constrained) {
+		r = encode_constraints(msg, lifetime, confirm, maxsign, NULL);
+		if (r)
+			goto out;
+	}
+	r = ssh_request_reply(sock, msg, msg);
+	if (r)
+		goto out;
+	r = sshbuf_get_u8(msg, &type);
+	if (r)
+		goto out;
+	r = (signed char)type;
+ out:
+	sshbuf_free(msg);
+	return r;
+}
+
+
 /*
  * Add/remove an token-based identity from the authentication server.
  * This call is intended only for use by ssh-add(1) and like applications.
diff --git a/authfd.h b/authfd.h
index c3bf6259a..1dd1070cb 100644
--- a/authfd.h
+++ b/authfd.h
@@ -38,6 +38,9 @@ int	ssh_remove_identity(int sock, struct sshkey *key);
 int	ssh_update_card(int sock, int add, const char *reader_id,
 	    const char *pin, u_int life, u_int confirm);
 int	ssh_remove_all_identities(int sock, int version);
+int	ssh_add_engine_key(int sock, const char *file, const char *engine,
+			   const char *pin, u_int lifetime, u_int confirm,
+			   u_int maxsign);
 
 int	ssh_agent_sign(int sock, const struct sshkey *key,
 	    u_char **sigp, size_t *lenp,
@@ -76,6 +79,10 @@ int	ssh_agent_sign(int sock, const struct sshkey *key,
 #define SSH2_AGENTC_ADD_ID_CONSTRAINED		25
 #define SSH_AGENTC_ADD_SMARTCARD_KEY_CONSTRAINED 26
 
+/* engine keys */
+#define SSH_AGENTC_ADD_ENGINE_KEY		27
+#define SSH_AGENTC_ADD_ENGINE_KEY_CONSTRAINED	28
+
 #define	SSH_AGENT_CONSTRAIN_LIFETIME		1
 #define	SSH_AGENT_CONSTRAIN_CONFIRM		2
 #define	SSH_AGENT_CONSTRAIN_MAXSIGN		3
diff --git a/ssh-add.c b/ssh-add.c
index f3b666c93..e988023a7 100644
--- a/ssh-add.c
+++ b/ssh-add.c
@@ -111,6 +111,29 @@ clear_pass(void)
 	}
 }
 
+static int
+add_engine_key(int agent_fd, const char *file, const char *engine)
+{
+	int ret;
+	char *pin = NULL;
+
+	ret = ssh_add_engine_key(agent_fd, file, engine, NULL, lifetime, confirm, maxsign);
+	if (ret == SSH_ERR_KEY_WRONG_PASSPHRASE) {
+		pin = read_passphrase("Enter engine key passphrase:", RP_ALLOW_STDIN);
+		if (!pin)
+			return -1;
+		ret = ssh_add_engine_key(agent_fd, file, engine, pin, lifetime, confirm, maxsign);
+	}
+	if (ret != SSH_AGENT_SUCCESS) {
+		fprintf(stderr, "failed to add engine key: %s\n", ssh_err(ret));
+	} else {
+		fprintf(stderr, "Engine Identity added: %s\n", file);
+	}
+	if (pin)
+		free (pin);
+	return ret;
+}
+
 static int
 delete_file(int agent_fd, const char *filename, int key_only, int qflag)
 {
@@ -609,6 +632,9 @@ usage(void)
 #ifdef WITH_XMSS
 "               [-M maxsign] [-m minleft]\n"
 #endif
+#ifdef USE_OPENSSL_ENGINE
+"               [-o engine]\n"
+#endif
 "               [file ...]\n"
 "       ssh-add -s pkcs11\n"
 "       ssh-add -e pkcs11\n"
@@ -622,7 +648,7 @@ main(int argc, char **argv)
 	extern char *optarg;
 	extern int optind;
 	int agent_fd;
-	char *pkcs11provider = NULL, *skprovider = NULL;
+	char *pkcs11provider = NULL, *skprovider = NULL, *opensslengine = NULL;
 	int r, i, ch, deleting = 0, ret = 0, key_only = 0, do_download = 0;
 	int xflag = 0, lflag = 0, Dflag = 0, qflag = 0, Tflag = 0;
 	SyslogFacility log_facility = SYSLOG_FACILITY_AUTH;
@@ -653,7 +679,7 @@ main(int argc, char **argv)
 
 	skprovider = getenv("SSH_SK_PROVIDER");
 
-	while ((ch = getopt(argc, argv, "vkKlLcdDTxXE:e:M:m:qs:S:t:")) != -1) {
+	while ((ch = getopt(argc, argv, "vkKlLcdDTxXE:e:M:m:qs:S:t:o:")) != -1) {
 		switch (ch) {
 		case 'v':
 			if (log_level == SYSLOG_LEVEL_INFO)
@@ -732,6 +758,9 @@ main(int argc, char **argv)
 		case 'T':
 			Tflag = 1;
 			break;
+		case 'o':
+			opensslengine = optarg;
+			break;
 		default:
 			usage();
 			ret = 1;
@@ -771,9 +800,13 @@ main(int argc, char **argv)
 		ret = r == 0 ? 0 : 1;
 		goto done;
 	}
-	if (pkcs11provider != NULL) {
+	if (opensslengine != NULL) {
+		if (add_engine_key(agent_fd, argv[0], opensslengine) < 0)
+		    ret = 1;
+		goto done;
+	} else if (pkcs11provider != NULL) {
 		if (update_card(agent_fd, !deleting, pkcs11provider,
-		    qflag) == -1)
+				qflag) == -1)
 			ret = 1;
 		goto done;
 	}
diff --git a/ssh-agent.c b/ssh-agent.c
index 5c9a9de60..4997b50e4 100644
--- a/ssh-agent.c
+++ b/ssh-agent.c
@@ -89,9 +89,14 @@
 #include "msg.h"
 #include "ssherr.h"
 #include "pathnames.h"
+#include "authfile.h"
 #include "ssh-pkcs11.h"
 #include "sk-api.h"
 
+#ifdef USE_OPENSSL_ENGINE
+#include "ssh-engine.h"
+#endif
+
 #ifndef DEFAULT_PROVIDER_WHITELIST
 # define DEFAULT_PROVIDER_WHITELIST "/usr/lib*/*,/usr/local/lib*/*"
 #endif
@@ -640,6 +645,78 @@ no_identities(SocketEntry *e)
 	sshbuf_free(msg);
 }
 
+#ifdef USE_OPENSSL_ENGINE
+static void
+process_add_engine_key(SocketEntry *e)
+{
+	char *engine, *pin, *file, *comment;
+	int r, confirm = 0;
+	u_int seconds;
+	time_t death = 0;
+	u_char type;
+	struct sshkey *k, *kp;
+	Identity *id;
+
+	if ((r = sshbuf_get_cstring(e->request, &engine, NULL)) != 0 ||
+	    (r = sshbuf_get_cstring(e->request, &file, NULL)) != 0 ||
+	    (r = sshbuf_get_cstring(e->request, &pin, NULL)) != 0)
+		fatal("%s: buffer error: %s", __func__, ssh_err(r));
+
+	while (sshbuf_len(e->request)) {
+		if ((r = sshbuf_get_u8(e->request, &type)) != 0)
+			fatal("%s: buffer error: %s", __func__, ssh_err(r));
+		switch (type) {
+		case SSH_AGENT_CONSTRAIN_LIFETIME:
+			if ((r = sshbuf_get_u32(e->request, &seconds)) != 0)
+				fatal("%s: buffer error: %s",
+				    __func__, ssh_err(r));
+			death = monotime() + seconds;
+			break;
+		case SSH_AGENT_CONSTRAIN_CONFIRM:
+			confirm = 1;
+			break;
+		default:
+			error("%s: Unknown constraint type %d", __func__, type);
+			goto send;
+		}
+	}
+	if (lifetime && !death)
+		death = monotime() + lifetime;
+
+	if ((r = engine_process_add(engine, file, pin, &k)) < 0)
+		goto send;
+
+	if (sshkey_load_public(file, &kp, &comment) < 0)
+		comment = xstrdup(file);
+	else
+		sshkey_free(kp);
+
+	r = SSH_AGENT_SUCCESS;
+	if (lookup_identity(k) == NULL) {
+		id = xcalloc(1, sizeof(Identity));
+		id->key = k;
+		id->provider = xstrdup(engine);
+		id->comment = comment;
+		id->death = death;
+		id->confirm = confirm;
+		TAILQ_INSERT_TAIL(&idtab->idlist, id, next);
+		idtab->nentries++;
+	} else {
+		/* key is already present, just return success */
+		sshkey_free(k);
+	}
+
+send:
+	free(pin);
+	free(engine);
+	free(file);
+	/* open code send_status because need to return actual error */
+	if (sshbuf_put_u32(e->output, 1) != 0 ||
+	    sshbuf_put_u8(e->output, r) != 0)
+		fatal("%s: buffer error", __func__);
+}
+#endif	/* USE_OPENSSL_ENGINE */
+
 #ifdef ENABLE_PKCS11
 static void
 process_add_smartcard_key(SocketEntry *e)
@@ -860,6 +937,12 @@ process_message(u_int socknum)
 		process_remove_smartcard_key(e);
 		break;
 #endif /* ENABLE_PKCS11 */
+#ifdef USE_OPENSSL_ENGINE
+	case SSH_AGENTC_ADD_ENGINE_KEY:
+	case SSH_AGENTC_ADD_ENGINE_KEY_CONSTRAINED:
+		process_add_engine_key(e);
+		break;
+#endif	/* USE_OPENSSL_ENGINE */
 	default:
 		/* Unknown message.  Respond with failure. */
 		error("Unknown message %d", type);
diff --git a/ssh-engine.c b/ssh-engine.c
new file mode 100644
index 000000000..90150c543
--- /dev/null
+++ b/ssh-engine.c
@@ -0,0 +1,154 @@
+#include "includes.h"
+
+#include <string.h>
+
+#include <openssl/engine.h>
+#include <openssl/evp.h>
+#include <openssl/ui.h>
+
+#include "log.h"
+#include "ssh-engine.h"
+#include "sshkey.h"
+#include "ssherr.h"
+#include "xmalloc.h"
+
+struct ui_data {
+	char *passphrase;
+	int ret;
+};
+
+static int
+ui_read(UI *ui, UI_STRING *uis)
+{
+	struct ui_data *d = UI_get0_user_data(ui);
+	d->ret = 0;
+
+	if (UI_get_string_type(uis) == UIT_PROMPT) {
+		if (d->passphrase == NULL || d->passphrase[0] == '\0') {
+			/* we sent no passphrase but get asked for one
+			 * send an interrupt event to avoid DA implications */
+			d->ret = -2;
+		} else {
+			UI_set_result(ui, uis, d->passphrase);
+			d->ret = 1;
+		}
+	}
+
+	return d->ret;
+}
+
+int
+engine_process_add(char *engine, char *file, char *pin,
+		   struct sshkey **k)
+{
+	EVP_PKEY *pk;
+	ENGINE *e;
+	struct sshkey *key;
+	int ret;
+	UI_METHOD *ui;
+	EVP_PKEY_CTX *ctx;
+	char hash[SHA256_DIGEST_LENGTH], result[1024];
+	size_t siglen;
+	struct ui_data d;
+
+	verbose("%s: add provider=%s, key=%s", __func__, engine, file);
+
+	ret = SSH_ERR_INTERNAL_ERROR;
+	e = ENGINE_by_id(engine);
+	if (!e) {
+		verbose("%s: failed to get engine %s", __func__, engine);
+		ERR_print_errors_fp(stderr);
+		return ret;
+	}
+
+	ui = UI_create_method("ssh-agent password writer");
+	if (!ui) {
+		verbose("%s: failed to create UI method", __func__);
+		ERR_print_errors_fp(stderr);
+		return ret;
+	}
+	UI_method_set_reader(ui, ui_read);
+
+	if (!ENGINE_init(e)) {
+		verbose("%s: failed to init engine %s", __func__, engine);
+		ERR_print_errors_fp(stderr);
+		return ret;
+	}
+
+	d.passphrase = pin;
+	pk = ENGINE_load_private_key(e, file, ui, &d);
+	ENGINE_finish(e);
+
+	if (d.ret == -2)
+		return SSH_ERR_KEY_WRONG_PASSPHRASE;
+
+	if (!pk) {
+		verbose("%s: engine returned no key", __func__);
+		ERR_print_errors_fp(stderr);
+		return ret;
+	}
+
+	/* here's a nasty problem: most engines don't tell us the password
+	 * was wrong until we try to use the key, so do a test to see */
+	ctx = EVP_PKEY_CTX_new(pk, NULL);
+	if (!ctx) {
+		verbose("%s: openssl context allocation failed", __func__);
+		ERR_print_errors_fp(stderr);
+		goto err_free_pkey;
+	}
+
+	EVP_PKEY_sign_init(ctx);
+
+	siglen=sizeof(result);
+	ret = EVP_PKEY_sign(ctx, result, &siglen, hash, sizeof(hash));
+	EVP_PKEY_CTX_free(ctx);
+
+	if (ret != 1 || siglen == 0) {
+		verbose("%s: trial signature failed with %d", __func__, ret);
+		ERR_print_errors_fp(stderr);
+		ret = SSH_ERR_KEY_WRONG_PASSPHRASE;
+		goto err_free_pkey;
+	}
+
+	ret = SSH_ERR_ALLOC_FAIL;
+
+	key = sshkey_new(KEY_UNSPEC);
+	key->flags |= SSHKEY_FLAG_EXT;
+	if (!key)
+		goto err_free_pkey;
+
+	switch (EVP_PKEY_id(pk)) {
+	case EVP_PKEY_RSA:
+		key->type = KEY_RSA;
+		key->rsa = EVP_PKEY_get1_RSA(pk);
+		break;
+	case EVP_PKEY_DSA:
+		key->type = KEY_DSA;
+		key->dsa = EVP_PKEY_get1_DSA(pk);
+		break;
+#ifdef OPENSSL_HAS_ECC
+	case EVP_PKEY_EC:
+		key->type = KEY_ECDSA;
+		key->ecdsa = EVP_PKEY_get1_EC_KEY(pk);
+		key->ecdsa_nid = sshkey_ecdsa_key_to_nid(key->ecdsa);
+		if (key->ecdsa_nid == -1 ||
+		    sshkey_curve_nid_to_name(key->ecdsa_nid) == NULL)
+			goto err_free_sshkey;
+		break;
+#endif
+	default:
+		verbose("%s: Unrecognised key type %d\n", __func__, EVP_PKEY_id(pk));
+		ret = SSH_ERR_INVALID_FORMAT;
+		goto err_free_sshkey;
+	}
+	*k = key;
+	key = NULL;
+	ret = 1;
+ err_free_sshkey:
+	if (key)
+		sshkey_free(key);
+ err_free_pkey:
+	EVP_PKEY_free(pk);
+	verbose("%s: returning %d", __func__, ret);
+	return ret;
+}
diff --git a/ssh-engine.h b/ssh-engine.h
new file mode 100644
index 000000000..b5e85f44d
--- /dev/null
+++ b/ssh-engine.h
@@ -0,0 +1,10 @@
+#ifndef _ENGINE_H
+#define _ENGINE_H
+
+#include "sshkey.h"
+
+int
+engine_process_add(char *engine, char *file, char *pin,
+		   struct sshkey **k);
+
+#endif
-- 
2.16.4



More information about the openssh-unix-dev mailing list