From fdb2894e806afd60a4787f0a776f1ad1d219f0b5 Mon Sep 17 00:00:00 2001 From: Christian Hohnstaedt Date: Sun, 14 Feb 2021 00:03:50 +0100 Subject: [PATCH] Close #142: Support Ed25519 Import private SSH2 key Support loading OpenSSH ED25519 private key Improve private key verification in general. ED25519 does not support EVP_PKEY_sign(), so use EVP_DigestSign(), which in turn only works since OpenSSL 1.1.1 resulting in a backward compatibility quirk in openssl_compat. --- lib/openssl_compat.h | 19 +++++++ lib/pki_evp.cpp | 133 ++++++++++++++++++++++++++++++++++++------- lib/pki_evp.h | 4 ++ lib/pki_key.h | 3 +- lib/pki_multi.cpp | 4 +- 5 files changed, 139 insertions(+), 24 deletions(-) diff --git a/lib/openssl_compat.h b/lib/openssl_compat.h index e3528e82..ee78b951 100644 --- a/lib/openssl_compat.h +++ b/lib/openssl_compat.h @@ -118,4 +118,23 @@ static inline void EVP_CIPHER_CTX_free(EVP_CIPHER_CTX *ctx) #endif +#if OPENSSL_VERSION_NUMBER < 0x10101000L + +static inline int +EVP_DigestSign(EVP_MD_CTX *ctx, unsigned char *sigret, + size_t *siglen, const unsigned char *tbs, size_t tbslen) +{ + return EVP_DigestSignUpdate(ctx, tbs, tbslen) && + EVP_DigestSignFinal(ctx, sigret, siglen); +} + +static inline int +EVP_DigestVerify(EVP_MD_CTX *ctx, const unsigned char *sigret, + size_t siglen, const unsigned char *tbs, size_t tbslen) +{ + return EVP_DigestVerifyUpdate(ctx, tbs, tbslen) && + EVP_DigestVerifyFinal(ctx, (unsigned char *)sigret, siglen); +} +#endif + #endif diff --git a/lib/pki_evp.cpp b/lib/pki_evp.cpp index fde8d942..5b959ddc 100644 --- a/lib/pki_evp.cpp +++ b/lib/pki_evp.cpp @@ -259,7 +259,9 @@ void pki_evp::fromPEMbyteArray(const QByteArray &ba, const QString &name) pass_info p(XCA_TITLE, tr("Please enter the password to decrypt the private key %1.") .arg(name)); - do { + pkey = load_ssh_ed25519_privatekey(ba, p); + + while (!pkey) { pkey = PEM_read_bio_PrivateKey(BioByteArray(ba).ro(), NULL, PwDialog::pwCallback, &p); if (openssl_pw_error()) @@ -268,11 +270,10 @@ void pki_evp::fromPEMbyteArray(const QByteArray &ba, const QString &name) throw p.getResult(); if (pki_ign_openssl_error()) break; - } while (!pkey); - + } if (!pkey) { pki_ign_openssl_error(); - pkey = PEM_read_bio_PUBKEY(BioByteArray(ba).ro(), NULL, NULL, 0); + pkey = PEM_read_bio_PUBKEY(BioByteArray(ba).ro(), NULL, NULL,0); } pki_openssl_error(); set_EVP_PKEY(pkey, name); @@ -338,6 +339,78 @@ void pki_evp::set_EVP_PKEY(EVP_PKEY *pkey, QString name) pki_openssl_error(); } +EVP_PKEY *pki_evp::load_ssh_ed25519_privatekey(const QByteArray &ba, + const pass_info &p) +{ + EVP_PKEY *pkey = NULL; + unsigned char *pdata; + long plen; + QByteArray chunk, enc_algo, kdfname, kdf, pub, priv; + + (void)p; // Will be used later for decryption + if (!PEM_bytes_read_bio(&pdata, &plen, NULL, PEM_STRING_OPENSSH_KEY, + BioByteArray(ba).ro(), NULL, NULL)) + return NULL; + + QByteArray content((const char*)pdata, plen); + OPENSSL_free(pdata); + + if (!content.startsWith("openssh-key-v1") || + // also check trailing \0 + content.constData()[sizeof "openssh-key-v1" -1]) + return NULL; + + content.remove(0, sizeof "openssh-key-v1"); + // encryption: "none", "aes256-ctr" + enc_algo = ssh_key_next_chunk(&content); + // KDFName "bcrypt" + kdfname = ssh_key_next_chunk(&content); + kdf = ssh_key_next_chunk(&content); + + if (enc_algo != "none" || kdfname != "none") { + qCritical("Encrypted SSH ED25519 keys not supported, yet"); + return NULL; + } + // check bytes 00 00 00 01 + const char *d = content.constData(); + if (d[0] || d[1] || d[2] || d[3] != 1) + return NULL; + content.remove(0, 4); + // Handle first occurance of the public key + pub = ssh_key_next_chunk(&content); + ssh_key_check_chunk(&pub, "ssh-ed25519"); + pub = ssh_key_next_chunk(&pub); + if (pub.count() != ED25519_KEYLEN) + return NULL; + + // Followed by the private key + priv = ssh_key_next_chunk(&content); + // Drop 64bit random nonce + priv.remove(0, 8); + + ssh_key_check_chunk(&priv, "ssh-ed25519"); + // The first pubkey must match the second occurance + // in front of the private one + if (pub != ssh_key_next_chunk(&priv)) + return NULL; + priv = ssh_key_next_chunk(&priv); + // The private key is concatenated by the public key in one chunk + if (priv.count() != 2 * ED25519_KEYLEN) + return NULL; + // The last ED25519_KEYLEN bytes must match the public key + if (pub != priv.mid(ED25519_KEYLEN)) + return NULL; + // The first ED25519_KEYLEN octets are the private key +#ifndef OPENSSL_NO_EC +#ifdef EVP_PKEY_ED25519 + pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, NULL, + (const unsigned char *)priv.constData(), ED25519_KEYLEN); +#endif +#endif + pki_openssl_error(); + return pkey; +} + void pki_evp::fload(const QString &fname) { pass_info p(XCA_TITLE, tr("Please enter the password to decrypt the private key from file:\n%1"). @@ -348,6 +421,7 @@ void pki_evp::fload(const QString &fname) XFile file(fname); file.open_read(); EVP_PKEY *pkey; + do { pkey = PEM_read_PrivateKey(file.fp(), NULL, cb, &p); if (openssl_pw_error()) @@ -358,6 +432,7 @@ void pki_evp::fload(const QString &fname) break; file.retry_read(); } while (!pkey); + if (!pkey) { pki_ign_openssl_error(); file.retry_read(); @@ -383,8 +458,12 @@ void pki_evp::fload(const QString &fname) pki_ign_openssl_error(); file.retry_read(); pkey = b2i_PVK_bio(file.bio(), cb, &p); - pki_openssl_error(); - } + } + if (!pkey) { + pki_ign_openssl_error(); + file.retry_read(); + pkey = load_ssh_ed25519_privatekey(file.read(10000), p); + } #endif if (!pkey) { pki_ign_openssl_error(); @@ -400,13 +479,13 @@ void pki_evp::fload(const QString &fname) pki_ign_openssl_error(); file.retry_read(); pkey = load_ssh2_key(file); - } + } #if OPENSSL_VERSION_NUMBER >= 0x10000000L if (!pkey) { pki_ign_openssl_error(); file.retry_read(); pkey = b2i_PublicKey_bio(file.bio()); - } + } #endif if (pki_ign_openssl_error() || !pkey) { if (pkey) @@ -808,37 +887,47 @@ bool pki_evp::verify_priv(EVP_PKEY *pkey) const { bool verify = true; #if OPENSSL_VERSION_NUMBER >= 0x10000000L - unsigned char md[32], sig[1024]; - size_t mdlen = sizeof md, siglen = sizeof sig; - EVP_PKEY_CTX *ctx = NULL; + unsigned char data[32], sig[1024]; + size_t datalen = sizeof data, siglen = sizeof sig; + EVP_MD_CTX *ctx = NULL; + const EVP_MD *md = EVP_sha256(); + EVP_PKEY_CTX *pkctx = NULL; if (!EVP_PKEY_isPrivKey(pkey)) return true; do { - ctx = EVP_PKEY_CTX_new(pkey, NULL); + ctx = EVP_MD_CTX_new(); pki_ign_openssl_error(); - RAND_bytes(md, mdlen); + RAND_bytes(data, datalen); check_oom(ctx); verify = false; - /* Sign some random data in "md" */ - if (EVP_PKEY_sign_init(ctx) <= 0) + /* Sign some random data in "data" */ +#ifdef EVP_PKEY_ED25519 + if (EVP_PKEY_id(pkey) == EVP_PKEY_ED25519) + md = NULL; +#endif + if (!EVP_DigestSignInit(ctx, &pkctx, md, NULL, pkey)) break; + if (EVP_PKEY_id(pkey) == EVP_PKEY_RSA) - EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_PADDING); - if (EVP_PKEY_CTX_set_signature_md(ctx, EVP_sha256()) <= 0) - break; - if (EVP_PKEY_sign(ctx, sig, &siglen, md, mdlen) <= 0) + EVP_PKEY_CTX_set_rsa_padding(pkctx, RSA_PKCS1_PADDING); + + if (!EVP_DigestSign(ctx, sig, &siglen, data, datalen)) break; + /* Verify the signature */ - if (EVP_PKEY_verify_init(ctx) <= 0) + if (!EVP_DigestVerifyInit(ctx, NULL, md, NULL, pkey)) break; - if (EVP_PKEY_verify(ctx, sig, siglen, md, mdlen) <= 0) + + if (EVP_DigestVerify(ctx, sig, siglen, data, datalen) != 1) break; + verify = true; } while (0); + if (ctx) - EVP_PKEY_CTX_free(ctx); + EVP_MD_CTX_free(ctx); #endif if (EVP_PKEY_id(pkey) == EVP_PKEY_RSA && EVP_PKEY_isPrivKey(pkey)) { RSA *rsa = EVP_PKEY_get0_RSA(pkey); diff --git a/lib/pki_evp.h b/lib/pki_evp.h index 19a26e55..c7dd5e69 100644 --- a/lib/pki_evp.h +++ b/lib/pki_evp.h @@ -17,6 +17,8 @@ #define VIEW_private_ownpass 9 +class pass_info; + class pki_evp: public pki_key { Q_OBJECT @@ -58,6 +60,8 @@ class pki_evp: public pki_key static QString removeTypeFromIntName(QString n); void fromPEMbyteArray(const QByteArray &ba, const QString &name); void fload(const QString &fname); + EVP_PKEY *load_ssh_ed25519_privatekey(const QByteArray &ba, + const pass_info &p); void writeDefault(const QString &dirname) const; void fromData(const unsigned char *p, db_header_t *head); void writeKey(XFile &file, const EVP_CIPHER *enc, diff --git a/lib/pki_key.h b/lib/pki_key.h index b86ebe91..bac20221 100644 --- a/lib/pki_key.h +++ b/lib/pki_key.h @@ -17,6 +17,7 @@ #include "openssl_compat.h" +#define PEM_STRING_OPENSSH_KEY "OPENSSH PRIVATE KEY" #define MAX_KEY_LENGTH 4096 #define ED25519_KEYLEN 32 @@ -165,13 +166,13 @@ class pki_key: public pki_base void PEM_file_comment(XFile &file) const; void collect_properties(QMap &prp) const; - private: BIGNUM *ssh_key_data2bn(QByteArray *ba) const; void ssh_key_check_chunk(QByteArray *ba, const char *expect) const; QByteArray ssh_key_next_chunk(QByteArray *ba) const; void ssh_key_QBA2data(const QByteArray &ba, QByteArray *data) const; void ssh_key_bn2data(const BIGNUM *bn, QByteArray *data) const; + private: mutable int useCount; // usage counter public: diff --git a/lib/pki_multi.cpp b/lib/pki_multi.cpp index 1b517192..1de8a4c2 100644 --- a/lib/pki_multi.cpp +++ b/lib/pki_multi.cpp @@ -87,7 +87,9 @@ static pki_base *pkiByPEM(QString text, int *skip) text.startsWith(PEM_STRING_ECDSA_PUBLIC D5) || text.startsWith(PEM_STRING_ECPRIVATEKEY D5) || text.startsWith(PEM_STRING_PKCS8 D5) || - text.startsWith(PEM_STRING_PKCS8INF D5)) + text.startsWith(PEM_STRING_PKCS8INF D5) || + text.startsWith(PEM_STRING_OPENSSH_KEY D5)) + return new pki_evp(); return NULL;