From e9aa28fe02e2ad23b2433af1158306185a369dba Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Sun, 21 Feb 2021 22:29:10 +0000 Subject: [PATCH] Restore the ability to write out PPK v2. This commit adds the capability in principle to ppk_save_sb, by adding a fmt_version field in the save parameters structure. As yet it's not connected up to any user interface in PuTTYgen, but I think I'll need to, because currently there's no way at all to convert PPK v3 back to v2, and surely people will need to interoperate with older installations of PuTTY, or with other PPK-consuming software. --- ssh.h | 5 +++++ sshpubk.c | 30 +++++++++++++++++++----------- test/cryptsuite.py | 19 +++++++++++++++---- testcrypt.c | 8 +++++--- testcrypt.h | 2 +- 5 files changed, 45 insertions(+), 19 deletions(-) diff --git a/ssh.h b/ssh.h index 45675ce8..99c51da1 100644 --- a/ssh.h +++ b/ssh.h @@ -1235,6 +1235,11 @@ int rsa1_load_f(const Filename *filename, RSAKey *key, const char *passphrase, const char **errorstr); typedef struct ppk_save_parameters { + unsigned fmt_version; /* currently 2 or 3 */ + + /* + * Parameters for fmt_version == 3 + */ Argon2Flavour argon2_flavour; uint32_t argon2_mem; /* in Kb */ bool argon2_passes_auto; diff --git a/sshpubk.c b/sshpubk.c index fc840f44..d5f5c70d 100644 --- a/sshpubk.c +++ b/sshpubk.c @@ -1410,6 +1410,8 @@ void base64_encode(FILE *fp, const unsigned char *data, int datalen, int cpl) } const ppk_save_parameters ppk_save_default_parameters = { + .fmt_version = 3, + /* * The Argon2 spec recommends the hybrid variant Argon2id, where * you don't have a good reason to go with the pure Argon2d or @@ -1505,19 +1507,25 @@ strbuf *ppk_save_sb(ssh2_userkey *key, const char *passphrase, * copy for us to retrieve. */ ppk_save_parameters params = *params_orig; - /* Invent a salt for the password hash. */ strbuf *passphrase_salt = strbuf_new(); - if (params.salt) - put_data(passphrase_salt, params.salt, params.saltlen); - else - random_read(strbuf_append(passphrase_salt, 16), 16); + + if (params.fmt_version == 3) { + /* Invent a salt for the password hash. */ + if (params.salt) + put_data(passphrase_salt, params.salt, params.saltlen); + else + random_read(strbuf_append(passphrase_salt, 16), 16); + } cipher_mac_keys_blob = strbuf_new(); - ssh2_ppk_derive_keys(3, ciphertype, + ssh2_ppk_derive_keys(params.fmt_version, ciphertype, ptrlen_from_asciz(passphrase ? passphrase : ""), cipher_mac_keys_blob, &cipherkey, &cipheriv, &mackey, ptrlen_from_strbuf(passphrase_salt), ¶ms); + const ssh2_macalg *macalg = (params.fmt_version == 2 ? + &ssh_hmac_sha1 : &ssh_hmac_sha256); + /* Now create the MAC. */ { strbuf *macdata; @@ -1529,8 +1537,7 @@ strbuf *ppk_save_sb(ssh2_userkey *key, const char *passphrase, put_string(macdata, pub_blob->s, pub_blob->len); put_string(macdata, priv_blob_encrypted, priv_encrypted_len); - mac_simple(&ssh_hmac_sha256, mackey, - ptrlen_from_strbuf(macdata), priv_mac); + mac_simple(macalg, mackey, ptrlen_from_strbuf(macdata), priv_mac); strbuf_free(macdata); } @@ -1541,12 +1548,13 @@ strbuf *ppk_save_sb(ssh2_userkey *key, const char *passphrase, } strbuf *out = strbuf_new_nm(); - strbuf_catf(out, "PuTTY-User-Key-File-3: %s\n", ssh_key_ssh_id(key->key)); + strbuf_catf(out, "PuTTY-User-Key-File-%u: %s\n", + params.fmt_version, ssh_key_ssh_id(key->key)); strbuf_catf(out, "Encryption: %s\n", cipherstr); strbuf_catf(out, "Comment: %s\n", key->comment); strbuf_catf(out, "Public-Lines: %d\n", base64_lines(pub_blob->len)); base64_encode_s(BinarySink_UPCAST(out), pub_blob->u, pub_blob->len, 64); - if (ciphertype->keylen != 0) { + if (params.fmt_version == 3 && ciphertype->keylen != 0) { strbuf_catf(out, "Key-Derivation: %s\n", params.argon2_flavour == Argon2d ? "Argon2d" : params.argon2_flavour == Argon2i ? "Argon2i" : "Argon2id"); @@ -1564,7 +1572,7 @@ strbuf *ppk_save_sb(ssh2_userkey *key, const char *passphrase, base64_encode_s(BinarySink_UPCAST(out), priv_blob_encrypted, priv_encrypted_len, 64); strbuf_catf(out, "Private-MAC: "); - for (i = 0; i < 32; i++) + for (i = 0; i < macalg->len; i++) strbuf_catf(out, "%02x", priv_mac[i]); strbuf_catf(out, "\n"); diff --git a/test/cryptsuite.py b/test/cryptsuite.py index 09ee5e8d..3aaf4a04 100755 --- a/test/cryptsuite.py +++ b/test/cryptsuite.py @@ -2163,17 +2163,21 @@ Private-MAC: 6f5e588e475e55434106ec2c3569695b03f423228b44993a9e97d52ffe7be5a8 salt = unhex('37c3911bfefc8c1d11ec579627d2b3d9') with queued_specific_random_data(salt): - self.assertEqual(ppk_save_sb(k1, comment, None, 'id', 8192, 13, 1), + self.assertEqual(ppk_save_sb(k1, comment, None, + 3, 'id', 8192, 13, 1), input_clear_key) with queued_specific_random_data(salt): - self.assertEqual(ppk_save_sb(k2, comment, None, 'id', 8192, 13, 1), + self.assertEqual(ppk_save_sb(k2, comment, None, + 3, 'id', 8192, 13, 1), input_clear_key) with queued_specific_random_data(salt): - self.assertEqual(ppk_save_sb(k1, comment, pp, 'id', 8192, 13, 1), + self.assertEqual(ppk_save_sb(k1, comment, pp, + 3, 'id', 8192, 13, 1), input_encrypted_key) with queued_specific_random_data(salt): - self.assertEqual(ppk_save_sb(k2, comment, pp, 'id', 8192, 13, 1), + self.assertEqual(ppk_save_sb(k2, comment, pp, + 3, 'id', 8192, 13, 1), input_encrypted_key) # And check we can still handle v2 key files. @@ -2219,6 +2223,13 @@ Private-MAC: 5b1f6f4cc43eb0060d2c3e181bc0129343adba2b self.assertEqual(ssh_key_private_blob(k1), privblob) self.assertEqual(ssh_key_private_blob(k2), privblob) + self.assertEqual(ppk_save_sb(k2, comment, None, + 2, 'id', 8192, 13, 1), + v2_clear_key) + self.assertEqual(ppk_save_sb(k1, comment, pp, + 2, 'id', 8192, 13, 1), + v2_encrypted_key) + def testRSA1LoadSave(self): # Stability test of SSH-1 RSA key-file load/save functions. input_clear_key = unhex( diff --git a/testcrypt.c b/testcrypt.c index bb05e1e1..6dc063ce 100644 --- a/testcrypt.c +++ b/testcrypt.c @@ -1144,9 +1144,10 @@ int rsa1_load_s_wrapper(BinarySource *src, RSAKey *rsa, char **comment, } #define rsa1_load_s rsa1_load_s_wrapper -strbuf *ppk_save_sb_wrapper(ssh_key *key, const char *comment, - const char *passphrase, Argon2Flavour flavour, - uint32_t mem, uint32_t passes, uint32_t parallel) +strbuf *ppk_save_sb_wrapper( + ssh_key *key, const char *comment, const char *passphrase, + unsigned fmt_version, Argon2Flavour flavour, + uint32_t mem, uint32_t passes, uint32_t parallel) { /* * For repeatable testing purposes, we never want a timing-dependent @@ -1154,6 +1155,7 @@ strbuf *ppk_save_sb_wrapper(ssh_key *key, const char *comment, */ ppk_save_parameters save_params; memset(&save_params, 0, sizeof(save_params)); + save_params.fmt_version = fmt_version; save_params.argon2_flavour = flavour; save_params.argon2_mem = mem; save_params.argon2_passes_auto = false; diff --git a/testcrypt.h b/testcrypt.h index 5918f179..be58d9b8 100644 --- a/testcrypt.h +++ b/testcrypt.h @@ -258,7 +258,7 @@ FUNC5(boolean, ppk_loadpub_s, val_string_binarysource, out_opt_val_string_asciz, FUNC4(int, rsa1_loadpub_s, val_string_binarysource, out_val_string_binarysink, out_opt_val_string_asciz, out_opt_val_string_asciz_const) FUNC4(opt_val_key, ppk_load_s, val_string_binarysource, out_opt_val_string_asciz, opt_val_string_asciz, out_opt_val_string_asciz_const) FUNC5(int, rsa1_load_s, val_string_binarysource, val_rsa, out_opt_val_string_asciz, opt_val_string_asciz, out_opt_val_string_asciz_const) -FUNC7(val_string, ppk_save_sb, val_key, opt_val_string_asciz, opt_val_string_asciz, argon2flavour, uint, uint, uint) +FUNC8(val_string, ppk_save_sb, val_key, opt_val_string_asciz, opt_val_string_asciz, uint, argon2flavour, uint, uint, uint) FUNC3(val_string, rsa1_save_sb, val_rsa, opt_val_string_asciz, opt_val_string_asciz) /*