summaryrefslogtreecommitdiffstats
path: root/sshkey.c
diff options
context:
space:
mode:
authordjm@openbsd.org <djm@openbsd.org>2019-06-21 04:21:04 +0000
committerDamien Miller <djm@mindrot.org>2019-06-21 14:24:35 +1000
commit4f7a56d5e02e3d04ab69eac1213817a7536d0562 (patch)
treeb17da67f20831b53f9b00c6647c5eb1bdf88d626 /sshkey.c
parent4cd6b12cc9c10bf59c8b425041f3ea5091285a0f (diff)
upstream: Add protection for private keys at rest in RAM against
speculation and memory sidechannel attacks like Spectre, Meltdown, Rowhammer and Rambleed. This change encrypts private keys when they are not in use with a symmetic key that is derived from a relatively large "prekey" consisting of random data (currently 16KB). Attackers must recover the entire prekey with high accuracy before they can attempt to decrypt the shielded private key, but the current generation of attacks have bit error rates that, when applied cumulatively to the entire prekey, make this unlikely. Implementation-wise, keys are encrypted "shielded" when loaded and then automatically and transparently unshielded when used for signatures or when being saved/serialised. Hopefully we can remove this in a few years time when computer architecture has become less unsafe. been in snaps for a bit already; thanks deraadt@ ok dtucker@ deraadt@ OpenBSD-Commit-ID: 19767213c312e46f94b303a512ef8e9218a39bd4
Diffstat (limited to 'sshkey.c')
-rw-r--r--sshkey.c299
1 files changed, 282 insertions, 17 deletions
diff --git a/sshkey.c b/sshkey.c
index 379a579c..7aa7e772 100644
--- a/sshkey.c
+++ b/sshkey.c
@@ -1,4 +1,4 @@
-/* $OpenBSD: sshkey.c,v 1.75 2019/05/20 00:20:35 djm Exp $ */
+/* $OpenBSD: sshkey.c,v 1.76 2019/06/21 04:21:05 djm Exp $ */
/*
* Copyright (c) 2000, 2001 Markus Friedl. All rights reserved.
* Copyright (c) 2008 Alexander von Gernler. All rights reserved.
@@ -78,7 +78,15 @@
/* Version identification string for SSH v1 identity files. */
#define LEGACY_BEGIN "SSH PRIVATE KEY FILE FORMAT 1.1\n"
-int sshkey_private_serialize_opt(const struct sshkey *key,
+/*
+ * Constants relating to "shielding" support; protection of keys expected
+ * to remain in memory for long durations
+ */
+#define SSHKEY_SHIELD_PREKEY_LEN (16 * 1024)
+#define SSHKEY_SHIELD_CIPHER "aes256-ctr" /* XXX want AES-EME* */
+#define SSHKEY_SHIELD_PREKEY_HASH SSH_DIGEST_SHA512
+
+int sshkey_private_serialize_opt(struct sshkey *key,
struct sshbuf *buf, enum sshkey_serialize_rep);
static int sshkey_from_blob_internal(struct sshbuf *buf,
struct sshkey **keyp, int allow_cert);
@@ -604,6 +612,8 @@ sshkey_free(struct sshkey *k)
}
if (sshkey_is_cert(k))
cert_free(k->cert);
+ freezero(k->shielded_private, k->shielded_len);
+ freezero(k->shield_prekey, k->shield_prekey_len);
freezero(k, sizeof(*k));
}
@@ -1869,6 +1879,218 @@ sshkey_from_private(const struct sshkey *k, struct sshkey **pkp)
return r;
}
+int
+sshkey_is_shielded(struct sshkey *k)
+{
+ return k != NULL && k->shielded_private != NULL;
+}
+
+int
+sshkey_shield_private(struct sshkey *k)
+{
+ struct sshbuf *prvbuf = NULL;
+ u_char *prekey = NULL, *enc = NULL, keyiv[SSH_DIGEST_MAX_LENGTH];
+ struct sshcipher_ctx *cctx = NULL;
+ const struct sshcipher *cipher;
+ size_t i, enclen = 0;
+ struct sshkey *kswap = NULL, tmp;
+ int r = SSH_ERR_INTERNAL_ERROR;
+
+#ifdef DEBUG_PK
+ fprintf(stderr, "%s: entering for %s\n", __func__, sshkey_ssh_name(k));
+#endif
+ if ((cipher = cipher_by_name(SSHKEY_SHIELD_CIPHER)) == NULL) {
+ r = SSH_ERR_INVALID_ARGUMENT;
+ goto out;
+ }
+ if (cipher_keylen(cipher) + cipher_ivlen(cipher) >
+ ssh_digest_bytes(SSHKEY_SHIELD_PREKEY_HASH)) {
+ r = SSH_ERR_INTERNAL_ERROR;
+ goto out;
+ }
+
+ /* Prepare a random pre-key, and from it an ephemeral key */
+ if ((prekey = malloc(SSHKEY_SHIELD_PREKEY_LEN)) == NULL) {
+ r = SSH_ERR_ALLOC_FAIL;
+ goto out;
+ }
+ arc4random_buf(prekey, SSHKEY_SHIELD_PREKEY_LEN);
+ if ((r = ssh_digest_memory(SSHKEY_SHIELD_PREKEY_HASH,
+ prekey, SSHKEY_SHIELD_PREKEY_LEN,
+ keyiv, SSH_DIGEST_MAX_LENGTH)) != 0)
+ goto out;
+#ifdef DEBUG_PK
+ fprintf(stderr, "%s: key+iv\n", __func__);
+ sshbuf_dump_data(keyiv, ssh_digest_bytes(SSHKEY_SHIELD_PREKEY_HASH),
+ stderr);
+#endif
+ if ((r = cipher_init(&cctx, cipher, keyiv, cipher_keylen(cipher),
+ keyiv + cipher_keylen(cipher), cipher_ivlen(cipher), 1)) != 0)
+ goto out;
+
+ /* Serialise and encrypt the private key using the ephemeral key */
+ if ((prvbuf = sshbuf_new()) == NULL) {
+ r = SSH_ERR_ALLOC_FAIL;
+ goto out;
+ }
+ if (sshkey_is_shielded(k) && (r = sshkey_unshield_private(k)) != 0)
+ goto out;
+ if ((r = sshkey_private_serialize_opt(k, prvbuf,
+ SSHKEY_SERIALIZE_FULL)) != 0)
+ goto out;
+ /* pad to cipher blocksize */
+ i = 0;
+ while (sshbuf_len(prvbuf) % cipher_blocksize(cipher)) {
+ if ((r = sshbuf_put_u8(prvbuf, ++i & 0xff)) != 0)
+ goto out;
+ }
+#ifdef DEBUG_PK
+ fprintf(stderr, "%s: serialised\n", __func__);
+ sshbuf_dump(prvbuf, stderr);
+#endif
+ /* encrypt */
+ enclen = sshbuf_len(prvbuf);
+ if ((enc = malloc(enclen)) == NULL) {
+ r = SSH_ERR_ALLOC_FAIL;
+ goto out;
+ }
+ if ((r = cipher_crypt(cctx, 0, enc,
+ sshbuf_ptr(prvbuf), sshbuf_len(prvbuf), 0, 0)) != 0)
+ goto out;
+#ifdef DEBUG_PK
+ fprintf(stderr, "%s: encrypted\n", __func__);
+ sshbuf_dump_data(enc, enclen, stderr);
+#endif
+
+ /* Make a scrubbed, public-only copy of our private key argument */
+ if ((r = sshkey_from_private(k, &kswap)) != 0)
+ goto out;
+
+ /* Swap the private key out (it will be destroyed below) */
+ tmp = *kswap;
+ *kswap = *k;
+ *k = tmp;
+
+ /* Insert the shielded key into our argument */
+ k->shielded_private = enc;
+ k->shielded_len = enclen;
+ k->shield_prekey = prekey;
+ k->shield_prekey_len = SSHKEY_SHIELD_PREKEY_LEN;
+ enc = prekey = NULL; /* transferred */
+ enclen = 0;
+
+ /* success */
+ r = 0;
+
+ out:
+ /* XXX behaviour on error - invalidate original private key? */
+ cipher_free(cctx);
+ explicit_bzero(enc, enclen);
+ explicit_bzero(keyiv, sizeof(keyiv));
+ explicit_bzero(&tmp, sizeof(tmp));
+ freezero(prekey, SSHKEY_SHIELD_PREKEY_LEN);
+ sshkey_free(kswap);
+ sshbuf_free(prvbuf);
+ return r;
+}
+
+int
+sshkey_unshield_private(struct sshkey *k)
+{
+ struct sshbuf *prvbuf = NULL;
+ u_char pad, *cp, keyiv[SSH_DIGEST_MAX_LENGTH];
+ struct sshcipher_ctx *cctx = NULL;
+ const struct sshcipher *cipher;
+ size_t i;
+ struct sshkey *kswap = NULL, tmp;
+ int r = SSH_ERR_INTERNAL_ERROR;
+
+#ifdef DEBUG_PK
+ fprintf(stderr, "%s: entering for %s\n", __func__, sshkey_ssh_name(k));
+#endif
+ if (!sshkey_is_shielded(k))
+ return 0; /* nothing to do */
+
+ if ((cipher = cipher_by_name(SSHKEY_SHIELD_CIPHER)) == NULL) {
+ r = SSH_ERR_INVALID_ARGUMENT;
+ goto out;
+ }
+ if (cipher_keylen(cipher) + cipher_ivlen(cipher) >
+ ssh_digest_bytes(SSHKEY_SHIELD_PREKEY_HASH)) {
+ r = SSH_ERR_INTERNAL_ERROR;
+ goto out;
+ }
+ /* check size of shielded key blob */
+ if (k->shielded_len < cipher_blocksize(cipher) ||
+ (k->shielded_len % cipher_blocksize(cipher)) != 0) {
+ r = SSH_ERR_INVALID_FORMAT;
+ goto out;
+ }
+
+ /* Calculate the ephemeral key from the prekey */
+ if ((r = ssh_digest_memory(SSHKEY_SHIELD_PREKEY_HASH,
+ k->shield_prekey, k->shield_prekey_len,
+ keyiv, SSH_DIGEST_MAX_LENGTH)) != 0)
+ goto out;
+ if ((r = cipher_init(&cctx, cipher, keyiv, cipher_keylen(cipher),
+ keyiv + cipher_keylen(cipher), cipher_ivlen(cipher), 0)) != 0)
+ goto out;
+#ifdef DEBUG_PK
+ fprintf(stderr, "%s: key+iv\n", __func__);
+ sshbuf_dump_data(keyiv, ssh_digest_bytes(SSHKEY_SHIELD_PREKEY_HASH),
+ stderr);
+#endif
+
+ /* Decrypt and parse the shielded private key using the ephemeral key */
+ if ((prvbuf = sshbuf_new()) == NULL) {
+ r = SSH_ERR_ALLOC_FAIL;
+ goto out;
+ }
+ if ((r = sshbuf_reserve(prvbuf, k->shielded_len, &cp)) != 0)
+ goto out;
+ /* decrypt */
+#ifdef DEBUG_PK
+ fprintf(stderr, "%s: encrypted\n", __func__);
+ sshbuf_dump_data(k->shielded_private, k->shielded_len, stderr);
+#endif
+ if ((r = cipher_crypt(cctx, 0, cp,
+ k->shielded_private, k->shielded_len, 0, 0)) != 0)
+ goto out;
+#ifdef DEBUG_PK
+ fprintf(stderr, "%s: serialised\n", __func__);
+ sshbuf_dump(prvbuf, stderr);
+#endif
+ /* Parse private key */
+ if ((r = sshkey_private_deserialize(prvbuf, &kswap)) != 0)
+ goto out;
+ /* Check deterministic padding */
+ i = 0;
+ while (sshbuf_len(prvbuf)) {
+ if ((r = sshbuf_get_u8(prvbuf, &pad)) != 0)
+ goto out;
+ if (pad != (++i & 0xff)) {
+ r = SSH_ERR_INVALID_FORMAT;
+ goto out;
+ }
+ }
+
+ /* Swap the parsed key back into place */
+ tmp = *kswap;
+ *kswap = *k;
+ *k = tmp;
+
+ /* success */
+ r = 0;
+
+ out:
+ cipher_free(cctx);
+ explicit_bzero(keyiv, sizeof(keyiv));
+ explicit_bzero(&tmp, sizeof(tmp));
+ sshkey_free(kswap);
+ sshbuf_free(prvbuf);
+ return r;
+}
+
static int
cert_parse(struct sshbuf *b, struct sshkey *key, struct sshbuf *certbuf)
{
@@ -2373,41 +2595,55 @@ sshkey_check_sigtype(const u_char *sig, size_t siglen,
}
int
-sshkey_sign(const struct sshkey *key,
+sshkey_sign(struct sshkey *key,
u_char **sigp, size_t *lenp,
const u_char *data, size_t datalen, const char *alg, u_int compat)
{
+ int was_shielded = sshkey_is_shielded(key);
+ int r2, r = SSH_ERR_INTERNAL_ERROR;
+
if (sigp != NULL)
*sigp = NULL;
if (lenp != NULL)
*lenp = 0;
if (datalen > SSH_KEY_MAX_SIGN_DATA_SIZE)
return SSH_ERR_INVALID_ARGUMENT;
+ if ((r = sshkey_unshield_private(key)) != 0)
+ return r;
switch (key->type) {
#ifdef WITH_OPENSSL
case KEY_DSA_CERT:
case KEY_DSA:
- return ssh_dss_sign(key, sigp, lenp, data, datalen, compat);
+ r = ssh_dss_sign(key, sigp, lenp, data, datalen, compat);
+ break;
# ifdef OPENSSL_HAS_ECC
case KEY_ECDSA_CERT:
case KEY_ECDSA:
- return ssh_ecdsa_sign(key, sigp, lenp, data, datalen, compat);
+ r = ssh_ecdsa_sign(key, sigp, lenp, data, datalen, compat);
+ break;
# endif /* OPENSSL_HAS_ECC */
case KEY_RSA_CERT:
case KEY_RSA:
- return ssh_rsa_sign(key, sigp, lenp, data, datalen, alg);
+ r = ssh_rsa_sign(key, sigp, lenp, data, datalen, alg);
+ break;
#endif /* WITH_OPENSSL */
case KEY_ED25519:
case KEY_ED25519_CERT:
- return ssh_ed25519_sign(key, sigp, lenp, data, datalen, compat);
+ r = ssh_ed25519_sign(key, sigp, lenp, data, datalen, compat);
+ break;
#ifdef WITH_XMSS
case KEY_XMSS:
case KEY_XMSS_CERT:
- return ssh_xmss_sign(key, sigp, lenp, data, datalen, compat);
+ r = ssh_xmss_sign(key, sigp, lenp, data, datalen, compat);
+ break;
#endif /* WITH_XMSS */
default:
- return SSH_ERR_KEY_TYPE_UNKNOWN;
+ r = SSH_ERR_KEY_TYPE_UNKNOWN;
+ break;
}
+ if (was_shielded && (r2 = sshkey_shield_private(key)) != 0)
+ return r2;
+ return r;
}
/*
@@ -2652,7 +2888,7 @@ sshkey_certify_custom(struct sshkey *k, struct sshkey *ca, const char *alg,
}
static int
-default_key_sign(const struct sshkey *key, u_char **sigp, size_t *lenp,
+default_key_sign(struct sshkey *key, u_char **sigp, size_t *lenp,
const u_char *data, size_t datalen,
const char *alg, u_int compat, void *ctx)
{
@@ -2762,15 +2998,21 @@ sshkey_format_cert_validity(const struct sshkey_cert *cert, char *s, size_t l)
}
int
-sshkey_private_serialize_opt(const struct sshkey *key, struct sshbuf *b,
+sshkey_private_serialize_opt(struct sshkey *key, struct sshbuf *buf,
enum sshkey_serialize_rep opts)
{
int r = SSH_ERR_INTERNAL_ERROR;
+ int was_shielded = sshkey_is_shielded(key);
+ struct sshbuf *b = NULL;
#ifdef WITH_OPENSSL
const BIGNUM *rsa_n, *rsa_e, *rsa_d, *rsa_iqmp, *rsa_p, *rsa_q;
const BIGNUM *dsa_p, *dsa_q, *dsa_g, *dsa_pub_key, *dsa_priv_key;
#endif /* WITH_OPENSSL */
+ if ((r = sshkey_unshield_private(key)) != 0)
+ return r;
+ if ((b = sshbuf_new()) == NULL)
+ return SSH_ERR_ALLOC_FAIL;
if ((r = sshbuf_put_cstring(b, sshkey_ssh_name(key))) != 0)
goto out;
switch (key->type) {
@@ -2896,14 +3138,23 @@ sshkey_private_serialize_opt(const struct sshkey *key, struct sshbuf *b,
r = SSH_ERR_INVALID_ARGUMENT;
goto out;
}
- /* success */
+ /*
+ * success (but we still need to append the output to buf after
+ * possibly re-shielding the private key)
+ */
r = 0;
out:
+ if (was_shielded)
+ r = sshkey_shield_private(key);
+ if (r == 0)
+ r = sshbuf_putb(buf, b);
+ sshbuf_free(b);
+
return r;
}
int
-sshkey_private_serialize(const struct sshkey *key, struct sshbuf *b)
+sshkey_private_serialize(struct sshkey *key, struct sshbuf *b)
{
return sshkey_private_serialize_opt(key, b,
SSHKEY_SERIALIZE_DEFAULT);
@@ -3358,7 +3609,7 @@ sshkey_dump_ec_key(const EC_KEY *key)
#endif /* WITH_OPENSSL && OPENSSL_HAS_ECC */
static int
-sshkey_private_to_blob2(const struct sshkey *prv, struct sshbuf *blob,
+sshkey_private_to_blob2(struct sshkey *prv, struct sshbuf *blob,
const char *passphrase, const char *comment, const char *ciphername,
int rounds)
{
@@ -3728,20 +3979,28 @@ sshkey_parse_private2(struct sshbuf *blob, int type, const char *passphrase,
#ifdef WITH_OPENSSL
/* convert SSH v2 key in OpenSSL PEM format */
static int
-sshkey_private_pem_to_blob(struct sshkey *key, struct sshbuf *blob,
+sshkey_private_pem_to_blob(struct sshkey *key, struct sshbuf *buf,
const char *_passphrase, const char *comment)
{
+ int was_shielded = sshkey_is_shielded(key);
int success, r;
int blen, len = strlen(_passphrase);
u_char *passphrase = (len > 0) ? (u_char *)_passphrase : NULL;
const EVP_CIPHER *cipher = (len > 0) ? EVP_aes_128_cbc() : NULL;
char *bptr;
BIO *bio = NULL;
+ struct sshbuf *blob;
if (len > 0 && len <= 4)
return SSH_ERR_PASSPHRASE_TOO_SHORT;
- if ((bio = BIO_new(BIO_s_mem())) == NULL)
+ if ((blob = sshbuf_new()) == NULL)
return SSH_ERR_ALLOC_FAIL;
+ if ((bio = BIO_new(BIO_s_mem())) == NULL) {
+ sshbuf_free(blob);
+ return SSH_ERR_ALLOC_FAIL;
+ }
+ if ((r = sshkey_unshield_private(key)) != 0)
+ goto out;
switch (key->type) {
case KEY_DSA:
@@ -3774,6 +4033,12 @@ sshkey_private_pem_to_blob(struct sshkey *key, struct sshbuf *blob,
goto out;
r = 0;
out:
+ if (was_shielded)
+ r = sshkey_shield_private(key);
+ if (r == 0)
+ r = sshbuf_putb(buf, blob);
+ sshbuf_free(blob);
+
BIO_free(bio);
return r;
}
@@ -4102,7 +4367,7 @@ sshkey_set_filename(struct sshkey *k, const char *filename)
}
#else
int
-sshkey_private_serialize_maxsign(const struct sshkey *k, struct sshbuf *b,
+sshkey_private_serialize_maxsign(struct sshkey *k, struct sshbuf *b,
u_int32_t maxsign, sshkey_printfn *pr)
{
return sshkey_private_serialize_opt(k, b, SSHKEY_SERIALIZE_DEFAULT);