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.
This commit is contained in:
Christian Hohnstaedt 2021-02-14 00:03:50 +01:00 committed by Christian Hohnstädt
parent 9cd58ff262
commit fdb2894e80
5 changed files with 139 additions and 24 deletions

View File

@ -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

View File

@ -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);

View File

@ -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,

View File

@ -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<QString, QString> &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:

View File

@ -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;