From e6f40952850d03d79d1226ac0697a53233266f8c Mon Sep 17 00:00:00 2001 From: Falko Strenzke Date: Thu, 21 Sep 2023 16:23:47 +0200 Subject: [PATCH 01/17] started PQC cli tests --- src/tests/cli_tests.py | 71 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/src/tests/cli_tests.py b/src/tests/cli_tests.py index f21514158..8382f02bf 100755 --- a/src/tests/cli_tests.py +++ b/src/tests/cli_tests.py @@ -328,6 +328,14 @@ def rnp_genkey_rsa(userid, bits=2048, pswd=PASSWORD): if ret != 0: raise_err('rsa key generation failed', err) +def rnp_genkey_pqc(userid, algo_cli_nr, pswd=PASSWORD): + pipe = algo_pipe = pipe(algo_cli_nr) + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--password', 'test', + '--notty', '--userid', userid, '--generate-key'], pipe) + os.close(pipe) + if ret != 0: + raise_err('pqc key generation failed', err) + def rnp_params_insert_z(params, pos, z): if z: if len(z) > 0 and z[0] != None: @@ -4605,6 +4613,69 @@ def test_encryption_and_signing(self): remove_files(dst, dec) + + def test_encryption_and_signing_pqc(self): + USERIDS = ['enc-sign25@rnp', 'enc-sign27@rnp', 'enc-sign28@rnp', 'enc-sign29@rnp', 'enc-sign30@rnp','enc-sign31@rnp','enc-sign32@rnp','enc-sign33@rnp','enc-sign34@rnp'] + ALGO = [25, 27, 28, 29, 30, 31, 32, 33, 34,] + AEAD_C = list_upto(rnp_supported_ciphers(True), Encryption.RUNS) + # Generate multiple keys and import to GnuPG + for uid, algo in zip(USERIDS, ALGO): + rnp_genkey_pqc(uid, algo, 'testpw') + + gpg_import_pubring() + gpg_import_secring() + + SIGNERS = list_upto(range(1, len(USERIDS) + 1), Encryption.RUNS) + KEYPSWD = tuple((t1, t2) for t1 in range(1, len(USERIDS) + 1) + for t2 in range(len(PASSWORDS) + 1)) + KEYPSWD = list_upto(KEYPSWD, Encryption.RUNS) + AEADS = self.fill_aeads(Encryption.RUNS) + ZS = list_upto([None, [None, 0]], Encryption.RUNS) + + src, dst, dec = reg_workfiles('cleartext', '.txt', '.rnp', '.dec') + # Generate random file of required size + random_text(src, 65500) + + for i in range(0, Encryption.RUNS): + signers = USERIDS[i] + #signpswd = KEYPASS[:SIGNERS[i]] + #keynum, pswdnum = KEYPSWD[i] + recipients = USERIDS[i] + passwords = PASSWORDS[:pswdnum] + aead = AEADS[i] + z = ZS[i] + cipher = AEAD_C[i] + first_pass = aead is None and ((pswdnum > 1) or ((pswdnum == 1) and (keynum > 0))) + try_gpg = self.gpg_supports(aead) + + rnp_encrypt_and_sign_file(src, dst, recipients, passwords, signers, + signpswd, aead, cipher, z) + # Decrypt file with each of the keys, we have different password for each key + for pswd in KEYPASS[:keynum]: + if not first_pass and try_gpg: + gpg_decrypt_file(dst, dec, pswd) + gpg_agent_clear_cache() + remove_files(dec) + rnp_decrypt_file(dst, dec, '\n'.join([pswd] * 5)) + remove_files(dec) + + # GPG decrypts only with first password, see T3795 + if first_pass and try_gpg: + gpg_decrypt_file(dst, dec, PASSWORDS[0]) + gpg_agent_clear_cache() + remove_files(dec) + + # Decrypt file with each of the passwords + for pswd in PASSWORDS[:pswdnum]: + if not first_pass and try_gpg: + gpg_decrypt_file(dst, dec, pswd) + gpg_agent_clear_cache() + remove_files(dec) + rnp_decrypt_file(dst, dec, '\n'.join([pswd] * 5)) + remove_files(dec) + + remove_files(dst, dec) + def test_encryption_weird_userids_special_1(self): uid = WEIRD_USERID_SPECIAL_CHARS pswd = 'encSpecial1Pass' From 6f3badd772b5935a87b0c6abb704831c8e3118c8 Mon Sep 17 00:00:00 2001 From: Falko Strenzke Date: Mon, 25 Sep 2023 13:16:19 +0200 Subject: [PATCH 02/17] pqc tests working (only keys WITH password protection) --- src/tests/cli_tests.py | 69 +++++++++++++++--------------------------- 1 file changed, 24 insertions(+), 45 deletions(-) diff --git a/src/tests/cli_tests.py b/src/tests/cli_tests.py index 8382f02bf..e248b6a53 100755 --- a/src/tests/cli_tests.py +++ b/src/tests/cli_tests.py @@ -329,12 +329,14 @@ def rnp_genkey_rsa(userid, bits=2048, pswd=PASSWORD): raise_err('rsa key generation failed', err) def rnp_genkey_pqc(userid, algo_cli_nr, pswd=PASSWORD): - pipe = algo_pipe = pipe(algo_cli_nr) - ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--password', 'test', - '--notty', '--userid', userid, '--generate-key'], pipe) - os.close(pipe) + algo_pipe = str(algo_cli_nr) + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--password', pswd, + '--notty', '--userid', userid, '--generate-key', '--expert'], algo_pipe) + #os.close(algo_pipe) if ret != 0: raise_err('pqc key generation failed', err) + else: + print("genkey PQC successful") def rnp_params_insert_z(params, pos, z): if z: @@ -406,6 +408,7 @@ def rnp_encrypt_and_sign_file(src, dst, recipients, encrpswd, signers, signpswd, if ret != 0: raise_err('rnp encrypt-and-sign failed', err) + def rnp_decrypt_file(src, dst, password = PASSWORD): pipe = pswd_pipe(password) ret, out, err = run_proc( @@ -4613,68 +4616,44 @@ def test_encryption_and_signing(self): remove_files(dst, dec) - - def test_encryption_and_signing_pqc(self): + """ zzz_ prefix makes it the last test. This is a workaround against a gnupg import error with the + pqc keys in other member function tests that follow this one. + """ + def test_zzz_encryption_and_signing_pqc(self): USERIDS = ['enc-sign25@rnp', 'enc-sign27@rnp', 'enc-sign28@rnp', 'enc-sign29@rnp', 'enc-sign30@rnp','enc-sign31@rnp','enc-sign32@rnp','enc-sign33@rnp','enc-sign34@rnp'] ALGO = [25, 27, 28, 29, 30, 31, 32, 33, 34,] - AEAD_C = list_upto(rnp_supported_ciphers(True), Encryption.RUNS) # Generate multiple keys and import to GnuPG + print("starting pqc sign / encrypt tests") for uid, algo in zip(USERIDS, ALGO): + print(" generating pqc key...") rnp_genkey_pqc(uid, algo, 'testpw') + print(" ... done") - gpg_import_pubring() - gpg_import_secring() + #gpg_import_pubring() + #gpg_import_secring() - SIGNERS = list_upto(range(1, len(USERIDS) + 1), Encryption.RUNS) - KEYPSWD = tuple((t1, t2) for t1 in range(1, len(USERIDS) + 1) - for t2 in range(len(PASSWORDS) + 1)) - KEYPSWD = list_upto(KEYPSWD, Encryption.RUNS) - AEADS = self.fill_aeads(Encryption.RUNS) - ZS = list_upto([None, [None, 0]], Encryption.RUNS) src, dst, dec = reg_workfiles('cleartext', '.txt', '.rnp', '.dec') # Generate random file of required size random_text(src, 65500) - for i in range(0, Encryption.RUNS): - signers = USERIDS[i] + for i in range(0, len(USERIDS)): + signers = [USERIDS[i]] #signpswd = KEYPASS[:SIGNERS[i]] #keynum, pswdnum = KEYPSWD[i] - recipients = USERIDS[i] - passwords = PASSWORDS[:pswdnum] - aead = AEADS[i] - z = ZS[i] - cipher = AEAD_C[i] - first_pass = aead is None and ((pswdnum > 1) or ((pswdnum == 1) and (keynum > 0))) - try_gpg = self.gpg_supports(aead) + recipients = [USERIDS[i]] + passwords = [] # SKESK for v6 not yet supported rnp_encrypt_and_sign_file(src, dst, recipients, passwords, signers, - signpswd, aead, cipher, z) + ['testpw']) # Decrypt file with each of the keys, we have different password for each key - for pswd in KEYPASS[:keynum]: - if not first_pass and try_gpg: - gpg_decrypt_file(dst, dec, pswd) - gpg_agent_clear_cache() - remove_files(dec) - rnp_decrypt_file(dst, dec, '\n'.join([pswd] * 5)) - remove_files(dec) + rnp_decrypt_file(dst, dec, 'testpw') + remove_files(dec) - # GPG decrypts only with first password, see T3795 - if first_pass and try_gpg: - gpg_decrypt_file(dst, dec, PASSWORDS[0]) - gpg_agent_clear_cache() - remove_files(dec) - # Decrypt file with each of the passwords - for pswd in PASSWORDS[:pswdnum]: - if not first_pass and try_gpg: - gpg_decrypt_file(dst, dec, pswd) - gpg_agent_clear_cache() - remove_files(dec) - rnp_decrypt_file(dst, dec, '\n'.join([pswd] * 5)) - remove_files(dec) remove_files(dst, dec) + print("finished pqc sign / encrypt tests") def test_encryption_weird_userids_special_1(self): uid = WEIRD_USERID_SPECIAL_CHARS From 5767182129d3b18fcb8a195a73523edc12a9ca39 Mon Sep 17 00:00:00 2001 From: Falko Strenzke Date: Mon, 25 Sep 2023 13:22:09 +0200 Subject: [PATCH 03/17] removed some debug output --- src/tests/cli_tests.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/tests/cli_tests.py b/src/tests/cli_tests.py index e248b6a53..a1b2f4bdb 100755 --- a/src/tests/cli_tests.py +++ b/src/tests/cli_tests.py @@ -335,8 +335,6 @@ def rnp_genkey_pqc(userid, algo_cli_nr, pswd=PASSWORD): #os.close(algo_pipe) if ret != 0: raise_err('pqc key generation failed', err) - else: - print("genkey PQC successful") def rnp_params_insert_z(params, pos, z): if z: @@ -4623,11 +4621,8 @@ def test_zzz_encryption_and_signing_pqc(self): USERIDS = ['enc-sign25@rnp', 'enc-sign27@rnp', 'enc-sign28@rnp', 'enc-sign29@rnp', 'enc-sign30@rnp','enc-sign31@rnp','enc-sign32@rnp','enc-sign33@rnp','enc-sign34@rnp'] ALGO = [25, 27, 28, 29, 30, 31, 32, 33, 34,] # Generate multiple keys and import to GnuPG - print("starting pqc sign / encrypt tests") for uid, algo in zip(USERIDS, ALGO): - print(" generating pqc key...") rnp_genkey_pqc(uid, algo, 'testpw') - print(" ... done") #gpg_import_pubring() #gpg_import_secring() @@ -4653,7 +4648,6 @@ def test_zzz_encryption_and_signing_pqc(self): remove_files(dst, dec) - print("finished pqc sign / encrypt tests") def test_encryption_weird_userids_special_1(self): uid = WEIRD_USERID_SPECIAL_CHARS From 546af5166ca1b3a5580f13aaffe1fdea3688dce7 Mon Sep 17 00:00:00 2001 From: Falko Strenzke Date: Mon, 25 Sep 2023 13:31:02 +0200 Subject: [PATCH 04/17] PQC tests now with and without passwd protected key --- src/tests/cli_tests.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/tests/cli_tests.py b/src/tests/cli_tests.py index a1b2f4bdb..c39f3c53d 100755 --- a/src/tests/cli_tests.py +++ b/src/tests/cli_tests.py @@ -4620,9 +4620,11 @@ def test_encryption_and_signing(self): def test_zzz_encryption_and_signing_pqc(self): USERIDS = ['enc-sign25@rnp', 'enc-sign27@rnp', 'enc-sign28@rnp', 'enc-sign29@rnp', 'enc-sign30@rnp','enc-sign31@rnp','enc-sign32@rnp','enc-sign33@rnp','enc-sign34@rnp'] ALGO = [25, 27, 28, 29, 30, 31, 32, 33, 34,] + passwds = [ ] + for x in range(len(ALGO)): passwds.append('testpw' if x % 1 == 0 else '') # Generate multiple keys and import to GnuPG - for uid, algo in zip(USERIDS, ALGO): - rnp_genkey_pqc(uid, algo, 'testpw') + for uid, algo, passwd in zip(USERIDS, ALGO, passwds): + rnp_genkey_pqc(uid, algo, passwd) #gpg_import_pubring() #gpg_import_secring() @@ -4638,11 +4640,12 @@ def test_zzz_encryption_and_signing_pqc(self): #keynum, pswdnum = KEYPSWD[i] recipients = [USERIDS[i]] passwords = [] # SKESK for v6 not yet supported + signerpws = [passwds[i]] rnp_encrypt_and_sign_file(src, dst, recipients, passwords, signers, - ['testpw']) + signerpws) # Decrypt file with each of the keys, we have different password for each key - rnp_decrypt_file(dst, dec, 'testpw') + rnp_decrypt_file(dst, dec, passwds[i]) remove_files(dec) From 4c0f6a2d52f4a485d72afe1c2554ec97881c5487 Mon Sep 17 00:00:00 2001 From: Falko Strenzke Date: Tue, 23 Apr 2024 16:40:11 +0200 Subject: [PATCH 05/17] added algo param for SLH-DSA tests --- src/tests/cli_tests.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/tests/cli_tests.py b/src/tests/cli_tests.py index c39f3c53d..721943146 100755 --- a/src/tests/cli_tests.py +++ b/src/tests/cli_tests.py @@ -328,8 +328,10 @@ def rnp_genkey_rsa(userid, bits=2048, pswd=PASSWORD): if ret != 0: raise_err('rsa key generation failed', err) -def rnp_genkey_pqc(userid, algo_cli_nr, pswd=PASSWORD): +def rnp_genkey_pqc(userid, algo_cli_nr, algo_param = None, pswd=PASSWORD): algo_pipe = str(algo_cli_nr) + if algo_param: + algo_pipe += "\n" + str(algo_param) ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--password', pswd, '--notty', '--userid', userid, '--generate-key', '--expert'], algo_pipe) #os.close(algo_pipe) @@ -4619,12 +4621,13 @@ def test_encryption_and_signing(self): """ def test_zzz_encryption_and_signing_pqc(self): USERIDS = ['enc-sign25@rnp', 'enc-sign27@rnp', 'enc-sign28@rnp', 'enc-sign29@rnp', 'enc-sign30@rnp','enc-sign31@rnp','enc-sign32@rnp','enc-sign33@rnp','enc-sign34@rnp'] - ALGO = [25, 27, 28, 29, 30, 31, 32, 33, 34,] + ALGO = [ 25, 27, 28, 29, 30, 31, 32, ] + ALGO_PARAM = [None, None, None, None, None, 1, 6, ] passwds = [ ] for x in range(len(ALGO)): passwds.append('testpw' if x % 1 == 0 else '') # Generate multiple keys and import to GnuPG - for uid, algo, passwd in zip(USERIDS, ALGO, passwds): - rnp_genkey_pqc(uid, algo, passwd) + for uid, algo, param, passwd in zip(USERIDS, ALGO, ALGO_PARAM, passwds): + rnp_genkey_pqc(uid, algo, param, passwd) #gpg_import_pubring() #gpg_import_secring() From 52cec8f05db2644ce8a26d0c26938a7d4ea55e72 Mon Sep 17 00:00:00 2001 From: Falko Strenzke Date: Wed, 24 Apr 2024 09:04:04 +0200 Subject: [PATCH 06/17] add check for PQC support, add test to encrypt to v4 key --- src/tests/cli_tests.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/tests/cli_tests.py b/src/tests/cli_tests.py index 721943146..b17a5b54e 100755 --- a/src/tests/cli_tests.py +++ b/src/tests/cli_tests.py @@ -876,7 +876,7 @@ def gpg_check_features(): print('GPG_BRAINPOOL: ' + str(GPG_BRAINPOOL)) def rnp_check_features(): - global RNP_TWOFISH, RNP_BRAINPOOL, RNP_AEAD, RNP_AEAD_EAX, RNP_AEAD_OCB, RNP_AEAD_OCB_AES, RNP_IDEA, RNP_BLOWFISH, RNP_CAST5, RNP_RIPEMD160 + global RNP_TWOFISH, RNP_BRAINPOOL, RNP_AEAD, RNP_AEAD_EAX, RNP_AEAD_OCB, RNP_AEAD_OCB_AES, RNP_IDEA, RNP_BLOWFISH, RNP_CAST5, RNP_RIPEMD160, RNP_PQC global RNP_BOTAN_OCB_AV ret, out, _ = run_proc(RNP, ['--version']) if ret != 0: @@ -903,6 +903,9 @@ def rnp_check_features(): RNP_BLOWFISH = re.match(r'(?s)^.*Encryption:.*BLOWFISH.*', out) is not None RNP_CAST5 = re.match(r'(?s)^.*Encryption:.*CAST5.*', out) is not None RNP_RIPEMD160 = re.match(r'(?s)^.*Hash:.*RIPEMD160.*', out) is not None + # Determine PQC support in general. If present, assume that all PQC schemes are supported. + pqc_strs = ['ML-KEM', 'ML-DSA'] + RNP_PQC = any([re.match('(?s)^.*Public key:.*' + scheme + '.*', out) is not None for scheme in pqc_strs]) print('RNP_TWOFISH: ' + str(RNP_TWOFISH)) print('RNP_BLOWFISH: ' + str(RNP_BLOWFISH)) print('RNP_IDEA: ' + str(RNP_IDEA)) @@ -913,6 +916,7 @@ def rnp_check_features(): print('RNP_AEAD_OCB: ' + str(RNP_AEAD_OCB)) print('RNP_AEAD_OCB_AES: ' + str(RNP_AEAD_OCB_AES)) print('RNP_BOTAN_OCB_AV: ' + str(RNP_BOTAN_OCB_AV)) + print('RNP_PQC: ' + str(RNP_PQC)) def setup(loglvl): # Setting up directories. @@ -4617,12 +4621,14 @@ def test_encryption_and_signing(self): remove_files(dst, dec) """ zzz_ prefix makes it the last test. This is a workaround against a gnupg import error with the - pqc keys in other member function tests that follow this one. + pqc keys in other member function tests that would otherwise follow this one. """ def test_zzz_encryption_and_signing_pqc(self): - USERIDS = ['enc-sign25@rnp', 'enc-sign27@rnp', 'enc-sign28@rnp', 'enc-sign29@rnp', 'enc-sign30@rnp','enc-sign31@rnp','enc-sign32@rnp','enc-sign33@rnp','enc-sign34@rnp'] - ALGO = [ 25, 27, 28, 29, 30, 31, 32, ] - ALGO_PARAM = [None, None, None, None, None, 1, 6, ] + if not RNP_PQC: + return + USERIDS = ['enc-sign24-v4-key@rnp', 'enc-sign25@rnp', 'enc-sign27@rnp', 'enc-sign28@rnp', 'enc-sign29@rnp', 'enc-sign30@rnp','enc-sign31@rnp','enc-sign32@rnp','enc-sign33@rnp','enc-sign34@rnp'] + ALGO = [25, 27, 28, 29, 30, 32, 32, 32, 24, ] + ALGO_PARAM = [None, None, None, None, None, 1, 2, 6, None, ] passwds = [ ] for x in range(len(ALGO)): passwds.append('testpw' if x % 1 == 0 else '') # Generate multiple keys and import to GnuPG From 0814ecb79918ac2d8fae4b2e907d3ef5f73697de Mon Sep 17 00:00:00 2001 From: Johannes Roth Date: Wed, 24 Apr 2024 09:46:35 +0200 Subject: [PATCH 07/17] fix PKESKv6 PQC encryption case --- src/librepgp/stream-packet.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/librepgp/stream-packet.cpp b/src/librepgp/stream-packet.cpp index b5983f935..1ed223984 100644 --- a/src/librepgp/stream-packet.cpp +++ b/src/librepgp/stream-packet.cpp @@ -1373,14 +1373,17 @@ pgp_pk_sesskey_t::write_material(const pgp_encrypted_material_t &material) FALLTHROUGH_STATEMENT; case PGP_PKA_KYBER768_BP256: FALLTHROUGH_STATEMENT; - case PGP_PKA_KYBER1024_BP384: + case PGP_PKA_KYBER1024_BP384: { pktbody.add(material.kyber_ecdh.composite_ciphertext); - pktbody.add_byte(static_cast(material.kyber_ecdh.wrapped_sesskey.size()) + 1); + uint8_t opt_salg_length = (version == PGP_PKSK_V3) ? 1 : 0; + pktbody.add_byte(static_cast(material.kyber_ecdh.wrapped_sesskey.size()) + + opt_salg_length); if (version == PGP_PKSK_V3) { pktbody.add_byte(salg); /* added as plaintext */ } pktbody.add(material.kyber_ecdh.wrapped_sesskey); break; + } #endif default: RNP_LOG("Unknown pk alg: %d", (int) alg); From 0600c833d9a52d9046fe4e56bf8c4f7ceaca8cef Mon Sep 17 00:00:00 2001 From: Falko Strenzke Date: Wed, 24 Apr 2024 10:18:15 +0200 Subject: [PATCH 08/17] corrected pqc test uids (informal only), added parsing of pqc enc subkey id --- src/tests/cli_tests.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/tests/cli_tests.py b/src/tests/cli_tests.py index b17a5b54e..3003b14c9 100755 --- a/src/tests/cli_tests.py +++ b/src/tests/cli_tests.py @@ -332,11 +332,12 @@ def rnp_genkey_pqc(userid, algo_cli_nr, algo_param = None, pswd=PASSWORD): algo_pipe = str(algo_cli_nr) if algo_param: algo_pipe += "\n" + str(algo_param) - ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--password', pswd, + ret, output, errout = run_proc(RNPK, ['--homedir', RNPDIR, '--password', pswd, '--notty', '--userid', userid, '--generate-key', '--expert'], algo_pipe) #os.close(algo_pipe) if ret != 0: - raise_err('pqc key generation failed', err) + raise_err('pqc key generation failed', errout) + return output def rnp_params_insert_z(params, pos, z): if z: @@ -4626,15 +4627,24 @@ def test_encryption_and_signing(self): def test_zzz_encryption_and_signing_pqc(self): if not RNP_PQC: return - USERIDS = ['enc-sign24-v4-key@rnp', 'enc-sign25@rnp', 'enc-sign27@rnp', 'enc-sign28@rnp', 'enc-sign29@rnp', 'enc-sign30@rnp','enc-sign31@rnp','enc-sign32@rnp','enc-sign33@rnp','enc-sign34@rnp'] + USERIDS = ['enc-sign25@rnp', 'enc-sign27@rnp', 'enc-sign28@rnp', 'enc-sign29@rnp', 'enc-sign30@rnp','enc-sign31@rnp','enc-sign32@rnp','enc-sign33@rnp','enc-sign34@rnp', 'enc-sign24-v4-key@rnp'] ALGO = [25, 27, 28, 29, 30, 32, 32, 32, 24, ] + # '24' in the above array creates a v4 primary signature key with a v4 pqc subkey without a Features Subpacket. This way we test PQC encryption to a v4 subkey. ALGO_PARAM = [None, None, None, None, None, 1, 2, 6, None, ] passwds = [ ] + pqc_subkey_ids = [ ] for x in range(len(ALGO)): passwds.append('testpw' if x % 1 == 0 else '') # Generate multiple keys and import to GnuPG for uid, algo, param, passwd in zip(USERIDS, ALGO, ALGO_PARAM, passwds): - rnp_genkey_pqc(uid, algo, param, passwd) - + output = rnp_genkey_pqc(uid, algo, param, passwd) + # parse output for PQC encryption subkey + pqc_enc_subkey_match = re.search(r'ssb * [0-9]+/ML-KEM-[0-9]+\+[^ ]* *([0-9a-fA-F]+)', output) + if pqc_enc_subkey_match: + pqc_subkey_ids.append(pqc_enc_subkey_match.group(1)) + print("appended found PQC subkey id = " + str(pqc_subkey_ids[-1])) + else: + pqc_subkey_ids.append(None) + # TODO: use pqc_enc_subkey_match during encryption call #gpg_import_pubring() #gpg_import_secring() From c97a65f73d49d1a0362576d45cb173267b2a2fc7 Mon Sep 17 00:00:00 2001 From: Falko Strenzke Date: Wed, 24 Apr 2024 12:29:53 +0200 Subject: [PATCH 09/17] reverted changes to parse for pqc encryption subkey --- src/tests/cli_tests.py | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/src/tests/cli_tests.py b/src/tests/cli_tests.py index 3003b14c9..1272672f2 100755 --- a/src/tests/cli_tests.py +++ b/src/tests/cli_tests.py @@ -332,12 +332,11 @@ def rnp_genkey_pqc(userid, algo_cli_nr, algo_param = None, pswd=PASSWORD): algo_pipe = str(algo_cli_nr) if algo_param: algo_pipe += "\n" + str(algo_param) - ret, output, errout = run_proc(RNPK, ['--homedir', RNPDIR, '--password', pswd, + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--password', pswd, '--notty', '--userid', userid, '--generate-key', '--expert'], algo_pipe) #os.close(algo_pipe) if ret != 0: - raise_err('pqc key generation failed', errout) - return output + raise_err('pqc key generation failed', err) def rnp_params_insert_z(params, pos, z): if z: @@ -4628,23 +4627,16 @@ def test_zzz_encryption_and_signing_pqc(self): if not RNP_PQC: return USERIDS = ['enc-sign25@rnp', 'enc-sign27@rnp', 'enc-sign28@rnp', 'enc-sign29@rnp', 'enc-sign30@rnp','enc-sign31@rnp','enc-sign32@rnp','enc-sign33@rnp','enc-sign34@rnp', 'enc-sign24-v4-key@rnp'] + + # '24' in the below array creates a v4 primary signature key with a v4 pqc subkey without a Features Subpacket. This way we test PQC encryption to a v4 subkey. ALGO = [25, 27, 28, 29, 30, 32, 32, 32, 24, ] - # '24' in the above array creates a v4 primary signature key with a v4 pqc subkey without a Features Subpacket. This way we test PQC encryption to a v4 subkey. ALGO_PARAM = [None, None, None, None, None, 1, 2, 6, None, ] passwds = [ ] - pqc_subkey_ids = [ ] for x in range(len(ALGO)): passwds.append('testpw' if x % 1 == 0 else '') # Generate multiple keys and import to GnuPG for uid, algo, param, passwd in zip(USERIDS, ALGO, ALGO_PARAM, passwds): - output = rnp_genkey_pqc(uid, algo, param, passwd) - # parse output for PQC encryption subkey - pqc_enc_subkey_match = re.search(r'ssb * [0-9]+/ML-KEM-[0-9]+\+[^ ]* *([0-9a-fA-F]+)', output) - if pqc_enc_subkey_match: - pqc_subkey_ids.append(pqc_enc_subkey_match.group(1)) - print("appended found PQC subkey id = " + str(pqc_subkey_ids[-1])) - else: - pqc_subkey_ids.append(None) - # TODO: use pqc_enc_subkey_match during encryption call + rnp_genkey_pqc(uid, algo, param, passwd) + #gpg_import_pubring() #gpg_import_secring() From c69a1d08780a2d26eee4aa9b738a38ea012d583d Mon Sep 17 00:00:00 2001 From: Falko Strenzke Date: Wed, 24 Apr 2024 13:15:08 +0200 Subject: [PATCH 10/17] fixed test data array, cli-encryption tests now running OK --- src/tests/cli_tests.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/tests/cli_tests.py b/src/tests/cli_tests.py index 1272672f2..1f2d984ad 100755 --- a/src/tests/cli_tests.py +++ b/src/tests/cli_tests.py @@ -4626,13 +4626,15 @@ def test_encryption_and_signing(self): def test_zzz_encryption_and_signing_pqc(self): if not RNP_PQC: return - USERIDS = ['enc-sign25@rnp', 'enc-sign27@rnp', 'enc-sign28@rnp', 'enc-sign29@rnp', 'enc-sign30@rnp','enc-sign31@rnp','enc-sign32@rnp','enc-sign33@rnp','enc-sign34@rnp', 'enc-sign24-v4-key@rnp'] + USERIDS = ['enc-sign25@rnp', 'enc-sign27@rnp', 'enc-sign28@rnp', 'enc-sign29@rnp', 'enc-sign30@rnp','enc-sign32a@rnp','enc-sign32b@rnp','enc-sign32c@rnp','enc-sign24-v4-key@rnp'] # '24' in the below array creates a v4 primary signature key with a v4 pqc subkey without a Features Subpacket. This way we test PQC encryption to a v4 subkey. ALGO = [25, 27, 28, 29, 30, 32, 32, 32, 24, ] ALGO_PARAM = [None, None, None, None, None, 1, 2, 6, None, ] passwds = [ ] for x in range(len(ALGO)): passwds.append('testpw' if x % 1 == 0 else '') + if any(len(USERIDS) != len(x) for x in [ALGO, ALGO_PARAM]): + raise RuntimeError("test_zzz_encryption_and_signing_pqc: internal error: lengths of test data arrays matching") # Generate multiple keys and import to GnuPG for uid, algo, param, passwd in zip(USERIDS, ALGO, ALGO_PARAM, passwds): rnp_genkey_pqc(uid, algo, param, passwd) From ba7317a53f59f403e241ec6a4c1c5de7e4d01e8e Mon Sep 17 00:00:00 2001 From: Johannes Roth Date: Wed, 24 Apr 2024 13:09:42 +0200 Subject: [PATCH 11/17] adjust find_suitable_key() to prefer PQC subkeys --- src/lib/pgp-key.cpp | 48 +++++++++++++++++++++++++++++++++++++++++++++ src/lib/pgp-key.h | 3 +++ 2 files changed, 51 insertions(+) diff --git a/src/lib/pgp-key.cpp b/src/lib/pgp-key.cpp index 3897f2693..a57b4ec3c 100644 --- a/src/lib/pgp-key.cpp +++ b/src/lib/pgp-key.cpp @@ -472,6 +472,19 @@ find_suitable_key(pgp_op_t op, pgp_key_t *key, rnp::KeyProvider *key_provider, b if (!cur || !cur->usable_for(op)) { continue; } +#if defined(ENABLE_PQC) + /* prefer PQC over non-PQC. Assume non-PQC key is only there for backwards + * compatibility. */ + if (subkey && subkey->is_pqc_alg() && !cur->is_pqc_alg()) { + /* do not override already found PQC key with non-PQC key */ + continue; + } + if (subkey && cur->is_pqc_alg() && !subkey->is_pqc_alg()) { + /* override non-PQC key with PQC key */ + subkey = cur; + continue; + } +#endif if (!subkey || (cur->creation() > subkey->creation())) { subkey = cur; } @@ -1301,6 +1314,41 @@ pgp_key_t::has_secret() const noexcept } } +#if defined(ENABLE_PQC) +bool +pgp_key_t::is_pqc_alg() const +{ + switch (alg()) { + case PGP_PKA_KYBER768_X25519: + FALLTHROUGH_STATEMENT; + case PGP_PKA_KYBER768_P256: + FALLTHROUGH_STATEMENT; + case PGP_PKA_KYBER1024_P384: + FALLTHROUGH_STATEMENT; + case PGP_PKA_KYBER768_BP256: + FALLTHROUGH_STATEMENT; + case PGP_PKA_KYBER1024_BP384: + FALLTHROUGH_STATEMENT; + case PGP_PKA_DILITHIUM3_ED25519: + FALLTHROUGH_STATEMENT; + case PGP_PKA_DILITHIUM3_P256: + FALLTHROUGH_STATEMENT; + case PGP_PKA_DILITHIUM5_P384: + FALLTHROUGH_STATEMENT; + case PGP_PKA_DILITHIUM3_BP256: + FALLTHROUGH_STATEMENT; + case PGP_PKA_DILITHIUM5_BP384: + FALLTHROUGH_STATEMENT; + case PGP_PKA_SPHINCSPLUS_SHA2: + FALLTHROUGH_STATEMENT; + case PGP_PKA_SPHINCSPLUS_SHAKE: + return true; + default: + return false; + } +} +#endif + bool pgp_key_t::usable_for(pgp_op_t op, bool if_secret) const { diff --git a/src/lib/pgp-key.h b/src/lib/pgp-key.h index 5bac8acaf..da7aaae0b 100644 --- a/src/lib/pgp-key.h +++ b/src/lib/pgp-key.h @@ -229,6 +229,9 @@ struct pgp_key_t { bool can_certify() const noexcept; bool can_encrypt() const noexcept; bool has_secret() const noexcept; +#if defined(ENABLE_PQC) + bool is_pqc_alg() const; +#endif /** * @brief Check whether key is usable for the specified operation. * From 2f6167f793827427057826778decd62481695f04 Mon Sep 17 00:00:00 2001 From: Falko Strenzke Date: Wed, 24 Apr 2024 13:28:10 +0200 Subject: [PATCH 12/17] edited only comment --- src/tests/cli_tests.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/tests/cli_tests.py b/src/tests/cli_tests.py index 1f2d984ad..f385e242b 100755 --- a/src/tests/cli_tests.py +++ b/src/tests/cli_tests.py @@ -4628,7 +4628,8 @@ def test_zzz_encryption_and_signing_pqc(self): return USERIDS = ['enc-sign25@rnp', 'enc-sign27@rnp', 'enc-sign28@rnp', 'enc-sign29@rnp', 'enc-sign30@rnp','enc-sign32a@rnp','enc-sign32b@rnp','enc-sign32c@rnp','enc-sign24-v4-key@rnp'] - # '24' in the below array creates a v4 primary signature key with a v4 pqc subkey without a Features Subpacket. This way we test PQC encryption to a v4 subkey. + # '24' in the below array creates a v4 primary signature key with a v4 pqc subkey without a Features Subpacket. This way we test PQC encryption to a v4 subkey. RNP prefers the PQC subkey in case of a certificate having a PQC and a + # non-PQC subkey. ALGO = [25, 27, 28, 29, 30, 32, 32, 32, 24, ] ALGO_PARAM = [None, None, None, None, None, 1, 2, 6, None, ] passwds = [ ] From f3a9b52079a86b0a5c16b83a3123c67fddc5fee5 Mon Sep 17 00:00:00 2001 From: Falko Strenzke Date: Wed, 24 Apr 2024 16:39:23 +0200 Subject: [PATCH 13/17] testing aead algorithms in pqc public test --- src/tests/cli_tests.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/tests/cli_tests.py b/src/tests/cli_tests.py index f385e242b..1cf3bc8e9 100755 --- a/src/tests/cli_tests.py +++ b/src/tests/cli_tests.py @@ -4632,8 +4632,10 @@ def test_zzz_encryption_and_signing_pqc(self): # non-PQC subkey. ALGO = [25, 27, 28, 29, 30, 32, 32, 32, 24, ] ALGO_PARAM = [None, None, None, None, None, 1, 2, 6, None, ] + aead_list = [] passwds = [ ] for x in range(len(ALGO)): passwds.append('testpw' if x % 1 == 0 else '') + for x in range(len(ALGO)): aead_list.append(None if x % 3 == 0 else ('ocb' if x % 3 == 1 else 'eax' )) if any(len(USERIDS) != len(x) for x in [ALGO, ALGO_PARAM]): raise RuntimeError("test_zzz_encryption_and_signing_pqc: internal error: lengths of test data arrays matching") # Generate multiple keys and import to GnuPG @@ -4657,7 +4659,7 @@ def test_zzz_encryption_and_signing_pqc(self): signerpws = [passwds[i]] rnp_encrypt_and_sign_file(src, dst, recipients, passwords, signers, - signerpws) + signerpws, aead=[aead_list[i]]) # Decrypt file with each of the keys, we have different password for each key rnp_decrypt_file(dst, dec, passwds[i]) remove_files(dec) From 4a7e55cb449318c4dd24ee6c628d66bf9113d179 Mon Sep 17 00:00:00 2001 From: Falko Strenzke Date: Thu, 25 Apr 2024 09:13:05 +0200 Subject: [PATCH 14/17] checking expected algo ID choices in key gen UI --- src/tests/cli_tests.py | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/src/tests/cli_tests.py b/src/tests/cli_tests.py index 1cf3bc8e9..8fb7ad73b 100755 --- a/src/tests/cli_tests.py +++ b/src/tests/cli_tests.py @@ -332,11 +332,12 @@ def rnp_genkey_pqc(userid, algo_cli_nr, algo_param = None, pswd=PASSWORD): algo_pipe = str(algo_cli_nr) if algo_param: algo_pipe += "\n" + str(algo_param) - ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--password', pswd, + ret, out, err = run_proc(RNPK, ['--homedir', RNPDIR, '--password', pswd, '--notty', '--userid', userid, '--generate-key', '--expert'], algo_pipe) #os.close(algo_pipe) if ret != 0: raise_err('pqc key generation failed', err) + return out def rnp_params_insert_z(params, pos, z): if z: @@ -4620,12 +4621,38 @@ def test_encryption_and_signing(self): remove_files(dst, dec) + + def verify_pqc_algo_ui_nb_to_algo_ui_str(self, stdout: str, algo_ui_exp_strs: list[str]) -> None: + stdout_lines = stdout.split('\n') + for expected_line in algo_ui_exp_strs: + found_this_entry : bool = False + for line in stdout_lines: + # compare ignore whitespaces and tabs: + re_patt_for_algo = r'[^\t ]' + char_list_expected = [c for c in expected_line if re.match(re_patt_for_algo, c)] + char_list_actual = [c for c in line if re.match(re_patt_for_algo, c)] + if char_list_expected == char_list_actual: + found_this_entry = True + break + + if not found_this_entry: + raise RuntimeError("did not match the expected UI choice for algorithm: " + expected_line) + """ zzz_ prefix makes it the last test. This is a workaround against a gnupg import error with the pqc keys in other member function tests that would otherwise follow this one. """ def test_zzz_encryption_and_signing_pqc(self): if not RNP_PQC: return + algo_ui_exp_strs = [ "(24) Ed25519Legacy + Curve25519Legacy + (ML-KEM-768 + X25519)", + "(25) (ML-DSA-65 + Ed25519) + (ML-KEM-768 + X25519)", + "(27) (ML-DSA-65 + ECDSA-NIST-P-256) + (ML-KEM-768 + ECDH-NIST-P-256)", + "(28) (ML-DSA-87 + ECDSA-NIST-P-384) + (ML-KEM-1024 + ECDH-NIST-P-384)", + "(29) (ML-DSA-65 + ECDSA-brainpoolP256r1) + (ML-KEM-768 + ECDH-brainpoolP256r1)", + "(30) (ML-DSA-87 + ECDSA-brainpoolP384r1) + (ML-KEM-1024 + ECDH-brainpoolP384r1)", + "(31) SLH-DSA-SHA2 + MLKEM-ECDH Composite", + "(32) SLH-DSA-SHAKE + MLKEM-ECDH Composite", + ] USERIDS = ['enc-sign25@rnp', 'enc-sign27@rnp', 'enc-sign28@rnp', 'enc-sign29@rnp', 'enc-sign30@rnp','enc-sign32a@rnp','enc-sign32b@rnp','enc-sign32c@rnp','enc-sign24-v4-key@rnp'] # '24' in the below array creates a v4 primary signature key with a v4 pqc subkey without a Features Subpacket. This way we test PQC encryption to a v4 subkey. RNP prefers the PQC subkey in case of a certificate having a PQC and a @@ -4639,8 +4666,12 @@ def test_zzz_encryption_and_signing_pqc(self): if any(len(USERIDS) != len(x) for x in [ALGO, ALGO_PARAM]): raise RuntimeError("test_zzz_encryption_and_signing_pqc: internal error: lengths of test data arrays matching") # Generate multiple keys and import to GnuPG + verified_algo_nums = False for uid, algo, param, passwd in zip(USERIDS, ALGO, ALGO_PARAM, passwds): - rnp_genkey_pqc(uid, algo, param, passwd) + stdout = rnp_genkey_pqc(uid, algo, param, passwd) + if not verified_algo_nums: + self.verify_pqc_algo_ui_nb_to_algo_ui_str(stdout, algo_ui_exp_strs) + verified_algo_nums = True #gpg_import_pubring() #gpg_import_secring() From 7035446fe8dd555b828335edbef6face168631cf Mon Sep 17 00:00:00 2001 From: Falko Strenzke Date: Wed, 29 May 2024 08:47:45 +0200 Subject: [PATCH 15/17] removed type hint causing problems on some platforms --- src/tests/cli_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tests/cli_tests.py b/src/tests/cli_tests.py index 8fb7ad73b..dec8ad0f0 100755 --- a/src/tests/cli_tests.py +++ b/src/tests/cli_tests.py @@ -4622,7 +4622,7 @@ def test_encryption_and_signing(self): remove_files(dst, dec) - def verify_pqc_algo_ui_nb_to_algo_ui_str(self, stdout: str, algo_ui_exp_strs: list[str]) -> None: + def verify_pqc_algo_ui_nb_to_algo_ui_str(self, stdout: str, algo_ui_exp_strs) -> None: stdout_lines = stdout.split('\n') for expected_line in algo_ui_exp_strs: found_this_entry : bool = False From 374cebc72e75683f89959fe7c8674f05eaf9e8c8 Mon Sep 17 00:00:00 2001 From: Johannes Roth Date: Tue, 24 Sep 2024 15:04:08 +0200 Subject: [PATCH 16/17] address review comments; add flag for preferring pqc encryption subkey --- include/rnp/rnp.h | 18 +++++++++++ src/lib/pgp-key.cpp | 28 ++++++++++------- src/lib/pgp-key.h | 3 +- src/lib/rnp.cpp | 35 ++++++++++++++++++--- src/librepgp/stream-ctx.h | 9 ++++-- src/tests/cli_tests.py | 1 - src/tests/ffi-enc.cpp | 65 +++++++++++++++++++++++++++++++++++++++ 7 files changed, 140 insertions(+), 19 deletions(-) diff --git a/include/rnp/rnp.h b/include/rnp/rnp.h index 033610b9c..b69b33dae 100644 --- a/include/rnp/rnp.h +++ b/include/rnp/rnp.h @@ -104,6 +104,9 @@ typedef uint32_t rnp_result_t; * Flags for default key selection. */ #define RNP_KEY_SUBKEYS_ONLY (1U << 0) +#if defined(RNP_EXPERIMENTAL_CRYPTO_REFRESH) +#define RNP_KEY_PREFER_PQC_ENC_SUBKEY (1U << 1) +#endif /** * User id type @@ -2349,6 +2352,9 @@ RNP_API rnp_result_t rnp_key_get_subkey_at(rnp_key_handle_t key, * @param flags possible values: RNP_KEY_SUBKEYS_ONLY - select only subkeys, * otherwise if flags is 0, primary key can be returned if * it is suitable for specified usage. + * Note: If RNP_EXPERIMENTAL_PQC is set, then the flag + * RNP_KEY_PREFER_PQC_ENC_SUBKEY can be used to prefer PQC-encryption subkeys + * over non-PQC-encryption subkeys * @param default_key on success resulting key handle will be stored here, otherwise it * will contain NULL value. You must free this handle after use with * rnp_key_handle_destroy(). @@ -3615,6 +3621,18 @@ RNP_API rnp_result_t rnp_op_encrypt_add_recipient(rnp_op_encrypt_t op, rnp_key_h RNP_API rnp_result_t rnp_op_encrypt_enable_pkesk_v6(rnp_op_encrypt_t op); #endif +#if defined(RNP_EXPERIMENTAL_PQC) +/** + * @brief Prefer using PQC subkeys over non-PQC subkeys when encrypting. + * NOTE: This is an experimental feature and this function can be replaced (or removed) + * at any time. + * + * @param op opaque encrypting context. Must be allocated and initialized. + * @return RNP_SUCCESS or errorcode if failed. + */ +RNP_API rnp_result_t rnp_op_encrypt_prefer_pqc_enc_subkey(rnp_op_encrypt_t op); +#endif + /** * @brief Add signature to encrypting context, so data will be encrypted and signed. * diff --git a/src/lib/pgp-key.cpp b/src/lib/pgp-key.cpp index a57b4ec3c..601c72a99 100644 --- a/src/lib/pgp-key.cpp +++ b/src/lib/pgp-key.cpp @@ -435,7 +435,11 @@ pgp_subkey_set_expiration(pgp_key_t * sub, } pgp_key_t * -find_suitable_key(pgp_op_t op, pgp_key_t *key, rnp::KeyProvider *key_provider, bool no_primary) +find_suitable_key(pgp_op_t op, + pgp_key_t * key, + rnp::KeyProvider *key_provider, + bool no_primary, + bool pref_pqc_sub) { if (!key || !key_provider) { return NULL; @@ -473,16 +477,18 @@ find_suitable_key(pgp_op_t op, pgp_key_t *key, rnp::KeyProvider *key_provider, b continue; } #if defined(ENABLE_PQC) - /* prefer PQC over non-PQC. Assume non-PQC key is only there for backwards - * compatibility. */ - if (subkey && subkey->is_pqc_alg() && !cur->is_pqc_alg()) { - /* do not override already found PQC key with non-PQC key */ - continue; - } - if (subkey && cur->is_pqc_alg() && !subkey->is_pqc_alg()) { - /* override non-PQC key with PQC key */ - subkey = cur; - continue; + if (pref_pqc_sub && op == PGP_OP_ENCRYPT) { + /* prefer PQC encryption over non-PQC encryption. Assume non-PQC key is only there + * for backwards compatibility. */ + if (subkey && subkey->is_pqc_alg() && !cur->is_pqc_alg()) { + /* do not override already found PQC key with non-PQC key */ + continue; + } + if (subkey && cur->is_pqc_alg() && !subkey->is_pqc_alg()) { + /* override non-PQC key with PQC key */ + subkey = cur; + continue; + } } #endif if (!subkey || (cur->creation() > subkey->creation())) { diff --git a/src/lib/pgp-key.h b/src/lib/pgp-key.h index da7aaae0b..5ff6de8d3 100644 --- a/src/lib/pgp-key.h +++ b/src/lib/pgp-key.h @@ -691,6 +691,7 @@ bool pgp_subkey_set_expiration(pgp_key_t * sub, pgp_key_t *find_suitable_key(pgp_op_t op, pgp_key_t * key, rnp::KeyProvider *key_provider, - bool no_primary = false); + bool no_primary = false, + bool pref_pqc_sub = false); #endif // RNP_PACKET_KEY_H diff --git a/src/lib/rnp.cpp b/src/lib/rnp.cpp index ebc9c6cda..73f538657 100644 --- a/src/lib/rnp.cpp +++ b/src/lib/rnp.cpp @@ -2594,8 +2594,16 @@ try { return RNP_ERROR_NULL_POINTER; } - pgp_key_t *key = find_suitable_key( - PGP_OP_ENCRYPT, get_key_prefer_public(handle), &handle->ffi->key_provider); +#if defined(ENABLE_PQC) + bool prefer_pqc = op->rnpctx.pref_pqc_enc_subkey; +#else + bool prefer_pqc = false; +#endif + pgp_key_t *key = find_suitable_key(PGP_OP_ENCRYPT, + get_key_prefer_public(handle), + &handle->ffi->key_provider, + false, + prefer_pqc); if (!key) { return RNP_ERROR_NO_SUITABLE_KEY; } @@ -2618,6 +2626,20 @@ try { FFI_GUARD #endif +#if defined(RNP_EXPERIMENTAL_PQC) +rnp_result_t +rnp_op_encrypt_prefer_pqc_enc_subkey(rnp_op_encrypt_t op) +try { + if (!op) { + return RNP_ERROR_NULL_POINTER; + } + + op->rnpctx.pref_pqc_enc_subkey = true; + return RNP_SUCCESS; +} +FFI_GUARD +#endif + rnp_result_t rnp_op_encrypt_add_signature(rnp_op_encrypt_t op, rnp_key_handle_t key, @@ -7296,6 +7318,11 @@ try { return RNP_ERROR_BAD_PARAMETERS; } bool no_primary = extract_flag(flags, RNP_KEY_SUBKEYS_ONLY); +#if defined(ENABLE_PQC) + bool prefer_pqc_enc_subkey = extract_flag(flags, RNP_KEY_PREFER_PQC_ENC_SUBKEY); +#else + bool prefer_pqc_enc_subkey = false; +#endif if (flags) { FFI_LOG(primary_key->ffi, "Invalid flags: %" PRIu32, flags); return RNP_ERROR_BAD_PARAMETERS; @@ -7321,8 +7348,8 @@ try { if (!key) { return RNP_ERROR_BAD_PARAMETERS; } - pgp_key_t *defkey = - find_suitable_key(op, key, &primary_key->ffi->key_provider, no_primary); + pgp_key_t *defkey = find_suitable_key( + op, key, &primary_key->ffi->key_provider, no_primary, prefer_pqc_enc_subkey); if (!defkey) { *default_key = NULL; return RNP_ERROR_NO_SUITABLE_KEY; diff --git a/src/librepgp/stream-ctx.h b/src/librepgp/stream-ctx.h index 12639bc4e..bc1c7a367 100644 --- a/src/librepgp/stream-ctx.h +++ b/src/librepgp/stream-ctx.h @@ -70,8 +70,10 @@ typedef struct rnp_symmetric_pass_info_t { * - halg : hash algorithm used during key derivation for password-based encryption * - ealg, aalg, abits : symmetric encryption algorithm and AEAD parameters if used * - recipients : list of key ids used to encrypt data to - * - enable_pkesk_v6 : if true and each recipient in the list of recipients has the - * capability, allows PKESKv5/SEIPDv2 + * - enable_pkesk_v6 (Only if defined: ENABLE_CRYPTO_REFRESH): if true and each recipient in + * the list of recipients has the capability, allows PKESKv6/SEIPDv2 + * - pref_pqc_enc_subkey (Only if defined: ENABLE_PQC): if true, prefers PQC subkey over + * non-PQC subkey for encryption. * - passwords : list of passwords used for password-based encryption * - filename, filemtime, zalg, zlevel : see previous * - pkeskv6_capable() : returns true if all keys support PKESKv6+SEIPDv2, false otherwise @@ -108,6 +110,9 @@ typedef struct rnp_ctx_t { bool no_wrap{}; /* do not wrap source in literal data packet */ #if defined(ENABLE_CRYPTO_REFRESH) bool enable_pkesk_v6{}; /* allows pkesk v6 if list of recipients is suitable */ +#endif +#if defined(ENABLE_CRYPTO_REFRESH) + bool pref_pqc_enc_subkey{}; /* prefer to encrypt to PQC subkey */ #endif std::list recipients{}; /* recipients of the encrypted message */ std::list passwords{}; /* passwords to encrypt message */ diff --git a/src/tests/cli_tests.py b/src/tests/cli_tests.py index dec8ad0f0..ec14ce87e 100755 --- a/src/tests/cli_tests.py +++ b/src/tests/cli_tests.py @@ -409,7 +409,6 @@ def rnp_encrypt_and_sign_file(src, dst, recipients, encrpswd, signers, signpswd, if ret != 0: raise_err('rnp encrypt-and-sign failed', err) - def rnp_decrypt_file(src, dst, password = PASSWORD): pipe = pswd_pipe(password) ret, out, err = run_proc( diff --git a/src/tests/ffi-enc.cpp b/src/tests/ffi-enc.cpp index c7308dbef..73709dd35 100644 --- a/src/tests/ffi-enc.cpp +++ b/src/tests/ffi-enc.cpp @@ -912,6 +912,71 @@ TEST_F(rnp_tests, test_ffi_decrypt_pqc_pkesk_test_vector) rnp_ffi_destroy(ffi); } + +TEST_F(rnp_tests, test_ffi_pqc_default_enc_subkey) +{ + rnp_ffi_t ffi = NULL; + rnp_key_handle_t key1 = NULL; + rnp_key_handle_t key2 = NULL; + rnp_key_handle_t defkey1 = NULL; + rnp_key_handle_t defkey2 = NULL; + rnp_op_generate_t op = NULL; + assert_rnp_success(rnp_ffi_create(&ffi, "GPG", "GPG")); + + /* generate key 1 */ + assert_rnp_success(rnp_op_generate_create(&op, ffi, "ML-DSA-65+ED25519")); + assert_rnp_success(rnp_op_generate_set_hash(op, "SHA3-256")); + assert_rnp_success(rnp_op_generate_execute(op)); + + assert_rnp_success(rnp_op_generate_get_key(op, &key1)); + assert_non_null(key1); + assert_rnp_success(rnp_op_generate_destroy(op)); + op = NULL; + assert_rnp_success(rnp_op_generate_subkey_create(&op, ffi, key1, "ML-KEM-768+X25519")); + assert_rnp_success(rnp_op_generate_execute(op)); + rnp_op_generate_destroy(op); + op = NULL; + assert_rnp_success(rnp_op_generate_subkey_create(&op, ffi, key1, "ECDH")); + assert_rnp_success(rnp_op_generate_set_curve(op, "NIST P-256")); + assert_rnp_success(rnp_op_generate_execute(op)); + rnp_op_generate_destroy(op); + op = NULL; + + /* generate key 2 */ + assert_rnp_success(rnp_op_generate_create(&op, ffi, "ML-DSA-65+ED25519")); + assert_rnp_success(rnp_op_generate_set_hash(op, "SHA3-256")); + assert_rnp_success(rnp_op_generate_execute(op)); + assert_rnp_success(rnp_op_generate_get_key(op, &key2)); + assert_non_null(key2); + assert_rnp_success(rnp_op_generate_destroy(op)); + op = NULL; + assert_rnp_success(rnp_op_generate_subkey_create(&op, ffi, key2, "ECDH")); + assert_rnp_success(rnp_op_generate_set_curve(op, "NIST P-256")); + assert_rnp_success(rnp_op_generate_execute(op)); + rnp_op_generate_destroy(op); + op = NULL; + assert_rnp_success(rnp_op_generate_subkey_create(&op, ffi, key2, "ML-KEM-768+X25519")); + assert_rnp_success(rnp_op_generate_execute(op)); + rnp_op_generate_destroy(op); + op = NULL; + + /* check default key */ + assert_rnp_success( + rnp_key_get_default_key(key1, "encrypt", RNP_KEY_PREFER_PQC_ENC_SUBKEY, &defkey1)); + // PQC key is older but preferred + assert(defkey1->pub->alg() == PGP_PKA_KYBER768_X25519); + assert_rnp_success( + rnp_key_get_default_key(key2, "encrypt", RNP_KEY_PREFER_PQC_ENC_SUBKEY, &defkey2)); + // PQC key is newer and preferred + assert(defkey2->pub->alg() == PGP_PKA_KYBER768_X25519); + + /* cleanup */ + rnp_key_handle_destroy(key1); + rnp_key_handle_destroy(key2); + rnp_key_handle_destroy(defkey1); + rnp_key_handle_destroy(defkey2); + rnp_ffi_destroy(ffi); +} #endif TEST_F(rnp_tests, test_ffi_encrypt_pk_with_v6_key) From 19df9dfa640e5b5c1aec86450b1961449b3eed3b Mon Sep 17 00:00:00 2001 From: Johannes Roth Date: Mon, 28 Oct 2024 13:57:13 +0100 Subject: [PATCH 17/17] address review comments --- include/rnp/rnp.h | 2 +- src/librepgp/stream-ctx.h | 2 +- src/tests/cli_tests.py | 59 +++++++++++++++++++-------------------- 3 files changed, 31 insertions(+), 32 deletions(-) diff --git a/include/rnp/rnp.h b/include/rnp/rnp.h index b69b33dae..d2d7930d8 100644 --- a/include/rnp/rnp.h +++ b/include/rnp/rnp.h @@ -104,7 +104,7 @@ typedef uint32_t rnp_result_t; * Flags for default key selection. */ #define RNP_KEY_SUBKEYS_ONLY (1U << 0) -#if defined(RNP_EXPERIMENTAL_CRYPTO_REFRESH) +#if defined(RNP_EXPERIMENTAL_PQC) #define RNP_KEY_PREFER_PQC_ENC_SUBKEY (1U << 1) #endif diff --git a/src/librepgp/stream-ctx.h b/src/librepgp/stream-ctx.h index bc1c7a367..fa140c313 100644 --- a/src/librepgp/stream-ctx.h +++ b/src/librepgp/stream-ctx.h @@ -111,7 +111,7 @@ typedef struct rnp_ctx_t { #if defined(ENABLE_CRYPTO_REFRESH) bool enable_pkesk_v6{}; /* allows pkesk v6 if list of recipients is suitable */ #endif -#if defined(ENABLE_CRYPTO_REFRESH) +#if defined(ENABLE_PQC) bool pref_pqc_enc_subkey{}; /* prefer to encrypt to PQC subkey */ #endif std::list recipients{}; /* recipients of the encrypted message */ diff --git a/src/tests/cli_tests.py b/src/tests/cli_tests.py index ec14ce87e..0bf79eadc 100755 --- a/src/tests/cli_tests.py +++ b/src/tests/cli_tests.py @@ -328,11 +328,11 @@ def rnp_genkey_rsa(userid, bits=2048, pswd=PASSWORD): if ret != 0: raise_err('rsa key generation failed', err) -def rnp_genkey_pqc(userid, algo_cli_nr, algo_param = None, pswd=PASSWORD): +def rnp_genkey_pqc(userid, algo_cli_nr, homedir, algo_param = None, pswd=PASSWORD): algo_pipe = str(algo_cli_nr) if algo_param: algo_pipe += "\n" + str(algo_param) - ret, out, err = run_proc(RNPK, ['--homedir', RNPDIR, '--password', pswd, + ret, out, err = run_proc(RNPK, ['--homedir', homedir, '--password', pswd, '--notty', '--userid', userid, '--generate-key', '--expert'], algo_pipe) #os.close(algo_pipe) if ret != 0: @@ -383,8 +383,10 @@ def rnp_encrypt_file_ex(src, dst, recipients=None, passwords=None, aead=None, ci raise_err('rnp encryption failed with ' + cipher, err) def rnp_encrypt_and_sign_file(src, dst, recipients, encrpswd, signers, signpswd, - aead=None, cipher=None, z=None, armor=False): - params = ['--homedir', RNPDIR, '--sign', '--encrypt', src, '--output', dst] + aead=None, cipher=None, z=None, armor=False, homedir=None): + if not homedir: + homedir = RNPDIR + params = ['--homedir', homedir, '--sign', '--encrypt', src, '--output', dst] pipe = pswd_pipe('\n'.join(encrpswd + signpswd)) params[2:2] = ['--pass-fd', str(pipe)] @@ -409,10 +411,12 @@ def rnp_encrypt_and_sign_file(src, dst, recipients, encrpswd, signers, signpswd, if ret != 0: raise_err('rnp encrypt-and-sign failed', err) -def rnp_decrypt_file(src, dst, password = PASSWORD): +def rnp_decrypt_file(src, dst, password = PASSWORD, homedir = None): + if not homedir: + homedir = RNPDIR pipe = pswd_pipe(password) ret, out, err = run_proc( - RNP, ['--homedir', RNPDIR, '--pass-fd', str(pipe), '--decrypt', src, '--output', dst]) + RNP, ['--homedir', homedir, '--pass-fd', str(pipe), '--decrypt', src, '--output', dst]) os.close(pipe) if ret != 0: raise_err('rnp decryption failed', out + err) @@ -4536,7 +4540,7 @@ def test_encryption_multiple_recipients(self): gpg_decrypt_file(dst, dec, pswd) gpg_agent_clear_cache() remove_files(dec) - rnp_decrypt_file(dst, dec, '\n'.join([pswd] * 5)) + rnp_decrypt_file(dst, dec, password='\n'.join([pswd] * 5)) remove_files(dec) # Decrypt file with each of the passwords (with gpg only first password is checked) @@ -4550,7 +4554,7 @@ def test_encryption_multiple_recipients(self): gpg_decrypt_file(dst, dec, pswd) gpg_agent_clear_cache() remove_files(dec) - rnp_decrypt_file(dst, dec, '\n'.join([pswd] * 5)) + rnp_decrypt_file(dst, dec, password='\n'.join([pswd] * 5)) remove_files(dec) remove_files(dst, dec) @@ -4600,7 +4604,7 @@ def test_encryption_and_signing(self): gpg_decrypt_file(dst, dec, pswd) gpg_agent_clear_cache() remove_files(dec) - rnp_decrypt_file(dst, dec, '\n'.join([pswd] * 5)) + rnp_decrypt_file(dst, dec, password='\n'.join([pswd] * 5)) remove_files(dec) # GPG decrypts only with first password, see T3795 @@ -4615,7 +4619,7 @@ def test_encryption_and_signing(self): gpg_decrypt_file(dst, dec, pswd) gpg_agent_clear_cache() remove_files(dec) - rnp_decrypt_file(dst, dec, '\n'.join([pswd] * 5)) + rnp_decrypt_file(dst, dec, password='\n'.join([pswd] * 5)) remove_files(dec) remove_files(dst, dec) @@ -4637,12 +4641,11 @@ def verify_pqc_algo_ui_nb_to_algo_ui_str(self, stdout: str, algo_ui_exp_strs) -> if not found_this_entry: raise RuntimeError("did not match the expected UI choice for algorithm: " + expected_line) - """ zzz_ prefix makes it the last test. This is a workaround against a gnupg import error with the - pqc keys in other member function tests that would otherwise follow this one. - """ - def test_zzz_encryption_and_signing_pqc(self): + def test_encryption_and_signing_pqc(self): if not RNP_PQC: return + RNPDIR_PQC = RNPDIR + 'PQC' + os.mkdir(RNPDIR_PQC, 0o700) algo_ui_exp_strs = [ "(24) Ed25519Legacy + Curve25519Legacy + (ML-KEM-768 + X25519)", "(25) (ML-DSA-65 + Ed25519) + (ML-KEM-768 + X25519)", "(27) (ML-DSA-65 + ECDSA-NIST-P-256) + (ML-KEM-768 + ECDH-NIST-P-256)", @@ -4663,19 +4666,15 @@ def test_zzz_encryption_and_signing_pqc(self): for x in range(len(ALGO)): passwds.append('testpw' if x % 1 == 0 else '') for x in range(len(ALGO)): aead_list.append(None if x % 3 == 0 else ('ocb' if x % 3 == 1 else 'eax' )) if any(len(USERIDS) != len(x) for x in [ALGO, ALGO_PARAM]): - raise RuntimeError("test_zzz_encryption_and_signing_pqc: internal error: lengths of test data arrays matching") + raise RuntimeError("test_encryption_and_signing_pqc: internal error: lengths of test data arrays matching") # Generate multiple keys and import to GnuPG verified_algo_nums = False for uid, algo, param, passwd in zip(USERIDS, ALGO, ALGO_PARAM, passwds): - stdout = rnp_genkey_pqc(uid, algo, param, passwd) + stdout = rnp_genkey_pqc(uid, algo, RNPDIR_PQC, param, passwd) if not verified_algo_nums: self.verify_pqc_algo_ui_nb_to_algo_ui_str(stdout, algo_ui_exp_strs) verified_algo_nums = True - #gpg_import_pubring() - #gpg_import_secring() - - src, dst, dec = reg_workfiles('cleartext', '.txt', '.rnp', '.dec') # Generate random file of required size random_text(src, 65500) @@ -4689,14 +4688,14 @@ def test_zzz_encryption_and_signing_pqc(self): signerpws = [passwds[i]] rnp_encrypt_and_sign_file(src, dst, recipients, passwords, signers, - signerpws, aead=[aead_list[i]]) + signerpws, aead=[aead_list[i]], homedir=RNPDIR_PQC) # Decrypt file with each of the keys, we have different password for each key - rnp_decrypt_file(dst, dec, passwds[i]) - remove_files(dec) - + rnp_decrypt_file(dst, dec, password=passwds[i], homedir=RNPDIR_PQC) + remove_files(dst, dec) + clear_workfiles() + shutil.rmtree(RNPDIR_PQC, ignore_errors=True) - remove_files(dst, dec) def test_encryption_weird_userids_special_1(self): uid = WEIRD_USERID_SPECIAL_CHARS @@ -4707,7 +4706,7 @@ def test_encryption_weird_userids_special_1(self): dst, dec = reg_workfiles('weird_userids_special_1', '.rnp', '.dec') rnp_encrypt_file_ex(src, dst, [uid], None, None) # Decrypt - rnp_decrypt_file(dst, dec, pswd) + rnp_decrypt_file(dst, dec, password=pswd) compare_files(src, dec, RNP_DATA_DIFFERS) clear_workfiles() @@ -4724,7 +4723,7 @@ def test_encryption_weird_userids_special_2(self): # Decrypt file with each of the passwords for pswd in KEYPASS: multiple_pass_attempts = (pswd + '\n') * len(KEYPASS) - rnp_decrypt_file(dst, dec, multiple_pass_attempts) + rnp_decrypt_file(dst, dec, password=multiple_pass_attempts) compare_files(src, dec, RNP_DATA_DIFFERS) remove_files(dec) # Cleanup @@ -4750,7 +4749,7 @@ def test_encryption_weird_userids_unicode(self): # Decrypt file with each of the passwords for pswd in KEYPASS: multiple_pass_attempts = (pswd + '\n') * len(KEYPASS) - rnp_decrypt_file(dst, dec, multiple_pass_attempts) + rnp_decrypt_file(dst, dec, password=multiple_pass_attempts) compare_files(src, dec, RNP_DATA_DIFFERS) remove_files(dec) # Cleanup @@ -4812,7 +4811,7 @@ def test_encryption_x25519(self): ret, out, _ = run_proc(RNP, ['--homedir', RNPDIR, '-es', '-r', 'eddsa_25519', '-u', 'eddsa_25519', '--password', PASSWORD, src, '--output', dst, '--armor']) # Decrypt and verify with RNP - rnp_decrypt_file(dst, dec, 'password') + rnp_decrypt_file(dst, dec, password='password') self.assertEqual(file_text(src), file_text(dec)) remove_files(dec) # Decrypt and verify with GPG @@ -4824,7 +4823,7 @@ def test_encryption_x25519(self): '-u', 'eddsa_25519', '--output', dst, '-es', src]) self.assertEqual(ret, 0) # Decrypt and verify with RNP - rnp_decrypt_file(dst, dec, 'password') + rnp_decrypt_file(dst, dec, password='password') self.assertEqual(file_text(src), file_text(dec)) # Encrypt/decrypt using the p256 key, making sure message is not displayed key = data_path('test_stream_key_load/ecc-p256-sec.asc')