From 831accb2a90b305422742ed2bb3eab5d927cee0e Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Fri, 24 Dec 2021 09:56:30 +0000 Subject: [PATCH] Expose openssh_bcrypt() to testcrypt, and test it. I happened to notice in passing that this function doesn't have any tests (although it will have been at least somewhat tested by the cmdgen interop test system). This involved writing a wrapper that passes the passphrase and salt as ptrlens, and I decided it made more sense to make the same change to the original function too and adjust the call sites appropriately. I derived a test case by getting OpenSSH itself to make an encrypted key file, and then using the inputs and output from the password hash operation that decrypted it again. --- crypto/bcrypt.c | 9 ++++----- import.c | 11 +++++------ ssh.h | 3 +-- test/cryptsuite.py | 14 ++++++++++++++ test/testcrypt-func.h | 3 +++ test/testcrypt.c | 9 +++++++++ 6 files changed, 36 insertions(+), 13 deletions(-) diff --git a/crypto/bcrypt.c b/crypto/bcrypt.c index 2b29b037..a4eb384a 100644 --- a/crypto/bcrypt.c +++ b/crypto/bcrypt.c @@ -69,8 +69,7 @@ void bcrypt_genblock(int counter, smemclr(&hashed_salt, sizeof(hashed_salt)); } -void openssh_bcrypt(const char *passphrase, - const unsigned char *salt, int saltbytes, +void openssh_bcrypt(ptrlen passphrase, ptrlen salt, int rounds, unsigned char *out, int outbytes) { unsigned char hashed_passphrase[64]; @@ -80,7 +79,7 @@ void openssh_bcrypt(const char *passphrase, int modulus, residue, i, j, round; /* Hash the passphrase to get the bcrypt key material */ - hash_simple(&ssh_sha512, ptrlen_from_asciz(passphrase), hashed_passphrase); + hash_simple(&ssh_sha512, passphrase, hashed_passphrase); /* We output key bytes in a scattered fashion to meld all output * key blocks into all parts of the output. To do this, we pick a @@ -97,8 +96,8 @@ void openssh_bcrypt(const char *passphrase, * by bcrypt in the following loop */ memset(outblock, 0, sizeof(outblock)); - thissalt = salt; - thissaltbytes = saltbytes; + thissalt = salt.ptr; + thissaltbytes = salt.len; for (round = 0; round < rounds; round++) { bcrypt_genblock(round == 0 ? residue+1 : 0, hashed_passphrase, diff --git a/import.c b/import.c index ccff2c14..41c06a9a 100644 --- a/import.c +++ b/import.c @@ -1363,9 +1363,8 @@ static ssh2_userkey *openssh_new_read( memset(keybuf, 0, keysize); break; case ON_K_BCRYPT: - openssh_bcrypt(passphrase, - key->kdfopts.bcrypt.salt.ptr, - key->kdfopts.bcrypt.salt.len, + openssh_bcrypt(ptrlen_from_asciz(passphrase), + key->kdfopts.bcrypt.salt, key->kdfopts.bcrypt.rounds, keybuf, keysize); break; @@ -1583,9 +1582,9 @@ static bool openssh_new_write( unsigned char keybuf[48]; ssh_cipher *cipher; - openssh_bcrypt(passphrase, - bcrypt_salt, sizeof(bcrypt_salt), bcrypt_rounds, - keybuf, sizeof(keybuf)); + openssh_bcrypt(ptrlen_from_asciz(passphrase), + make_ptrlen(bcrypt_salt, sizeof(bcrypt_salt)), + bcrypt_rounds, keybuf, sizeof(keybuf)); cipher = ssh_cipher_new(&ssh_aes256_sdctr); ssh_cipher_setkey(cipher, keybuf); diff --git a/ssh.h b/ssh.h index 77cddf99..294afada 100644 --- a/ssh.h +++ b/ssh.h @@ -1413,8 +1413,7 @@ void aes256_decrypt_pubkey(const void *key, const void *iv, void des_encrypt_xdmauth(const void *key, void *blk, int len); void des_decrypt_xdmauth(const void *key, void *blk, int len); -void openssh_bcrypt(const char *passphrase, - const unsigned char *salt, int saltbytes, +void openssh_bcrypt(ptrlen passphrase, ptrlen salt, int rounds, unsigned char *out, int outbytes); /* diff --git a/test/cryptsuite.py b/test/cryptsuite.py index e331bcf6..4971a599 100755 --- a/test/cryptsuite.py +++ b/test/cryptsuite.py @@ -2106,6 +2106,20 @@ culpa qui officia deserunt mollit anim id est laborum. "aeae2a21201eef5e347de22c922192e8f46274b0c9d33e965155a91e7686" "9d530e")) + def testOpenSSHBcrypt(self): + # Test case created by making an OpenSSH private key file + # using their own ssh-keygen, then decrypting it successfully + # using PuTTYgen and printing the inputs and outputs to + # openssh_bcrypt in the process. So this output key is known + # to agree with OpenSSH's own answer. + + self.assertEqualBin( + openssh_bcrypt('test passphrase', + unhex('d0c3b40ace4afeaf8c0f81202ae36718'), + 16, 48), + unhex('d78ba86e7273de0e007ab0ba256646823d5c902bc44293ae' + '78547e9a7f629be928cc78ff78a75a4feb7aa6f125079c7d')) + def testRSAVerify(self): def blobs(n, e, d, p, q, iqmp): pubblob = ssh_string(b"ssh-rsa") + ssh2_mpint(e) + ssh2_mpint(n) diff --git a/test/testcrypt-func.h b/test/testcrypt-func.h index b8e53a96..2cb0b3dc 100644 --- a/test/testcrypt-func.h +++ b/test/testcrypt-func.h @@ -447,6 +447,9 @@ FUNC_WRAPPED(val_string, argon2, ARG(argon2flavour, flavour), ARG(uint, mem), ARG(val_string_ptrlen, K), ARG(val_string_ptrlen, X)) FUNC(val_string, argon2_long_hash, ARG(uint, length), ARG(val_string_ptrlen, data)) +FUNC_WRAPPED(val_string, openssh_bcrypt, ARG(val_string_ptrlen, passphrase), + ARG(val_string_ptrlen, salt), ARG(uint, rounds), + ARG(uint, outbytes)) /* * Key generation functions. diff --git a/test/testcrypt.c b/test/testcrypt.c index 5936a248..5e0b73db 100644 --- a/test/testcrypt.c +++ b/test/testcrypt.c @@ -1087,6 +1087,15 @@ strbuf *argon2_wrapper(Argon2Flavour flavour, uint32_t mem, uint32_t passes, return out; } +strbuf *openssh_bcrypt_wrapper(ptrlen passphrase, ptrlen salt, + unsigned rounds, unsigned outbytes) +{ + strbuf *out = strbuf_new(); + openssh_bcrypt(passphrase, salt, rounds, + strbuf_append(out, outbytes), outbytes); + return out; +} + strbuf *get_implementations_commasep(ptrlen alg) { strbuf *out = strbuf_new();