diff options
Diffstat (limited to 'src/tests/cli_tests.py')
-rwxr-xr-x | src/tests/cli_tests.py | 5048 |
1 files changed, 5048 insertions, 0 deletions
diff --git a/src/tests/cli_tests.py b/src/tests/cli_tests.py new file mode 100755 index 0000000..e6f5ed7 --- /dev/null +++ b/src/tests/cli_tests.py @@ -0,0 +1,5048 @@ +#!/usr/bin/env python + +import logging +import os +import os.path +import re +import shutil +import sys +import tempfile +import time +import unittest +from platform import architecture + +from cli_common import (file_text, find_utility, is_windows, list_upto, + path_for_gpg, pswd_pipe, raise_err, random_text, + run_proc, decode_string_escape, CONSOLE_ENCODING, + set_workdir) +from gnupg import GnuPG as GnuPG +from rnp import Rnp as Rnp + +WORKDIR = '' +RNP = '' +RNPK = '' +GPG = '' +GPGCONF = '' +RNPDIR = '' +GPGHOME = None +PASSWORD = 'password' +RMWORKDIR = True +GPG_AEAD = False +GPG_AEAD_EAX = False +GPG_AEAD_OCB = False +GPG_NO_OLD = False +GPG_BRAINPOOL = False +TESTS_SUCCEEDED = [] +TESTS_FAILED = [] +TEST_WORKFILES = [] + +# Supported features +RNP_TWOFISH = True +RNP_BRAINPOOL = True +RNP_AEAD_EAX = True +RNP_AEAD_OCB = True +RNP_AEAD_OCB_AES = False +RNP_AEAD = True +RNP_IDEA = True +RNP_BLOWFISH = True +RNP_CAST5 = True +RNP_RIPEMD160 = True + +if sys.version_info >= (3,): + unichr = chr + +def escape_regex(str): + return '^' + ''.join((c, "[\\x{:02X}]".format(ord(c)))[0 <= ord(c) <= 0x20 \ + or c in ['[',']','(',')','|','"','$','.','*','^','$','\\','+','?','{','}']] for c in str) + '$' + +UNICODE_LATIN_CAPITAL_A_GRAVE = unichr(192) +UNICODE_LATIN_SMALL_A_GRAVE = unichr(224) +UNICODE_LATIN_CAPITAL_A_MACRON = unichr(256) +UNICODE_LATIN_SMALL_A_MACRON = unichr(257) +UNICODE_GREEK_CAPITAL_HETA = unichr(880) +UNICODE_GREEK_SMALL_HETA = unichr(881) +UNICODE_GREEK_CAPITAL_OMEGA = unichr(937) +UNICODE_GREEK_SMALL_OMEGA = unichr(969) +UNICODE_CYRILLIC_CAPITAL_A = unichr(0x0410) +UNICODE_CYRILLIC_SMALL_A = unichr(0x0430) +UNICODE_CYRILLIC_CAPITAL_YA = unichr(0x042F) +UNICODE_CYRILLIC_SMALL_YA = unichr(0x044F) +UNICODE_SEQUENCE_1 = UNICODE_LATIN_CAPITAL_A_GRAVE + UNICODE_LATIN_SMALL_A_MACRON \ + + UNICODE_GREEK_CAPITAL_HETA + UNICODE_GREEK_SMALL_OMEGA \ + + UNICODE_CYRILLIC_CAPITAL_A + UNICODE_CYRILLIC_SMALL_YA +UNICODE_SEQUENCE_2 = UNICODE_LATIN_SMALL_A_GRAVE + UNICODE_LATIN_CAPITAL_A_MACRON \ + + UNICODE_GREEK_SMALL_HETA + UNICODE_GREEK_CAPITAL_OMEGA \ + + UNICODE_CYRILLIC_SMALL_A + UNICODE_CYRILLIC_CAPITAL_YA +WEIRD_USERID_UNICODE_1 = unichr(160) + unichr(161) \ + + UNICODE_SEQUENCE_1 + unichr(40960) + u'@rnp' +WEIRD_USERID_UNICODE_2 = unichr(160) + unichr(161) \ + + UNICODE_SEQUENCE_2 + unichr(40960) + u'@rnp' +WEIRD_USERID_SPECIAL_CHARS = '\\}{][)^*.+(\t\n|$@rnp' +WEIRD_USERID_SPACE = ' ' +WEIRD_USERID_QUOTE = '"' +WEIRD_USERID_SPACE_AND_QUOTE = ' "' +WEIRD_USERID_QUOTE_AND_SPACE = '" ' +WEIRD_USERID_TOO_LONG = 'x' * 125 + '@rnp' # totaling 129 (MAX_USER_ID + 1) + +# Key userids +KEY_ENCRYPT = 'encryption@rnp' +KEY_SIGN_RNP = 'signing@rnp' +KEY_SIGN_GPG = 'signing@gpg' +KEY_ENC_RNP = 'enc@rnp' +AT_EXAMPLE = '@example.com' + +# Keyrings +PUBRING = 'pubring.gpg' +SECRING = 'secring.gpg' +PUBRING_1 = 'keyrings/1/pubring.gpg' +SECRING_1 = 'keyrings/1/secring.gpg' +KEYRING_DIR_1 = 'keyrings/1' +KEYRING_DIR_3 = 'keyrings/3' +SECRING_G10 = 'test_stream_key_load/g10' +KEY_ALICE_PUB = 'test_key_validity/alice-pub.asc' +KEY_ALICE_SUB_PUB = 'test_key_validity/alice-sub-pub.pgp' +KEY_ALICE_SEC = 'test_key_validity/alice-sec.asc' +KEY_ALICE_SUB_SEC = 'test_key_validity/alice-sub-sec.pgp' +KEY_ALICE = 'Alice <alice@rnp>' +KEY_25519_NOTWEAK_SEC = 'test_key_edge_cases/key-25519-non-tweaked-sec.asc' + +# Messages +MSG_TXT = 'test_messages/message.txt' +MSG_ES_25519 = 'test_messages/message.txt.enc-sign-25519' +MSG_SIG_CRCR = 'test_messages/message.text-sig-crcr.sig' + +# Extensions +EXT_SIG = '.txt.sig' +EXT_ASC = '.txt.asc' +EXT_PGP = '.txt.pgp' + +# Misc +GPG_LOOPBACK = '--pinentry-mode=loopback' + +# Regexps +RE_RSA_KEY = r'(?s)^' \ +r'# .*' \ +r':public key packet:\s+' \ +r'version 4, algo 1, created \d+, expires 0\s+' \ +r'pkey\[0\]: \[(\d{4}) bits\]\s+' \ +r'pkey\[1\]: \[17 bits\]\s+' \ +r'keyid: ([0-9A-F]{16})\s+' \ +r'# .*' \ +r':user ID packet: "(.+)"\s+' \ +r'# .*' \ +r':signature packet: algo 1, keyid \2\s+' \ +r'.*' \ +r'# .*' \ +r':public sub key packet:' \ +r'.*' \ +r':signature packet: algo 1, keyid \2\s+' \ +r'.*$' + +RE_RSA_KEY_LIST = r'^\s*' \ +r'2 keys found\s+' \ +r'pub\s+(\d{4})/RSA ([0-9a-z]{16}) \d{4}-\d{2}-\d{2} \[.*\]\s+' \ +r'([0-9a-z]{40})\s+' \ +r'uid\s+(.+)\s+' \ +r'sub.+\s+' \ +r'[0-9a-z]{40}\s+$' + +RE_MULTIPLE_KEY_LIST = r'(?s)^\s*(\d+) (?:key|keys) found.*$' +RE_MULTIPLE_KEY_5 = r'(?s)^\s*' \ +r'10 keys found.*' \ +r'.+uid\s+0@rnp-multiple' \ +r'.+uid\s+1@rnp-multiple' \ +r'.+uid\s+2@rnp-multiple' \ +r'.+uid\s+3@rnp-multiple' \ +r'.+uid\s+4@rnp-multiple.*$' + +RE_MULTIPLE_SUBKEY_3 = r'(?s)^\s*' \ +r'3 keys found.*$' + +RE_MULTIPLE_SUBKEY_8 = r'(?s)^\s*' \ +r'8 keys found.*$' + +RE_GPG_SINGLE_RSA_KEY = r'(?s)^\s*' \ +r'.+-+\s*' \ +r'pub\s+rsa.+' \ +r'\s+([0-9A-F]{40})\s*' \ +r'uid\s+.+rsakey@gpg.*' + +RE_GPG_GOOD_SIGNATURE = r'(?s)^.*' \ +r'gpg: Signature made .*' \ +r'gpg: Good signature from "(.*)".*' + +RE_RNP_GOOD_SIGNATURE = r'(?s)^.*' \ +r'Good signature made .*' \ +r'using .* key .*' \ +r'pub .*' \ +r'uid\s+(.*)\s*' \ +r'Signature\(s\) verified successfully.*$' + +RE_RNP_ENCRYPTED_KEY = r'(?s)^.*' \ +r'Secret key packet.*' \ +r'secret key material:.*' \ +r'encrypted secret key data:.*' \ +r'UserID packet.*' \ +r'id: enc@rnp.*' \ +r'Secret subkey packet.*' \ +r'secret key material:.*' \ +r'encrypted secret key data:.*$' + +RE_RNP_REVOCATION_SIG = r'(?s)' \ +r':armored input\n' \ +r':off 0: packet header .* \(tag 2, len .*' \ +r'Signature packet.*' \ +r'version: 4.*' \ +r'type: 32 \(Key revocation signature\).*' \ +r'public key algorithm:.*' \ +r'hashed subpackets:.*' \ +r':type 33, len 21.*' \ +r'issuer fingerprint:.*' \ +r':type 2, len 4.*' \ +r'signature creation time:.*' \ +r':type 29.*' \ +r'reason for revocation: (.*)' \ +r'message: (.*)' \ +r'unhashed subpackets:.*' \ +r':type 16, len 8.*' \ +r'issuer key ID: .*$' + +RE_GPG_REVOCATION_IMPORT = r'(?s)^.*' \ +r'key 0451409669FFDE3C: "Alice <alice@rnp>" revocation certificate imported.*' \ +r'Total number processed: 1.*' \ +r'new key revocations: 1.*$' + +RE_SIG_1_IMPORT = r'(?s)^.*Import finished: 1 new signature, 0 unchanged, 0 unknown.*' + +RE_KEYSTORE_INFO = r'(?s)^.*fatal: cannot set keystore info' + +RNP_TO_GPG_ZALGS = { 'zip' : '1', 'zlib' : '2', 'bzip2' : '3' } +# These are mostly identical +RNP_TO_GPG_CIPHERS = {'AES' : 'aes128', 'AES192' : 'aes192', 'AES256' : 'aes256', + 'TWOFISH' : 'twofish', 'CAMELLIA128' : 'camellia128', + 'CAMELLIA192' : 'camellia192', 'CAMELLIA256' : 'camellia256', + 'IDEA' : 'idea', '3DES' : '3des', 'CAST5' : 'cast5', + 'BLOWFISH' : 'blowfish'} + +# Error messages +RNP_DATA_DIFFERS = 'rnp decrypted data differs' +GPG_DATA_DIFFERS = 'gpg decrypted data differs' +KEY_GEN_FAILED = 'key generation failed' +KEY_LIST_FAILED = 'key list failed' +KEY_LIST_WRONG = 'wrong key list output' +PKT_LIST_FAILED = 'packet listing failed' +ALICE_IMPORT_FAIL = 'Alice key import failed' +ENC_FAILED = 'encryption failed' +DEC_FAILED = 'decryption failed' +DEC_DIFFERS = 'Decrypted data differs' +GPG_IMPORT_FAILED = 'gpg key import failed' + +def check_packets(fname, regexp): + ret, output, err = run_proc(GPG, ['--homedir', '.', + '--list-packets', path_for_gpg(fname)]) + if ret != 0: + logging.error(err) + return None + else: + result = re.match(regexp, output) + if not result: + logging.debug('Wrong packets:') + logging.debug(output) + return result + +def clear_keyrings(): + shutil.rmtree(RNPDIR, ignore_errors=True) + os.mkdir(RNPDIR, 0o700) + + run_proc(GPGCONF, ['--homedir', GPGHOME, '--kill', 'gpg-agent']) + while os.path.isdir(GPGDIR): + try: + shutil.rmtree(GPGDIR) + except Exception: + time.sleep(0.1) + os.mkdir(GPGDIR, 0o700) + +def allow_y2k38_on_32bit(filename): + if architecture()[0] == '32bit': + return [filename, filename + '_y2k38'] + else: + return [filename] + +def compare_files(src, dst, message): + if file_text(src) != file_text(dst): + raise_err(message) + +def compare_file(src, string, message): + if file_text(src) != string: + raise_err(message) + +def compare_file_any(srcs, string, message): + for src in srcs: + if file_text(src) == string: + return + raise_err(message) + +def compare_file_ex(src, string, message, symbol='?'): + ftext = file_text(src) + if len(ftext) != len(string): + raise_err(message) + for i in range(0, len(ftext)): + if (ftext[i] != symbol[0]) and (ftext[i] != string[i]): + raise_err(message) + +def remove_files(*args): + for fpath in args: + try: + os.remove(fpath) + except Exception: + # Ignore if file cannot be removed + pass + +def reg_workfiles(mainname, *exts): + global TEST_WORKFILES + res = [] + for ext in exts: + fpath = os.path.join(WORKDIR, mainname + ext) + if fpath in TEST_WORKFILES: + logging.warn('Warning! Path {} is already in TEST_WORKFILES'.format(fpath)) + else: + TEST_WORKFILES += [fpath] + res += [fpath] + return res + +def clear_workfiles(): + global TEST_WORKFILES + remove_files(*TEST_WORKFILES) + TEST_WORKFILES = [] + +def rnp_genkey_rsa(userid, bits=2048, pswd=PASSWORD): + pipe = pswd_pipe(pswd) + ret, _, err = run_proc(RNPK, ['--numbits', str(bits), '--homedir', RNPDIR, '--pass-fd', str(pipe), + '--notty', '--s2k-iterations', '50000', '--userid', userid, '--generate-key']) + os.close(pipe) + if ret != 0: + raise_err('rsa key generation failed', err) + +def rnp_params_insert_z(params, pos, z): + if z: + if len(z) > 0 and z[0] != None: + params[pos:pos] = ['--' + z[0]] + if len(z) > 1 and z[1] != None: + params[pos:pos] = ['-z', str(z[1])] + +def rnp_params_insert_aead(params, pos, aead): + if aead != None: + params[pos:pos] = ['--aead=' + aead[0]] if len(aead) > 0 and aead[0] != None else ['--aead'] + if len(aead) > 1 and aead[1] != None: + params[pos + 1:pos + 1] = ['--aead-chunk-bits=' + str(aead[1])] + +def rnp_encrypt_file_ex(src, dst, recipients=None, passwords=None, aead=None, cipher=None, + z=None, armor=False, s2k_iter=False, s2k_msec=False): + params = ['--homedir', RNPDIR, src, '--output', dst] + # Recipients. None disables PK encryption, [] to use default key. Otherwise list of ids. + if recipients != None: + params[2:2] = ['--encrypt'] + for userid in reversed(recipients): + params[2:2] = ['-r', escape_regex(userid)] + # Passwords to encrypt to. None or [] disables password encryption. + if passwords: + if recipients is None: + params[2:2] = ['-c'] + if s2k_iter != False: + params += ['--s2k-iterations', str(s2k_iter)] + if s2k_msec != False: + params += ['--s2k-msec', str(s2k_msec)] + pipe = pswd_pipe('\n'.join(passwords)) + params[2:2] = ['--pass-fd', str(pipe), '--passwords', str(len(passwords))] + + # Cipher or None for default + if cipher: params[2:2] = ['--cipher', cipher] + # Armor + if armor: params += ['--armor'] + rnp_params_insert_aead(params, 2, aead) + rnp_params_insert_z(params, 2, z) + ret, _, err = run_proc(RNP, params) + if passwords: os.close(pipe) + if ret != 0: + 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] + pipe = pswd_pipe('\n'.join(encrpswd + signpswd)) + params[2:2] = ['--pass-fd', str(pipe)] + + # Encrypting passwords if any + if encrpswd: + params[2:2] = ['--passwords', str(len(encrpswd))] + # Adding recipients. If list is empty then default will be used. + for userid in reversed(recipients): + params[2:2] = ['-r', escape_regex(userid)] + # Adding signers. If list is empty then default will be used. + for signer in reversed(signers): + params[2:2] = ['-u', escape_regex(signer)] + # Cipher or None for default + if cipher: params[2:2] = ['--cipher', cipher] + # Armor + if armor: params += ['--armor'] + rnp_params_insert_aead(params, 2, aead) + rnp_params_insert_z(params, 2, z) + + ret, _, err = run_proc(RNP, params) + os.close(pipe) + 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( + RNP, ['--homedir', RNPDIR, '--pass-fd', str(pipe), '--decrypt', src, '--output', dst]) + os.close(pipe) + if ret != 0: + raise_err('rnp decryption failed', out + err) + +def rnp_sign_file_ex(src, dst, signers, passwords, options = None): + pipe = pswd_pipe('\n'.join(passwords)) + params = ['--homedir', RNPDIR, '--pass-fd', str(pipe), src] + if dst: params += ['--output', dst] + if 'cleartext' in options: + params[4:4] = ['--clearsign'] + else: + params[4:4] = ['--sign'] + if 'armor' in options: params += ['--armor'] + if 'detached' in options: params += ['--detach'] + + for signer in reversed(signers): + params[4:4] = ['--userid', escape_regex(signer)] + + ret, _, err = run_proc(RNP, params) + os.close(pipe) + if ret != 0: + raise_err('rnp signing failed', err) + + +def rnp_sign_file(src, dst, signers, passwords, armor=False): + options = [] + if armor: options += ['armor'] + rnp_sign_file_ex(src, dst, signers, passwords, options) + + +def rnp_sign_detached(src, signers, passwords, armor=False): + options = ['detached'] + if armor: options += ['armor'] + rnp_sign_file_ex(src, None, signers, passwords, options) + + +def rnp_sign_cleartext(src, dst, signers, passwords): + rnp_sign_file_ex(src, dst, signers, passwords, ['cleartext']) + + +def rnp_verify_file(src, dst, signer=None): + params = ['--homedir', RNPDIR, '--verify-cat', src, '--output', dst] + ret, out, err = run_proc(RNP, params) + if ret != 0: + raise_err('rnp verification failed', err + out) + # Check RNP output + match = re.match(RE_RNP_GOOD_SIGNATURE, err) + if not match: + raise_err('wrong rnp verification output', err) + if signer and (match.group(1).strip() != signer.strip()): + raise_err('rnp verification failed, wrong signer') + + +def rnp_verify_detached(sig, signer=None): + ret, out, err = run_proc(RNP, ['--homedir', RNPDIR, '--verify', sig]) + if ret != 0: + raise_err('rnp detached verification failed', err + out) + # Check RNP output + match = re.match(RE_RNP_GOOD_SIGNATURE, err) + if not match: + raise_err('wrong rnp detached verification output', err) + if signer and (match.group(1).strip() != signer.strip()): + raise_err('rnp detached verification failed, wrong signer'.format()) + + +def rnp_verify_cleartext(src, signer=None): + params = ['--homedir', RNPDIR, '--verify', src] + ret, out, err = run_proc(RNP, params) + if ret != 0: + raise_err('rnp verification failed', err + out) + # Check RNP output + match = re.match(RE_RNP_GOOD_SIGNATURE, err) + if not match: + raise_err('wrong rnp verification output', err) + if signer and (match.group(1).strip() != signer.strip()): + raise_err('rnp verification failed, wrong signer') + + +def gpg_import_pubring(kpath=None): + if not kpath: + kpath = os.path.join(RNPDIR, PUBRING) + ret, _, err = run_proc( + GPG, ['--display-charset', CONSOLE_ENCODING, '--batch', '--homedir', GPGHOME, '--import', kpath]) + if ret != 0: + raise_err(GPG_IMPORT_FAILED, err) + + +def gpg_import_secring(kpath=None, password = PASSWORD): + if not kpath: + kpath = os.path.join(RNPDIR, SECRING) + ret, _, err = run_proc( + GPG, ['--display-charset', CONSOLE_ENCODING, '--batch', '--passphrase', password, '--homedir', GPGHOME, '--import', kpath]) + if ret != 0: + raise_err('gpg secret key import failed', err) + + +def gpg_export_secret_key(userid, password, keyfile): + ret, _, err = run_proc(GPG, ['--batch', '--homedir', GPGHOME, GPG_LOOPBACK, + '--yes', '--passphrase', password, '--output', + path_for_gpg(keyfile), '--export-secret-key', userid]) + + if ret != 0: + raise_err('gpg secret key export failed', err) + +def gpg_params_insert_z(params, pos, z): + if z: + if len(z) > 0 and z[0] != None: + params[pos:pos] = ['--compress-algo', RNP_TO_GPG_ZALGS[z[0]]] + if len(z) > 1 and z[1] != None: + params[pos:pos] = ['-z', str(z[1])] + +def gpg_encrypt_file(src, dst, cipher=None, z=None, armor=False): + src = path_for_gpg(src) + dst = path_for_gpg(dst) + params = ['--homedir', GPGHOME, '-e', '-r', KEY_ENCRYPT, '--batch', + '--trust-model', 'always', '--output', dst, src] + if z: gpg_params_insert_z(params, 3, z) + if cipher: params[3:3] = ['--cipher-algo', RNP_TO_GPG_CIPHERS[cipher]] + if armor: params[2:2] = ['--armor'] + if GPG_NO_OLD: params[2:2] = ['--allow-old-cipher-algos'] + + ret, out, err = run_proc(GPG, params) + if ret != 0: + raise_err('gpg encryption failed for cipher ' + cipher, err) + +def gpg_symencrypt_file(src, dst, cipher=None, z=None, armor=False, aead=None): + src = path_for_gpg(src) + dst = path_for_gpg(dst) + params = ['--homedir', GPGHOME, '-c', '--s2k-count', '65536', '--batch', + '--passphrase', PASSWORD, '--output', dst, src] + if z: gpg_params_insert_z(params, 3, z) + if cipher: params[3:3] = ['--cipher-algo', RNP_TO_GPG_CIPHERS[cipher]] + if GPG_NO_OLD: params[3:3] = ['--allow-old-cipher-algos'] + if armor: params[2:2] = ['--armor'] + if aead != None: + if len(aead) > 0 and aead[0] != None: + params[3:3] = ['--aead-algo', aead[0]] + if len(aead) > 1 and aead[1] != None: + params[3:3] = ['--chunk-size', str(aead[1] + 6)] + params[3:3] = ['--rfc4880bis', '--force-aead'] + + ret, out, err = run_proc(GPG, params) + if ret != 0: + raise_err('gpg symmetric encryption failed for cipher ' + cipher, err) + + +def gpg_decrypt_file(src, dst, keypass): + src = path_for_gpg(src) + dst = path_for_gpg(dst) + ret, out, err = run_proc(GPG, ['--display-charset', CONSOLE_ENCODING, '--homedir', GPGHOME, GPG_LOOPBACK, '--batch', + '--yes', '--passphrase', keypass, '--trust-model', + 'always', '-o', dst, '-d', src]) + if ret != 0: + raise_err('gpg decryption failed', err) + + +def gpg_verify_file(src, dst, signer=None): + src = path_for_gpg(src) + dst = path_for_gpg(dst) + ret, out, err = run_proc(GPG, ['--display-charset', CONSOLE_ENCODING, '--homedir', GPGHOME, '--batch', + '--yes', '--trust-model', 'always', '-o', dst, '--verify', src]) + if ret != 0: + raise_err('gpg verification failed', err) + # Check GPG output + match = re.match(RE_GPG_GOOD_SIGNATURE, err) + if not match: + raise_err('wrong gpg verification output', err) + if signer and (match.group(1) != signer): + raise_err('gpg verification failed, wrong signer') + + +def gpg_verify_detached(src, sig, signer=None): + src = path_for_gpg(src) + sig = path_for_gpg(sig) + ret, _, err = run_proc(GPG, ['--display-charset', CONSOLE_ENCODING, '--homedir', GPGHOME, '--batch', '--yes', '--trust-model', + 'always', '--verify', sig, src]) + if ret != 0: + raise_err('gpg detached verification failed', err) + # Check GPG output + match = re.match(RE_GPG_GOOD_SIGNATURE, err) + if not match: + raise_err('wrong gpg detached verification output', err) + if signer and (match.group(1) != signer): + raise_err('gpg detached verification failed, wrong signer') + + +def gpg_verify_cleartext(src, signer=None): + src = path_for_gpg(src) + ret, _, err = run_proc( + GPG, ['--display-charset', CONSOLE_ENCODING, '--homedir', GPGHOME, '--batch', '--yes', '--trust-model', 'always', '--verify', src]) + if ret != 0: + raise_err('gpg cleartext verification failed', err) + # Check GPG output + match = re.match(RE_GPG_GOOD_SIGNATURE, err) + if not match: + raise_err('wrong gpg verification output', err) + if signer and (match.group(1) != signer): + raise_err('gpg verification failed, wrong signer') + + +def gpg_sign_file(src, dst, signer, z=None, armor=False): + src = path_for_gpg(src) + dst = path_for_gpg(dst) + params = ['--homedir', GPGHOME, GPG_LOOPBACK, '--batch', '--yes', + '--passphrase', PASSWORD, '--trust-model', 'always', '-u', signer, '-o', + dst, '-s', src] + if z: gpg_params_insert_z(params, 3, z) + if armor: params.insert(2, '--armor') + ret, _, err = run_proc(GPG, params) + if ret != 0: + raise_err('gpg signing failed', err) + + +def gpg_sign_detached(src, signer, armor=False, textsig=False): + src = path_for_gpg(src) + params = ['--homedir', GPGHOME, GPG_LOOPBACK, '--batch', '--yes', + '--passphrase', PASSWORD, '--trust-model', 'always', '-u', signer, + '--detach-sign', src] + if armor: params.insert(2, '--armor') + if textsig: params.insert(2, '--text') + ret, _, err = run_proc(GPG, params) + if ret != 0: + raise_err('gpg detached signing failed', err) + + +def gpg_sign_cleartext(src, dst, signer): + src = path_for_gpg(src) + dst = path_for_gpg(dst) + params = ['--homedir', GPGHOME, GPG_LOOPBACK, '--batch', '--yes', '--passphrase', + PASSWORD, '--trust-model', 'always', '-u', signer, '-o', dst, '--clearsign', src] + ret, _, err = run_proc(GPG, params) + if ret != 0: + raise_err('gpg cleartext signing failed', err) + + +def gpg_agent_clear_cache(): + run_proc(GPGCONF, ['--homedir', GPGHOME, '--kill', 'gpg-agent']) + +''' + Things to try here later on: + - different symmetric algorithms + - different file sizes (block len/packet len tests) + - different public key algorithms + - different compression levels/algorithms +''' + + +def gpg_to_rnp_encryption(filesize, cipher=None, z=None): + ''' + Encrypts with GPG and decrypts with RNP + ''' + src, dst, dec = reg_workfiles('cleartext', '.txt', '.gpg', '.rnp') + # Generate random file of required size + random_text(src, filesize) + for armor in [False, True]: + # Encrypt cleartext file with GPG + gpg_encrypt_file(src, dst, cipher, z, armor) + # Decrypt encrypted file with RNP + rnp_decrypt_file(dst, dec) + compare_files(src, dec, RNP_DATA_DIFFERS) + remove_files(dst, dec) + clear_workfiles() + + +def file_encryption_rnp_to_gpg(filesize, z=None): + ''' + Encrypts with RNP and decrypts with GPG and RNP + ''' + # TODO: Would be better to do "with reg_workfiles() as src,dst,enc ... and + # do cleanup at the end" + src, dst, enc = reg_workfiles('cleartext', '.txt', '.gpg', '.rnp') + # Generate random file of required size + random_text(src, filesize) + for armor in [False, True]: + # Encrypt cleartext file with RNP + rnp_encrypt_file_ex(src, enc, [KEY_ENCRYPT], None, None, None, z, armor) + # Decrypt encrypted file with GPG + gpg_decrypt_file(enc, dst, PASSWORD) + compare_files(src, dst, GPG_DATA_DIFFERS) + remove_files(dst) + # Decrypt encrypted file with RNP + rnp_decrypt_file(enc, dst) + compare_files(src, dst, RNP_DATA_DIFFERS) + remove_files(enc, dst) + clear_workfiles() + +''' + Things to try later: + - different public key algorithms + - decryption with generated by GPG and imported keys +''' + + +def rnp_sym_encryption_gpg_to_rnp(filesize, cipher = None, z = None): + src, dst, dec = reg_workfiles('cleartext', '.txt', '.gpg', '.rnp') + # Generate random file of required size + random_text(src, filesize) + for armor in [False, True]: + # Encrypt cleartext file with GPG + gpg_symencrypt_file(src, dst, cipher, z, armor) + # Decrypt encrypted file with RNP + rnp_decrypt_file(dst, dec) + compare_files(src, dec, RNP_DATA_DIFFERS) + remove_files(dst, dec) + clear_workfiles() + + +def rnp_sym_encryption_rnp_to_gpg(filesize, cipher = None, z = None, s2k_iter = False, s2k_msec = False): + src, dst, enc = reg_workfiles('cleartext', '.txt', '.gpg', '.rnp') + # Generate random file of required size + random_text(src, filesize) + for armor in [False, True]: + # Encrypt cleartext file with RNP + rnp_encrypt_file_ex(src, enc, None, [PASSWORD], None, cipher, z, armor, s2k_iter, s2k_msec) + # Decrypt encrypted file with GPG + gpg_decrypt_file(enc, dst, PASSWORD) + compare_files(src, dst, GPG_DATA_DIFFERS) + remove_files(dst) + # Decrypt encrypted file with RNP + rnp_decrypt_file(enc, dst) + compare_files(src, dst, RNP_DATA_DIFFERS) + remove_files(enc, dst) + clear_workfiles() + +def rnp_sym_encryption_rnp_aead(filesize, cipher = None, z = None, aead = None, usegpg = False): + src, dst, enc = reg_workfiles('cleartext', '.txt', '.rnp', '.enc') + # Generate random file of required size + random_text(src, filesize) + # Encrypt cleartext file with RNP + rnp_encrypt_file_ex(src, enc, None, [PASSWORD], aead, cipher, z) + # Decrypt encrypted file with RNP + rnp_decrypt_file(enc, dst) + compare_files(src, dst, RNP_DATA_DIFFERS) + remove_files(dst) + + if usegpg: + # Decrypt encrypted file with GPG + gpg_decrypt_file(enc, dst, PASSWORD) + compare_files(src, dst, GPG_DATA_DIFFERS) + remove_files(dst, enc) + # Encrypt cleartext file with GPG + gpg_symencrypt_file(src, enc, cipher, z, False, aead) + # Decrypt encrypted file with RNP + rnp_decrypt_file(enc, dst) + compare_files(src, dst, RNP_DATA_DIFFERS) + + clear_workfiles() + +def rnp_signing_rnp_to_gpg(filesize): + src, sig, ver = reg_workfiles('cleartext', '.txt', '.sig', '.ver') + # Generate random file of required size + random_text(src, filesize) + for armor in [False, True]: + # Sign file with RNP + rnp_sign_file(src, sig, [KEY_SIGN_RNP], [PASSWORD], armor) + # Verify signed file with RNP + rnp_verify_file(sig, ver, KEY_SIGN_RNP) + compare_files(src, ver, 'rnp verified data differs') + remove_files(ver) + # Verify signed message with GPG + gpg_verify_file(sig, ver, KEY_SIGN_RNP) + compare_files(src, ver, 'gpg verified data differs') + remove_files(sig, ver) + clear_workfiles() + + +def rnp_detached_signing_rnp_to_gpg(filesize): + src, sig, asc = reg_workfiles('cleartext', '.txt', EXT_SIG, EXT_ASC) + # Generate random file of required size + random_text(src, filesize) + for armor in [True, False]: + # Sign file with RNP + rnp_sign_detached(src, [KEY_SIGN_RNP], [PASSWORD], armor) + sigpath = asc if armor else sig + # Verify signature with RNP + rnp_verify_detached(sigpath, KEY_SIGN_RNP) + # Verify signed message with GPG + gpg_verify_detached(src, sigpath, KEY_SIGN_RNP) + remove_files(sigpath) + clear_workfiles() + + +def rnp_cleartext_signing_rnp_to_gpg(filesize): + src, asc = reg_workfiles('cleartext', '.txt', EXT_ASC) + # Generate random file of required size + random_text(src, filesize) + # Sign file with RNP + rnp_sign_cleartext(src, asc, [KEY_SIGN_RNP], [PASSWORD]) + # Verify signature with RNP + rnp_verify_cleartext(asc, KEY_SIGN_RNP) + # Verify signed message with GPG + gpg_verify_cleartext(asc, KEY_SIGN_RNP) + clear_workfiles() + + +def rnp_signing_gpg_to_rnp(filesize, z=None): + src, sig, ver = reg_workfiles('cleartext', '.txt', '.sig', '.ver') + # Generate random file of required size + random_text(src, filesize) + for armor in [True, False]: + # Sign file with GPG + gpg_sign_file(src, sig, KEY_SIGN_GPG, z, armor) + # Verify file with RNP + rnp_verify_file(sig, ver, KEY_SIGN_GPG) + compare_files(src, ver, 'rnp verified data differs') + remove_files(sig, ver) + clear_workfiles() + + +def rnp_detached_signing_gpg_to_rnp(filesize, textsig=False): + src, sig, asc = reg_workfiles('cleartext', '.txt', EXT_SIG, EXT_ASC) + # Generate random file of required size + random_text(src, filesize) + for armor in [True, False]: + # Sign file with GPG + gpg_sign_detached(src, KEY_SIGN_GPG, armor, textsig) + sigpath = asc if armor else sig + # Verify file with RNP + rnp_verify_detached(sigpath, KEY_SIGN_GPG) + clear_workfiles() + +def rnp_cleartext_signing_gpg_to_rnp(filesize): + src, asc = reg_workfiles('cleartext', '.txt', EXT_ASC) + # Generate random file of required size + random_text(src, filesize) + # Sign file with GPG + gpg_sign_cleartext(src, asc, KEY_SIGN_GPG) + # Verify signature with RNP + rnp_verify_cleartext(asc, KEY_SIGN_GPG) + # Verify signed message with GPG + gpg_verify_cleartext(asc, KEY_SIGN_GPG) + clear_workfiles() + +def gpg_check_features(): + global GPG_AEAD, GPG_AEAD_EAX, GPG_AEAD_OCB, GPG_NO_OLD, GPG_BRAINPOOL + _, out, _ = run_proc(GPG, ["--version"]) + # AEAD + GPG_AEAD_EAX = re.match(r'(?s)^.*AEAD:.*EAX.*', out) is not None + GPG_AEAD_OCB = re.match(r'(?s)^.*AEAD:.*OCB.*', out) is not None + # Version 2.3.0-beta1598 and up drops support of 64-bit block algos + match = re.match(r'(?s)^.*gpg \(GnuPG\) (\d+)\.(\d+)\.(\d+)(-beta(\d+))?.*$', out) + if not match: + raise_err('Failed to parse GnuPG version.') + ver = [int(match.group(1)), int(match.group(2)), int(match.group(3))] + beta = int(match.group(5)) if match.group(5) else 0 + if not beta: + GPG_NO_OLD = ver >= [2, 3, 0] + else: + GPG_NO_OLD = ver == [2, 3, 0] and (beta >= 1598) + # Version 2.4.0 and up doesn't support EAX and doesn't has AEAD in output + if ver >= [2, 4, 0]: + GPG_AEAD_OCB = True + GPG_AEAD_EAX = False + GPG_AEAD = GPG_AEAD_OCB or GPG_AEAD_EAX + # Check whether Brainpool curves are supported + _, out, _ = run_proc(GPG, ["--with-colons", "--list-config", "curve"]) + GPG_BRAINPOOL = re.match(r'(?s)^.*brainpoolP256r1.*', out) is not None + print('GPG_AEAD_EAX: ' + str(GPG_AEAD_EAX)) + print('GPG_AEAD_OCB: ' + str(GPG_AEAD_OCB)) + print('GPG_NO_OLD: ' + str(GPG_NO_OLD)) + 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 + ret, out, _ = run_proc(RNP, ['--version']) + if ret != 0: + raise_err('Failed to get RNP version.') + # AEAD + RNP_AEAD_EAX = re.match(r'(?s)^.*AEAD:.*EAX.*', out) is not None + RNP_AEAD_OCB = re.match(r'(?s)^.*AEAD:.*OCB.*', out) is not None + RNP_AEAD = RNP_AEAD_EAX or RNP_AEAD_OCB + RNP_AEAD_OCB_AES = RNP_AEAD_OCB and re.match(r'(?s)^.*Backend.*OpenSSL.*', out) is not None + # Twofish + RNP_TWOFISH = re.match(r'(?s)^.*Encryption:.*TWOFISH.*', out) is not None + # Brainpool curves + RNP_BRAINPOOL = re.match(r'(?s)^.*Curves:.*brainpoolP256r1.*brainpoolP384r1.*brainpoolP512r1.*', out) is not None + # IDEA encryption algorithm + RNP_IDEA = re.match(r'(?s)^.*Encryption:.*IDEA.*', out) is not None + 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 + print('RNP_TWOFISH: ' + str(RNP_TWOFISH)) + print('RNP_BLOWFISH: ' + str(RNP_BLOWFISH)) + print('RNP_IDEA: ' + str(RNP_IDEA)) + print('RNP_CAST5: ' + str(RNP_CAST5)) + print('RNP_RIPEMD160: ' + str(RNP_RIPEMD160)) + print('RNP_BRAINPOOL: ' + str(RNP_BRAINPOOL)) + print('RNP_AEAD_EAX: ' + str(RNP_AEAD_EAX)) + print('RNP_AEAD_OCB: ' + str(RNP_AEAD_OCB)) + print('RNP_AEAD_OCB_AES: ' + str(RNP_AEAD_OCB_AES)) + +def setup(loglvl): + # Setting up directories. + global RMWORKDIR, WORKDIR, RNPDIR, RNP, RNPK, GPG, GPGDIR, GPGHOME, GPGCONF + logging.basicConfig(stream=sys.stderr, format="%(message)s") + logging.getLogger().setLevel(loglvl) + WORKDIR = tempfile.mkdtemp(prefix='rnpctmp') + set_workdir(WORKDIR) + RMWORKDIR = True + + logging.info('Running in ' + WORKDIR) + + RNPDIR = os.path.join(WORKDIR, '.rnp') + RNP = os.getenv('RNP_TESTS_RNP_PATH') or 'rnp' + RNPK = os.getenv('RNP_TESTS_RNPKEYS_PATH') or 'rnpkeys' + shutil.rmtree(RNPDIR, ignore_errors=True) + os.mkdir(RNPDIR, 0o700) + + os.environ["RNP_LOG_CONSOLE"] = "1" + + GPGDIR = os.path.join(WORKDIR, '.gpg') + GPGHOME = path_for_gpg(GPGDIR) if is_windows() else GPGDIR + GPG = os.getenv('RNP_TESTS_GPG_PATH') or find_utility('gpg') + GPGCONF = os.getenv('RNP_TESTS_GPGCONF_PATH') or find_utility('gpgconf') + gpg_check_features() + rnp_check_features() + shutil.rmtree(GPGDIR, ignore_errors=True) + os.mkdir(GPGDIR, 0o700) + +def data_path(subpath): + ''' Constructs path to the tests data file/dir''' + return os.path.join(os.path.dirname(os.path.realpath(__file__)), 'data', subpath) + +def key_path(file_base_name, secret): + ''' Constructs path to the .gpg file''' + path=os.path.join(os.path.dirname(os.path.realpath(__file__)), 'data/cli_EncryptSign', + file_base_name) + return ''.join([path, '-sec' if secret else '', '.gpg']) + +def rnp_supported_ciphers(aead = False): + ciphers = ['AES', 'AES192', 'AES256'] + if aead and RNP_AEAD_OCB_AES: + return ciphers + ciphers += ['CAMELLIA128', 'CAMELLIA192', 'CAMELLIA256'] + if RNP_TWOFISH: + ciphers += ['TWOFISH'] + # AEAD supports only 128-bit block ciphers + if aead: + return ciphers + ciphers += ['3DES'] + if RNP_IDEA: + ciphers += ['IDEA'] + if RNP_BLOWFISH: + ciphers += ['BLOWFISH'] + if RNP_CAST5: + ciphers += ['CAST5'] + return ciphers + +class TestIdMixin(object): + + @property + def test_id(self): + return "".join(self.id().split('.')[1:3]) + +class KeyLocationChooserMixin(object): + def __init__(self): + # If set it will try to import a key from provided location + # otherwise it will try to generate a key + self.__op_key_location = None + self.__op_key_gen_cmd = None + + @property + def operation_key_location(self): + return self.__op_key_location + + @operation_key_location.setter + def operation_key_location(self, key): + if (type(key) is not tuple): raise RuntimeError("Key must be tuple(pub,sec)") + self.__op_key_location = key + self.__op_key_gen_cmd = None + + @property + def operation_key_gencmd(self): + return self.__op_key_gen_cmd + + @operation_key_gencmd.setter + def operation_key_gencmd(self, cmd): + self.__op_key_gen_cmd = cmd + self.__op_key_location = None + +''' + Things to try here later on: + - different public key algorithms + - different key protection levels/algorithms + - armored import/export +''' +class Keystore(unittest.TestCase): + + @classmethod + def setUpClass(cls): + clear_keyrings() + + @classmethod + def tearDownClass(cls): + clear_keyrings() + + def tearDown(self): + clear_workfiles() + + def _rnpkey_generate_rsa(self, bits= None): + # Setup command line params + if bits: + params = ['--numbits', str(bits)] + else: + params = [] + bits = 2048 + + userid = str(bits) + '@rnptest' + # Open pipe for password + pipe = pswd_pipe(PASSWORD) + params = params + ['--homedir', RNPDIR, '--pass-fd', str(pipe), + '--userid', userid, '--s2k-iterations', '50000', '--generate-key'] + # Run key generation + ret, _, _ = run_proc(RNPK, params) + os.close(pipe) + self.assertEqual(ret, 0, KEY_GEN_FAILED) + # Check packets using the gpg + match = check_packets(os.path.join(RNPDIR, PUBRING), RE_RSA_KEY) + self.assertTrue(match, 'generated key check failed') + keybits = int(match.group(1)) + self.assertLessEqual(keybits, bits, 'too much bits') + self.assertGreater(keybits, bits - 8, 'too few bits') + keyid = match.group(2) + self.assertEqual(match.group(3), userid, 'wrong user id') + # List keys using the rnpkeys + ret, out, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--list-keys']) + self.assertEqual(ret, 0, KEY_LIST_FAILED) + match = re.match(RE_RSA_KEY_LIST, out) + # Compare key ids + self.assertTrue(match, 'wrong RSA key list output') + self.assertEqual(match.group(3)[-16:], match.group(2), 'wrong fp') + self.assertEqual(match.group(2), keyid.lower(), 'wrong keyid') + self.assertEqual(match.group(1), str(bits), 'wrong key bits in list') + # Import key to the gnupg + ret, _, _ = run_proc(GPG, ['--batch', '--passphrase', PASSWORD, '--homedir', + GPGHOME, '--import', + path_for_gpg(os.path.join(RNPDIR, PUBRING)), + path_for_gpg(os.path.join(RNPDIR, SECRING))]) + self.assertEqual(ret, 0, GPG_IMPORT_FAILED) + # Cleanup and return + clear_keyrings() + + def test_generate_default_rsa_key(self): + self._rnpkey_generate_rsa() + + def test_rnpkeys_keygen_invalid_parameters(self): + # Pass invalid numbits + ret, _, err = run_proc(RNPK, ['--numbits', 'wrong', '--homedir', RNPDIR, '--password', 'password', + '--userid', 'wrong', '--generate-key']) + self.assertNotEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*wrong bits value: wrong.*') + # Too small + ret, _, err = run_proc(RNPK, ['--numbits', '768', '--homedir', RNPDIR, '--password', 'password', + '--userid', '768', '--generate-key']) + self.assertNotEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*wrong bits value: 768.*') + # Wrong hash algorithm + ret, _, err = run_proc(RNPK, ['--hash', 'BAD_HASH', '--homedir', RNPDIR, '--password', 'password', + '--userid', 'bad_hash', '--generate-key']) + self.assertNotEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*Unsupported hash algorithm: BAD_HASH.*') + # Wrong S2K iterations + ret, _, err = run_proc(RNPK, ['--s2k-iterations', 'WRONG_ITER', '--homedir', RNPDIR, '--password', 'password', + '--userid', 'wrong_iter', '--generate-key']) + self.assertNotEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*Wrong iterations value: WRONG_ITER.*') + # Wrong S2K msec + ret, _, err = run_proc(RNPK, ['--s2k-msec', 'WRONG_MSEC', '--homedir', RNPDIR, '--password', 'password', + '--userid', 'wrong_msec', '--generate-key']) + self.assertNotEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*Invalid s2k msec value: WRONG_MSEC.*') + # Wrong cipher + ret, _, err = run_proc(RNPK, ['--cipher', 'WRONG_AES', '--homedir', RNPDIR, '--password', 'password', + '--userid', 'wrong_aes', '--generate-key']) + self.assertNotEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*Unsupported encryption algorithm: WRONG_AES.*Failed to process argument --cipher.*') + + def test_generate_multiple_rsa_key__check_if_available(self): + ''' + Generate multiple RSA keys and check if they are all available + ''' + clear_keyrings() + # Generate 5 keys with different user ids + for i in range(0, 5): + # generate the next key + pipe = pswd_pipe(PASSWORD) + userid = str(i) + '@rnp-multiple' + ret, _, _ = run_proc(RNPK, ['--numbits', '2048', '--homedir', RNPDIR, '--s2k-msec', '100', + '--cipher', 'AES-128', '--pass-fd', str(pipe), '--userid', userid, + '--generate-key']) + os.close(pipe) + self.assertEqual(ret, 0, KEY_GEN_FAILED) + # list keys using the rnpkeys, checking whether it reports correct key + # number + ret, out, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--list-keys']) + self.assertEqual(ret, 0, KEY_LIST_FAILED) + match = re.match(RE_MULTIPLE_KEY_LIST, out) + self.assertTrue(match, KEY_LIST_WRONG) + self.assertEqual(match.group(1), str((i + 1) * 2), 'wrong key count') + + # Checking the 5 keys output + ret, out, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--list-keys']) + self.assertEqual(ret, 0, KEY_LIST_FAILED) + self.assertRegex(out, RE_MULTIPLE_KEY_5, KEY_LIST_WRONG) + + # Cleanup and return + clear_keyrings() + + def test_generate_key_with_gpg_import_to_rnp(self): + ''' + Generate key with GnuPG and import it to rnp + ''' + # Generate key in GnuPG + ret, _, _ = run_proc(GPG, ['--batch', '--homedir', GPGHOME, '--passphrase', + '', '--quick-generate-key', 'rsakey@gpg', 'rsa']) + self.assertEqual(ret, 0, 'gpg key generation failed') + # Getting fingerprint of the generated key + ret, out, err = run_proc(GPG, ['--batch', '--homedir', GPGHOME, '--list-keys']) + match = re.match(RE_GPG_SINGLE_RSA_KEY, out) + self.assertTrue(match, 'wrong gpg key list output') + keyfp = match.group(1) + # Exporting generated public key + ret, out, err = run_proc( + GPG, ['--batch', '--homedir', GPGHOME, '--armor', '--export', keyfp]) + self.assertEqual(ret, 0, 'gpg : public key export failed') + pubpath = os.path.join(RNPDIR, keyfp + '-pub.asc') + with open(pubpath, 'w+') as f: + f.write(out) + # Exporting generated secret key + ret, out, err = run_proc( + GPG, ['--batch', '--homedir', GPGHOME, '--armor', '--export-secret-key', keyfp]) + self.assertEqual(ret, 0, 'gpg : secret key export failed') + secpath = os.path.join(RNPDIR, keyfp + '-sec.asc') + with open(secpath, 'w+') as f: + f.write(out) + # Importing public key to rnp + ret, out, err = run_proc(RNPK, ['--homedir', RNPDIR, '--import-key', pubpath]) + self.assertEqual(ret, 0, 'rnp : public key import failed') + # Importing secret key to rnp + ret, out, err = run_proc(RNPK, ['--homedir', RNPDIR, '--import-key', secpath]) + self.assertEqual(ret, 0, 'rnp : secret key import failed') + + def test_generate_with_rnp_import_to_gpg(self): + ''' + Generate key with RNP and export it and then import to GnuPG + ''' + # Open pipe for password + pipe = pswd_pipe(PASSWORD) + # Run key generation + ret, _, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--pass-fd', str(pipe), + '--userid', 'rsakey@rnp', '--generate-key']) + os.close(pipe) + self.assertEqual(ret, 0, KEY_GEN_FAILED) + # Export key + ret, out, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--export-key', 'rsakey@rnp']) + self.assertEqual(ret, 0, 'key export failed') + pubpath = os.path.join(RNPDIR, 'rnpkey-pub.asc') + with open(pubpath, 'w+') as f: + f.write(out) + # Import key with GPG + ret, _, _ = run_proc(GPG, ['--batch', '--homedir', GPGHOME, '--import', + path_for_gpg(pubpath)]) + self.assertEqual(ret, 0, 'gpg : public key import failed') + + def test_generate_to_kbx(self): + ''' + Generate KBX with RNP and ensurethat the key can be read with GnuPG + ''' + clear_keyrings() + pipe = pswd_pipe(PASSWORD) + kbx_userid_tracker = 'kbx_userid_tracker@rnp' + # Run key generation + ret, out, err = run_proc(RNPK, ['--gen-key', '--keystore-format', 'GPG21', + '--userid', kbx_userid_tracker, '--homedir', + RNPDIR, '--pass-fd', str(pipe)]) + os.close(pipe) + self.assertEqual(ret, 0, KEY_GEN_FAILED) + # Read KBX with GPG + ret, out, err = run_proc(GPG, ['--homedir', path_for_gpg(RNPDIR), '--list-keys']) + self.assertEqual(ret, 0, 'gpg : failed to read KBX') + self.assertTrue(kbx_userid_tracker in out, 'gpg : failed to read expected key from KBX') + clear_keyrings() + + def test_generate_protection_pass_fd(self): + ''' + Generate key with RNP, using the --pass-fd parameter, and make sure key is encrypted + ''' + clear_keyrings() + # Open pipe for password + pipe = pswd_pipe(PASSWORD) + # Run key generation + ret, _, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--pass-fd', str(pipe), + '--userid', KEY_ENC_RNP, '--generate-key']) + os.close(pipe) + self.assertEqual(ret, 0, KEY_GEN_FAILED) + # Check packets using the gpg + params = ['--homedir', RNPDIR, '--list-packets', os.path.join(RNPDIR, SECRING)] + ret, out, _ = run_proc(RNP, params) + self.assertEqual(ret, 0) + self.assertRegex(out, RE_RNP_ENCRYPTED_KEY, 'wrong encrypted secret key listing') + + def test_generate_protection_password(self): + ''' + Generate key with RNP, using the --password parameter, and make sure key is encrypted + ''' + clear_keyrings() + params = ['--homedir', RNPDIR, '--password', 'password', '--userid', KEY_ENC_RNP, '--generate-key'] + ret, _, _ = run_proc(RNPK, params) + self.assertEqual(ret, 0, KEY_GEN_FAILED) + # Check packets using the gpg + params = ['--homedir', RNPDIR, '--list-packets', os.path.join(RNPDIR, SECRING)] + ret, out, _ = run_proc(RNP, params) + self.assertEqual(ret, 0) + self.assertRegex(out, RE_RNP_ENCRYPTED_KEY, 'wrong encrypted secret key listing') + + def test_generate_unprotected_key(self): + ''' + Generate key with RNP, using the --password parameter, and make sure key is encrypted + ''' + clear_keyrings() + params = ['--homedir', RNPDIR, '--password=', '--userid', KEY_ENC_RNP, '--generate-key'] + ret, _, _ = run_proc(RNPK, params) + self.assertEqual(ret, 0, KEY_GEN_FAILED) + # Check packets using the gpg + params = ['--homedir', RNPDIR, '--list-packets', os.path.join(RNPDIR, SECRING)] + ret, out, _ = run_proc(RNP, params) + self.assertEqual(ret, 0) + self.assertNotRegex(out, RE_RNP_ENCRYPTED_KEY, 'wrong unprotected secret key listing') + + def test_generate_preferences(self): + pipe = pswd_pipe(PASSWORD) + ret, _, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--pass-fd', str(pipe), '--userid', + 'eddsa_25519_prefs', '--generate-key', '--expert'], '22\n') + os.close(pipe) + self.assertEqual(ret, 0) + ret, out, _ = run_proc(RNP, ['--list-packets', os.path.join(RNPDIR, PUBRING)]) + self.assertRegex(out, r'.*preferred symmetric algorithms: AES-256, AES-192, AES-128 \(9, 8, 7\).*') + self.assertRegex(out, r'.*preferred hash algorithms: SHA256, SHA384, SHA512, SHA224 \(8, 9, 10, 11\).*') + + def test_import_signatures(self): + clear_keyrings() + RE_SIG_2_UNCHANGED = r'(?s)^.*Import finished: 0 new signatures, 2 unchanged, 0 unknown.*' + # Import command without the path parameter + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--import-sigs']) + self.assertNotEqual(ret, 0, 'Sigs import without file failed') + self.assertRegex(err, r'(?s)^.*Import path isn\'t specified.*', 'Sigs import without file wrong output') + # Import command with invalid path parameter + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--import-sigs', data_path('test_key_validity/alice-rev-no-file.pgp')]) + self.assertNotEqual(ret, 0, 'Sigs import with invalid path failed') + self.assertRegex(err, r'(?s)^.*Failed to create input for .*', 'Sigs import with invalid path wrong output') + # Try to import signature to empty keyring + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--import-sigs', data_path('test_key_validity/alice-rev.pgp')]) + self.assertEqual(ret, 0, 'Alice key rev import failed') + self.assertRegex(err, r'(?s)^.*Import finished: 0 new signatures, 0 unchanged, 1 unknown.*', 'Alice key rev import wrong output') + # Import Basil's key + ret, _, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--import', data_path('test_key_validity/basil-pub.asc')]) + self.assertEqual(ret, 0, 'Basil key import failed') + # Try to import Alice's signatures with Basil's key only + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--import', data_path('test_key_validity/alice-sigs.pgp')]) + self.assertEqual(ret, 0, 'Alice sigs import failed') + self.assertRegex(err, r'(?s)^.*Import finished: 0 new signatures, 0 unchanged, 2 unknown.*', 'Alice sigs import wrong output') + # Import Alice's key without revocation/direct-key signatures + ret, _, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--import', data_path(KEY_ALICE_PUB)]) + self.assertEqual(ret, 0, ALICE_IMPORT_FAIL) + # Import key revocation signature + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--import-sigs', data_path('test_key_validity/alice-rev.pgp')]) + self.assertEqual(ret, 0, 'Alice key rev import failed') + self.assertRegex(err, RE_SIG_1_IMPORT, 'Alice key rev import wrong output') + # Import direct-key signature + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--import', data_path('test_key_validity/alice-revoker-sig.pgp')]) + self.assertEqual(ret, 0, 'Alice direct-key sig import failed') + self.assertRegex(err, RE_SIG_1_IMPORT, 'Alice direct-key sig import wrong output') + # Try to import two signatures again + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--import', data_path('test_key_validity/alice-sigs.pgp')]) + self.assertEqual(ret, 0, 'Alice sigs reimport failed') + self.assertRegex(err, RE_SIG_2_UNCHANGED, 'Alice sigs file reimport wrong output') + # Import two signatures again via stdin + stext = file_text(data_path('test_key_validity/alice-sigs.asc')) + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--import', '-'], stext) + self.assertEqual(ret, 0, 'Alice sigs stdin reimport failed') + self.assertRegex(err, RE_SIG_2_UNCHANGED, 'Alice sigs stdin reimport wrong output') + # Import two signatures via env variable + os.environ["SIG_FILE"] = stext + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--import', 'env:SIG_FILE']) + self.assertEqual(ret, 0, 'Alice sigs env reimport failed') + self.assertRegex(err, RE_SIG_2_UNCHANGED, 'Alice sigs var reimport wrong output') + # Try to import malformed signatures + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--import', data_path('test_key_validity/alice-sigs-malf.pgp')]) + self.assertNotEqual(ret, 0, 'Alice malformed sigs import failed') + self.assertRegex(err, r'(?s)^.*Failed to import signatures from .*', 'Alice malformed sigs wrong output') + + def test_export_revocation(self): + clear_keyrings() + OUT_NO_REV = 'no-revocation.pgp' + OUT_ALICE_REV = 'alice-revocation.pgp' + # Import Alice's public key and be unable to export revocation + ret, _, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--import', data_path(KEY_ALICE_PUB)]) + self.assertEqual(ret, 0, ALICE_IMPORT_FAIL) + ret, out, err = run_proc(RNPK, ['--homedir', RNPDIR, '--export-rev', 'alice']) + self.assertNotEqual(ret, 0) + self.assertEqual(len(out), 0) + self.assertRegex(err, r'(?s)^.*Revoker secret key not found.*', 'Wrong pubkey revocation export output') + # Import Alice's secret key and subkey + ret, _, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--import', data_path(KEY_ALICE_SUB_SEC)]) + self.assertEqual(ret, 0, 'Alice secret key import failed') + # Attempt to export revocation without specifying key + pipe = pswd_pipe(PASSWORD) + ret, out, err = run_proc(RNPK, ['--homedir', RNPDIR, '--export-rev', '--pass-fd', str(pipe)]) + os.close(pipe) + self.assertNotEqual(ret, 0) + self.assertEqual(len(out), 0) + self.assertRegex(err, r'(?s)^.*You need to specify key to generate revocation for.*', 'Wrong no key revocation export output') + # Attempt to export revocation for unknown key + ret, out, err = run_proc(RNPK, ['--homedir', RNPDIR, '--export-rev', 'basil']) + self.assertNotEqual(ret, 0) + self.assertEqual(len(out), 0) + self.assertRegex(err, r'(?s)^.*Key matching \'basil\' not found.*', 'Wrong unknown key revocation export output') + # Attempt to export revocation for subkey + pipe = pswd_pipe(PASSWORD) + ret, out, err = run_proc(RNPK, ['--homedir', RNPDIR, '--export-rev', 'DD23CEB7FEBEFF17']) + os.close(pipe) + self.assertNotEqual(ret, 0) + self.assertEqual(len(out), 0) + self.assertRegex(err, r'(?s)^.*Key matching \'DD23CEB7FEBEFF17\' not found.*', 'Wrong subkey revocation export output') + # Attempt to export revocation with too broad search + ret, _, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--import', data_path('test_key_validity/basil-sec.asc')]) + self.assertEqual(ret, 0, 'Basil secret key import failed') + pipe = pswd_pipe(PASSWORD) + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--export-rev', 'rnp', '--pass-fd', str(pipe), + '--output', OUT_NO_REV, '--force']) + os.close(pipe) + self.assertNotEqual(ret, 0, 'Failed to fail to export revocation') + self.assertFalse(os.path.isfile(OUT_NO_REV), 'Failed to fail to export revocation') + self.assertRegex(err, r'(?s)^.*Ambiguous input: too many keys found for \'rnp\'.*', 'Wrong revocation export output') + # Finally successfully export revocation + pipe = pswd_pipe(PASSWORD) + ret, _, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--export-rev', '0451409669FFDE3C', '--pass-fd', str(pipe), + '--output', OUT_ALICE_REV, '--overwrite']) + os.close(pipe) + self.assertEqual(ret, 0) + self.assertTrue(os.path.isfile(OUT_ALICE_REV)) + with open(OUT_ALICE_REV, "rb") as armored: + self.assertRegex(armored.read().decode('utf-8'), r'-----END PGP PUBLIC KEY BLOCK-----\r\n$', 'Armor tail not found') + # Check revocation contents + ret, out, _ = run_proc(RNP, ['--homedir', RNPDIR, '--list-packets', OUT_ALICE_REV]) + self.assertEqual(ret, 0) + self.assertNotEqual(len(out), 0) + match = re.match(RE_RNP_REVOCATION_SIG, out) + self.assertTrue(match, 'Wrong revocation signature contents') + self.assertEqual(match.group(1).strip(), '0 (No reason)', 'Wrong revocation signature reason') + self.assertEqual(match.group(2).strip(), '', 'Wrong revocation signature message') + # Make sure it can be imported back + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--import-sigs', OUT_ALICE_REV]) + self.assertEqual(ret, 0, 'Failed to import revocation back') + self.assertRegex(err, RE_SIG_1_IMPORT, 'Revocation import wrong output') + # Make sure file will not be overwritten with --force parameter + with open(OUT_ALICE_REV, 'w+') as f: + f.truncate(10) + pipe = pswd_pipe(PASSWORD) + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--export-rev', '0451409669FFDE3C', '--pass-fd', str(pipe), '--output', OUT_ALICE_REV, '--force', '--notty'], '\n\n') + os.close(pipe) + self.assertNotEqual(ret, 0, 'Revocation was overwritten with --force') + self.assertEqual(10, os.stat(OUT_ALICE_REV).st_size, 'Revocation was overwritten with --force') + # Make sure file will not be overwritten without --overwrite parameter + pipe = pswd_pipe(PASSWORD) + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--export-rev', '0451409669FFDE3C', '--pass-fd', str(pipe), '--output', OUT_ALICE_REV, '--notty'], '\n\n') + os.close(pipe) + self.assertNotEqual(ret, 0, 'Revocation was overwritten without --overwrite and --force') + self.assertTrue(os.path.isfile(OUT_ALICE_REV), 'Revocation was overwritten without --overwrite') + self.assertEqual(10, os.stat(OUT_ALICE_REV).st_size, 'Revocation was overwritten without --overwrite') + # Make sure file will be overwritten with --overwrite parameter + pipe = pswd_pipe(PASSWORD) + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--export-rev', '0451409669FFDE3C', '--pass-fd', str(pipe), '--output', OUT_ALICE_REV, '--overwrite']) + os.close(pipe) + self.assertEqual(ret, 0) + self.assertGreater(os.stat(OUT_ALICE_REV).st_size, 10) + # Create revocation with wrong code - 'no longer valid' (which is usable only for userid) + pipe = pswd_pipe(PASSWORD) + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--export-rev', 'alice', '--rev-type', 'no longer valid', + '--pass-fd', str(pipe), '--output', OUT_NO_REV, '--force']) + os.close(pipe) + self.assertNotEqual(ret, 0, 'Failed to use wrong revocation reason') + self.assertFalse(os.path.isfile(OUT_NO_REV)) + self.assertRegex(err, r'(?s)^.*Wrong key revocation code: 32.*', 'Wrong revocation export output') + # Create revocation without rev-code parameter + pipe = pswd_pipe(PASSWORD) + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--export-rev', 'alice', '--pass-fd', str(pipe), + '--output', OUT_NO_REV, '--force', '--rev-type']) + os.close(pipe) + self.assertNotEqual(ret, 0, 'Failed to use rev-type without parameter') + self.assertFalse(os.path.isfile(OUT_NO_REV), 'Failed to use rev-type without parameter') + # Create another revocation with custom code/reason + revcodes = {"0" : "0 (No reason)", "1" : "1 (Superseded)", "2" : "2 (Compromised)", + "3" : "3 (Retired)", "no" : "0 (No reason)", "superseded" : "1 (Superseded)", + "compromised" : "2 (Compromised)", "retired" : "3 (Retired)"} + for revcode in revcodes: + revreason = 'Custom reason: ' + revcode + pipe = pswd_pipe(PASSWORD) + ret, _, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--export-rev', '0451409669FFDE3C', '--pass-fd', str(pipe), + '--output', OUT_ALICE_REV, '--overwrite', '--rev-type', revcode, '--rev-reason', revreason]) + os.close(pipe) + self.assertEqual(ret, 0, 'Failed to export revocation with code ' + revcode) + self.assertTrue(os.path.isfile(OUT_ALICE_REV), 'Failed to export revocation with code ' + revcode) + # Check revocation contents + with open(OUT_ALICE_REV, "rb") as armored: + self.assertRegex(armored.read().decode('utf-8'), r'-----END PGP PUBLIC KEY BLOCK-----\r\n$', 'Armor tail not found') + ret, out, _ = run_proc(RNP, ['--homedir', RNPDIR, '--list-packets', OUT_ALICE_REV]) + self.assertEqual(ret, 0, 'Failed to list exported revocation packets') + self.assertNotEqual(len(out), 0, 'Failed to list exported revocation packets') + match = re.match(RE_RNP_REVOCATION_SIG, out) + self.assertTrue(match) + self.assertEqual(match.group(1).strip(), revcodes[revcode], 'Wrong revocation signature revcode') + self.assertEqual(match.group(2).strip(), revreason, 'Wrong revocation signature reason') + # Make sure it is also imported back + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--import-sigs', OUT_ALICE_REV]) + self.assertEqual(ret, 0) + self.assertRegex(err, RE_SIG_1_IMPORT, 'Revocation import wrong output') + # Now let's import it with GnuPG + gpg_import_pubring(data_path(KEY_ALICE_PUB)) + ret, _, err = run_proc(GPG, ['--batch', '--homedir', GPGHOME, '--import', OUT_ALICE_REV]) + self.assertEqual(ret, 0, 'gpg signature revocation import failed') + self.assertRegex(err, RE_GPG_REVOCATION_IMPORT, 'Wrong gpg revocation import output') + + os.remove(OUT_ALICE_REV) + clear_keyrings() + + def test_import_keys(self): + clear_keyrings() + # try to import non-existing file + ret, out, err = run_proc(RNPK, ['--homedir', RNPDIR, '--import-key', data_path('thiskeyfiledoesnotexist')]) + self.assertEqual(ret, 1) + self.assertRegex(err, r'(?s)^.*Failed to create input for .*thiskeyfiledoesnotexist.*') + # try malformed file + ret, out, err = run_proc(RNPK, ['--homedir', RNPDIR, '--import-key', data_path('test_key_validity/alice-sigs-malf.pgp')]) + self.assertEqual(ret, 1) + self.assertRegex(err, r'(?s)^.*failed to import key\(s\) from .*test_key_validity/alice-sigs-malf.pgp, stopping\..*') + self.assertRegex(err, r'(?s)^.*Import finished: 0 keys processed, 0 new public keys, 0 new secret keys, 0 updated, 0 unchanged\..*') + # try --import + ret, out, err = run_proc(RNPK, ['--homedir', RNPDIR, '--import', data_path(KEY_ALICE_SUB_PUB)]) + self.assertEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*Import finished: 2 keys processed, 2 new public keys, 0 new secret keys, 0 updated, 0 unchanged\..*') + ret, out, err = run_proc(RNPK, ['--homedir', RNPDIR, '--import', data_path(KEY_ALICE_SUB_PUB)]) + self.assertEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*Import finished: 2 keys processed, 0 new public keys, 0 new secret keys, 0 updated, 2 unchanged\..*') + ret, out, err = run_proc(RNPK, ['--homedir', RNPDIR, '--import', data_path('test_stream_key_merge/key-both.asc')]) + self.assertEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*Import finished: 6 keys processed, 3 new public keys, 3 new secret keys, 0 updated, 0 unchanged\..*') + ret, out, err = run_proc(RNPK, ['--homedir', RNPDIR, '--import', data_path('test_stream_key_merge/key-both.asc')]) + self.assertEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*Import finished: 6 keys processed, 0 new public keys, 0 new secret keys, 0 updated, 6 unchanged\..*') + ret, out, err = run_proc(RNPK, ['--homedir', RNPDIR, '--import', data_path('test_key_validity/alice-sign-sub-exp-pub.asc')]) + self.assertEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*Import finished: 2 keys processed, 1 new public keys, 0 new secret keys, 1 updated, 0 unchanged\..*') + clear_keyrings() + # try --import-key + ret, out, err = run_proc(RNPK, ['--homedir', RNPDIR, '--import-key', data_path(KEY_ALICE_SUB_PUB)]) + self.assertEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*Import finished: 2 keys processed, 2 new public keys, 0 new secret keys, 0 updated, 0 unchanged\..*') + ret, out, err = run_proc(RNPK, ['--homedir', RNPDIR, '--import-key', data_path(KEY_ALICE_SUB_PUB)]) + self.assertEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*Import finished: 2 keys processed, 0 new public keys, 0 new secret keys, 0 updated, 2 unchanged\..*') + ret, out, err = run_proc(RNPK, ['--homedir', RNPDIR, '--import-key', data_path('test_stream_key_merge/key-both.asc')]) + self.assertEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*Import finished: 6 keys processed, 3 new public keys, 3 new secret keys, 0 updated, 0 unchanged\..*') + ret, out, err = run_proc(RNPK, ['--homedir', RNPDIR, '--import-key', data_path('test_stream_key_merge/key-both.asc')]) + self.assertEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*Import finished: 6 keys processed, 0 new public keys, 0 new secret keys, 0 updated, 6 unchanged\..*') + ret, out, err = run_proc(RNPK, ['--homedir', RNPDIR, '--import-key', data_path('test_key_validity/alice-sign-sub-exp-pub.asc')]) + self.assertEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*Import finished: 2 keys processed, 1 new public keys, 0 new secret keys, 1 updated, 0 unchanged\..*') + clear_keyrings() + + def test_export_keys(self): + PUB_KEY = r'(?s)^.*' \ + r'-----BEGIN PGP PUBLIC KEY BLOCK-----.*' \ + r'-----END PGP PUBLIC KEY BLOCK-----.*$' + PUB_KEY_PKTS = r'(?s)^.*' \ + r'Public key packet.*' \ + r'keyid: 0x0451409669ffde3c.*' \ + r'Public subkey packet.*' \ + r'keyid: 0xdd23ceb7febeff17.*$' + SEC_KEY = r'(?s)^.*' \ + r'-----BEGIN PGP PRIVATE KEY BLOCK-----.*' \ + r'-----END PGP PRIVATE KEY BLOCK-----.*$' + SEC_KEY_PKTS = r'(?s)^.*' \ + r'Secret key packet.*' \ + r'keyid: 0x0451409669ffde3c.*' \ + r'Secret subkey packet.*' \ + r'keyid: 0xdd23ceb7febeff17.*$' + KEY_OVERWRITE = r'(?s)^.*' \ + r'File \'.*alice-key.pub.asc\' already exists.*' \ + r'Would you like to overwrite it\? \(y/N\).*' \ + r'Please enter the new filename:.*$' + + clear_keyrings() + # Import Alice's public key + ret, _, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--import', data_path(KEY_ALICE_SUB_PUB)]) + self.assertEqual(ret, 0) + # Attempt to export no key + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--export-key']) + self.assertNotEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*No key specified\.$') + # Attempt to export wrong key + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--export-key', 'boris']) + self.assertNotEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*Key\(s\) matching \'boris\' not found\.$') + # Export it to the stdout + ret, out, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--export-key', 'alice']) + self.assertEqual(ret, 0) + self.assertRegex(out, PUB_KEY) + ret, out, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--export-key', 'alice', '--output', '-']) + self.assertEqual(ret, 0) + self.assertRegex(out, PUB_KEY) + # Export key via --userid parameter + ret, out, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--export-key', '--userid', 'alice']) + self.assertEqual(ret, 0) + self.assertRegex(out, PUB_KEY) + # Export with empty --userid parameter + ret, out, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--export-key', '--userid']) + self.assertNotEqual(ret, 0) + # Export it to the file + kpub, ksec, kren = reg_workfiles('alice-key', '.pub.asc', '.sec.asc', '.pub.ren-asc') + ret, _, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--export-key', 'alice', '--output', kpub]) + self.assertEqual(ret, 0) + self.assertRegex(file_text(kpub), PUB_KEY) + # Try to export again to the same file without additional parameters + ret, out, err = run_proc(RNPK, ['--homedir', RNPDIR, '--export-key', 'alice', '--output', kpub, '--notty'], '\n\n') + self.assertNotEqual(ret, 0) + self.assertRegex(out, KEY_OVERWRITE) + self.assertRegex(err, r'(?s)^.*Operation failed: file \'.*alice-key.pub.asc\' already exists.*$') + # Try to export with --force parameter + ret, out, err = run_proc(RNPK, ['--homedir', RNPDIR, '--export-key', 'alice', '--output', kpub, '--force', '--notty'], '\n\n') + self.assertNotEqual(ret, 0) + self.assertRegex(out, KEY_OVERWRITE) + self.assertRegex(err, r'(?s)^.*Operation failed: file \'.*alice-key.pub.asc\' already exists.*$') + # Export with --overwrite parameter + with open(kpub, 'w+') as f: + f.truncate(10) + ret, out, err = run_proc(RNPK, ['--homedir', RNPDIR, '--export-key', 'alice', '--output', kpub, '--overwrite']) + self.assertEqual(ret, 0) + # Re-import it, making sure file was correctly overwritten + ret, _, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--import', kpub]) + self.assertEqual(ret, 0) + # Enter 'y' in ovewrite prompt + with open(kpub, 'w+') as f: + f.truncate(10) + ret, out, err = run_proc(RNPK, ['--homedir', RNPDIR, '--export-key', 'alice', '--output', kpub, '--notty'], 'y\n') + self.assertEqual(ret, 0) + ret, _, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--import', kpub]) + self.assertEqual(ret, 0) + # Enter new filename in overwrite prompt + with open(kpub, 'w+') as f: + f.truncate(10) + ret, out, err = run_proc(RNPK, ['--homedir', RNPDIR, '--export-key', 'alice', '--output', kpub, '--notty'], 'n\n' + kren + '\n') + self.assertEqual(ret, 0) + self.assertEqual(os.path.getsize(kpub), 10) + ret, _, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--import', kren]) + self.assertEqual(ret, 0) + # Attempt to export secret key + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--export-key', '--secret', 'alice']) + self.assertNotEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*Key\(s\) matching \'alice\' not found\.$') + # Import Alice's secret key and subkey + ret, _, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--import', data_path(KEY_ALICE_SUB_SEC)]) + self.assertEqual(ret, 0) + # Make sure secret key is not exported when public is requested + ret, _, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--export-key', 'alice', '--output', ksec]) + self.assertEqual(ret, 0) + self.assertRegex(file_text(ksec), PUB_KEY) + ret, out, _ = run_proc(RNP, ['--list-packets', ksec]) + self.assertEqual(ret, 0) + self.assertRegex(out, PUB_KEY_PKTS) + # Make sure secret key is correctly exported + ret, _, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--export-key', '--secret', 'alice', '--output', ksec, '--overwrite']) + self.assertEqual(ret, 0) + self.assertRegex(file_text(ksec), SEC_KEY) + ret, out, _ = run_proc(RNP, ['--list-packets', ksec]) + self.assertEqual(ret, 0) + self.assertRegex(out, SEC_KEY_PKTS) + clear_keyrings() + + def test_userid_escape(self): + clear_keyrings() + tracker_beginning = 'tracker' + tracker_end = '@rnp' + tracker_1 = tracker_beginning + ''.join(map(chr, range(1,0x10))) + tracker_end + tracker_2 = tracker_beginning + ''.join(map(chr, range(0x10,0x20))) + tracker_end + #Run key generation + rnp_genkey_rsa(tracker_1, 1024) + rnp_genkey_rsa(tracker_2, 1024) + #Read with rnpkeys + ret, out_rnp, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--list-keys']) + self.assertEqual(ret, 0, 'rnpkeys : failed to read keystore') + #Read with GPG + ret, out_gpg, _ = run_proc(GPG, ['--homedir', path_for_gpg(RNPDIR), '--list-keys']) + self.assertEqual(ret, 0, 'gpg : failed to read keystore') + tracker_rnp = re.findall(r'' + tracker_beginning + '.*' + tracker_end + '', out_rnp) + tracker_gpg = re.findall(r'' + tracker_beginning + '.*' + tracker_end + '', out_gpg) + self.assertEqual(len(tracker_rnp), 2, 'failed to find expected rnp userids') + self.assertEqual(len(tracker_gpg), 2, 'failed to find expected gpg userids') + self.assertEqual(tracker_rnp, tracker_gpg, 'userids from rnpkeys and gpg don\'t match') + clear_keyrings() + + def test_key_revoke(self): + clear_keyrings() + # Import Alice's public key and be unable to revoke + ret, _, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--import', data_path(KEY_ALICE_PUB)]) + self.assertEqual(ret, 0, ALICE_IMPORT_FAIL) + ret, out, err = run_proc(RNPK, ['--homedir', RNPDIR, '--revoke-key', 'alice']) + self.assertNotEqual(ret, 0) + self.assertEqual(len(out), 0) + self.assertRegex(err, r'(?s)^.*Revoker secret key not found.*Failed to revoke a key.*') + # Import Alice's secret key and subkey + ret, _, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--import', data_path(KEY_ALICE_SUB_SEC)]) + self.assertEqual(ret, 0) + # Attempt to revoke without specifying a key + pipe = pswd_pipe(PASSWORD) + ret, out, err = run_proc(RNPK, ['--homedir', RNPDIR, '--revoke', '--pass-fd', str(pipe)]) + os.close(pipe) + self.assertNotEqual(ret, 0) + self.assertEqual(len(out), 0) + self.assertRegex(err, r'(?s)^.*You need to specify key or subkey to revoke.*') + # Attempt to revoke unknown key + ret, out, err = run_proc(RNPK, ['--homedir', RNPDIR, '--revoke', 'basil']) + self.assertNotEqual(ret, 0) + self.assertEqual(len(out), 0) + self.assertRegex(err, r'(?s)^.*Key matching \'basil\' not found.*') + # Attempt to revoke with too broad search + ret, _, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--import', data_path('test_key_validity/basil-sec.asc')]) + self.assertEqual(ret, 0) + pipe = pswd_pipe(PASSWORD) + ret, out, err = run_proc(RNPK, ['--homedir', RNPDIR, '--revoke', 'rnp', '--pass-fd', str(pipe)]) + os.close(pipe) + self.assertRegex(err, r'(?s)^.*Ambiguous input: too many keys found for \'rnp\'.*') + # Revoke a primary key + pipe = pswd_pipe(PASSWORD) + ret, out, err = run_proc(RNPK, ['--homedir', RNPDIR, '--revoke', '0451409669FFDE3C', '--pass-fd', str(pipe)]) + os.close(pipe) + self.assertEqual(ret, 0) + ret, out, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--list-keys']) + self.assertEqual(ret, 0) + self.assertRegex(out, r'(?s)^.*pub.*0451409669ffde3c.*\[REVOKED\].*73edcc9119afc8e2dbbdcde50451409669ffde3c.*') + # Try again without the '--force' parameter + pipe = pswd_pipe(PASSWORD) + ret, out, err = run_proc(RNPK, ['--homedir', RNPDIR, '--revoke', '0451409669FFDE3C', '--pass-fd', str(pipe)]) + os.close(pipe) + self.assertNotEqual(ret, 0) + self.assertEqual(len(out), 0) + self.assertRegex(err, r'(?s)^.*Error: key \'0451409669FFDE3C\' is revoked already. Use --force to generate another revocation signature.*') + # Try again with --force parameter + pipe = pswd_pipe(PASSWORD) + ret, _, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--revoke', '0451409669FFDE3C', '--pass-fd', str(pipe), "--force", "--rev-type", "3", "--rev-reason", "Custom"]) + os.close(pipe) + self.assertEqual(ret, 0) + ret, out, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--list-keys']) + self.assertEqual(ret, 0) + self.assertRegex(out, r'(?s)^.*pub.*0451409669ffde3c.*\[REVOKED\].*73edcc9119afc8e2dbbdcde50451409669ffde3c.*') + # Revoke a subkey + pipe = pswd_pipe(PASSWORD) + ret, _, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--revoke', 'DD23CEB7FEBEFF17', '--pass-fd', str(pipe)]) + os.close(pipe) + self.assertEqual(ret, 0) + ret, out, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--list-keys']) + self.assertEqual(ret, 0) + self.assertRegex(out, r'(?s)^.*sub.*dd23ceb7febeff17.*\[REVOKED\].*a4bbb77370217bca2307ad0ddd23ceb7febeff17.*') + # Try again without the '--force' parameter + pipe = pswd_pipe(PASSWORD) + ret, out, err = run_proc(RNPK, ['--homedir', RNPDIR, '--revoke', 'DD23CEB7FEBEFF17', '--pass-fd', str(pipe)]) + os.close(pipe) + self.assertNotEqual(ret, 0) + self.assertEqual(len(out), 0) + self.assertRegex(err, r'(?s)^.*Error: key \'DD23CEB7FEBEFF17\' is revoked already. Use --force to generate another revocation signature.*', err) + # Try again with --force parameter + pipe = pswd_pipe(PASSWORD) + ret, _, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--revoke', 'DD23CEB7FEBEFF17', '--pass-fd', str(pipe), "--force", "--rev-type", "2", "--rev-reason", "Other"]) + os.close(pipe) + self.assertEqual(ret, 0) + ret, out, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--list-keys']) + self.assertEqual(ret, 0) + self.assertRegex(out, r'(?s)^.*sub.*dd23ceb7febeff17.*\[REVOKED\].*a4bbb77370217bca2307ad0ddd23ceb7febeff17.*') + + def _test_userid_genkey(self, userid_beginning, weird_part, userid_end, weird_part2=''): + clear_keyrings() + USERS = [userid_beginning + weird_part + userid_end] + if weird_part2: + USERS.append(userid_beginning + weird_part2 + userid_end) + # Run key generation + for userid in USERS: + rnp_genkey_rsa(userid, 1024) + # Read with GPG + ret, out, err = run_proc(GPG, ['--homedir', path_for_gpg(RNPDIR), '--list-keys', '--charset', CONSOLE_ENCODING]) + self.assertEqual(ret, 0, 'gpg : failed to read keystore') + tracker_escaped = re.findall(r'' + userid_beginning + '.*' + userid_end + '', out) + tracker_gpg = list(map(decode_string_escape, tracker_escaped)) + self.assertEqual(tracker_gpg, USERS, 'gpg : failed to find expected userids from keystore') + # Read with rnpkeys + ret, out, err = run_proc(RNPK, ['--homedir', RNPDIR, '--list-keys']) + self.assertEqual(ret, 0, 'rnpkeys : failed to read keystore') + tracker_escaped = re.findall(r'' + userid_beginning + '.*' + userid_end + '', out) + tracker_rnp = list(map(decode_string_escape, tracker_escaped)) + self.assertEqual(tracker_rnp, USERS, 'rnpkeys : failed to find expected userids from keystore') + clear_keyrings() + + def test_userid_unicode_genkeys(self): + self._test_userid_genkey('track', WEIRD_USERID_UNICODE_1, 'end', WEIRD_USERID_UNICODE_2) + + def test_userid_special_chars_genkeys(self): + self._test_userid_genkey('track', WEIRD_USERID_SPECIAL_CHARS, 'end') + self._test_userid_genkey('track', WEIRD_USERID_SPACE, 'end') + self._test_userid_genkey('track', WEIRD_USERID_QUOTE, 'end') + self._test_userid_genkey('track', WEIRD_USERID_SPACE_AND_QUOTE, 'end') + + def test_userid_too_long_genkeys(self): + clear_keyrings() + userid = WEIRD_USERID_TOO_LONG + # Open pipe for password + pipe = pswd_pipe(PASSWORD) + # Run key generation + ret, _, _ = run_proc(RNPK, ['--gen-key', '--userid', userid, + '--homedir', RNPDIR, '--pass-fd', str(pipe)]) + os.close(pipe) + self.assertNotEqual(ret, 0, 'should have failed on too long id') + + def test_key_remove(self): + if RNP_CAST5: + MSG_KEYS_NOT_FOUND = r'Key\(s\) not found\.' + clear_keyrings() + # Import public keyring + ret, _, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--import', data_path(PUBRING_1)]) + self.assertEqual(ret, 0) + # Remove without parameters + ret, out, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--remove-key']) + self.assertNotEqual(ret, 0) + # Remove all imported public keys with subkeys + ret, _, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--remove-key', '7bc6709b15c23a4a', '2fcadf05ffa501bb']) + self.assertEqual(ret, 0) + # Check that keyring is empty + ret, out, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--list-keys']) + self.assertNotEqual(ret, 0) + self.assertRegex(out, MSG_KEYS_NOT_FOUND, 'Invalid no-keys output') + # Import secret keyring + ret, _, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--import', data_path('keyrings/1/secring.gpg')]) + self.assertEqual(ret, 0, 'Secret keyring import failed') + # Remove all secret keys with subkeys + ret, _, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--remove-key', '7bc6709b15c23a4a', '2fcadf05ffa501bb', '--force']) + self.assertEqual(ret, 0, 'Failed to remove 2 secret keys') + # Check that keyring is empty + ret, out, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--list-keys']) + self.assertNotEqual(ret, 0) + self.assertRegex(out, MSG_KEYS_NOT_FOUND, 'Failed to remove secret keys') + # Import public keyring + ret, _, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--import', data_path(PUBRING_1)]) + self.assertEqual(ret, 0, 'Public keyring import failed') + # Remove all subkeys + ret, out, err = run_proc(RNPK, ['--homedir', RNPDIR, '--remove-key', + '326ef111425d14a5', '54505a936a4a970e', '8a05b89fad5aded1', '1d7e8a5393c997a8', '1ed63ee56fadc34d']) + self.assertEqual(ret, 0, 'Failed to remove 5 keys') + # Check that subkeys are removed + ret, out, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--list-keys']) + self.assertEqual(ret, 0) + self.assertRegex(out, r'2 keys found', 'Failed to remove subkeys') + self.assertFalse(re.search('326ef111425d14a5|54505a936a4a970e|8a05b89fad5aded1|1d7e8a5393c997a8|1ed63ee56fadc34d', out)) + # Remove remaining public keys + ret, _, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--remove-key', '7bc6709b15c23a4a', '2fcadf05ffa501bb']) + self.assertEqual(ret, 0, 'Failed to remove public keys') + # Try to remove again + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--remove-key', '7bc6709b15c23a4a']) + self.assertNotEqual(ret, 0) + self.assertRegex(err, r'Key matching \'7bc6709b15c23a4a\' not found\.', 'Unexpected result') + # Check that keyring is empty + ret, out, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--list-keys']) + self.assertRegex(out, MSG_KEYS_NOT_FOUND, 'Failed to list empty keyring') + # Import public keyring + ret, _, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--import', data_path(PUBRING_1)]) + self.assertEqual(ret, 0, 'Public keyring import failed') + # Try to remove by uid substring, should match multiple keys and refuse to remove + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--remove-key', 'uid0']) + self.assertNotEqual(ret, 0) + self.assertRegex(err, r'Ambiguous input: too many keys found for \'uid0\'\.', 'Unexpected result') + # Remove keys by uids + ret, _, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--remove-key', 'key0-uid0', 'key1-uid1']) + self.assertEqual(ret, 0, 'Failed to remove keys') + # Check that keyring is empty + ret, out, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--list-keys']) + self.assertNotEqual(ret, 0) + self.assertRegex(out, MSG_KEYS_NOT_FOUND, 'Failed to remove keys') + + def test_additional_subkeys_default(self): + ''' + Generate default key (primary + sub) then add more subkeys. + ''' + # Open pipe for password + pipe = pswd_pipe(PASSWORD) + # Run key generation + ret, _, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--pass-fd', str(pipe), + '--userid', 'primary_for_many_subs@rnp', '--generate-key']) + os.close(pipe) + self.assertEqual(ret, 0, KEY_GEN_FAILED) + # Edit generated key, generate & add one more subkey with default parameters + pipe = pswd_pipe(PASSWORD) + ret, _, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--pass-fd', str(pipe), + '--edit-key', '--add-subkey', 'primary_for_many_subs@rnp']) + self.assertEqual(ret, 0, 'Failed to add new subkey') + # list keys, check result + ret, out, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--list-keys']) + self.assertEqual(ret, 0, KEY_LIST_FAILED) + self.assertRegex(out, RE_MULTIPLE_SUBKEY_3, KEY_LIST_WRONG) + clear_keyrings() + + def test_additional_subkeys_invalid_parameters(self): + # Run primary key generation + ret, _, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--password', PASSWORD, + '--userid', 'primary_for_many_subs@rnp', '--generate-key']) + self.assertEqual(ret, 0, KEY_GEN_FAILED) + # Attempt to generate subkey for non-existing key + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--password', PASSWORD, + '--edit-key', '--add-subkey', 'unknown']) + self.assertEqual(ret, 1) + self.assertRegex(err, r'Secret keys matching \'unknown\' not found.') + # Attempt to generate subkey using the invalid password + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--password', 'wrong', + '--edit-key', '--add-subkey', 'primary_for_many_subs@rnp']) + self.assertEqual(ret, 1) + self.assertRegex(err, r'(?s)Failed to unlock primary key.*Subkey generation failed') + # Attempt to generate subkey using the invalid password, asked via tty + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--notty', '--edit-key', + '--add-subkey', 'primary_for_many_subs@rnp'], 'password2\n') + self.assertEqual(ret, 1) + self.assertRegex(err, r'(?s)Failed to unlock primary key.*Subkey generation failed') + # Attempt to generate ECDH subkey with invalid curve + ret, out, err = run_proc(RNPK, ['--homedir', RNPDIR, '--password', PASSWORD, '--edit-key', '--add-subkey', + 'primary_for_many_subs@rnp', '--expert'], + '\n\n0\n101\n18\n-10\n0\n0\n0\n0\n0\n0\n0\n0\n0\n0\n0\n') + self.assertEqual(ret, 1) + self.assertRegex(out, r'(?s)Too many attempts. Aborting.') + self.assertRegex(err, r'(?s)Subkey generation setup failed') + # Attempt to generate ECDSA subkey with invalid curve + ret, out, err = run_proc(RNPK, ['--homedir', RNPDIR, '--password', PASSWORD, '--edit-key', '--add-subkey', + 'primary_for_many_subs@rnp', '--expert'], + '19\n-10\n0\n0\n0\n0\n0\n0\n0\n0\n0\n0\n0\n') + self.assertEqual(ret, 1) + self.assertRegex(out, r'(?s)Too many attempts. Aborting.') + self.assertRegex(err, r'(?s)Subkey generation setup failed') + # Pass invalid numbits + ret, _, err = run_proc(RNPK, ['--numbits', 'wrong', '--homedir', RNPDIR, '--password', PASSWORD, + '--userid', 'wrong', '--edit-key', '--add-subkey', 'primary_for_many_subs@rnp']) + self.assertNotEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*wrong bits value: wrong.*') + # Too small + ret, _, err = run_proc(RNPK, ['--numbits', '768', '--homedir', RNPDIR, '--password', PASSWORD, + '--userid', '768', '--edit-key', '--add-subkey', 'primary_for_many_subs@rnp']) + self.assertNotEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*wrong bits value: 768.*') + # ElGamal too large and wrong numbits + ret, out, err = run_proc(RNPK, ['--homedir', RNPDIR, '--password', 'wrong', '--edit-key', '--add-subkey', 'primary_for_many_subs@rnp', + '--expert'], '16\n2048zzz\n99999999999999999999999999\n2048\n') + self.assertRegex(err, r'(?s)Unexpected end of line.*Number out of range.*') + self.assertEqual(ret, 1) + # Wrong hash algorithm + ret, _, err = run_proc(RNPK, ['--hash', 'BAD_HASH', '--homedir', RNPDIR, '--password', PASSWORD, + '--userid', 'bad_hash', '--edit-key', '--add-subkey', 'primary_for_many_subs@rnp']) + self.assertNotEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*Unsupported hash algorithm: BAD_HASH.*') + # Wrong S2K iterations + ret, _, err = run_proc(RNPK, ['--s2k-iterations', 'WRONG_ITER', '--homedir', RNPDIR, '--password', PASSWORD, + '--userid', 'wrong_iter', '--edit-key', '--add-subkey', 'primary_for_many_subs@rnp']) + self.assertNotEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*Wrong iterations value: WRONG_ITER.*') + # Wrong S2K msec + ret, _, err = run_proc(RNPK, ['--s2k-msec', 'WRONG_MSEC', '--homedir', RNPDIR, '--password', PASSWORD, + '--userid', 'wrong_msec', '--edit-key', '--add-subkey', 'primary_for_many_subs@rnp']) + self.assertNotEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*Invalid s2k msec value: WRONG_MSEC.*') + # Wrong cipher + ret, _, err = run_proc(RNPK, ['--cipher', 'WRONG_AES', '--homedir', RNPDIR, '--password', PASSWORD, + '--userid', 'wrong_aes', '--edit-key', '--add-subkey', 'primary_for_many_subs@rnp']) + self.assertNotEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*Unsupported encryption algorithm: WRONG_AES.*Failed to process argument --cipher.*') + # Ambiguous primary key + ret, _, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--password', PASSWORD, + '--userid', 'primary_for_many_subs2@rnp', '--generate-key']) + self.assertEqual(ret, 0, KEY_GEN_FAILED) + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--password', PASSWORD, + '--edit-key', '--add-subkey', 'primary_for_many']) + self.assertEqual(ret, 1) + self.assertRegex(err, r'Ambiguous input: too many keys found for \'primary_for_many\'') + + clear_keyrings() + + def test_additional_subkeys_expert_mode(self): + # Run primary key generation + ret, _, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--password=', + '--userid', 'primary_for_many_subs@rnp', '--generate-key']) + # RSA subkey + ret, out, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--password=', '--edit-key', '--add-subkey', 'primary_for_many_subs@rnp', + '--expert'], '\n\n0\n101\n1\n1023\n4097\n3072\n') + self.assertEqual(ret, 0, KEY_GEN_FAILED) + # ElGamal subkey + ret, out, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--password=', '--edit-key', '--add-subkey', 'primary_for_many_subs@rnp', + '--expert'], '\n\n0\n101\n16\n1023\n4097\n1025\n') + self.assertEqual(ret, 0, KEY_GEN_FAILED) + # DSA subkey + ret, out, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--password=', '--edit-key', '--add-subkey', 'primary_for_many_subs@rnp', + '--expert'], '\n\n0\n101\n17\n1023\n3073\n1025\n') + self.assertEqual(ret, 0, KEY_GEN_FAILED) + # ECDH subkey + ret, out, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--password=', '--edit-key', '--add-subkey', 'primary_for_many_subs@rnp', + '--expert'], '\n\n0\n101\n18\n0\n8\n1\n') + self.assertEqual(ret, 0, KEY_GEN_FAILED) + # ECDSA subkey + ret, out, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--password=', '--edit-key', '--add-subkey', 'primary_for_many_subs@rnp', + '--expert'], '\n\n0\n101\n19\n0\n8\n1\n') + self.assertEqual(ret, 0, KEY_GEN_FAILED) + # EDDSA subkey + ret, out, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--password=', '--edit-key', '--add-subkey', 'primary_for_many_subs@rnp', + '--expert'], '\n\n0\n101\n22\n') + self.assertEqual(ret, 0, KEY_GEN_FAILED) + # list keys, check result + ret, out, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--list-keys']) + self.assertEqual(ret, 0, KEY_LIST_FAILED) + self.assertRegex(out, RE_MULTIPLE_SUBKEY_8, KEY_LIST_WRONG) + + clear_keyrings() + + def test_additional_subkeys_reuse_password(self): + pipe = pswd_pipe('primarypassword') + # Primary key with password + ret, _, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--pass-fd', str(pipe), + '--userid', 'primary_for_many_subs@rnp', '--generate-key']) + os.close(pipe) + self.assertEqual(ret, 0, KEY_GEN_FAILED) + # Provide password to add subkey, reuse password for subkey, say "yes" + stdinstr = 'primarypassword\ny\n' + ret, out, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--notty', '--edit-key', '--add-subkey', 'primary_for_many_subs@rnp'], + stdinstr) + self.assertEqual(ret, 0, 'Failed to add new subkey') + self.assertRegex(out, r'Would you like to use the same password to protect subkey') + # Do not reuse same password for subkey, say "no" + stdinstr = 'primarypassword\nN\nsubkeypassword\nsubkeypassword\n' + ret, _, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--notty', '--edit-key', '--add-subkey', 'primary_for_many_subs@rnp'], + stdinstr) + self.assertEqual(ret, 0) + # Primary key with empty password + ret, _, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--password=', + '--userid', 'primary_with_empty_password@rnp', '--generate-key']) + self.assertEqual(ret, 0, KEY_GEN_FAILED) + # Set empty password for generated subkey + stdinstr = '\n\ny\n' + ret, _, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--notty', '--edit-key', '--add-subkey', 'primary_with_empty_password@rnp'], + stdinstr) + self.assertEqual(ret, 0) + # Set password for generated subkey + stdinstr = 'subkeypassword\nsubkeypassword\n' + ret, _, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--notty', '--edit-key', '--add-subkey', 'primary_with_empty_password@rnp'], + stdinstr) + self.assertEqual(ret, 0) + clear_keyrings() + + def test_edit_key_single_option(self): + ret, _, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--import', data_path(KEY_25519_NOTWEAK_SEC)]) + self.assertEqual(ret, 0) + # Try to pass multiple --edit-key sub-options at once + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--edit-key', '--check-cv25519-bits', '--fix-cv25519-bits', + '--add-subkey', '--set-expire', '0', '3176fc1486aa2528']) + self.assertNotEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*Only one key edit option can be executed at a time..*$') + clear_keyrings() + + def test_set_expire(self): + kpath = os.path.join(RNPDIR, PUBRING) + # Primary key with empty password + ret, _, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--password=', + '--userid', 'primary_with_empty_password@rnp', '--generate-key']) + self.assertEqual(ret, 0, KEY_GEN_FAILED) + + # Wrong expiration argument + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--edit-key', '--set-expire', '-1', 'primary_with_empty_password@rnp']) + self.assertNotEqual(ret, 0) + self.assertRegex(err, r'Failed to set key expiration.') + + ret, out, _ = run_proc(RNP, ['--list-packets', kpath]) + self.assertEqual(ret, 0) + matches = re.findall(r'(key expiration time: 63072000 seconds \(730 days\))', out) + self.assertEqual(len(matches), 2) + + # Non-existing key argument + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--edit-key', '--set-expire', '0', 'wrongkey']) + self.assertNotEqual(ret, 0) + self.assertRegex(err, r'Secret keys matching \'wrongkey\' not found.') + + # Remove expiration date + ret, out, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--edit-key', '--set-expire', '0', 'primary_with_empty_password@rnp']) + self.assertEqual(ret, 0) + self.assertNotRegex(out, r'(?s)^.*\[EXPIRES .*', 'Failed to remove expiration!') + + ret, out, _ = run_proc(RNP, ['--list-packets', kpath]) + self.assertEqual(ret, 0) + matches = re.findall(r'(key expiration time: 63072000 seconds \(730 days\))', out) + self.assertEqual(len(matches), 1) + + # Expires in 10 seconds + ret, out, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--edit-key', '--set-expire', '10', 'primary_with_empty_password@rnp']) + self.assertEqual(ret, 0) + self.assertRegex(out, r'(?s)^.*\[EXPIRES .*') + + ret, out, _ = run_proc(RNP, ['--list-packets', kpath]) + self.assertEqual(ret, 0) + self.assertRegex(out, r'(?s)^.*key expiration time: 10 seconds \(0 days\).*') + + # Expires in 10 hours + ret, out, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--edit-key', '--set-expire', '10h', 'primary_with_empty_password@rnp']) + self.assertEqual(ret, 0) + self.assertRegex(out, r'(?s)^.*\[EXPIRES .*') + ret, out, _ = run_proc(RNP, ['--list-packets', kpath]) + self.assertEqual(ret, 0) + self.assertRegex(out, r'(?s)^.*key expiration time: 36000 seconds \(0 days\).*') + + # Expires in 10 months + ret, out, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--edit-key', '--set-expire', '10m', 'primary_with_empty_password@rnp']) + self.assertEqual(ret, 0) + self.assertRegex(out, r'(?s)^.*\[EXPIRES .*') + ret, out, _ = run_proc(RNP, ['--list-packets', kpath]) + self.assertEqual(ret, 0) + self.assertRegex(out, r'(?s)^.*key expiration time: 26784000 seconds \(310 days\).*') + + # Expires in 10 years + ret, out, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--edit-key', '--set-expire', '10y', 'primary_with_empty_password@rnp']) + self.assertEqual(ret, 0) + self.assertRegex(out, r'(?s)^.*\[EXPIRES .*') + ret, out, _ = run_proc(RNP, ['--list-packets', kpath]) + self.assertEqual(ret, 0) + self.assertRegex(out, r'(?s)^.*key expiration time: 315360000 seconds \(3650 days\).*') + + # Additional primary for ambiguous key uid + ret, _, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--password=', + '--userid', 'primary2@rnp', '--generate-key']) + self.assertEqual(ret, 0, KEY_GEN_FAILED) + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--edit-key', '--set-expire', '0', 'primary']) + self.assertNotEqual(ret, 0) + self.assertRegex(err, r'Ambiguous input: too many keys found for \'primary\'') + + clear_keyrings() + +class Misc(unittest.TestCase): + + @classmethod + def setUpClass(cls): + rnp_genkey_rsa(KEY_ENCRYPT) + rnp_genkey_rsa(KEY_SIGN_GPG) + gpg_import_pubring() + gpg_import_secring() + + @classmethod + def tearDownClass(cls): + clear_keyrings() + + def tearDown(self): + clear_workfiles() + + def test_encryption_unicode(self): + if sys.version_info >= (3,): + filename = UNICODE_SEQUENCE_1 + else: + filename = UNICODE_SEQUENCE_1.encode(CONSOLE_ENCODING) + + src, dst, dec = reg_workfiles(filename, '.txt', '.rnp', '.dec') + # Generate random file of required size + random_text(src, 128000) + + rnp_encrypt_file_ex(src, dst, [KEY_ENCRYPT]) + rnp_decrypt_file(dst, dec) + compare_files(src, dec, RNP_DATA_DIFFERS) + + remove_files(src, dst, dec) + + def test_encryption_no_mdc(self): + src, dst, dec = reg_workfiles('cleartext', '.txt', '.gpg', '.rnp') + # Generate random file of required size + random_text(src, 64000) + # Encrypt cleartext file with GPG + params = ['--homedir', GPGHOME, '-c', '-z', '0', '--disable-mdc', '--s2k-count', + '65536', '--batch', '--passphrase', PASSWORD, '--output', + path_for_gpg(dst), path_for_gpg(src)] + ret, _, _ = run_proc(GPG, params) + self.assertEqual(ret, 0, 'gpg symmetric encryption failed') + # Decrypt encrypted file with RNP + rnp_decrypt_file(dst, dec) + compare_files(src, dec, RNP_DATA_DIFFERS) + + def test_encryption_s2k(self): + src, dst, dec = reg_workfiles('cleartext', '.txt', '.gpg', '.rnp') + random_text(src, 1000) + + ciphers = rnp_supported_ciphers(False) + hashes = ['SHA1', 'RIPEMD160', 'SHA256', 'SHA384', 'SHA512', 'SHA224'] + s2kmodes = [0, 1, 3] + + if not RNP_RIPEMD160: + hashes.remove('RIPEMD160') + + def rnp_encryption_s2k_gpg(cipher, hash_alg, s2k=None, iterations=None): + params = ['--homedir', GPGHOME, '-c', '--s2k-cipher-algo', cipher, + '--s2k-digest-algo', hash_alg, '--batch', '--passphrase', PASSWORD, + '--output', dst, src] + + if s2k is not None: + params.insert(7, '--s2k-mode') + params.insert(8, str(s2k)) + + if iterations is not None: + params.insert(9, '--s2k-count') + params.insert(10, str(iterations)) + + if GPG_NO_OLD: + params.insert(3, '--allow-old-cipher-algos') + + ret, _, _ = run_proc(GPG, params) + self.assertEqual(ret, 0, 'gpg symmetric encryption failed') + rnp_decrypt_file(dst, dec) + compare_files(src, dec, RNP_DATA_DIFFERS) + remove_files(dst, dec) + + for i in range(0, 20): + rnp_encryption_s2k_gpg(ciphers[i % len(ciphers)], hashes[ + i % len(hashes)], s2kmodes[i % len(s2kmodes)]) + + def test_armor(self): + src_beg, dst_beg, dst_mid, dst_fin = reg_workfiles('beg', '.src', '.dst', + '.mid.dst', '.fin.dst') + armor_types = [('msg', 'MESSAGE'), ('pubkey', 'PUBLIC KEY BLOCK'), + ('seckey', 'PRIVATE KEY BLOCK'), ('sign', 'SIGNATURE')] + + random_text(src_beg, 1000) + # Wrong armor type + ret, _, err = run_proc(RNP, ['--enarmor=wrong', src_beg, '--output', dst_beg]) + self.assertNotEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*Wrong enarmor argument: wrong.*$') + + # Default armor type + ret, _, _ = run_proc(RNP, ['--enarmor', src_beg, '--output', dst_beg]) + self.assertEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*Wrong enarmor argument: wrong.*$') + txt = file_text(dst_beg).strip('\r\n') + self.assertTrue(txt.startswith('-----BEGIN PGP MESSAGE-----'), 'wrong armor header') + self.assertTrue(txt.endswith('-----END PGP MESSAGE-----'), 'wrong armor trailer') + remove_files(dst_beg) + + for data_type, header in armor_types: + prefix = '-----BEGIN PGP ' + header + '-----' + suffix = '-----END PGP ' + header + '-----' + + ret, _, _ = run_proc(RNP, ['--enarmor=' + data_type, src_beg, '--output', dst_beg]) + self.assertEqual(ret, 0) + txt = file_text(dst_beg).strip('\r\n') + + self.assertTrue(txt.startswith(prefix), 'wrong armor header') + self.assertTrue(txt.endswith(suffix), 'wrong armor trailer') + + ret, _, _ = run_proc(RNP, ['--dearmor', dst_beg, '--output', dst_mid]) + self.assertEqual(ret, 0) + ret, _, _ = run_proc(RNP, ['--enarmor=' + data_type, dst_mid, '--output', dst_fin]) + self.assertEqual(ret, 0) + + compare_files(dst_beg, dst_fin, "RNP armor/dearmor test failed") + compare_files(src_beg, dst_mid, "RNP armor/dearmor test failed") + remove_files(dst_beg, dst_mid, dst_fin) + + # 3-byte last chunk with missing crc + msg = '-----BEGIN PGP MESSAGE-----\n\nMTIzNDU2Nzg5\n-----END PGP MESSAGE-----\n' + ret, out, err = run_proc(RNP, ['--dearmor'], msg) + self.assertEqual(ret, 0) + self.assertRegex(out, r'(?s)^.*123456789.*') + self.assertRegex(err, r'(?s)^.*Warning: missing or malformed CRC line.*') + # No invalid CRC message + R_CRC = r'(?s)^.*Warning: CRC mismatch.*$' + dec = 'decoded.pgp' + ret, _, err = run_proc(RNP, ['--dearmor', data_path('test_stream_key_load/ecc-25519-pub.asc'), '--output', dec]) + remove_files(dec) + self.assertEqual(ret, 0) + self.assertNotRegex(err, R_CRC) + # Invalid CRC message + ret, _, err = run_proc(RNP, ['--dearmor', data_path('test_stream_armor/ecc-25519-pub-bad-crc.asc'), '--output', dec]) + remove_files(dec) + self.assertEqual(ret, 0) + self.assertRegex(err, R_CRC) + + def test_rnpkeys_lists(self): + KEYRING_1 = data_path(KEYRING_DIR_1) + KEYRING_2 = data_path('keyrings/2') + KEYRING_3 = data_path(KEYRING_DIR_3) + KEYRING_5 = data_path('keyrings/5') + path = data_path('test_cli_rnpkeys') + '/' + + if RNP_CAST5: + _, out, _ = run_proc(RNPK, ['--homedir', KEYRING_1, '--list-keys']) + compare_file_any(allow_y2k38_on_32bit(path + 'keyring_1_list_keys'), out, 'keyring 1 key listing failed') + _, out, _ = run_proc(RNPK, ['--home', KEYRING_1, '-l', '--with-sigs']) + compare_file_any(allow_y2k38_on_32bit(path + 'keyring_1_list_sigs'), out, 'keyring 1 sig listing failed') + _, out, _ = run_proc(RNPK, ['--home', KEYRING_1, '--list-keys', '--secret']) + compare_file_any(allow_y2k38_on_32bit(path + 'keyring_1_list_keys_sec'), out, 'keyring 1 sec key listing failed') + _, out, _ = run_proc(RNPK, ['--home', KEYRING_1, '--list-keys', + '--secret', '--with-sigs']) + compare_file_any(allow_y2k38_on_32bit(path + 'keyring_1_list_sigs_sec'), out, 'keyring 1 sec sig listing failed') + + _, out, _ = run_proc(RNPK, ['--homedir', KEYRING_2, '--list-keys']) + compare_file(path + 'keyring_2_list_keys', out, 'keyring 2 key listing failed') + _, out, _ = run_proc(RNPK, ['--homedir', KEYRING_2, '-l', '--with-sigs']) + compare_file(path + 'keyring_2_list_sigs', out, 'keyring 2 sig listing failed') + + _, out, _ = run_proc(RNPK, ['--homedir', KEYRING_3, '--list-keys']) + compare_file_any(allow_y2k38_on_32bit(path + 'keyring_3_list_keys'), out, 'keyring 3 key listing failed') + _, out, _ = run_proc(RNPK, ['--homedir', KEYRING_3, '-l', '--with-sigs']) + compare_file_any(allow_y2k38_on_32bit(path + 'keyring_3_list_sigs'), out, 'keyring 3 sig listing failed') + + _, out, _ = run_proc(RNPK, ['--homedir', KEYRING_5, '--list-keys']) + compare_file(path + 'keyring_5_list_keys', out, 'keyring 5 key listing failed') + _, out, _ = run_proc(RNPK, ['--homedir', KEYRING_5, '-l', '--with-sigs']) + compare_file(path + 'keyring_5_list_sigs', out, 'keyring 5 sig listing failed') + + _, out, _ = run_proc(RNPK, ['--homedir', data_path(SECRING_G10), + '--list-keys']) + if RNP_BRAINPOOL: + self.assertEqual(file_text(path + 'test_stream_key_load_keys'), out, 'g10 keyring key listing failed') + else: + self.assertEqual(file_text(path + 'test_stream_key_load_keys_no_bp'), out, 'g10 keyring key listing failed') + _, out, _ = run_proc(RNPK, ['--homedir', data_path(SECRING_G10), + '-l', '--with-sigs']) + if RNP_BRAINPOOL: + self.assertEqual(file_text(path + 'test_stream_key_load_sigs'), out, 'g10 keyring sig listing failed') + else: + self.assertEqual(file_text(path + 'test_stream_key_load_sigs_no_bp'), out, 'g10 keyring sig listing failed') + # Below are disabled until we have some kind of sorting which doesn't depend on + # readdir order + #_, out, _ = run_proc(RNPK, ['--homedir', data_path(SECRING_G10), + # '-l', '--secret']) + #compare_file(path + 'test_stream_key_load_keys_sec', out, + # 'g10 sec keyring key listing failed') + #_, out, _ = run_proc(RNPK, ['--homedir', data_path(SECRING_G10), + # '-l', '--secret', '--with-sigs']) + #compare_file(path + 'test_stream_key_load_sigs_sec', out, + # 'g10 sec keyring sig listing failed') + + if RNP_CAST5: + _, out, _ = run_proc(RNPK, ['--homedir', KEYRING_1, '-l', '2fcadf05ffa501bb']) + compare_file_any(allow_y2k38_on_32bit(path + 'getkey_2fcadf05ffa501bb'), out, 'list key 2fcadf05ffa501bb failed') + _, out, _ = run_proc(RNPK, ['--homedir', KEYRING_1, '-l', + '--with-sigs', '2fcadf05ffa501bb']) + compare_file_any(allow_y2k38_on_32bit(path + 'getkey_2fcadf05ffa501bb_sig'), out, 'list sig 2fcadf05ffa501bb failed') + _, out, _ = run_proc(RNPK, ['--homedir', KEYRING_1, '-l', + '--secret', '2fcadf05ffa501bb']) + compare_file_any(allow_y2k38_on_32bit(path + 'getkey_2fcadf05ffa501bb_sec'), out, 'list sec 2fcadf05ffa501bb failed') + + _, out, _ = run_proc(RNPK, ['--homedir', KEYRING_1, '-l', '00000000']) + compare_file(path + 'getkey_00000000', out, 'list key 00000000 failed') + _, out, _ = run_proc(RNPK, ['--homedir', KEYRING_1, '-l', 'zzzzzzzz']) + compare_file(path + 'getkey_zzzzzzzz', out, 'list key zzzzzzzz failed') + + _, out, _ = run_proc(RNPK, ['--homedir', KEYRING_1, '-l', '--userid', '2fcadf05ffa501bb']) + compare_file_any(allow_y2k38_on_32bit(path + 'getkey_2fcadf05ffa501bb'), out, 'list key 2fcadf05ffa501bb failed') + + def test_rnpkeys_list_invalid_keys(self): + RNPDIR2 = RNPDIR + '2' + os.mkdir(RNPDIR2, 0o700) + ret, _, _ = run_proc(RNPK, ['--homedir', RNPDIR2, '--import', data_path('test_forged_keys/eddsa-2012-md5-pub.pgp')]) + self.assertEqual(ret, 0) + ret, out, err = run_proc(RNPK, ['--homedir', RNPDIR2, '--list-keys', '--with-sigs']) + self.assertEqual(ret, 0) + self.assertRegex(out, r'(?s)2 keys found.*8801eafbd906bd21.*\[INVALID\].*expired-md5-key-sig.*\[INVALID\].*sig.*\[unknown\] \[invalid\]') + self.assertRegex(err, r'(?s)Insecure hash algorithm 1, marking signature as invalid') + shutil.rmtree(RNPDIR2, ignore_errors=True) + + def test_rnpkeys_g10_list_order(self): + ret, out, _ = run_proc(RNPK, ['--homedir', data_path(SECRING_G10), '--list-keys']) + self.assertEqual(ret, 0) + if RNP_BRAINPOOL: + self.assertEqual(file_text(data_path('test_cli_rnpkeys/g10_list_keys')), out, 'g10 key listing failed') + else: + self.assertEqual(file_text(data_path('test_cli_rnpkeys/g10_list_keys_no_bp')), out, 'g10 key listing failed') + ret, out, _ = run_proc(RNPK, ['--homedir', data_path(SECRING_G10), '--secret', '--list-keys']) + self.assertEqual(ret, 0) + if RNP_BRAINPOOL: + self.assertEqual(file_text(data_path('test_cli_rnpkeys/g10_list_keys_sec')), out, 'g10 secret key listing failed') + else: + self.assertEqual(file_text(data_path('test_cli_rnpkeys/g10_list_keys_sec_no_bp')), out, 'g10 secret key listing failed') + + def test_rnpkeys_g10_def_key(self): + RE_SIG = r'(?s)^.*' \ + r'Good signature made .*' \ + r'using (.*) key (.*)' \ + r'pub .*' \ + r'b54fdebbb673423a5d0aa54423674f21b2441527.*' \ + r'uid\s+(ecc-p256)\s*' \ + r'Signature\(s\) verified successfully.*$' + + src, dst = reg_workfiles('cleartext', '.txt', '.rnp') + random_text(src, 1000) + # Sign file with rnp using the default g10 key + params = ['--homedir', data_path('test_cli_g10_defkey/g10'), + '--password', PASSWORD, '--output', dst, '-s', src] + ret, _, err = run_proc(RNP, params) + self.assertEqual(ret, 0, 'rnp signing failed') + # Verify signed file + params = ['--homedir', data_path('test_cli_g10_defkey/g10'), '-v', dst] + ret, _, err = run_proc(RNP, params) + self.assertEqual(ret, 0, 'verification failed') + self.assertRegex(err, RE_SIG, 'wrong rnp g10 verification output') + + def test_large_packet(self): + # Verifying large packet file with GnuPG + kpath = path_for_gpg(data_path(PUBRING_1)) + dpath = path_for_gpg(data_path('test_large_packet/4g.bzip2.gpg')) + ret, _, _ = run_proc(GPG, ['--homedir', GPGHOME, '--no-default-keyring', '--keyring', kpath, '--verify', dpath]) + self.assertEqual(ret, 0, 'large packet verification failed') + + def test_partial_length_signature(self): + # Verifying partial length signature with GnuPG + kpath = path_for_gpg(data_path(PUBRING_1)) + mpath = path_for_gpg(data_path('test_partial_length/message.txt.partial-signed')) + ret, _, _ = run_proc(GPG, ['--homedir', GPGHOME, '--no-default-keyring', '--keyring', kpath, '--verify', mpath]) + self.assertNotEqual(ret, 0, 'partial length signature packet should result in failure but did not') + + def test_partial_length_public_key(self): + # Reading keyring that has a public key packet with partial length using GnuPG + kpath = data_path('test_partial_length/pubring.gpg.partial') + ret, _, _ = run_proc(GPG, ['--homedir', GPGHOME, '--no-default-keyring', '--keyring', kpath, '--list-keys']) + self.assertNotEqual(ret, 0, 'partial length public key packet should result in failure but did not') + + def test_partial_length_zero_last_chunk(self): + # Verifying message in partial packets having 0-size last chunk with GnuPG + kpath = path_for_gpg(data_path(PUBRING_1)) + mpath = path_for_gpg(data_path('test_partial_length/message.txt.partial-zero-last')) + ret, _, _ = run_proc(GPG, ['--homedir', GPGHOME, '--no-default-keyring', '--keyring', kpath, '--verify', mpath]) + self.assertEqual(ret, 0, 'message in partial packets having 0-size last chunk verification failed') + + def test_partial_length_largest(self): + # Verifying message having largest possible partial packet with GnuPG + kpath = path_for_gpg(data_path(PUBRING_1)) + mpath = path_for_gpg(data_path('test_partial_length/message.txt.partial-1g')) + ret, _, _ = run_proc(GPG, ['--homedir', GPGHOME, '--no-default-keyring', '--keyring', kpath, '--verify', mpath]) + self.assertEqual(ret, 0, 'message having largest possible partial packet verification failed') + + def test_rnp_single_export(self): + # Import key with subkeys, then export it, test that it is exported once. + # See issue #1153 + clear_keyrings() + # Import Alice's secret key and subkey + ret, _, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--import', data_path(KEY_ALICE_SUB_SEC)]) + self.assertEqual(ret, 0, 'Alice secret key import failed') + # Export key + ret, out, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--export', 'Alice']) + self.assertEqual(ret, 0, 'key export failed') + pubpath = os.path.join(RNPDIR, 'Alice-export-test.asc') + with open(pubpath, 'w+') as f: + f.write(out) + # List exported key packets + params = ['--list-packets', pubpath] + ret, out, _ = run_proc(RNP, params) + self.assertEqual(ret, 0, PKT_LIST_FAILED) + compare_file_ex(data_path('test_single_export_subkeys/list_key_export_single.txt'), out, + 'exported packets mismatch') + + def test_rnp_permissive_key_import(self): + # Import keys while skipping bad packets, see #1160 + clear_keyrings() + # Try to import without --permissive option, should fail. + ret, _, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--import-keys', data_path('test_key_edge_cases/pubring-malf-cert.pgp')]) + self.assertNotEqual(ret, 0, 'Imported bad packets without --permissive option set!') + # Import with --permissive + ret, _, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--import-keys', '--permissive',data_path('test_key_edge_cases/pubring-malf-cert.pgp')]) + self.assertEqual(ret, 0, 'Failed to import keys with --permissive option') + + # List imported keys and sigs + params = ['--homedir', RNPDIR, '--list-keys', '--with-sigs'] + ret, out, _ = run_proc(RNPK, params) + self.assertEqual(ret, 0, PKT_LIST_FAILED) + compare_file_any(allow_y2k38_on_32bit(data_path('test_cli_rnpkeys/pubring-malf-cert-permissive-import.txt')), + out, 'listing mismatch') + + def test_rnp_autocrypt_key_import(self): + R_25519 = r'(?s)^.*pub.*255/EdDSA.*21fc68274aae3b5de39a4277cc786278981b0728.*$' + R_256K1 = r'(?s)^.*pub.*3ea5bb6f9692c1a0.*7635401f90d3e533.*$' + # Import misc configurations of base64-encoded autocrypt keys + clear_keyrings() + ret, out, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--import-keys', data_path('test_stream_key_load/ecc-25519-pub.b64')]) + self.assertEqual(ret, 0) + self.assertRegex(out, R_25519) + # No trailing EOL after the base64 data + clear_keyrings() + ret, out, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--import-keys', data_path('test_stream_key_load/ecc-25519-pub-2.b64')]) + self.assertEqual(ret, 0) + self.assertRegex(out, R_25519) + # Extra spaces/eols/tabs after the base64 data + clear_keyrings() + ret, out, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--import-keys', data_path('test_stream_key_load/ecc-25519-pub-3.b64')]) + self.assertEqual(ret, 0) + self.assertRegex(out, R_25519) + # Invalid symbols after the base64 data + clear_keyrings() + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--import-keys', data_path('test_stream_key_load/ecc-25519-pub-4.b64')]) + self.assertEqual(ret, 1) + self.assertRegex(err, r'(?s)^.*wrong base64 padding: ==zz.*Failed to init/check dearmor.*failed to import key\(s\) from .*, stopping.*') + # Binary data size is multiple of 3, single base64 line + clear_keyrings() + ret, out, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--import-keys', data_path('test_stream_key_load/ecc-p256k1-pub.b64')]) + self.assertEqual(ret, 0) + self.assertRegex(out, R_256K1) + # Binary data size is multiple of 3, multiple base64 lines + clear_keyrings() + ret, out, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--import-keys', data_path('test_stream_key_load/ecc-p256k1-pub-2.b64')]) + self.assertEqual(ret, 0) + self.assertRegex(out, R_256K1) + # Too long base64 trailer ('===') + clear_keyrings() + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--import-keys', data_path('test_stream_armor/long_b64_trailer.b64')]) + self.assertEqual(ret, 1) + self.assertRegex(err, r'(?s)^.*wrong base64 padding length 3.*Failed to init/check dearmor.*$') + # Extra data after the base64-encoded data + ret, out, err = run_proc(RNPK, ['--homedir', RNPDIR, '--import-keys', data_path('test_stream_armor/b64_trailer_extra_data.b64')]) + self.assertEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*warning: extra data after the base64 stream.*Failed to init/check dearmor.*warning: not all data was processed.*') + self.assertRegex(out, R_25519) + + def test_rnp_list_packets(self): + KEY_P256 = data_path('test_list_packets/ecc-p256-pub.asc') + # List packets in humand-readable format + params = ['--list-packets', KEY_P256] + ret, out, _ = run_proc(RNP, params) + self.assertEqual(ret, 0, PKT_LIST_FAILED) + compare_file_ex(data_path('test_list_packets/list_standard.txt'), out, + 'standard listing mismatch') + # List packets with mpi values + params = ['--mpi', '--list-packets', KEY_P256] + ret, out, _ = run_proc(RNP, params) + self.assertEqual(ret, 0, 'packet listing with mpi failed') + compare_file_ex(data_path('test_list_packets/list_mpi.txt'), out, 'mpi listing mismatch') + # List packets with grip/fingerprint values + params = ['--list-packets', KEY_P256, '--grips'] + ret, out, _ = run_proc(RNP, params) + self.assertEqual(ret, 0, 'packet listing with grips failed') + compare_file_ex(data_path('test_list_packets/list_grips.txt'), out, + 'grips listing mismatch') + # List packets with raw packet contents + params = ['--list-packets', KEY_P256, '--raw'] + ret, out, _ = run_proc(RNP, params) + self.assertEqual(ret, 0, 'packet listing with raw packets failed') + compare_file_ex(data_path('test_list_packets/list_raw.txt'), out, 'raw listing mismatch') + # List packets with all options enabled + params = ['--list-packets', KEY_P256, '--grips', '--raw', '--mpi'] + ret, out, _ = run_proc(RNP, params) + self.assertEqual(ret, 0, 'packet listing with all options failed') + compare_file_ex(data_path('test_list_packets/list_all.txt'), out, 'all listing mismatch') + + # List packets with JSON output + params = ['--json', '--list-packets', KEY_P256] + ret, out, _ = run_proc(RNP, params) + self.assertEqual(ret, 0, 'json packet listing failed') + compare_file_ex(data_path('test_list_packets/list_json.txt'), out, 'json listing mismatch') + # List packets with mpi values, JSON output + params = ['--json', '--mpi', '--list-packets', KEY_P256] + ret, out, _ = run_proc(RNP, params) + self.assertEqual(ret, 0, 'json mpi packet listing failed') + compare_file_ex(data_path('test_list_packets/list_json_mpi.txt'), out, + 'json mpi listing mismatch') + # List packets with grip/fingerprint values, JSON output + params = ['--json', '--grips', '--list-packets', KEY_P256] + ret, out, _ = run_proc(RNP, params) + self.assertEqual(ret, 0, 'json grips packet listing failed') + compare_file_ex(data_path('test_list_packets/list_json_grips.txt'), out, + 'json grips listing mismatch') + # List packets with raw packet values, JSON output + params = ['--json', '--raw', '--list-packets', KEY_P256] + ret, out, _ = run_proc(RNP, params) + self.assertEqual(ret, 0, 'json raw packet listing failed') + compare_file_ex(data_path('test_list_packets/list_json_raw.txt'), out, + 'json raw listing mismatch') + # List packets with all values, JSON output + params = ['--json', '--raw', '--list-packets', KEY_P256, '--mpi', '--grips'] + ret, out, err = run_proc(RNP, params) + self.assertEqual(ret, 0, 'json all listing failed') + compare_file_ex(data_path('test_list_packets/list_json_all.txt'), out, + 'json all listing mismatch') + # List packets with notations + params = ['--list-packets', data_path('test_key_edge_cases/key-critical-notations.pgp')] + ret, out, _ = run_proc(RNP, params) + self.assertEqual(ret, 0) + self.assertRegex(out, r'(?s)^.*notation data: critical text = critical value.*$') + self.assertRegex(out, r'(?s)^.*notation data: critical binary = 0x000102030405060708090a0b0c0d0e0f \(16 bytes\).*$') + # List packets with notations via JSON + params = ['--list-packets', '--json', data_path('test_key_edge_cases/key-critical-notations.pgp')] + ret, out, _ = run_proc(RNP, params) + self.assertEqual(ret, 0) + self.assertRegex(out, r'(?s)^.*\"human\":true.*\"name\":\"critical text\".*\"value\":\"critical value\".*$') + self.assertRegex(out, r'(?s)^.*\"human\":false.*\"name\":\"critical binary\".*\"value\":\"000102030405060708090a0b0c0d0e0f\".*$') + # List test file with critical notation + params = ['--list-packets', data_path('test_messages/message.txt.signed.crit-notation')] + ret, out, _ = run_proc(RNP, params) + self.assertEqual(ret, 0) + self.assertRegex(out, r'(?s)^.*:type 20, len 35, critical.*notation data: critical text = critical value.*$') + # List signature with signer's userid subpacket + params = ['--list-packets', data_path(MSG_SIG_CRCR)] + ret, out, _ = run_proc(RNP, params) + self.assertEqual(ret, 0) + self.assertRegex(out, r'(?s)^.*:type 28, len 9.*signer\'s user ID: alice@rnp.*$') + # JSON list signature with signer's userid subpacket + params = ['--list-packets', '--json', data_path(MSG_SIG_CRCR)] + ret, out, _ = run_proc(RNP, params) + self.assertEqual(ret, 0) + self.assertRegex(out, r'(?s)^.*"type.str":"signer\'s user ID".*"length":9.*"uid":"alice@rnp".*$') + # List signature with reason for revocation subpacket + params = ['--list-packets', data_path('test_uid_validity/key-sig-revocation.pgp')] + ret, out, _ = run_proc(RNP, params) + self.assertEqual(ret, 0) + self.assertRegex(out, r'(?s)^.*:type 29, len 24.*reason for revocation: 32 \(No longer valid\).*message: Testing revoked userid.*$') + # JSON list signature with reason for revocation subpacket + params = ['--list-packets', '--json', data_path('test_uid_validity/key-sig-revocation.pgp')] + ret, out, _ = run_proc(RNP, params) + self.assertEqual(ret, 0) + self.assertRegex(out, r'(?s)^.*"type.str":"reason for revocation".*"code":32.*"message":"Testing revoked userid.".*$') + + def test_rnp_list_packets_edge_cases(self): + KEY_EMPTY_UID = data_path('test_key_edge_cases/key-empty-uid.pgp') + # List empty key packets + params = ['--list-packets', data_path('test_key_edge_cases/key-empty-packets.pgp')] + ret, out, _ = run_proc(RNP, params) + self.assertEqual(ret, 0, PKT_LIST_FAILED) + compare_file_ex(data_path('test_key_edge_cases/key-empty-packets.txt'), out, + 'key-empty-packets listing mismatch') + + # List empty key packets json + params = ['--list-packets', '--json', data_path('test_key_edge_cases/key-empty-packets.pgp')] + ret, _, _ = run_proc(RNP, params) + self.assertEqual(ret, 0, PKT_LIST_FAILED) + + # List empty uid + params = ['--list-packets', KEY_EMPTY_UID] + ret, out, _ = run_proc(RNP, params) + self.assertEqual(ret, 0, PKT_LIST_FAILED) + compare_file_ex(data_path('test_key_edge_cases/key-empty-uid.txt'), out, + 'key-empty-uid listing mismatch') + + # List empty uid with raw packet contents + params = ['--list-packets', '--raw', KEY_EMPTY_UID] + ret, out, _ = run_proc(RNP, params) + self.assertEqual(ret, 0, PKT_LIST_FAILED) + compare_file_ex(data_path('test_key_edge_cases/key-empty-uid-raw.txt'), out, + 'key-empty-uid-raw listing mismatch') + + # List empty uid packet contents to JSON + params = ['--list-packets', '--json', KEY_EMPTY_UID] + ret, out, _ = run_proc(RNP, params) + self.assertEqual(ret, 0, PKT_LIST_FAILED) + compare_file_ex(data_path('test_key_edge_cases/key-empty-uid.json'), out, + 'key-empty-uid json listing mismatch') + + # List experimental subpackets + params = ['--list-packets', data_path('test_key_edge_cases/key-subpacket-101-110.pgp')] + ret, out, _ = run_proc(RNP, params) + self.assertEqual(ret, 0, PKT_LIST_FAILED) + compare_file_ex(data_path('test_key_edge_cases/key-subpacket-101-110.txt'), out, + 'key-subpacket-101-110 listing mismatch') + + # List experimental subpackets JSON + params = ['--list-packets', '--json', data_path('test_key_edge_cases/key-subpacket-101-110.pgp')] + ret, out, _ = run_proc(RNP, params) + self.assertEqual(ret, 0, PKT_LIST_FAILED) + compare_file_ex(data_path('test_key_edge_cases/key-subpacket-101-110.json'), out, + 'key-subpacket-101-110 json listing mismatch') + + # List malformed signature + params = ['--list-packets', data_path('test_key_edge_cases/key-malf-sig.pgp')] + ret, out, _ = run_proc(RNP, params) + self.assertEqual(ret, 0, PKT_LIST_FAILED) + compare_file_ex(data_path('test_key_edge_cases/key-malf-sig.txt'), out, + 'key-malf-sig listing mismatch') + + # List malformed signature JSON + params = ['--list-packets', '--json', data_path('test_key_edge_cases/key-malf-sig.pgp')] + ret, out, _ = run_proc(RNP, params) + self.assertEqual(ret, 0, PKT_LIST_FAILED) + compare_file_ex(data_path('test_key_edge_cases/key-malf-sig.json'), out, + 'key-malf-sig json listing mismatch') + + def test_debug_log(self): + if RNP_CAST5: + run_proc(RNPK, ['--homedir', data_path(KEYRING_DIR_1), '--list-keys', '--debug', '--all']) + run_proc(RNPK, ['--homedir', data_path('keyrings/2'), '--list-keys', '--debug', '--all']) + run_proc(RNPK, ['--homedir', data_path(KEYRING_DIR_3), '--list-keys', '--debug', '--all']) + run_proc(RNPK, ['--homedir', data_path(SECRING_G10), + '--list-keys', '--debug', '--all']) + + def test_pubring_loading(self): + NO_PUBRING = r'(?s)^.*warning: keyring at path \'.*/pubring.gpg\' doesn\'t exist.*$' + EMPTY_HOME = r'(?s)^.*Keyring directory .* is empty.*rnpkeys.*GnuPG.*' + NO_USERID = 'No userid or default key for operation' + + test_dir = tempfile.mkdtemp(prefix='rnpctmp') + test_data = data_path(MSG_TXT) + output = os.path.join(test_dir, 'output') + params = ['--symmetric', '--password', 'pass', '--homedir', test_dir, test_data, '--output', output] + ret, _, err = run_proc(RNP, ['--encrypt'] + params) + self.assertEqual(ret, 1, 'encrypt w/o pubring didn\'t fail') + self.assertNotRegex(err, NO_PUBRING, 'wrong no-keyring message') + self.assertRegex(err, EMPTY_HOME) + self.assertIn(NO_USERID, err, 'Unexpected no key output') + self.assertIn('Failed to build recipients key list', err, 'Unexpected key list output') + + ret, _, err = run_proc(RNP, ['--sign'] + params) + self.assertEqual(ret, 1, 'sign w/o pubring didn\'t fail') + self.assertNotRegex(err, NO_PUBRING, 'wrong failure output') + self.assertRegex(err, EMPTY_HOME) + self.assertIn(NO_USERID, err, 'wrong no userid message') + self.assertIn('Failed to build signing keys list', err, 'wrong signing list failure message') + + ret, _, err = run_proc(RNP, ['--clearsign'] + params) + self.assertEqual(ret, 1, 'clearsign w/o pubring didn\'t fail') + self.assertNotRegex(err, NO_PUBRING, 'wrong clearsign no pubring message') + self.assertRegex(err, EMPTY_HOME) + self.assertIn(NO_USERID, err, 'Unexpected clearsign no key output') + self.assertIn('Failed to build signing keys list', err, 'Unexpected clearsign key list output') + + ret, _, _ = run_proc(RNP, params) + self.assertEqual(ret, 0, 'symmetric w/o pubring failed') + + shutil.rmtree(test_dir) + + def test_homedir_accessibility(self): + ret, _, err = run_proc(RNPK, ['--homedir', os.path.join(RNPDIR, 'non-existing'), '--generate', '--password=none']) + self.assertNotEqual(ret, 0, 'failed to check for homedir accessibility') + self.assertRegex(err, r'(?s)^.*Home directory .*.rnp.non-existing.* does not exist or is not writable!') + self.assertRegex(err, RE_KEYSTORE_INFO) + os.mkdir(os.path.join(RNPDIR, 'existing'), 0o700) + ret, _, err = run_proc(RNPK, ['--homedir', os.path.join(RNPDIR, 'existing'), '--generate', '--password=none']) + self.assertEqual(ret, 0, 'failed to use writeable and existing homedir') + self.assertNotRegex(err, r'(?s)^.*Home directory .* does not exist or is not writable!') + self.assertNotRegex(err, RE_KEYSTORE_INFO) + + def test_no_home_dir(self): + home = os.environ['HOME'] + del os.environ['HOME'] + ret, _, err = run_proc(RNP, ['-v', 'non-existing.pgp']) + os.environ['HOME'] = home + self.assertEqual(ret, 2, 'failed to run without HOME env variable') + self.assertRegex(err, r'(?s)^.*Home directory .* does not exist or is not writable!') + self.assertRegex(err, RE_KEYSTORE_INFO) + + def test_exit_codes(self): + ret, _, _ = run_proc(RNP, ['--homedir', RNPDIR, '--help']) + self.assertEqual(ret, 0, 'invalid exit code of \'rnp --help\'') + ret, _, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--help']) + self.assertEqual(ret, 0, 'invalid exit code of \'rnpkeys --help\'') + ret, _, _ = run_proc(RNP, ['--homedir', RNPDIR, '--unknown-option', '--help']) + self.assertNotEqual(ret, 0, 'rnp should return non-zero exit code for unknown command line options') + ret, _, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--unknown-option', '--help']) + self.assertNotEqual(ret, 0, 'rnpkeys should return non-zero exit code for unknown command line options') + + def test_input_from_specifier(self): + KEY_LIST = r'(?s)^.*' \ + r'1 key found.*' \ + r'pub .*255/EdDSA.*0451409669ffde3c.*' \ + r'73edcc9119afc8e2dbbdcde50451409669ffde3c.*$' + NO_KEY_LIST = r'(?s)^.*' \ + r'Key\(s\) not found.*$' + WRONG_VAR = r'(?s)^.*' \ + r'Failed to get value of the environment variable \'SOMETHING_UNSET\'.*' \ + r'Failed to create input for env:SOMETHING_UNSET.*$' + WRONG_DATA = r'(?s)^.*' \ + r'failed to import key\(s\) from env:KEY_FILE, stopping.*$' + PGP_MSG = r'(?s)^.*' \ + r'-----BEGIN PGP MESSAGE-----.*' \ + r'-----END PGP MESSAGE-----.*$' + ENV_KEY = 'env:KEY_FILE' + + clear_keyrings() + # Import key from the stdin + ktext = file_text(data_path(KEY_ALICE_SEC)) + ret, _, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--import', '-'], ktext) + self.assertEqual(ret, 0, 'failed to import key from stdin') + ret, out, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--list-keys']) + self.assertEqual(ret, 0, KEY_LIST_FAILED) + self.assertRegex(out, KEY_LIST, KEY_LIST_WRONG) + # Cleanup and import key from the env variable + clear_keyrings() + ret, out, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--list-keys']) + self.assertNotEqual(ret, 0, 'no key list failed') + self.assertRegex(out, NO_KEY_LIST, KEY_LIST_WRONG) + # Pass unset variable + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--import', 'env:SOMETHING_UNSET']) + self.assertNotEqual(ret, 0, 'key import from env must fail') + self.assertRegex(err, WRONG_VAR, 'wrong output') + # Pass incorrect value in environment variable + os.environ['KEY_FILE'] = "something" + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--import', ENV_KEY]) + self.assertNotEqual(ret, 0, 'key import failed') + self.assertRegex(err, WRONG_DATA, 'wrong output') + # Now import the correct key + os.environ['KEY_FILE'] = ktext + ret, out, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--import', ENV_KEY]) + self.assertEqual(ret, 0, 'key import failed') + ret, out, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--list-keys']) + self.assertEqual(ret, 0, KEY_LIST_FAILED) + self.assertRegex(out, KEY_LIST, KEY_LIST_WRONG) + + # Sign message from the stdin, using the env keyfile + ret, out, _ = run_proc(RNP, ['-s', '-', '--password', 'password', '--armor', '--keyfile', ENV_KEY], 'Message to sign') + self.assertEqual(ret, 0, 'Message signing failed') + self.assertRegex(out, PGP_MSG, 'wrong signing output') + os.environ['SIGN_MSG'] = out + # Verify message from the env variable + ret, out, _ = run_proc(RNP, ['-d', 'env:SIGN_MSG', '--keyfile', ENV_KEY]) + self.assertEqual(ret, 0, 'Message verification failed') + self.assertEqual(out, 'Message to sign', 'wrong verification output') + + def test_output_to_specifier(self): + src, enc, encasc, dec = reg_workfiles('source', '.txt', EXT_PGP, EXT_ASC, '.dec') + with open(src, 'w+') as f: + f.write('Hello world') + # Encrypt file and make sure result is stored with .pgp extension + ret, out, _ = run_proc(RNP, ['-c', src, '--password', 'password']) + self.assertEqual(ret, 0, ENC_FAILED) + ret, _, _ = run_proc(RNP, ['--homedir', RNPDIR, '-d', enc, '--output', dec, '--password', 'password']) + self.assertEqual(ret, 0, DEC_FAILED) + self.assertEqual(file_text(src), file_text(dec), DEC_DIFFERS) + remove_files(enc, dec) + # Encrypt file with armor and make sure result is stored with .asc extension + ret, _, _ = run_proc(RNP, ['-c', src, '--armor', '--password', 'password']) + self.assertEqual(ret, 0, ENC_FAILED) + ret, out, _ = run_proc(RNP, ['--homedir', RNPDIR, '-d', encasc, '--output', '-', '--password', 'password']) + self.assertEqual(ret, 0, DEC_FAILED) + self.assertEqual(file_text(src), out, DEC_DIFFERS) + remove_files(encasc) + # Encrypt file and write result to the stdout + ret, out, _ = run_proc(RNP, ['-c', src, '--armor', '--output', '-', '--password', 'password']) + self.assertEqual(ret, 0, ENC_FAILED) + ret, _, _ = run_proc(RNP, ['--homedir', RNPDIR, '-d', '--output', dec, '--password', 'password', '-'], out) + self.assertEqual(ret, 0, DEC_FAILED) + self.assertEqual(file_text(src), file_text(dec), DEC_DIFFERS) + remove_files(dec) + # Encrypt file and write armored result to the stdout + ret, out, _ = run_proc(RNP, ['-c', src, '--armor','--output', '-', '--password', 'password']) + self.assertEqual(ret, 0, ENC_FAILED) + ret, out, _ = run_proc(RNP, ['--homedir', RNPDIR, '-d', '--output', '-', '--password', 'password', '-'], out) + self.assertEqual(ret, 0, DEC_FAILED) + self.assertEqual(file_text(src), out, DEC_DIFFERS) + # Encrypt stdin and write result to the stdout + srctxt = file_text(src) + ret, out, _ = run_proc(RNP, ['-c', '--armor', '--password', 'password'], srctxt) + self.assertEqual(ret, 0, ENC_FAILED) + ret, out, _ = run_proc(RNP, ['--homedir', RNPDIR, '-d', '--password', 'password'], out) + self.assertEqual(ret, 0, DEC_FAILED) + self.assertEqual(out, srctxt, DEC_DIFFERS) + # Encrypt stdin and attempt to write to non-existing dir + ret, _, err = run_proc(RNP, ['-c', '--armor', '--password', 'password', '--output', 'nonexisting/output.pgp'], srctxt) + self.assertNotEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*init_file_dest.*failed to create file.*output.pgp.*Error 2.*$') + self.assertNotRegex(err, r'(?s)^.*failed to initialize encryption.*$') + self.assertRegex(err, r'(?s)^.*failed to open source or create output.*$') + # Sign stdin and then verify it using non-existing directory for output + ret, out, _ = run_proc(RNP, ['--homedir', RNPDIR, '--armor', '--password', 'password', '-s'], srctxt) + self.assertEqual(ret, 0) + self.assertRegex(out, r'(?s)^.*BEGIN PGP MESSAGE.*END PGP MESSAGE.*$') + ret, _, err = run_proc(RNP, ['--homedir', RNPDIR, '-v', '--output', 'nonexisting/output.pgp'], out) + self.assertEqual(ret, 1) + self.assertRegex(err, r'(?s)^.*init_file_dest.*failed to create file.*output.pgp.*Error 2.*$') + + def test_literal_filename(self): + EMPTY_FNAME = r'(?s)^.*literal data packet.*mode b.*created 0, name="".*$' + HELLO_FNAME = r'(?s)^.*literal data packet.*mode b.*created 0, name="hello".*$' + src, enc, dec = reg_workfiles('source', '.txt', EXT_PGP, '.dec') + with open(src, 'w+') as f: + f.write('Literal filename check') + # Encrypt file and make sure it's name is stored in literal data packet + ret, out, _ = run_proc(RNP, ['-c', src, '--password', 'password']) + self.assertEqual(ret, 0) + ret, out, err = run_proc(GPG, ['--homedir', GPGHOME, GPG_LOOPBACK, '--passphrase', 'password', '--list-packets', enc]) + self.assertEqual(ret, 0) + self.assertRegex(out, r'(?s)^.*literal data packet.*mode b.*created \d+.*name="source.txt".*$') + remove_files(enc) + # Encrypt file, overriding it's name + ret, out, _ = run_proc(RNP, ['--set-filename', 'hello', '-c', src, '--password', 'password']) + self.assertEqual(ret, 0) + ret, out, err = run_proc(GPG, ['--homedir', GPGHOME, GPG_LOOPBACK, '--passphrase', 'password', '--list-packets', enc]) + self.assertEqual(ret, 0) + self.assertRegex(out, HELLO_FNAME) + remove_files(enc) + # Encrypt file, using empty name + ret, out, _ = run_proc(RNP, ['--set-filename', '', '-c', src, '--password', 'password']) + self.assertEqual(ret, 0) + ret, out, err = run_proc(GPG, ['--homedir', GPGHOME, GPG_LOOPBACK, '--passphrase', 'password', '--list-packets', enc]) + self.assertEqual(ret, 0) + self.assertRegex(out, EMPTY_FNAME) + remove_files(enc) + # Encrypt stdin, making sure empty name is stored + ret, out, _ = run_proc(RNP, ['-c', '--password', 'password', '--output', enc], 'Data from stdin') + self.assertEqual(ret, 0) + ret, out, err = run_proc(GPG, ['--homedir', GPGHOME, GPG_LOOPBACK, '--passphrase', 'password', '--list-packets', enc]) + self.assertEqual(ret, 0) + self.assertRegex(out, EMPTY_FNAME) + remove_files(enc) + # Encrypt stdin, setting the file name + ret, out, _ = run_proc(RNP, ['--set-filename', 'hello', '-c', '--password', 'password', '--output', enc], 'Data from stdin') + self.assertEqual(ret, 0) + ret, out, err = run_proc(GPG, ['--homedir', GPGHOME, GPG_LOOPBACK, '--passphrase', 'password', '--list-packets', enc]) + self.assertEqual(ret, 0) + self.assertRegex(out, HELLO_FNAME) + remove_files(enc) + # Encrypt env, making sure empty name is stored + ret, out, _ = run_proc(RNP, ['-c', 'env:HOME', '--password', 'password', '--output', enc]) + self.assertEqual(ret, 0) + ret, out, err = run_proc(GPG, ['--homedir', GPGHOME, GPG_LOOPBACK, '--passphrase', 'password', '--list-packets', enc]) + self.assertEqual(ret, 0) + self.assertRegex(out, EMPTY_FNAME) + remove_files(enc) + # Encrypt env, setting the file name + ret, out, _ = run_proc(RNP, ['--set-filename', 'hello', '-c', 'env:HOME', '--password', 'password', '--output', enc]) + self.assertEqual(ret, 0) + ret, out, err = run_proc(GPG, ['--homedir', GPGHOME, GPG_LOOPBACK, '--passphrase', 'password', '--list-packets', enc]) + self.assertEqual(ret, 0) + self.assertRegex(out, HELLO_FNAME) + remove_files(enc) + + def test_empty_keyrings(self): + NO_KEYRING = r'(?s)^.*' \ + r'warning: keyring at path \'.*.\.rnp.pubring\.gpg\' doesn\'t exist.*' \ + r'warning: keyring at path \'.*.\.rnp.secring\.gpg\' doesn\'t exist.*$' + EMPTY_KEYRING = r'(?s)^.*' \ + r'Warning: no keys were loaded from the keyring \'.*.\.rnp.pubring\.gpg\'.*' \ + r'Warning: no keys were loaded from the keyring \'.*.\.rnp.secring\.gpg\'.*$' + PUB_IMPORT= r'(?s)^.*pub\s+255/EdDSA 0451409669ffde3c .* \[SC\].*$' + EMPTY_SECRING = r'(?s)^.*Warning: no keys were loaded from the keyring \'.*\.rnp.secring.gpg\'.*$' + SEC_IMPORT= r'(?s)^.*sec\s+255/EdDSA 0451409669ffde3c .* \[SC\].*$' + EMPTY_HOME = r'(?s)^.*Keyring directory .* is empty.*rnpkeys.*GnuPG.*' + + os.rename(RNPDIR, RNPDIR + '-old') + home = os.environ['HOME'] + os.environ['HOME'] = WORKDIR + try: + self.assertFalse(os.path.isdir(RNPDIR), '.rnp directory should not exists') + src, enc, dec = reg_workfiles('source', '.txt', EXT_PGP, '.dec') + random_text(src, 2000) + # Run symmetric encryption/decryption without .rnp home directory + ret, _, err = run_proc(RNP, ['-c', src, '--password', 'password']) + self.assertEqual(ret, 0, 'Symmetric encryption without home failed') + self.assertNotRegex(err, NO_KEYRING, 'No keyring msg in encryption output') + ret, _, err = run_proc(RNP, ['-d', enc, '--password', 'password', '--output', dec]) + self.assertEqual(ret, 0, 'Symmetric decryption without home failed') + self.assertNotRegex(err, NO_KEYRING, 'No keyring msg in decryption output') + self.assertRegex(err, EMPTY_HOME) + self.assertIn(WORKDIR, err, 'No workdir in decryption output') + compare_files(src, dec, DEC_DIFFERS) + remove_files(enc, dec) + # Import key without .rnp home directory + ret, out, err = run_proc(RNPK, ['--import', data_path(KEY_ALICE_PUB)]) + self.assertEqual(ret, 0, 'Key import failed without home') + self.assertNotRegex(err, NO_KEYRING, 'No keyring msg in key import output') + self.assertRegex(err, EMPTY_HOME) + self.assertIn(WORKDIR, err, 'No workdir in key import output') + self.assertRegex(out, PUB_IMPORT, 'Wrong key import output') + ret, out, err = run_proc(RNPK, ['--import', data_path(KEY_ALICE_SEC)]) + self.assertEqual(ret, 0, 'Secret key import without home failed') + self.assertNotRegex(err, NO_KEYRING, 'no keyring message in key import output') + self.assertNotRegex(err, EMPTY_HOME) + self.assertRegex(err, EMPTY_SECRING, 'no empty secring in key import output') + self.assertIn(WORKDIR, err, 'no workdir in key import output') + self.assertRegex(out, SEC_IMPORT, 'Wrong secret key import output') + # Run with empty .rnp home directory + shutil.rmtree(RNPDIR, ignore_errors=True) + os.mkdir(RNPDIR, 0o700) + ret, _, err = run_proc(RNP, ['-c', src, '--password', 'password']) + self.assertEqual(ret, 0) + self.assertNotRegex(err, NO_KEYRING) + ret, out, err = run_proc(RNP, ['-d', enc, '--password', 'password', '--output', dec]) + self.assertEqual(ret, 0, 'Symmetric decryption failed') + self.assertRegex(err, EMPTY_HOME) + self.assertNotRegex(err, NO_KEYRING, 'No keyring message in decryption output') + self.assertIn(WORKDIR, err, 'No workdir in decryption output') + compare_files(src, dec, DEC_DIFFERS) + remove_files(enc, dec) + # Import key with empty .rnp home directory + ret, out, err = run_proc(RNPK, ['--import', data_path(KEY_ALICE_PUB)]) + self.assertEqual(ret, 0, 'Public key import with empty home failed') + self.assertNotRegex(err, NO_KEYRING, 'No keyring message in key import output') + self.assertRegex(err, EMPTY_HOME) + self.assertIn(WORKDIR, err, 'No workdir in key import output') + self.assertRegex(out, PUB_IMPORT, 'Wrong pub key import output') + ret, out, err = run_proc(RNPK, ['--import', data_path(KEY_ALICE_SEC)]) + self.assertEqual(ret, 0, 'Secret key import failed') + self.assertNotRegex(err, NO_KEYRING, 'No-keyring message in secret key import output') + self.assertRegex(err, EMPTY_SECRING, 'No empty secring msg in secret key import output') + self.assertNotRegex(err, EMPTY_HOME) + self.assertIn(WORKDIR, err, 'No workdir in secret key import output') + self.assertRegex(out, SEC_IMPORT, 'wrong secret key import output') + if not is_windows(): + # Attempt ro run with non-writable HOME + newhome = os.path.join(WORKDIR, 'new') + os.mkdir(newhome, 0o400) + os.environ['HOME'] = newhome + ret, out, err = run_proc(RNPK, ['--import', data_path(KEY_ALICE_PUB)]) + self.assertEqual(ret, 1) + self.assertRegex(err, r'(?s)^.*Home directory \'.*new\' does not exist or is not writable!') + self.assertRegex(err, RE_KEYSTORE_INFO) + self.assertIn(WORKDIR, err) + os.environ['HOME'] = WORKDIR + shutil.rmtree(newhome, ignore_errors=True) + # Attempt to load keyring with invalid permissions + os.chmod(os.path.join(RNPDIR, PUBRING), 0o000) + ret, out, err = run_proc(RNPK, ['--list-keys']) + self.assertEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*Warning: failed to open keyring at path \'.*pubring\.gpg\' for reading.') + self.assertRegex(out, r'(?s)^.*Alice <alice@rnp>') + os.chmod(os.path.join(RNPDIR, SECRING), 0o000) + ret, out, err = run_proc(RNPK, ['--list-keys']) + self.assertEqual(ret, 1) + self.assertRegex(err, r'(?s)^.*Warning: failed to open keyring at path \'.*pubring\.gpg\' for reading.') + self.assertRegex(err, r'(?s)^.*Warning: failed to open keyring at path \'.*secring\.gpg\' for reading.') + self.assertRegex(out, r'(?s)^.*Key\(s\) not found.') + # Attempt to load keyring with random data + shutil.rmtree(RNPDIR, ignore_errors=True) + os.mkdir(RNPDIR, 0o700) + random_text(os.path.join(RNPDIR, PUBRING), 1000) + random_text(os.path.join(RNPDIR, SECRING), 1000) + ret, out, err = run_proc(RNPK, ['--list-keys']) + self.assertEqual(ret, 1) + self.assertRegex(err, r'(?s)^.*Error: failed to load keyring from \'.*pubring\.gpg\'') + self.assertNotRegex(err, r'(?s)^.*Error: failed to load keyring from \'.*secring\.gpg\'') + self.assertRegex(out, r'(?s)^.*Key\(s\) not found.') + # Run with .rnp home directory with empty keyrings + shutil.rmtree(RNPDIR, ignore_errors=True) + os.mkdir(RNPDIR, 0o700) + random_text(os.path.join(RNPDIR, PUBRING), 0) + random_text(os.path.join(RNPDIR, SECRING), 0) + ret, out, err = run_proc(RNP, ['-c', src, '--password', 'password']) + self.assertEqual(ret, 0, 'Symmetric encryption failed') + self.assertNotRegex(err, EMPTY_KEYRING, 'Invalid encryption output') + ret, out, err = run_proc(RNP, ['-d', enc, '--password', 'password', '--output', dec]) + self.assertEqual(ret, 0, 'Symmetric decryption failed') + self.assertRegex(err, EMPTY_KEYRING, 'wrong decryption output') + self.assertIn(WORKDIR, err, 'wrong decryption output') + compare_files(src, dec, DEC_DIFFERS) + remove_files(enc, dec) + # Import key with empty keyrings in .rnp home directory + ret, out, err = run_proc(RNPK, ['--import', data_path(KEY_ALICE_PUB)]) + self.assertEqual(ret, 0, 'Public key import failed') + self.assertRegex(err, EMPTY_KEYRING, 'No empty keyring msg in key import output') + self.assertIn(WORKDIR, err, 'No workdir in empty keyring key import output') + self.assertRegex(out, PUB_IMPORT, 'Wrong pubkey import output') + ret, out, err = run_proc(RNPK, ['--import', data_path(KEY_ALICE_SEC)]) + self.assertEqual(ret, 0, 'Secret key import failed') + self.assertNotRegex(err, EMPTY_KEYRING, 'No empty keyring in key import output') + self.assertRegex(err, EMPTY_SECRING, 'No empty secring in key import output') + self.assertIn(WORKDIR, err, 'wrong key import output') + self.assertRegex(out, SEC_IMPORT, 'wrong secret key import output') + finally: + os.environ['HOME'] = home + shutil.rmtree(RNPDIR, ignore_errors=True) + os.rename(RNPDIR + '-old', RNPDIR) + clear_workfiles() + + def test_alg_aliases(self): + src, enc = reg_workfiles('source', '.txt', EXT_PGP) + with open(src, 'w+') as f: + f.write('Hello world') + # Encrypt file but forget to pass cipher name + ret, _, err = run_proc(RNP, ['-c', src, '--password', 'password', '--cipher']) + self.assertEqual(ret, 1) + self.assertRegex(err, r'(?s)^.*rnp(|\.exe): option( .--cipher.|) requires an argument.*') + # Encrypt file using the unknown symmetric algorithm + ret, _, err = run_proc(RNP, ['-c', src, '--cipher', 'bad', '--password', 'password']) + self.assertNotEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*Unsupported encryption algorithm: bad.*$') + # Encrypt file but forget to pass hash algorithm name + ret, _, err = run_proc(RNP, ['-c', src, '--password', 'password', '--hash']) + self.assertNotEqual(ret, 0) + # Encrypt file using the unknown hash algorithm + ret, _, err = run_proc(RNP, ['-c', src, '--hash', 'bad', '--password', 'password']) + self.assertNotEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*Unsupported hash algorithm: bad.*$') + # Encrypt file using the AES algorithm instead of AES-128 + ret, _, err = run_proc(RNP, ['-c', src, '--cipher', 'AES', '--password', 'password']) + self.assertEqual(ret, 0) + self.assertNotRegex(err, r'(?s)^.*Warning, unsupported encryption algorithm: AES.*$') + self.assertNotRegex(err, r'(?s)^.*Unsupported encryption algorithm: AES.*$') + # Make sure AES-128 was used + ret, out, _ = run_proc(RNP, ['--list-packets', enc]) + self.assertEqual(ret, 0) + self.assertRegex(out,r'(?s)^.*Symmetric-key encrypted session key packet.*symmetric algorithm: 7 \(AES-128\).*$') + remove_files(enc) + # Encrypt file using the 3DES instead of tripledes + ret, _, err = run_proc(RNP, ['-c', src, '--cipher', '3DES', '--password', 'password']) + self.assertEqual(ret, 0) + self.assertNotRegex(err, r'(?s)^.*Warning, unsupported encryption algorithm: 3DES.*$') + self.assertNotRegex(err, r'(?s)^.*Unsupported encryption algorithm: 3DES.*$') + # Make sure 3DES was used + ret, out, _ = run_proc(RNP, ['--list-packets', enc]) + self.assertEqual(ret, 0) + self.assertRegex(out,r'(?s)^.*Symmetric-key encrypted session key packet.*symmetric algorithm: 2 \(TripleDES\).*$') + remove_files(enc) + if RNP_RIPEMD160: + # Use ripemd-160 hash instead of RIPEMD160 + ret, _, err = run_proc(RNP, ['-c', src, '--hash', 'ripemd-160', '--password', 'password']) + self.assertEqual(ret, 0) + self.assertNotRegex(err, r'(?s)^.*Unsupported hash algorithm: ripemd-160.*$') + # Make sure RIPEMD160 was used + ret, out, _ = run_proc(RNP, ['--list-packets', enc]) + self.assertEqual(ret, 0) + self.assertRegex(out,r'(?s)^.*Symmetric-key encrypted session key packet.*s2k hash algorithm: 3 \(RIPEMD160\).*$') + remove_files(enc) + + def test_core_dumps(self): + CORE_DUMP = r'(?s)^.*warning: core dumps may be enabled, sensitive data may be leaked to disk.*$' + NO_CORE_DUMP = r'(?s)^.*warning: --coredumps doesn\'t make sense on windows systems.*$' + # Check rnpkeys for the message + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--list-keys']) + self.assertEqual(ret, 0) + self.assertNotRegex(err, CORE_DUMP) + # Check rnp for the message + ret, _, err = run_proc(RNP, ['--homedir', RNPDIR, '--armor', '--password', 'password', '-c'], 'message') + self.assertEqual(ret, 0) + self.assertNotRegex(err, CORE_DUMP) + # Enable coredumps for rnpkeys + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--list-keys', '--coredumps']) + self.assertEqual(ret, 0) + if is_windows(): + self.assertNotRegex(err, CORE_DUMP) + self.assertRegex(err, NO_CORE_DUMP) + else: + self.assertRegex(err, CORE_DUMP) + self.assertNotRegex(err, NO_CORE_DUMP) + # Enable coredumps for rnp + ret, _, err = run_proc(RNP, ['--homedir', RNPDIR, '--armor', '--password', 'password', '-c', '--coredumps'], 'message') + self.assertEqual(ret, 0) + if is_windows(): + self.assertNotRegex(err, CORE_DUMP) + self.assertRegex(err, NO_CORE_DUMP) + else: + self.assertRegex(err, CORE_DUMP) + self.assertNotRegex(err, NO_CORE_DUMP) + + def test_backend_version(self): + BOTAN_BACKEND_VERSION = r'(?s)^.*.' \ + 'Backend: Botan.*' \ + 'Backend version: ([a-zA-z\.0-9]+).*$' + OPENSSL_BACKEND_VERSION = r'(?s)^.*' \ + 'Backend: OpenSSL.*' \ + 'Backend version: ([a-zA-z\.0-9]+).*$' + # Run without parameters and make sure it matches + ret, out, _ = run_proc(RNP, []) + self.assertNotEqual(ret, 0) + match = re.match(BOTAN_BACKEND_VERSION, out) or re.match(OPENSSL_BACKEND_VERSION, out) + self.assertTrue(match) + # Run with version parameters + ret, out, err = run_proc(RNP, ['--version']) + self.assertEqual(ret, 0) + match = re.match(BOTAN_BACKEND_VERSION, out) + backend_prog = 'botan' + if not match: + match = re.match(OPENSSL_BACKEND_VERSION, out) + backend_prog = 'openssl' + openssl_root = os.getenv('OPENSSL_ROOT_DIR') + else: + openssl_root = None + self.assertTrue(match) + # check there is no unexpected output + self.assertNotRegex(err, r'(?is)^.*Unsupported.*$') + self.assertNotRegex(err, r'(?is)^.*pgp_sa_to_openssl_string.*$') + + # In case when there are several openssl installations + # testing environment is supposed to point to the right one + # through OPENSSL_ROOT_DIR environment variable + if openssl_root is not None: + backen_prog_ext = shutil.which(backend_prog, path = openssl_root + '/bin') + else: + # In all other cases + # check that botan or openssl executable binary exists in PATH + backen_prog_ext = shutil.which(backend_prog) + + if backen_prog_ext is not None: + ret, out, _ = run_proc(backen_prog_ext, ['version']) + self.assertEqual(ret, 0) + self.assertIn(match.group(1), out) + + def test_help_message(self): + # rnp help message + # short -h option + ret, out, _ = run_proc(RNP, ['-h']) + self.assertEqual(ret, 0) + short_h = out + # long --help option + ret, out, _ = run_proc(RNP, ['--help']) + self.assertEqual(ret, 0) + long_h = out + self.assertEqual(short_h, long_h) + # rnpkeys help message + # short -h option + ret, out, _ = run_proc(RNPK, ['-h']) + self.assertEqual(ret, 0) + short_h = out + # long --help options + ret, out, _ = run_proc(RNPK, ['--help']) + self.assertEqual(ret, 0) + long_h = out + self.assertEqual(short_h, long_h) + + def test_wrong_mpi_bit_count(self): + WRONG_MPI_BITS = r'(?s)^.*Warning! Wrong mpi bit count: got [0-9]+, but actual is [0-9]+.*$' + # Make sure message is not displayed on normal keys + ret, _, err = run_proc(RNP, ['--list-packets', data_path(PUBRING_1)]) + self.assertEqual(ret, 0) + self.assertNotRegex(err, WRONG_MPI_BITS) + # Make sure message is displayed on wrong mpi + ret, _, err = run_proc(RNP, ['--list-packets', data_path('test_key_edge_cases/alice-wrong-mpi-bit-count.pgp')]) + self.assertEqual(ret, 0) + self.assertRegex(err, WRONG_MPI_BITS) + + def test_eddsa_small_x(self): + os.rename(RNPDIR, RNPDIR + '-old') + home = os.environ['HOME'] + os.environ['HOME'] = WORKDIR + try: + self.assertFalse(os.path.isdir(RNPDIR), '.rnp directory should not exists') + src, sig, ver = reg_workfiles('source', '.txt', EXT_PGP, '.dec') + random_text(src, 2000) + # load just public key and verify pre-signed message + ret, _, _ = run_proc(RNPK, ['--import', data_path('test_key_edge_cases/key-eddsa-small-x-pub.asc')]) + self.assertEqual(ret, 0) + ret, _, err = run_proc(RNP, ['--verify', data_path('test_messages/message.txt.sign-small-eddsa-x')]) + self.assertEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*Good signature made .*using EdDSA key 7bc55b9bdce36e18.*$') + # load secret key and sign message + ret, out, _ = run_proc(RNPK, ['--import', data_path('test_key_edge_cases/key-eddsa-small-x-sec.asc')]) + self.assertEqual(ret, 0) + self.assertRegex(out, r'(?s)^.*sec.*255/EdDSA.*7bc55b9bdce36e18.*eddsa_small_x.*ssb.*c6c35ea115368a0b.*$') + ret, _, _ = run_proc(RNP, ['--password', PASSWORD, '--sign', src, '--output', sig]) + self.assertEqual(ret, 0) + # verify back + ret, _, err = run_proc(RNP, ['--verify', sig, '--output', ver]) + self.assertEqual(ret, 0) + self.assertEqual(file_text(src), file_text(ver)) + self.assertRegex(err, r'(?s)^.*Good signature made .*using EdDSA key 7bc55b9bdce36e18.*$') + # verify back with GnuPG + os.remove(ver) + gpg_import_pubring(data_path('test_key_edge_cases/key-eddsa-small-x-pub.asc')) + gpg_verify_file(sig, ver, 'eddsa_small_x') + finally: + os.environ['HOME'] = home + shutil.rmtree(RNPDIR, ignore_errors=True) + os.rename(RNPDIR + '-old', RNPDIR) + clear_workfiles() + + def test_cv25519_bit_fix(self): + RE_NOT_25519 = r'(?s)^.*Error: specified key is not Curve25519 ECDH subkey.*$' + # Import and tweak non-protected secret key + ret, _, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--import', data_path(KEY_25519_NOTWEAK_SEC)]) + self.assertEqual(ret, 0) + # Check some --edit-key invalid options combinations + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--edit-key']) + self.assertNotEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*You need to specify a key or subkey to edit.*$') + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--edit-key', '3176fc1486aa2528']) + self.assertNotEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*You should specify one of the editing options for --edit-key.*$') + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--edit-key', '--check-cv25519-bits']) + self.assertNotEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*You need to specify a key or subkey to edit.*$') + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--edit-key', '--check-cv25519-bits', 'key']) + self.assertNotEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*Secret keys matching \'key\' not found.*$') + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--edit-key', '--check-cv25519-bits', 'eddsa-25519-non-tweaked']) + self.assertNotEqual(ret, 0) + self.assertRegex(err, RE_NOT_25519) + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--edit-key', '--check-cv25519-bits', '3176fc1486aa2528']) + self.assertNotEqual(ret, 0) + self.assertRegex(err, RE_NOT_25519) + ret, out, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--notty', '--edit-key', '--check-cv25519-bits', '950ee0cd34613dba']) + self.assertNotEqual(ret, 0) + self.assertRegex(out, r'(?s)^.*Warning: Cv25519 key bits need fixing.*$') + # Tweak bits + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--edit-key', '--fix-cv25519-bits', '3176fc1486aa2528']) + self.assertNotEqual(ret, 0) + self.assertRegex(err, RE_NOT_25519) + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--edit-key', '--fix-cv25519-bits', '950ee0cd34613dba']) + self.assertEqual(ret, 0) + # Make sure bits are correctly tweaked and key may be used to decrypt and imported to GnuPG + ret, out, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--notty', '--edit-key', '--check-cv25519-bits', '950ee0cd34613dba']) + self.assertEqual(ret, 0) + self.assertRegex(out, r'(?s)^.*Cv25519 key bits are set correctly and do not require fixing.*$') + ret, _, _ = run_proc(RNP, ['--homedir', RNPDIR, '-d', data_path(MSG_ES_25519)]) + self.assertEqual(ret, 0) + ret, _, _ = run_proc(GPG, ['--batch', '--homedir', GPGHOME, '--import', os.path.join(RNPDIR, SECRING)]) + self.assertEqual(ret, 0) + ret, _, _ = run_proc(GPG, ['--batch', '--homedir', GPGHOME, '-d', data_path(MSG_ES_25519)]) + self.assertEqual(ret, 0) + # Remove key + ret, _, _ = run_proc(GPG, ['--batch', '--homedir', GPGHOME, '--yes', '--delete-secret-key', 'dde0ee539c017d2bd3f604a53176fc1486aa2528']) + self.assertEqual(ret, 0) + ret, _, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--remove-key', '--force', 'dde0ee539c017d2bd3f604a53176fc1486aa2528']) + self.assertEqual(ret, 0) + # Make sure protected secret key works the same way + ret, _, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--import', data_path('test_key_edge_cases/key-25519-non-tweaked-sec-prot.asc')]) + self.assertEqual(ret, 0) + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--password', 'wrong', '--edit-key', '--check-cv25519-bits', '950ee0cd34613dba']) + self.assertNotEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*Error: failed to unlock key. Did you specify valid password\\?.*$') + ret, out, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--password', 'password', '--notty', '--edit-key', '--check-cv25519-bits', '950ee0cd34613dba']) + self.assertNotEqual(ret, 0) + self.assertRegex(out, r'(?s)^.*Warning: Cv25519 key bits need fixing.*$') + # Tweak bits + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--password', 'wrong', '--edit-key', '--fix-cv25519-bits', '950ee0cd34613dba']) + self.assertNotEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*Error: failed to unlock key. Did you specify valid password\\?.*$') + ret, _, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--password', 'password', '--edit-key', '--fix-cv25519-bits', '950ee0cd34613dba']) + self.assertEqual(ret, 0) + # Make sure key is protected with the same options + ret, out, _ = run_proc(RNP, ['--list-packets', os.path.join(RNPDIR, SECRING)]) + self.assertEqual(ret, 0) + self.assertRegex(out, r'(?s)^.*Secret subkey packet.*254.*AES-256.*3.*SHA256.*58720256.*0x950ee0cd34613dba.*$') + # Make sure bits are correctly tweaked and key may be used to decrypt and imported to GnuPG + ret, out, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--notty', '--password', 'password', '--edit-key', '--check-cv25519-bits', '950ee0cd34613dba']) + self.assertEqual(ret, 0) + self.assertRegex(out, r'(?s)^.*Cv25519 key bits are set correctly and do not require fixing.*$') + ret, _, _ = run_proc(RNP, ['--homedir', RNPDIR, '--password', 'password', '-d', data_path(MSG_ES_25519)]) + self.assertEqual(ret, 0) + ret, _, _ = run_proc(GPG, ['--batch', '--homedir', GPGHOME, '--batch', '--passphrase', 'password', '--import', os.path.join(RNPDIR, SECRING)]) + self.assertEqual(ret, 0) + ret, _, _ = run_proc(GPG, ['--batch', '--homedir', GPGHOME, GPG_LOOPBACK, '--batch', '--passphrase', 'password', + '--trust-model', 'always', '-d', data_path(MSG_ES_25519)]) + self.assertEqual(ret, 0) + # Remove key + ret, _, _ = run_proc(GPG, ['--batch', '--homedir', GPGHOME, '--yes', '--delete-secret-key', 'dde0ee539c017d2bd3f604a53176fc1486aa2528']) + self.assertEqual(ret, 0) + ret, _, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--remove-key', '--force', 'dde0ee539c017d2bd3f604a53176fc1486aa2528']) + self.assertEqual(ret, 0) + + def test_aead_last_chunk_zero_length(self): + # Cover case with last AEAD chunk of the zero size + os.rename(RNPDIR, RNPDIR + '-old') + os.mkdir(RNPDIR) + try: + dec, enc = reg_workfiles('cleartext', '.dec', '.enc') + srctxt = data_path('test_messages/message.aead-last-zero-chunk.txt') + srceax = data_path('test_messages/message.aead-last-zero-chunk.enc') + srcocb = data_path('test_messages/message.aead-last-zero-chunk.enc-ocb') + eax_size = os.path.getsize(srceax) + ocb_size = os.path.getsize(srcocb) + self.assertEqual(eax_size - 1, ocb_size) + # Import Alice's key + ret, _, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--import', data_path(KEY_ALICE_SUB_SEC)]) + self.assertEqual(ret, 0) + # Decrypt already existing file + if RNP_AEAD_EAX and RNP_BRAINPOOL: + ret, _, _ = run_proc(RNP, ['--homedir', RNPDIR, '--password', PASSWORD, '-d', srceax, '--output', dec]) + self.assertEqual(ret, 0) + self.assertEqual(file_text(srctxt), file_text(dec)) + os.remove(dec) + if RNP_AEAD_OCB and RNP_BRAINPOOL: + ret, _, _ = run_proc(RNP, ['--homedir', RNPDIR, '--password', PASSWORD, '-d', srcocb, '--output', dec]) + self.assertEqual(ret, 0) + self.assertEqual(file_text(srctxt), file_text(dec)) + os.remove(dec) + # Decrypt with gnupg + if GPG_AEAD and GPG_BRAINPOOL: + ret, _, _ = run_proc(GPG, ['--batch', '--passphrase', PASSWORD, '--homedir', + GPGHOME, '--import', data_path(KEY_ALICE_SUB_SEC)]) + self.assertEqual(ret, 0, GPG_IMPORT_FAILED) + if GPG_AEAD_EAX: + gpg_decrypt_file(srceax, dec, PASSWORD) + self.assertEqual(file_text(srctxt), file_text(dec)) + os.remove(dec) + if GPG_AEAD_OCB: + gpg_decrypt_file(srcocb, dec, PASSWORD) + self.assertEqual(file_text(srctxt), file_text(dec)) + os.remove(dec) + if RNP_AEAD_EAX and RNP_BRAINPOOL: + # Encrypt with RNP + ret, _, _ = run_proc(RNP, ['--homedir', RNPDIR, '--password', PASSWORD, '-z', '0', '-r', 'alice', '--aead=eax', + '--set-filename', 'cleartext-z0.txt', '--aead-chunk-bits=1', '-e', srctxt, '--output', enc]) + self.assertEqual(ret, 0) + self.assertEqual(os.path.getsize(enc), eax_size) + # Decrypt with RNP again + ret, _, _ = run_proc(RNP, ['--homedir', RNPDIR, '--password', PASSWORD, '-d', enc, '--output', dec]) + self.assertEqual(file_text(srctxt), file_text(dec)) + os.remove(dec) + if GPG_AEAD_EAX and GPG_BRAINPOOL: + # Decrypt with GnuPG + gpg_decrypt_file(enc, dec, PASSWORD) + self.assertEqual(file_text(srctxt), file_text(dec)) + os.remove(enc) + if RNP_AEAD_OCB and RNP_BRAINPOOL: + # Encrypt with RNP + ret, _, _ = run_proc(RNP, ['--homedir', RNPDIR, '--password', PASSWORD, '-z', '0', '-r', 'alice', '--aead=ocb', + '--set-filename', 'cleartext-z0.txt', '--aead-chunk-bits=1', '-e', srctxt, '--output', enc]) + self.assertEqual(ret, 0) + self.assertEqual(os.path.getsize(enc), ocb_size) + # Decrypt with RNP again + ret, _, _ = run_proc(RNP, ['--homedir', RNPDIR, '--password', PASSWORD, '-d', enc, '--output', dec]) + self.assertEqual(file_text(srctxt), file_text(dec)) + os.remove(dec) + if GPG_AEAD_OCB and GPG_BRAINPOOL: + # Decrypt with GnuPG + gpg_decrypt_file(enc, dec, PASSWORD) + self.assertEqual(file_text(srctxt), file_text(dec)) + finally: + shutil.rmtree(RNPDIR, ignore_errors=True) + os.rename(RNPDIR + '-old', RNPDIR) + clear_workfiles() + + def test_text_sig_crcr(self): + # Cover case with line ending with multiple CRs + srcsig = data_path(MSG_SIG_CRCR) + srctxt = data_path('test_messages/message.text-sig-crcr') + # Verify with RNP + ret, _, _ = run_proc(RNP, ['--homedir', RNPDIR, '--keyfile', data_path(KEY_ALICE_SUB_PUB), '-v', srcsig]) + self.assertEqual(ret, 0) + # Verify with GPG + if GPG_BRAINPOOL: + ret, _, _ = run_proc(GPG, ['--batch', '--homedir', GPGHOME, '--import', data_path(KEY_ALICE_SUB_PUB)]) + self.assertEqual(ret, 0, GPG_IMPORT_FAILED) + gpg_verify_detached(srctxt, srcsig, KEY_ALICE) + + def test_encrypted_password_wrong(self): + # Test symmetric decryption with wrong password used + srcenc = data_path('test_messages/message.enc-password') + ret, _, err = run_proc(RNP, ['--homedir', RNPDIR, '--password', 'password1', '-d', srcenc]) + self.assertNotEqual(ret, 0) + self.assertIn('checksum check failed', err) + ret, _, err = run_proc(RNP, ['--homedir', RNPDIR, '--password', 'password', '-d', srcenc, '--output', 'decrypted']) + self.assertEqual(ret, 0) + os.remove('decrypted') + + def test_clearsign_long_lines(self): + # Cover case with cleartext signed file with long lines and filesize > 32k (buffer size) + [sig] = reg_workfiles('cleartext', '.sig') + srctxt = data_path('test_messages/message.4k-long-lines') + srcsig = data_path('test_messages/message.4k-long-lines.asc') + pubkey = data_path(KEY_ALICE_SUB_PUB) + seckey = data_path(KEY_ALICE_SUB_SEC) + # Verify already existing file + ret, _, err = run_proc(RNP, ['--homedir', RNPDIR, '--keyfile', pubkey, '-v', srcsig]) + self.assertEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*Good signature made.*73edcc9119afc8e2dbbdcde50451409669ffde3c.*') + # Verify with gnupg + if GPG_BRAINPOOL: + ret, _, _ = run_proc(GPG, ['--batch', '--homedir', GPGHOME, '--import', pubkey]) + self.assertEqual(ret, 0, GPG_IMPORT_FAILED) + gpg_verify_cleartext(srcsig, KEY_ALICE) + # Sign again with RNP + ret, _, _ = run_proc(RNP, ['--homedir', RNPDIR, '--keyfile', seckey, '--password', PASSWORD, '--clearsign', srctxt, '--output', sig]) + self.assertEqual(ret, 0) + # Verify with RNP again + ret, _, err = run_proc(RNP, ['--homedir', RNPDIR, '--keyfile', pubkey, '-v', sig]) + self.assertEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*Good signature made.*73edcc9119afc8e2dbbdcde50451409669ffde3c.*') + # Verify with gnupg again + if GPG_BRAINPOOL: + gpg_verify_cleartext(sig, KEY_ALICE) + clear_workfiles() + + def test_eddsa_sig_lead_zero(self): + # Cover case with lead zeroes in EdDSA signature + srcs = data_path('test_messages/eddsa-zero-s.txt.sig') + srcr = data_path('test_messages/eddsa-zero-r.txt.sig') + # Verify with RNP + ret, _, _ = run_proc(RNP, ['--homedir', RNPDIR, '--keyfile', data_path(KEY_ALICE_SUB_PUB), '-v', srcs]) + self.assertEqual(ret, 0) + ret, _, _ = run_proc(RNP, ['--homedir', RNPDIR, '--keyfile', data_path(KEY_ALICE_SUB_PUB), '-v', srcr]) + self.assertEqual(ret, 0) + # Verify with GPG + if GPG_BRAINPOOL: + [dst] = reg_workfiles('eddsa-zero', '.txt') + ret, _, _ = run_proc(GPG, ['--batch', '--homedir', GPGHOME, '--import', data_path(KEY_ALICE_SUB_PUB)]) + self.assertEqual(ret, 0, GPG_IMPORT_FAILED) + gpg_verify_file(srcs, dst, KEY_ALICE) + os.remove(dst) + gpg_verify_file(srcr, dst, KEY_ALICE) + clear_workfiles() + + def test_eddsa_seckey_lead_zero(self): + # Load and use *unencrypted* EdDSA secret key with 2 leading zeroes + seckey = data_path('test_stream_key_load/eddsa-00-sec.pgp') + pubkey = data_path('test_stream_key_load/eddsa-00-pub.pgp') + src, sig = reg_workfiles('source', '.txt', '.sig') + random_text(src, 2000) + + # Sign with RNP + ret, _, _ = run_proc(RNP, ['--homedir', RNPDIR, '--keyfile', seckey, '-s', src, '--output', sig]) + self.assertEqual(ret, 0) + # Verify with RNP + ret, _, _ = run_proc(RNP, ['--homedir', RNPDIR, '--keyfile', pubkey, '-v', sig]) + self.assertEqual(ret, 0) + # Verify with GnuPG + ret, _, _ = run_proc(GPG, ['--batch', '--homedir', GPGHOME, '--import', pubkey]) + ret, _, err = run_proc(GPG, ['--batch', '--homedir', GPGHOME, '--verify', sig]) + self.assertEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*Signature made.*8BF2223370F61F8D965B.*Good signature from "eddsa-lead-zero".*$') + clear_workfiles() + + def test_verify_detached_source(self): + if RNP_CAST5: + # Test --source parameter for the detached signature verification. + src = data_path(MSG_TXT) + sig = data_path(MSG_TXT + '.sig') + sigasc = data_path(MSG_TXT + '.asc') + keys = data_path(KEYRING_DIR_1) + # Just verify + ret, _, err = run_proc(RNP, ['--homedir', keys, '-v', sig]) + self.assertEqual(ret, 0) + R_GOOD = r'(?s)^.*Good signature made.*e95a3cbf583aa80a2ccc53aa7bc6709b15c23a4a.*' + self.assertRegex(err, R_GOOD) + # Verify .asc + ret, _, err = run_proc(RNP, ['--homedir', keys, '-v', sigasc]) + self.assertEqual(ret, 0) + self.assertRegex(err, R_GOOD) + # Do not provide source + ret, _, err = run_proc(RNP, ['--homedir', keys, '-v', sig, '--source']) + self.assertEqual(ret, 1) + self.assertRegex(err, r'(?s)^.*rnp(|\.exe): option( .--source.|) requires an argument.*') + # Verify by specifying the correct path + ret, _, err = run_proc(RNP, ['--homedir', keys, '--source', src, '-v', sig]) + self.assertEqual(ret, 0) + self.assertRegex(err, R_GOOD) + # Verify by specifying the incorrect path + ret, _, err = run_proc(RNP, ['--homedir', keys, '--source', src + '.wrong', '-v', sig]) + self.assertNotEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*Failed to open source for detached signature verification.*') + # Verify detached signature with non-asc/sig extension + [csig] = reg_workfiles('message', '.dat') + shutil.copy(sig, csig) + ret, _, err = run_proc(RNP, ['--homedir', keys, '-v', csig]) + self.assertNotEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*Unsupported detached signature extension. Use --source to override.*') + # Verify by reading data from stdin + srcdata = "" + with open(src, "rb") as srcf: + srcdata = srcf.read().decode('utf-8') + ret, _, err = run_proc(RNP, ['--homedir', keys, '--source', '-', '-v', csig], srcdata) + self.assertEqual(ret, 0) + self.assertRegex(err, R_GOOD) + # Verify by reading data from env + os.environ["SIGNED_DATA"] = srcdata + ret, _, err = run_proc(RNP, ['--homedir', keys, '--source', 'env:SIGNED_DATA', '-v', csig]) + self.assertEqual(ret, 0) + self.assertRegex(err, R_GOOD) + del os.environ["SIGNED_DATA"] + # Attempt to verify by specifying bot sig and data from stdin + sigtext = file_text(sigasc) + ret, _, err = run_proc(RNP, ['--homedir', keys, '--source', '-', '-v'], sigtext) + self.assertNotEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*Detached signature and signed source cannot be both stdin.*') + + clear_workfiles() + + def test_onepass_edge_cases(self): + key = data_path('test_key_validity/alice-pub.asc') + onepass22 = data_path('test_messages/message.txt.signed-2-2-onepass-v10') + # Verify one-pass which doesn't match the signature - different keyid + ret, _, err = run_proc(RNP, ['--keyfile', key, '-v', data_path('test_messages/message.txt.signed-wrong-onepass')]) + self.assertEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*Warning: signature doesn\'t match one-pass.*Good signature made.*0451409669ffde3c.*') + # Verify one-pass with unknown hash algorithm + ret, _, err = run_proc(RNP, ['--keyfile', key, '-v', data_path('test_messages/message.txt.signed-unknown-onepass-hash')]) + self.assertEqual(ret, 1) + self.assertRegex(err, r'(?s)^.*Failed to create hash 136 for onepass 0.*') + # Verify one-pass with hash algorithm which doesn't match sig's one + ret, _, err = run_proc(RNP, ['--keyfile', key, '-v', data_path('test_messages/message.txt.signed-wrong-onepass-hash')]) + self.assertEqual(ret, 1) + self.assertRegex(err, r'(?s)^.*failed to get hash context.*BAD signature.*0451409669ffde3c.*') + # Extra one-pass without the corresponding signature + ret, _, err = run_proc(RNP, ['--keyfile', key, '-v', data_path('test_messages/message.txt.signed-2-onepass')]) + self.assertEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*Warning: premature end of signatures.*Good signature made.*0451409669ffde3c.*') + # Two one-passes and two equal signatures + ret, _, err = run_proc(RNP, ['--keyfile', key, '-v', data_path('test_messages/message.txt.signed-2-2-onepass')]) + self.assertEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*Good signature made.*0451409669ffde3c.*Good signature made.*0451409669ffde3c.*') + # Two one-passes and two sigs, but first one-pass is of unknown version + ret, _, err = run_proc(RNP, ['--keyfile', key, '-v', onepass22]) + self.assertEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*wrong packet version.*warning: unexpected data on the stream end.*Good signature made.*0451409669ffde3c.*') + # Dump it as well + ret, out, err = run_proc(RNP, ['--list-packets', onepass22]) + self.assertEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*wrong packet version.*failed to process packet.*') + self.assertRegex(out, r'(?s)^.*:off 0: packet header 0xc40d.*:off 15: packet header 0xc40d.*One-pass signature packet.*') + # Dump it in JSON + ret, out, err = run_proc(RNP, ['--list-packets', '--json', onepass22]) + self.assertEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*wrong packet version.*failed to process packet.*') + self.assertRegex(out, r'(?s)^.*"offset":0.*"tag":4.*"offset":15.*"tag":4.*"version":3.*"nested":true.*') + # Two one-passes and sig of the unknown version + ret, _, err = run_proc(RNP, ['--keyfile', key, '-v', data_path('test_messages/message.txt.signed-2-2-sig-v10')]) + self.assertEqual(ret, 1) + R_VER_10 = r'(?s)^.*unknown signature version: 10.*failed to parse signature.*UNKNOWN signature.*Good signature made.*0451409669ffde3c.*' + R_1_UNK = r'(?s)^.*Signature verification failure: 1 unknown signature.*' + self.assertRegex(err, R_VER_10) + self.assertRegex(err, R_1_UNK) + # Two one-passes and sig of the unknown version (second) + ret, _, err = run_proc(RNP, ['--keyfile', key, '-v', data_path('test_messages/message.txt.signed-2-2-sig-v10-2')]) + self.assertEqual(ret, 1) + self.assertRegex(err, r'(?s)^.*unknown signature version: 10.*failed to parse signature.*Good signature made.*0451409669ffde3c.*UNKNOWN signature.*') + self.assertRegex(err, R_1_UNK) + # 2 detached signatures, first is of version 10 + ret, _, err = run_proc(RNP, ['--keyfile', key, '-v', data_path('test_messages/message.txt.2sigs'), '--source', data_path(MSG_TXT)]) + self.assertEqual(ret, 1) + self.assertRegex(err, R_VER_10) + self.assertRegex(err, R_1_UNK) + # 2 detached signatures, second is of version 10 + ret, _, err = run_proc(RNP, ['--keyfile', key, '-v', data_path('test_messages/message.txt.2sigs-2'), '--source', data_path(MSG_TXT)]) + self.assertEqual(ret, 1) + self.assertRegex(err, r'(?s)^.*unknown signature version: 10.*failed to parse signature.*Good signature made.*0451409669ffde3c.*UNKNOWN signature.*') + self.assertRegex(err, R_1_UNK) + # Two cleartext signatures, first is of unknown version + ret, _, err = run_proc(RNP, ['--keyfile', key, '-v', data_path('test_messages/message.txt.clear-2-sigs')]) + self.assertEqual(ret, 1) + self.assertRegex(err, R_VER_10) + self.assertRegex(err, R_1_UNK) + # Two cleartext signatures, second is of unknown version + ret, _, err = run_proc(RNP, ['--keyfile', key, '-v', data_path('test_messages/message.txt.clear-2-sigs-2')]) + self.assertEqual(ret, 1) + self.assertRegex(err, r'(?s)^.*unknown signature version: 11.*failed to parse signature.*Good signature made.*0451409669ffde3c.*UNKNOWN signature.*') + self.assertRegex(err, R_1_UNK) + + def test_pkesk_skesk_wrong_version(self): + key = data_path('test_stream_key_load/ecc-p256-sec.asc') + msg = data_path('test_messages/message.txt.pkesk-skesk-v10') + msg2 = data_path('test_messages/message.txt.pkesk-skesk-v10-only') + # Decrypt with secret key + ret, out, err = run_proc(RNP, ['--keyfile', key, '--password', PASSWORD, '-d', msg]) + self.assertEqual(ret, 0) + self.assertRegex(out, r'(?s)^.*This is test message to be signed, and/or encrypted, cleartext signed and detached signed.*') + self.assertRegex(err, r'(?s)^.*wrong packet version.*Failed to parse PKESK, skipping.*wrong packet version.*Failed to parse SKESK, skipping.*') + # Decrypt with password + ret, out, err = run_proc(RNP, ['--homedir', RNPDIR, '--password', PASSWORD, '-d', msg]) + self.assertEqual(ret, 0) + self.assertRegex(out, r'(?s)^.*This is test message to be signed, and/or encrypted, cleartext signed and detached signed.*') + self.assertRegex(err, r'(?s)^.*wrong packet version.*Failed to parse PKESK, skipping.*wrong packet version.*Failed to parse SKESK, skipping.*') + # Attempt to decrypt message with only invalid PKESK/SKESK + ret, _, err = run_proc(RNP, ['--keyfile', key, '--password', PASSWORD, '-d', msg2]) + self.assertEqual(ret, 1) + self.assertRegex(err, r'(?s)^.*wrong packet version.*Failed to parse PKESK, skipping.*wrong packet version.*Failed to parse SKESK, skipping.*failed to obtain decrypting key or password.*') + + def test_ext_adding_stripping(self): + # Check whether rnp correctly strip .pgp/.gpg/.asc extension + seckey = data_path('test_stream_key_load/ecc-p256-sec.asc') + pubkey = data_path('test_stream_key_load/ecc-p256-pub.asc') + src, src2, asc, pgp, gpg, some = reg_workfiles('cleartext', '.txt', '.txt2', '.txt.asc', '.txt.pgp', '.txt.gpg', '.txt.some') + with open(src, 'w+') as f: + f.write('Hello world') + # Encrypt with binary output + ret, _, _ = run_proc(RNP, ['--homedir', RNPDIR, '--keyfile', pubkey, '-e', src]) + self.assertEqual(ret, 0) + self.assertTrue(os.path.isfile(pgp)) + # Decrypt binary output, it must be put in cleartext.txt if it doesn't exists + os.remove(src) + ret, _, _ = run_proc(RNP, ['--homedir', RNPDIR, '--keyfile', seckey, '--password', PASSWORD, '-d', pgp]) + self.assertEqual(ret, 0) + self.assertTrue(os.path.isfile(src)) + # Decrypt binary output with the rename prompt + ret, out, _ = run_proc(RNP, ['--keyfile', seckey, '--password', PASSWORD, '--notty', '-d', pgp], "n\n" + src2 + "\n") + self.assertEqual(ret, 0) + self.assertTrue(os.path.isfile(src2)) + self.assertRegex(out, r'(?s)^.*File.*cleartext.txt.*already exists. Would you like to overwrite it.*Please enter the new filename:.*$') + self.assertIn(src, out) + self.assertTrue(os.path.isfile(src2)) + os.remove(src2) + # Rename from .pgp to .gpg and try again + os.remove(src) + os.rename(pgp, gpg) + ret, _, _ = run_proc(RNP, ['--homedir', RNPDIR, '--keyfile', seckey, '--password', PASSWORD, '-d', gpg]) + self.assertEqual(ret, 0) + self.assertTrue(os.path.isfile(src)) + # Rename from .pgp to .some and check that all is put in stdout + os.rename(gpg, some) + ret, out, _ = run_proc(RNP, ['--keyfile', seckey, '--password', PASSWORD, '--notty', '-d', some], "\n\n") + self.assertEqual(ret, 0) + self.assertRegex(out, r'(?s)^\s*Hello world\s*$') + os.remove(some) + # Encrypt with armored output + ret, _, _ = run_proc(RNP, ['--homedir', RNPDIR, '--keyfile', pubkey, '-e', src, '--armor']) + self.assertEqual(ret, 0) + self.assertTrue(os.path.isfile(asc)) + # Decrypt armored output, it must be put in cleartext.txt if it doesn't exists + os.remove(src) + ret, _, _ = run_proc(RNP, ['--homedir', RNPDIR, '--keyfile', seckey, '--password', PASSWORD, '-d', asc]) + self.assertEqual(ret, 0) + self.assertTrue(os.path.isfile(src)) + # Enarmor - must be put in .asc file + os.remove(asc) + ret, _, _ = run_proc(RNP, ['--homedir', RNPDIR, '--enarmor=msg', src]) + self.assertEqual(ret, 0) + self.assertTrue(os.path.isfile(asc)) + # Dearmor asc - must be outputed to src + os.remove(src) + ret, _, _ = run_proc(RNP, ['--homedir', RNPDIR, '--dearmor', asc]) + self.assertEqual(ret, 0) + self.assertTrue(os.path.isfile(src)) + # Dearmor unknown extension - must be put to stdout + os.rename(asc, some) + ret, out, _ = run_proc(RNP, ['--homedir', RNPDIR, '--dearmor', some]) + self.assertEqual(ret, 0) + self.assertRegex(out, r'(?s)^\s*Hello world\s*$') + + + def test_interactive_password(self): + # Reuse password for subkey, say "yes" + stdinstr = 'password\npassword\ny\n' + ret, out, err = run_proc(RNPK, ['--homedir', RNPDIR, '--notty', '--generate-key'], stdinstr) + self.assertEqual(ret, 0) + # Do not reuse same password for subkey, say "no" + stdinstr = 'password\npassword\nN\nsubkeypassword\nsubkeypassword\n' + ret, out, err = run_proc(RNPK, ['--homedir', RNPDIR, '--notty', '--generate-key'], stdinstr) + self.assertEqual(ret, 0) + # Set empty password and reuse it + stdinstr = '\n\ny\ny\n' + ret, out, err = run_proc(RNPK, ['--homedir', RNPDIR, '--notty', '--generate-key'], stdinstr) + self.assertEqual(ret, 0) + + def test_set_current_time(self): + RNP2 = RNPDIR + '2' + os.mkdir(RNP2, 0o700) + + # Generate key back in the past + ret, out, _ = run_proc(RNPK, ['--homedir', RNP2, '--notty', '--password', PASSWORD, '--generate-key', '--current-time', '2015-02-02', '--userid', 'key-2015']) + self.assertEqual(ret, 0) + self.assertRegex(out, r'(?s)^.*Generating a new key\.\.\..*sec.*2015\-02\-0.*EXPIRES 2017\-.*ssb.*2015\-02\-0.*EXPIRES 2017\-.*$') + # List keys + ret, out, _ = run_proc(RNPK, ['--homedir', RNP2, '--notty', '--list-keys']) + self.assertEqual(ret, 0) + self.assertRegex(out, r'(?s)^.*pub.*2015-02-0.*EXPIRED 2017.*sub.*2015-02-0.* \[EXPIRED 2017.*$') + self.assertNotRegex(out, r'(?s)^.*\[INVALID\].*$') + ret, out, _ = run_proc(RNPK, ['--homedir', RNP2, '--notty', '--current-time', '2015-02-04', '--list-keys']) + self.assertEqual(ret, 0) + self.assertRegex(out, r'(?s)^.*pub.*2015-02-0.*EXPIRES 2017.*sub.*2015-02-0.*EXPIRES 2017.*$') + # Create workfile + src, sig, enc = reg_workfiles('cleartext', '.txt', '.txt.sig', '.txt.enc') + with open(src, 'w+') as f: + f.write('Hello world') + # Sign with key from the past + ret, _, err = run_proc(RNP, ['--homedir', RNP2, '--password', PASSWORD, '-u', 'key-2015', '-s', src, '--output', sig]) + self.assertEqual(ret, 1) + self.assertRegex(err, r'(?s)^.*Failed to add signature.*$') + ret, _, _ = run_proc(RNP, ['--homedir', RNP2, '--password', PASSWORD, '-u', 'key-2015', '--current-time', '2015-02-03', '-s', src, '--output', sig]) + self.assertEqual(ret, 0) + # List packets + ret, out, _ = run_proc(RNP, ['--homedir', RNP2, '--list-packets', sig]) + self.assertEqual(ret, 0) + self.assertRegex(out, r'(?s)^.*signature creation time.*2015\).*signature expiration time.*$') + # Verify with the expired key + ret, out, err = run_proc(RNP, ['--homedir', RNP2, '-v', sig, '--output', '-']) + self.assertEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*Good signature made.*2015.*pub.*\[SC\] \[EXPIRED 2017.*$') + self.assertRegex(out, r'(?s)^.*Hello world.*$') + # Encrypt with the expired key + ret, _, err = run_proc(RNP, ['--homedir', RNP2, '-r', 'key-2015', '-e', src, '--output', enc]) + self.assertEqual(ret, 1) + self.assertRegex(err, r'(?s)^.*Failed to add recipient.*$') + ret, _, _ = run_proc(RNP, ['--homedir', RNP2, '-r', 'key-2015', '--current-time', '2015-02-03', '-e', src, '--output', enc]) + self.assertEqual(ret, 0) + # Decrypt with the expired key + ret, out, _ = run_proc(RNP, ['--homedir', RNP2, '--password', PASSWORD, '-d', enc, '--output', '-']) + self.assertEqual(ret, 0) + self.assertRegex(out, r'(?s)^.*Hello world.*$') + + shutil.rmtree(RNP2, ignore_errors=True) + clear_workfiles() + + def test_wrong_passfd(self): + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--pass-fd', '999', '--userid', + 'test_wrong_passfd', '--generate-key', '--expert'], '22\n') + self.assertEqual(ret, 1) + self.assertRegex(err, r'(?s)^.*Cannot open fd 999 for reading') + self.assertRegex(err, r'(?s)^.*fatal: failed to initialize rnpkeys') + + def test_keystore_formats(self): + # Use wrong keystore format + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--keystore-format', 'WRONG', '--list-keys']) + self.assertEqual(ret, 1) + self.assertRegex(err, r'(?s)^.*Unsupported keystore format: "WRONG"') + # Use G10 keystore format + RNPG10 = RNPDIR + '/g10' + #os.mkdir(RNPG10, 0o700) + kring = shutil.copytree(data_path(KEYRING_DIR_3), RNPG10) + ret, _, err = run_proc(RNPK, ['--homedir', kring, '--keystore-format', 'G10', '--list-keys']) + self.assertEqual(ret, 1) + self.assertRegex(err, r'(?s)^.*Warning: no keys were loaded from the keyring \'.*private-keys-v1.d\'') + # Use G21 keystore format + ret, out, _ = run_proc(RNPK, ['--homedir', kring, '--keystore-format', 'GPG21', '--list-keys']) + self.assertEqual(ret, 0) + self.assertRegex(out, r'(?s)^.*2 keys found') + shutil.rmtree(RNPG10, ignore_errors=True) + + def test_no_twofish(self): + if (RNP_TWOFISH): + return + src, dst, dec = reg_workfiles('cleartext', '.txt', '.pgp', '.dec') + random_text(src, 100) + # Attempt to encrypt to twofish + ret, _, err = run_proc(RNP, ['--homedir', RNPDIR, '--cipher', 'twofish', '--output', dst, '-e', src]) + self.assertEqual(ret, 2) + self.assertFalse(os.path.isfile(dst)) + self.assertRegex(err, r'(?s)^.*Unsupported encryption algorithm: twofish') + # Symmetrically encrypt with GnuPG + gpg_symencrypt_file(src, dst, 'TWOFISH') + # Attempt to decrypt + ret, _, err = run_proc(RNP, ['--homedir', RNPDIR, '--password', PASSWORD, '--output', dec, '-d', dst]) + self.assertEqual(ret, 1) + self.assertFalse(os.path.isfile(dec)) + self.assertRegex(err, r'(?s)^.*failed to start cipher') + # Public-key encrypt with GnuPG + kpath = path_for_gpg(data_path(PUBRING_1)) + os.remove(dst) + ret, _, _ = run_proc(GPG, ['--homedir', GPGHOME, '--no-default-keyring', '--batch', '--keyring', kpath, '-r', 'key0-uid0', + '--trust-model', 'always', '--cipher-algo', 'TWOFISH', '--output', dst, '-e', src]) + self.assertEqual(ret, 0) + # Attempt to decrypt + ret, _, err = run_proc(RNP, ['--keyfile', data_path(SECRING_1), '--password', PASSWORD, '--output', dec, '-d', dst]) + self.assertEqual(ret, 1) + self.assertFalse(os.path.isfile(dec)) + self.assertRegex(err, r'(?s)^.*Unsupported symmetric algorithm 10') + clear_workfiles() + + def test_no_idea(self): + if (RNP_IDEA): + return + src, dst, dec = reg_workfiles('cleartext', '.txt', '.pgp', '.dec') + random_text(src, 100) + # Attempt to encrypt to twofish + ret, _, err = run_proc(RNP, ['--homedir', RNPDIR, '--cipher', 'idea', '--output', dst, '-e', src]) + self.assertEqual(ret, 2) + self.assertFalse(os.path.isfile(dst)) + self.assertRegex(err, r'(?s)^.*Unsupported encryption algorithm: idea') + # Symmetrically encrypt with GnuPG + gpg_symencrypt_file(src, dst, 'IDEA') + # Attempt to decrypt + ret, _, err = run_proc(RNP, ['--homedir', RNPDIR, '--password', PASSWORD, '--output', dec, '-d', dst]) + self.assertEqual(ret, 1) + self.assertFalse(os.path.isfile(dec)) + self.assertRegex(err, r'(?s)^.*failed to start cipher') + # Public-key encrypt with GnuPG + kpath = path_for_gpg(data_path(PUBRING_1)) + os.remove(dst) + params = ['--no-default-keyring', '--batch', '--keyring', kpath, '-r', 'key0-uid0', '--trust-model', 'always', '--cipher-algo', 'IDEA', '--output', dst, '-e', src] + if GPG_NO_OLD: + params.insert(1, '--allow-old-cipher-algos') + ret, _, _ = run_proc(GPG, params) + self.assertEqual(ret, 0) + # Attempt to decrypt + ret, _, err = run_proc(RNP, ['--keyfile', data_path(SECRING_1), '--password', PASSWORD, '--output', dec, '-d', dst]) + self.assertEqual(ret, 1) + self.assertFalse(os.path.isfile(dec)) + self.assertRegex(err, r'(?s)^.*Unsupported symmetric algorithm 1') + # List secret key, encrypted with IDEA + ret, out, err = run_proc(RNP, ['--homedir', RNPDIR, '--list-packets', data_path('keyrings/4/rsav3-s.asc')]) + self.assertEqual(ret, 0) + self.assertNotRegex(out, r'(?s)^.*failed to process packet') + self.assertRegex(out, r'(?s)^.*secret key material.*symmetric algorithm: 1 .IDEA.') + # Import secret key - must succeed. + RNP2 = RNPDIR + '2' + os.mkdir(RNP2, 0o700) + ret, out, err = run_proc(RNPK, ['--homedir', RNP2, '--import', data_path('keyrings/4/rsav3-s.asc')]) + self.assertEqual(ret, 0) + self.assertRegex(out, r'(?s)^.*sec.*7d0bc10e933404c9.*INVALID') + shutil.rmtree(RNP2, ignore_errors=True) + clear_workfiles() + + def test_subkey_binding_on_uid(self): + RNP2 = RNPDIR + '2' + os.mkdir(RNP2, 0o700) + + # Import key with deleted subkey packet (so subkey binding is attached to the uid) + ret, out, _ = run_proc(RNPK, ['--homedir', RNP2, '--import', data_path('test_key_edge_cases/alice-uid-binding.pgp')]) + self.assertEqual(ret, 0) + self.assertRegex(out, r'(?s)^.*pub.*0451409669ffde3c.*alice@rnp.*$') + # List keys - make sure rnp doesn't attempt to validate wrong sig + ret, out, err = run_proc(RNPK, ['--homedir', RNP2, '--list-keys', '--with-sigs']) + self.assertEqual(ret, 0) + self.assertNotRegex(err, r'(?s)^.*wrong lbits.*$') + self.assertRegex(err, r'(?s)^.*Invalid binding signature key type.*$') + self.assertRegex(out, r'(?s)^.*sig.*alice@rnp.*.*sig.*alice@rnp.*invalid.*$') + + shutil.rmtree(RNP2, ignore_errors=True) + + def test_key_locate(self): + seckey = data_path(SECRING_1) + pubkey = data_path(PUBRING_1) + src, sig = reg_workfiles('cleartext', '.txt', '.sig') + random_text(src, 1200) + # Try non-existing key + ret, _, err = run_proc(RNP, ['--keyfile', seckey, '-u', 'alice', '--sign', src, '--output', sig]) + self.assertEqual(ret, 1) + self.assertRegex(err, r'(?s)^.*Cannot find key matching "alice".*') + # Match via partial uid + ret, _, _ = run_proc(RNP, ['--keyfile', seckey, '-u', 'key0', '--password', PASSWORD, '--sign', src, '--output', sig]) + self.assertEqual(ret, 0) + ret, _, err = run_proc(RNP, ['--keyfile', pubkey, '-v', sig]) + self.assertEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*Good signature made.*7bc6709b15c23a4a.*Signature\(s\) verified successfully.*') + remove_files(sig) + R_GOOD_SIG = r'(?s)^.*Good signature made.*2fcadf05ffa501bb.*Signature\(s\) verified successfully.*' + # Match via keyid with hex prefix + ret, _, _ = run_proc(RNP, ['--keyfile', seckey, '-u', '0x2fcadf05ffa501bb', '--password', PASSWORD, '--sign', src, '--output', sig]) + self.assertEqual(ret, 0) + ret, _, err = run_proc(RNP, ['--keyfile', pubkey, '-v', sig]) + self.assertEqual(ret, 0) + self.assertRegex(err, R_GOOD_SIG) + remove_files(sig) + # Match via keyid with spaces/tabs + ret, _, _ = run_proc(RNP, ['--keyfile', seckey, '-u', '0X 2FCA DF05\tFFA5\t01BB', '--password', PASSWORD, '--sign', src, '--output', sig]) + self.assertEqual(ret, 0) + ret, _, err = run_proc(RNP, ['--keyfile', pubkey, '-v', sig]) + self.assertEqual(ret, 0) + self.assertRegex(err, R_GOOD_SIG) + remove_files(sig) + # Match via half of the keyid + ret, _, _ = run_proc(RNP, ['--keyfile', seckey, '-u', 'FFA501BB', '--password', PASSWORD, '--sign', src, '--output', sig]) + self.assertEqual(ret, 0) + ret, _, err = run_proc(RNP, ['--keyfile', pubkey, '-v', sig]) + self.assertEqual(ret, 0) + self.assertRegex(err, R_GOOD_SIG) + remove_files(sig) + # Match via fingerprint + ret, _, _ = run_proc(RNP, ['--keyfile', seckey, '-u', 'be1c4ab9 51F4C2F6 b604c7f8 2FCADF05 ffa501bb', '--password', PASSWORD, '--sign', src, '--output', sig]) + self.assertEqual(ret, 0) + ret, _, err = run_proc(RNP, ['--keyfile', pubkey, '-v', sig]) + self.assertEqual(ret, 0) + self.assertRegex(err, R_GOOD_SIG) + remove_files(sig) + # Match via grip + ret, _, _ = run_proc(RNP, ['--keyfile', seckey, '-u', '0xb2a7f6c34aa2c15484783e9380671869a977a187', '--password', PASSWORD, '--sign', src, '--output', sig]) + self.assertEqual(ret, 0) + ret, _, err = run_proc(RNP, ['--keyfile', pubkey, '-v', sig]) + self.assertEqual(ret, 0) + self.assertRegex(err, R_GOOD_SIG) + remove_files(sig) + # Match via regexp + ret, _, _ = run_proc(RNP, ['--keyfile', seckey, '-u', 'key[12].uid.', '--password', PASSWORD, '--sign', src, '--output', sig]) + self.assertEqual(ret, 0) + ret, _, err = run_proc(RNP, ['--keyfile', pubkey, '-v', sig]) + self.assertEqual(ret, 0) + self.assertRegex(err, R_GOOD_SIG) + remove_files(sig) + clear_workfiles() + + def test_conflicting_commands(self): + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '--generate-key', '--import', '--revoke-key', '--list-keys']) + self.assertNotEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*Conflicting commands!*') + ret, _, err = run_proc(RNPK, ['--homedir', RNPDIR, '-g', '-l']) + self.assertNotEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*Conflicting commands!*') + ret, _, err = run_proc(RNP, ['--homedir', RNPDIR, '--sign', '--verify', '--decrypt', '--list-packets']) + self.assertNotEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*Conflicting commands!*') + ret, _, err = run_proc(RNP, ['--homedir', RNPDIR, '-s', '-v']) + self.assertNotEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*Conflicting commands!*') + + def test_hidden_recipient(self): + seckey = data_path(SECRING_1) + msg1 = data_path('test_messages/message.txt.enc-hidden-1') + msg2 = data_path('test_messages/message.txt.enc-hidden-2') + pswd = 'password\n' + pswds = 'password\npassword\npassword\npassword\n' + R_MSG = r'(?s)^.*This is test message to be signed.*' + H_MSG1 = r'(?s)^.*Warning: message has hidden recipient, but it was ignored. Use --allow-hidden to override this.*' + H_MSG2 = r'(?s)^.*This message has hidden recipient. Will attempt to use all secret keys for decryption.*' + # Try to decrypt message without valid key + ret, out, err = run_proc(RNP, ['--keyfile', data_path(KEY_ALICE_SUB_SEC), '--notty', '-d', msg1], pswd) + self.assertEqual(ret, 1) + self.assertNotRegex(out, R_MSG) + self.assertNotRegex(out, r'(?s)^.*Enter password for key.*') + self.assertRegex(err, H_MSG1) + # Try to decrypt message with first recipient hidden, it must not be asked for + ret, out, _ = run_proc(RNP, ['--keyfile', seckey, '--notty', '-d', msg1], pswd) + self.assertEqual(ret, 0) + self.assertRegex(out, R_MSG) + self.assertRegex(out, r'(?s)^.*Enter password for key 0x326EF111425D14A5 to decrypt.*') + self.assertNotRegex(out, r'(?s)^.*Enter password.*Enter password.*') + # Try to decrypt message with first recipient hidden, providing wrong password + ret, out, err = run_proc(RNP, ['--keyfile', seckey, '--notty', '-d', msg1], '123\n') + self.assertEqual(ret, 1) + self.assertRegex(out, r'(?s)^.*Enter password for key 0x326EF111425D14A5 to decrypt.*') + self.assertNotRegex(out, r'(?s)^.*Enter password.*Enter password.*') + self.assertRegex(err, H_MSG1) + # Try to decrypt message with second recipient hidden + ret, out, _ = run_proc(RNP, ['--keyfile', seckey, '--notty', '-d', msg2], pswd) + self.assertEqual(ret, 0) + self.assertRegex(out, R_MSG) + self.assertRegex(out, r'(?s)^.*Enter password for key 0x8A05B89FAD5ADED1 to decrypt.*') + self.assertNotRegex(out, r'(?s)^.*Enter password.*Enter password.*') + # Try to decrypt message with second recipient hidden, wrong password + ret, out, err = run_proc(RNP, ['--keyfile', seckey, '--notty', '-d', msg2], '123\n') + self.assertEqual(ret, 1) + self.assertRegex(out, r'(?s)^.*Enter password for key 0x8A05B89FAD5ADED1 to decrypt.*') + self.assertNotRegex(out, r'(?s)^.*Enter password.*Enter password.*') + self.assertRegex(err, H_MSG1) + # Allow hidden recipient, specifying valid password + ret, out, err = run_proc(RNP, ['--keyfile', seckey, '--notty', '--allow-hidden', '-d', msg1], pswds) + self.assertEqual(ret, 0) + self.assertRegex(err, H_MSG2) + self.assertRegex(out, R_MSG) + self.assertRegex(out, r'(?s)^.*Enter password for key 0x1ED63EE56FADC34D.*0x8A05B89FAD5ADED1.*') + self.assertNotRegex(out, r'(?s)^.*Enter password.*Enter password.*Enter password.*') + # Allow hidden recipient, specifying all wrong passwords + ret, out, err = run_proc(RNP, ['--keyfile', seckey, '--notty', '--allow-hidden', '-d', msg1], '1\n1\n1\n1\n') + self.assertEqual(ret, 1) + self.assertRegex(err, H_MSG2) + self.assertRegex(out, r'(?s)^.*Enter password for key 0x1ED63EE56FADC34D.*0x8A05B89FAD5ADED1.*0x326EF111425D14A5.*') + self.assertNotRegex(out, r'(?s)^.*Enter password.*Enter password.*Enter password.*Enter password.*') + # Allow hidden recipient, specifying invalid password for first recipient and valid password for hidden, message 2 + ret, out, err = run_proc(RNP, ['--keyfile', seckey, '--notty', '--allow-hidden', '-d', msg2], '1\npassword\npassword\n') + self.assertEqual(ret, 0) + self.assertRegex(err, H_MSG2) + self.assertRegex(out, R_MSG) + self.assertRegex(out, r'(?s)^.*Enter password for key 0x8A05B89FAD5ADED1.*0x54505A936A4A970E.*0x326EF111425D14A5.*') + self.assertNotRegex(out, r'(?s)^.*Enter password.*Enter password.*Enter password.*Enter password.*') + # Allow hidden recipient, specifying invalid password for all, message 2 + ret, out, err = run_proc(RNP, ['--keyfile', seckey, '--notty', '--allow-hidden', '-d', msg2], '1\n1\n1\n1\n') + self.assertEqual(ret, 1) + self.assertRegex(err, H_MSG2) + self.assertRegex(out, r'(?s)^.*Enter password for key 0x8A05B89FAD5ADED1.*0x54505A936A4A970E.*0x326EF111425D14A5.*') + self.assertNotRegex(out, r'(?s)^.*Enter password.*Enter password.*Enter password.*Enter password.*') + + def test_allow_weak_hash(self): + RNP2 = RNPDIR + '2' + os.mkdir(RNP2, 0o700) + # rnpkeys, force weak hashes for key generation + ret, _, err = run_proc(RNPK, ['--homedir', RNP2, '-g', '--password=', '--hash', 'MD5']) + self.assertNotEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*Hash algorithm \'MD5\' is cryptographically weak!.*Weak hash algorithm detected. Pass --allow-weak-hash option if you really want to use it\..*') + ret, _, err = run_proc(RNPK, ['--homedir', RNP2, '-g', '--password=', '--hash', 'MD5', '--allow-weak-hash']) + self.assertEqual(ret, 0) + + ret, _, err = run_proc(RNPK, ['--homedir', RNP2, '-g', '--password=', '--hash', 'SHA1']) + self.assertNotEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*Hash algorithm \'SHA1\' is cryptographically weak!.*Weak hash algorithm detected. Pass --allow-weak-hash option if you really want to use it\..*') + ret, _, err = run_proc(RNPK, ['--homedir', RNP2, '-g', '--password=', '--hash', 'SHA1', '--allow-weak-hash']) + self.assertEqual(ret, 0) + + # check non-weak hash + ret, _, err = run_proc(RNPK, ['--homedir', RNP2, '-g', '--password=', '--hash', 'SHA3-512']) + self.assertEqual(ret, 0) + self.assertNotRegex(err, r'(?s)^.*Hash algorithm \'SHA3\-512\' is cryptographically weak!.*') + ret, _, err = run_proc(RNPK, ['--homedir', RNP2, '-g', '--password=', '--hash', 'SHA3-512', '--allow-weak-hash']) + self.assertEqual(ret, 0) + + # rnp, force weak hashes for signature + src, sig = reg_workfiles('cleartext', '.txt', '.sig') + random_text(src, 120) + + ret, _, err = run_proc(RNP, ['--keyfile', data_path(SECRING_1), '--password', PASSWORD, '--sign', src, '--output', sig, '--hash', 'MD5']) + self.assertNotEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*Hash algorithm \'MD5\' is cryptographically weak!.*Weak hash algorithm detected. Pass --allow-weak-hash option if you really want to use it\..*') + ret, _, err = run_proc(RNP, ['--keyfile', data_path(SECRING_1), '--password', PASSWORD, '--sign', src, '--output', sig, '--hash', 'MD5', '--allow-weak-hash']) + self.assertEqual(ret, 0) + remove_files(sig) + + ret, _, err = run_proc(RNP, ['--keyfile', data_path(SECRING_1), '--password', PASSWORD, '--sign', src, '--output', sig, '--hash', 'SHA1']) + self.assertNotEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*Hash algorithm \'SHA1\' is cryptographically weak!.*Weak hash algorithm detected. Pass --allow-weak-hash option if you really want to use it\..*') + ret, _, err = run_proc(RNP, ['--keyfile', data_path(SECRING_1), '--password', PASSWORD, '--sign', src, '--output', sig, '--hash', 'SHA1', '--allow-weak-hash']) + self.assertEqual(ret, 0) + remove_files(sig) + + # check non-weak hash + ret, _, err = run_proc(RNP, ['--keyfile', data_path(SECRING_1), '--password', PASSWORD, '--sign', src, '--output', sig, '--hash', 'SHA3-512']) + self.assertEqual(ret, 0) + self.assertNotRegex(err, r'(?s)^.*Hash algorithm \'SHA3\-512\' is cryptographically weak!.*') + remove_files(sig) + ret, _, err = run_proc(RNP, ['--keyfile', data_path(SECRING_1), '--password', PASSWORD, '--sign', src, '--output', sig, '--hash', 'SHA3-512', '--allow-weak-hash']) + self.assertEqual(ret, 0) + remove_files(sig) + + clear_workfiles() + shutil.rmtree(RNP2, ignore_errors=True) + +class Encryption(unittest.TestCase): + ''' + Things to try later: + - different public key algorithms + - different hash algorithms where applicable + + TODO: + Tests in this test case should be split into many algorithm-specific tests + (potentially auto generated) + Reason being - if you have a problem with BLOWFISH size 1000000, you don't want + to wait until everything else gets + tested before your failing BLOWFISH + ''' + # Ciphers list tro try during encryption. None will use default + CIPHERS = [None] + SIZES = [20, 40, 120, 600, 1000, 5000, 20000, 250000] + # Compression parameters to try during encryption(s) + Z = [[None, 0], ['zip'], ['zlib'], ['bzip2'], [None, 1], [None, 9]] + # Number of test runs - each run picks next encryption algo and size, wrapping on array + RUNS = 20 + + @classmethod + def setUpClass(cls): + # Generate keypair in RNP + rnp_genkey_rsa(KEY_ENCRYPT) + # Add some other keys to the keyring + rnp_genkey_rsa('dummy1@rnp', 1024) + rnp_genkey_rsa('dummy2@rnp', 1024) + gpg_import_pubring() + gpg_import_secring() + Encryption.CIPHERS += rnp_supported_ciphers(False) + Encryption.CIPHERS_R = list_upto(Encryption.CIPHERS, Encryption.RUNS) + Encryption.SIZES_R = list_upto(Encryption.SIZES, Encryption.RUNS) + Encryption.Z_R = list_upto(Encryption.Z, Encryption.RUNS) + + @classmethod + def tearDownClass(cls): + clear_keyrings() + + def tearDown(self): + clear_workfiles() + + # Encrypt cleartext file with GPG and decrypt it with RNP, + # using different ciphers and file sizes + def test_file_encryption__gpg_to_rnp(self): + for size, cipher in zip(Encryption.SIZES_R, Encryption.CIPHERS_R): + gpg_to_rnp_encryption(size, cipher) + + # Encrypt with RNP and decrypt with GPG + def test_file_encryption__rnp_to_gpg(self): + for size in Encryption.SIZES: + file_encryption_rnp_to_gpg(size) + + def test_sym_encryption__gpg_to_rnp(self): + # Encrypt cleartext with GPG and decrypt with RNP + for size, cipher, z in zip(Encryption.SIZES_R, Encryption.CIPHERS_R, Encryption.Z_R): + rnp_sym_encryption_gpg_to_rnp(size, cipher, z) + + def test_sym_encryption__rnp_to_gpg(self): + # Encrypt cleartext with RNP and decrypt with GPG + for size, cipher, z in zip(Encryption.SIZES_R, Encryption.CIPHERS_R, Encryption.Z_R): + rnp_sym_encryption_rnp_to_gpg(size, cipher, z, 1024) + + def test_sym_encryption_s2k_iter(self): + src, enc = reg_workfiles('cleartext', '.txt', '.gpg') + # Generate random file of required size + random_text(src, 20) + def s2k_iter_run(input_iterations, expected_iterations): + # Encrypt cleartext file with RNP + ret, _, err = run_proc(RNP, ['--homedir', RNPDIR, '--output', enc, '--password', PASSWORD, '-c', '--s2k-iterations', str(input_iterations), src]) + if ret != 0: + raise_err('rnp encryption failed', err) + ret, out, _ = run_proc(RNP, ['--list-packets', enc]) + self.assertEqual(ret, 0) + self.assertRegex(out, r'(?s)^.*s2k iterations: [0-9]+ \(encoded as [0-9]+\).*') + matches = re.findall(r'(?s)^.*s2k iterations: ([0-9]+) \(encoded as [0-9]+\).*', out) + if int(matches[0]) != expected_iterations: + raise_err('unexpected iterations number', matches[0]) + remove_files(enc) + + for iters in [1024, 1088, 0x3e00000]: + s2k_iter_run(iters, iters) + clear_workfiles() + + def test_sym_encryption_s2k_msec(self): + src, enc = reg_workfiles('cleartext', '.txt', '.gpg') + # Generate random file of required size + random_text(src, 20) + def s2k_msec_iters(msec): + # Encrypt cleartext file with RNP + ret, _, err = run_proc(RNP, ['--homedir', RNPDIR, '--output', enc, '--password', PASSWORD, '-c', '--s2k-msec', str(msec), src]) + if ret != 0: + raise_err('rnp encryption failed', err) + ret, out, _ = run_proc(RNP, ['--list-packets', enc]) + self.assertEqual(ret, 0) + self.assertRegex(out, r'(?s)^.*s2k iterations: [0-9]+ \(encoded as [0-9]+\).*') + matches = re.findall(r'(?s)^.*s2k iterations: ([0-9]+) \(encoded as [0-9]+\).*', out) + remove_files(enc) + return int(matches[0]) + + iters1msec = s2k_msec_iters(1) + iters10msec = s2k_msec_iters(10) + iters100msec = s2k_msec_iters(100) + + disable_test = os.getenv('DISABLE_TEST_S2K_MSEC') + if disable_test is None: + self.assertGreaterEqual(iters10msec, iters1msec) + self.assertGreaterEqual(iters100msec, iters10msec) + clear_workfiles() + + def test_sym_encryption_wrong_s2k(self): + src, dst, enc = reg_workfiles('cleartext', '.txt', '.rnp', '.enc') + random_text(src, 1001) + # Wrong S2K iterations + ret, _, err = run_proc(RNP, ['--s2k-iterations', 'WRONG_ITER', '--homedir', RNPDIR, '--password', PASSWORD, + '--output', enc, '-c', src]) + self.assertNotEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*Wrong iterations value: WRONG_ITER.*') + # Wrong S2K msec + ret, _, err = run_proc(RNP, ['--s2k-msec', 'WRONG_MSEC', '--homedir', RNPDIR, '--password', PASSWORD, + '--output', enc, '-c', src]) + self.assertNotEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*Invalid s2k msec value: WRONG_MSEC.*') + # Overflow + ret, _, err = run_proc(RNP, ['--s2k-iterations', '999999999999', '--homedir', RNPDIR, '--password', PASSWORD, + '--output', enc, '-c', src]) + self.assertNotEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*Wrong iterations value: 999999999999.*') + self.assertNotRegex(err, r'(?s)^.*std::out_of_range.*') + + ret, _, err = run_proc(RNP, ['--s2k-msec', '999999999999', '--homedir', RNPDIR, '--password', PASSWORD, + '--output', enc, '-c', src]) + self.assertNotEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*Invalid s2k msec value: 999999999999.*') + self.assertNotRegex(err, r'(?s)^.*std::out_of_range.*') + + remove_files(src, dst, enc) + + def test_sym_encryption__rnp_aead(self): + if not RNP_AEAD: + print('AEAD is not available for RNP - skipping.') + return + CIPHERS = rnp_supported_ciphers(True) + AEADS = [None, 'eax', 'ocb'] + if not RNP_AEAD_EAX: + AEADS.remove('eax') + AEAD_C = list_upto(CIPHERS, Encryption.RUNS) + AEAD_M = list_upto(AEADS, Encryption.RUNS) + AEAD_B = list_upto([None, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 14, 16], Encryption.RUNS) + + # Encrypt and decrypt cleartext using the AEAD + for size, cipher, aead, bits, z in zip(Encryption.SIZES_R, AEAD_C, + AEAD_M, AEAD_B, Encryption.Z_R): + rnp_sym_encryption_rnp_aead(size, cipher, z, [aead, bits], GPG_AEAD) + + def test_aead_chunk_edge_cases(self): + if not RNP_AEAD: + print('AEAD is not available for RNP - skipping.') + return + src, dst, enc = reg_workfiles('cleartext', '.txt', '.rnp', '.enc') + # Cover lines from src_skip() where > 16 bytes must be skipped + random_text(src, 1001) + ret, _, err = run_proc(RNP, ['--homedir', RNPDIR, '--password', PASSWORD, '--output', enc, '--aead=eax', '--aead-chunk-bits', '2', '-z', '0', '-c', src]) + if RNP_AEAD_EAX: + self.assertEqual(ret, 0) + rnp_decrypt_file(enc, dst) + remove_files(dst, enc) + else: + self.assertEqual(ret, 1) + self.assertRegex(err, r'(?s)^.*Invalid AEAD algorithm: EAX') + # Check non-AES OCB mode + ret, _, err = run_proc(RNP, ['--homedir', RNPDIR, '--password', PASSWORD, '--output', enc, '--cipher', 'CAMELLIA192', '--aead=ocb', '--aead-chunk-bits', '2', '-z', '0', '-c', src]) + if RNP_AEAD_OCB_AES: + self.assertEqual(ret, 1) + self.assertRegex(err, r'(?s)^.*Only AES-OCB is supported by the OpenSSL backend') + else: + self.assertEqual(ret, 0) + rnp_decrypt_file(enc, dst) + remove_files(dst, enc) + # Check default (AES) OCB + ret, _, err = run_proc(RNP, ['--homedir', RNPDIR, '--password', PASSWORD, '--output', enc, '--aead=ocb', '--aead-chunk-bits', '2', '-z', '0', '-c', src]) + self.assertEqual(ret, 0) + rnp_decrypt_file(enc, dst) + remove_files(src, dst, enc) + # Cover case with AEAD chunk start on the data end + random_text(src, 1002) + if RNP_AEAD_EAX: + ret, _, err = run_proc(RNP, ['--homedir', RNPDIR, '--password', PASSWORD, '--output', enc, '--aead=eax', '--aead-chunk-bits', '2', '-z', '0', '-c', src]) + self.assertEqual(ret, 0) + rnp_decrypt_file(enc, dst) + remove_files(dst, enc) + if RNP_AEAD_OCB: + ret, _, err = run_proc(RNP, ['--homedir', RNPDIR, '--password', PASSWORD, '--output', enc, '--aead-chunk-bits', '2', '-z', '0', '-c', src]) + self.assertEqual(ret, 0) + rnp_decrypt_file(enc, dst) + remove_files(src, dst, enc) + + def fill_aeads(self, runs): + aead = [None, [None]] + if RNP_AEAD_EAX: + aead += [['eax']] + if RNP_AEAD_OCB: + aead += [['ocb']] + return list_upto(aead, runs) + + def gpg_supports(self, aead): + if (aead == ['eax']) and not GPG_AEAD_EAX: + return False + if (aead == ['ocb']) and not GPG_AEAD_OCB: + return False + if (aead == [None]) and not GPG_AEAD_OCB: + return False + return True + + def test_encryption_multiple_recipients(self): + USERIDS = ['key1@rnp', 'key2@rnp', 'key3@rnp'] + KEYPASS = ['key1pass', 'key2pass', 'key3pass'] + PASSWORDS = ['password1', 'password2', 'password3'] + # Generate multiple keys and import to GnuPG + for uid, pswd in zip(USERIDS, KEYPASS): + rnp_genkey_rsa(uid, 1024, pswd) + + gpg_import_pubring() + gpg_import_secring() + + KEYPSWD = tuple((t1, t2) for t1 in range(len(USERIDS) + 1) + for t2 in range(len(PASSWORDS) + 1)) + KEYPSWD = list_upto(KEYPSWD, Encryption.RUNS) + AEADS = self.fill_aeads(Encryption.RUNS) + + src, dst, dec = reg_workfiles('cleartext', '.txt', '.rnp', '.dec') + # Generate random file of required size + random_text(src, 65500) + + for kpswd, aead in zip(KEYPSWD, AEADS): + keynum, pswdnum = kpswd + if (keynum == 0) and (pswdnum == 0): + continue + uids = USERIDS[:keynum] if keynum else None + pswds = PASSWORDS[:pswdnum] if pswdnum else None + + rnp_encrypt_file_ex(src, dst, uids, pswds, aead) + + # Decrypt file with each of the keys, we have different password for each key + # For CFB mode there is ~5% probability that GnuPG will attempt to decrypt + # message's SESK with a wrong password, see T3795 on dev.gnupg.org + first_pass = aead is None and ((pswdnum > 1) or ((pswdnum == 1) and (keynum > 0))) + try_gpg = self.gpg_supports(aead) + 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) + + # Decrypt file with each of the passwords (with gpg only first password is checked) + if first_pass and try_gpg: + gpg_decrypt_file(dst, dec, PASSWORDS[0]) + gpg_agent_clear_cache() + remove_files(dec) + + 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) + + clear_workfiles() + + def test_encryption_and_signing(self): + USERIDS = ['enc-sign1@rnp', 'enc-sign2@rnp', 'enc-sign3@rnp'] + KEYPASS = ['encsign1pass', 'encsign2pass', 'encsign3pass'] + PASSWORDS = ['password1', 'password2', 'password3'] + AEAD_C = list_upto(rnp_supported_ciphers(True), Encryption.RUNS) + # Generate multiple keys and import to GnuPG + for uid, pswd in zip(USERIDS, KEYPASS): + rnp_genkey_rsa(uid, 1024, pswd) + + 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[:SIGNERS[i]] + signpswd = KEYPASS[:SIGNERS[i]] + keynum, pswdnum = KEYPSWD[i] + recipients = USERIDS[:keynum] + 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' + rnp_genkey_rsa(uid, 1024, pswd) + # Encrypt + src = data_path(MSG_TXT) + 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) + compare_files(src, dec, RNP_DATA_DIFFERS) + clear_workfiles() + + def test_encryption_weird_userids_special_2(self): + USERIDS = [WEIRD_USERID_SPACE, WEIRD_USERID_QUOTE, WEIRD_USERID_SPACE_AND_QUOTE, WEIRD_USERID_QUOTE_AND_SPACE] + KEYPASS = ['encSpecial2Pass1', 'encSpecial2Pass2', 'encSpecial2Pass3', 'encSpecial2Pass4'] + # Generate multiple keys + for uid, pswd in zip(USERIDS, KEYPASS): + rnp_genkey_rsa(uid, 1024, pswd) + # Encrypt to all recipients + src = data_path(MSG_TXT) + dst, dec = reg_workfiles('weird_userids_special_2', '.rnp', '.dec') + rnp_encrypt_file_ex(src, dst, list(map(lambda uid: uid, USERIDS)), None, None) + # 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) + compare_files(src, dec, RNP_DATA_DIFFERS) + remove_files(dec) + # Cleanup + clear_workfiles() + + def test_encryption_weird_userids_unicode(self): + USERIDS_1 = [ + WEIRD_USERID_UNICODE_1, WEIRD_USERID_UNICODE_2] + USERIDS_2 = [ + WEIRD_USERID_UNICODE_1, WEIRD_USERID_UNICODE_2] + # The idea is to generate keys with USERIDS_1 and encrypt with USERIDS_2 + # (that differ only in case) + # But currently Unicode case-insensitive search is not working, + # so we're encrypting with exactly the same recipient + KEYPASS = ['encUnicodePass1', 'encUnicodePass2'] + # Generate multiple keys + for uid, pswd in zip(USERIDS_1, KEYPASS): + rnp_genkey_rsa(uid, 1024, pswd) + # Encrypt to all recipients + src = data_path('test_messages') + '/message.txt' + dst, dec = reg_workfiles('weird_unicode', '.rnp', '.dec') + rnp_encrypt_file_ex(src, dst, list(map(lambda uid: uid, USERIDS_2)), None, None) + # 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) + compare_files(src, dec, RNP_DATA_DIFFERS) + remove_files(dec) + # Cleanup + clear_workfiles() + + def test_encryption_x25519(self): + # Make sure that we support import and decryption using both tweaked and non-tweaked keys + KEY_IMPORT = r'(?s)^.*' \ + r'sec.*255/EdDSA.*3176fc1486aa2528.*' \ + r'uid.*eddsa-25519-non-tweaked.*' \ + r'ssb.*255/ECDH.*950ee0cd34613dba.*$' + BITS_MSG = r'(?s)^.*Warning: bits of 25519 secret key are not tweaked.*$' + + ret, out, err = run_proc(RNPK, ['--homedir', RNPDIR, '--import', data_path(KEY_25519_NOTWEAK_SEC)]) + self.assertEqual(ret, 0) + self.assertRegex(out, KEY_IMPORT) + ret, _, err = run_proc(RNP, ['--homedir', RNPDIR, '-d', data_path(MSG_ES_25519)]) + self.assertEqual(ret, 0) + self.assertRegex(err, BITS_MSG) + self.assertRegex(err, r'(?s)^.*Signature\(s\) verified successfully.*$') + ret, _, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--remove-key', 'eddsa-25519-non-tweaked', '--force']) + self.assertEqual(ret, 0) + ret, out, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--import', data_path('test_key_edge_cases/key-25519-tweaked-sec.asc')]) + self.assertEqual(ret, 0) + self.assertRegex(out, KEY_IMPORT) + ret, _, err = run_proc(RNP, ['--homedir', RNPDIR, '-d', data_path(MSG_ES_25519)]) + self.assertEqual(ret, 0) + self.assertNotRegex(err, BITS_MSG) + ret, _, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--remove-key', 'eddsa-25519-non-tweaked', '--force']) + self.assertEqual(ret, 0) + # Due to issue in GnuPG it reports successful import of non-tweaked secret key in batch mode + ret, _, _ = run_proc(GPG, ['--batch', '--homedir', GPGHOME, '--import', data_path(KEY_25519_NOTWEAK_SEC)]) + self.assertEqual(ret, 0) + ret, _, _ = run_proc(GPG, ['--batch', '--homedir', GPGHOME, '-d', data_path(MSG_ES_25519)]) + self.assertNotEqual(ret, 0) + ret, _, _ = run_proc(GPG, ['--batch', '--homedir', GPGHOME, '--yes', '--delete-secret-key', 'dde0ee539c017d2bd3f604a53176fc1486aa2528']) + self.assertEqual(ret, 0) + # Make sure GPG imports tweaked key and successfully decrypts message + ret, _, _ = run_proc(GPG, ['--batch', '--homedir', GPGHOME, '--import', data_path('test_key_edge_cases/key-25519-tweaked-sec.asc')]) + self.assertEqual(ret, 0) + ret, _, _ = run_proc(GPG, ['--batch', '--homedir', GPGHOME, '-d', data_path(MSG_ES_25519)]) + self.assertEqual(ret, 0) + # Generate + pipe = pswd_pipe(PASSWORD) + ret, _, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--pass-fd', str(pipe), '--userid', + 'eddsa_25519', '--generate-key', '--expert'], '22\n') + os.close(pipe) + self.assertEqual(ret, 0) + # Export + ret, out, _ = run_proc(RNPK, ['--homedir', RNPDIR, '--export', '--secret', 'eddsa_25519']) + self.assertEqual(ret, 0) + # Import key with GPG + ret, out, _ = run_proc(GPG, ['--batch', '--homedir', GPGHOME, '--import'], out) + self.assertEqual(ret, 0) + src, dst, dec = reg_workfiles('cleartext', '.txt', '.rnp', '.dec') + # Generate random file of required size + random_text(src, 1000) + # Encrypt and sign with RNP + 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') + self.assertEqual(file_text(src), file_text(dec)) + remove_files(dec) + # Decrypt and verify with GPG + gpg_decrypt_file(dst, dec, 'password') + self.assertEqual(file_text(src), file_text(dec)) + remove_files(dst, dec) + # Encrypt and sign with GnuPG + ret, _, _ = run_proc(GPG, ['--batch', '--homedir', GPGHOME, '--always-trust', '-r', 'eddsa_25519', + '-u', 'eddsa_25519', '--output', dst, '-es', src]) + self.assertEqual(ret, 0) + # Decrypt and verify with RNP + rnp_decrypt_file(dst, dec, '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') + remove_files(dst, dec) + ret, _, err = run_proc(RNP, ['--keyfile', key, '-es', '-r', 'ecc-p256', '-u', 'ecc-p256', '--password', PASSWORD, src, '--output', dst]) + self.assertEqual(ret, 0) + self.assertNotRegex(err, BITS_MSG) + ret, _, err = run_proc(RNP, ['--keyfile', key, '-d', '--password', PASSWORD, dst, '--output', dec]) + self.assertEqual(ret, 0) + self.assertNotRegex(err, BITS_MSG) + # Cleanup + clear_workfiles() + + def test_encryption_aead_defs(self): + if not RNP_AEAD or not RNP_BRAINPOOL: + return + # Encrypt with RNP + pubkey = data_path(KEY_ALICE_SUB_PUB) + src, enc, dec = reg_workfiles('cleartext', '.txt', '.enc', '.dec') + random_text(src, 120000) + ret, _, _ = run_proc(RNP, ['--keyfile', pubkey, '-z', '0', '-r', 'alice', '--aead', '-e', src, '--output', enc]) + self.assertEqual(ret, 0) + # List packets + ret, out, _ = run_proc(RNP, ['--list-packets', enc]) + self.assertEqual(ret, 0) + self.assertRegex(out, r'(?s)^.*tag 20, partial len.*AEAD-encrypted data packet.*version: 1.*AES-256.*OCB.*chunk size: 12.*') + # Attempt to encrypt with too high AEAD bits value + ret, _, err = run_proc(RNP, ['--keyfile', pubkey, '-r', 'alice', '--aead', '--aead-chunk-bits', '17', '-e', src, '--output', enc]) + self.assertEqual(ret, 2) + self.assertRegex(err, r'(?s)^.*Wrong argument value 17 for aead-chunk-bits, must be 0..16.*') + # Attempt to encrypt with wrong AEAD bits value + ret, _, err = run_proc(RNP, ['--keyfile', pubkey, '-r', 'alice', '--aead', '--aead-chunk-bits', 'banana', '-e', src, '--output', enc]) + self.assertEqual(ret, 2) + self.assertRegex(err, r'(?s)^.*Wrong argument value banana for aead-chunk-bits, must be 0..16.*') + # Attempt to encrypt with another wrong AEAD bits value + ret, _, err = run_proc(RNP, ['--keyfile', pubkey, '-r', 'alice', '--aead', '--aead-chunk-bits', '5banana', '-e', src, '--output', enc]) + self.assertEqual(ret, 2) + self.assertRegex(err, r'(?s)^.*Wrong argument value 5banana for aead-chunk-bits, must be 0..16.*') + clear_workfiles() + + def test_encryption_no_wrap(self): + src, sig, enc, dec = reg_workfiles('cleartext', '.txt', '.sig', '.enc', '.dec') + random_text(src, 2000) + # Sign with GnuPG + ret, _, _ = run_proc(GPG, ['--batch', '--homedir', GPGHOME, GPG_LOOPBACK, '--passphrase', PASSWORD, '-u', KEY_ENCRYPT, '--output', sig, '-s', src]) + # Additionally encrypt with RNP + ret, _, _ = run_proc(RNP, ['--homedir', RNPDIR, '-r', 'dummy1@rnp', '--no-wrap', '-e', sig, '--output', enc]) + self.assertEqual(ret, 0) + # List packets + ret, out, err = run_proc(GPG, ['--batch', '--homedir', GPGHOME, GPG_LOOPBACK, '--passphrase', PASSWORD, '--list-packets', enc]) + self.assertEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*gpg: encrypted with .*dummy1@rnp.*') + self.assertRegex(out, r'(?s)^.*:pubkey enc packet: version 3.*:encrypted data packet:.*mdc_method: 2.*' \ + r':compressed packet.*:onepass_sig packet:.*:literal data packet.*:signature packet.*') + # Decrypt with GnuPG + ret, _, err = run_proc(GPG, ['--batch', '--homedir', GPGHOME, GPG_LOOPBACK, '--passphrase', PASSWORD, '--output', dec, '-d', enc]) + self.assertEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*gpg: encrypted with .*dummy1@rnp.*gpg: Good signature from "encryption@rnp".*') + self.assertEqual(file_text(dec), file_text(src)) + remove_files(dec) + # Decrypt with RNP + ret, _, err = run_proc(RNP, ['--homedir', RNPDIR, '--password', PASSWORD, '--output', dec, '-d', enc]) + self.assertEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*Good signature.*uid\s+encryption@rnp.*Signature\(s\) verified successfully.*') + self.assertEqual(file_text(dec), file_text(src)) + clear_workfiles() + +class Compression(unittest.TestCase): + @classmethod + def setUpClass(cls): + # Compression is currently implemented only for encrypted messages + rnp_genkey_rsa(KEY_ENCRYPT) + rnp_genkey_rsa(KEY_SIGN_GPG) + gpg_import_pubring() + gpg_import_secring() + + @classmethod + def tearDownClass(cls): + clear_keyrings() + + def tearDown(self): + clear_workfiles() + + def test_rnp_compression(self): + runs = 30 + levels = list_upto([None, 0, 2, 4, 6, 9], runs) + algosrnp = list_upto([None, 'zip', 'zlib', 'bzip2'], runs) + sizes = list_upto([20, 1000, 5000, 15000, 250000], runs) + + for level, algo, size in zip(levels, algosrnp, sizes): + z = [algo, level] + gpg_to_rnp_encryption(size, None, z) + file_encryption_rnp_to_gpg(size, z) + rnp_signing_gpg_to_rnp(size, z) + +class SignDefault(unittest.TestCase): + ''' + Things to try later: + - different public key algorithms + - different hash algorithms where applicable + - cleartext signing/verification + - detached signing/verification + ''' + # Message sizes to be tested + SIZES = [20, 1000, 5000, 20000, 150000, 1000000] + + @classmethod + def setUpClass(cls): + # Generate keypair in RNP + rnp_genkey_rsa(KEY_SIGN_RNP) + rnp_genkey_rsa(KEY_SIGN_GPG) + gpg_import_pubring() + gpg_import_secring() + + @classmethod + def tearDownClass(cls): + clear_keyrings() + + # TODO: This script should generate one test case per message size. + # Not sure how to do it yet + def test_rnp_to_gpg_default_key(self): + for size in Sign.SIZES: + rnp_signing_rnp_to_gpg(size) + rnp_detached_signing_rnp_to_gpg(size) + rnp_cleartext_signing_rnp_to_gpg(size) + + def test_gpg_to_rnp_default_key(self): + for size in Sign.SIZES: + rnp_signing_gpg_to_rnp(size) + rnp_detached_signing_gpg_to_rnp(size) + rnp_detached_signing_gpg_to_rnp(size, True) + rnp_cleartext_signing_gpg_to_rnp(size) + + def test_rnp_multiple_signers(self): + USERIDS = ['sign1@rnp', 'sign2@rnp', 'sign3@rnp'] + KEYPASS = ['sign1pass', 'sign2pass', 'sign3pass'] + + # Generate multiple keys and import to GnuPG + for uid, pswd in zip(USERIDS, KEYPASS): + rnp_genkey_rsa(uid, 1024, pswd) + + gpg_import_pubring() + gpg_import_secring() + + src, dst, sig, ver = reg_workfiles('cleartext', '.txt', '.rnp', EXT_SIG, '.ver') + # Generate random file of required size + random_text(src, 128000) + + for keynum in range(1, len(USERIDS) + 1): + # Normal signing + rnp_sign_file(src, dst, USERIDS[:keynum], KEYPASS[:keynum]) + gpg_verify_file(dst, ver) + remove_files(ver) + rnp_verify_file(dst, ver) + remove_files(dst, ver) + + # Detached signing + rnp_sign_detached(src, USERIDS[:keynum], KEYPASS[:keynum]) + gpg_verify_detached(src, sig) + rnp_verify_detached(sig) + remove_files(sig) + + # Cleartext signing + rnp_sign_cleartext(src, dst, USERIDS[:keynum], KEYPASS[:keynum]) + gpg_verify_cleartext(dst) + rnp_verify_cleartext(dst) + remove_files(dst) + + clear_workfiles() + + def test_sign_weird_userids(self): + USERIDS = [WEIRD_USERID_SPECIAL_CHARS, WEIRD_USERID_SPACE, WEIRD_USERID_QUOTE, + WEIRD_USERID_SPACE_AND_QUOTE, WEIRD_USERID_QUOTE_AND_SPACE, + WEIRD_USERID_UNICODE_1, WEIRD_USERID_UNICODE_2] + KEYPASS = ['signUnicodePass1', 'signUnicodePass2', 'signUnicodePass3', 'signUnicodePass4', + 'signUnicodePass5', 'signUnicodePass6', 'signUnicodePass7'] + + # Generate multiple keys + for uid, pswd in zip(USERIDS, KEYPASS): + rnp_genkey_rsa(uid, 1024, pswd) + + gpg_import_pubring() + gpg_import_secring() + + src, dst, sig, ver = reg_workfiles('cleartext', '.txt', '.rnp', EXT_SIG, '.ver') + # Generate random file of required size + random_text(src, 128000) + + for keynum in range(1, len(USERIDS) + 1): + # Normal signing + rnp_sign_file(src, dst, USERIDS[:keynum], KEYPASS[:keynum]) + gpg_verify_file(dst, ver) + remove_files(ver) + rnp_verify_file(dst, ver) + remove_files(dst, ver) + + # Detached signing + rnp_sign_detached(src, USERIDS[:keynum], KEYPASS[:keynum]) + gpg_verify_detached(src, sig) + rnp_verify_detached(sig) + remove_files(sig) + + # Cleartext signing + rnp_sign_cleartext(src, dst, USERIDS[:keynum], KEYPASS[:keynum]) + gpg_verify_cleartext(dst) + rnp_verify_cleartext(dst) + remove_files(dst) + + clear_workfiles() + + def test_verify_bad_sig_class(self): + ret, _, err = run_proc(RNP, ['--keyfile', data_path(KEY_ALICE_SEC), '--verify', data_path('test_messages/message.txt.signed-class19')]) + self.assertNotEqual(ret, 0) + self.assertRegex(err, r'(?s)^.*Invalid document signature type: 19.*') + self.assertNotRegex(err, r'(?s)^.*Good signature.*') + self.assertRegex(err, r'(?s)^.*BAD signature.*Signature verification failure: 1 invalid signature') + +class Encrypt(unittest.TestCase, TestIdMixin, KeyLocationChooserMixin): + def _encrypt_decrypt(self, e1, e2, failenc = False, faildec = False): + keyfile, src, enc_out, dec_out = reg_workfiles(self.test_id, '.gpg', + '.in', '.enc', '.dec') + random_text(src, 0x1337) + + if not self.operation_key_location and not self.operation_key_gencmd: + raise RuntimeError("key not found") + + if self.operation_key_location: + self.assertTrue(e1.import_key(self.operation_key_location[0])) + self.assertTrue(e1.import_key(self.operation_key_location[1], True)) + else: + self.assertTrue(e1.generate_key_batch(self.operation_key_gencmd)) + + self.assertTrue(e1.export_key(keyfile, False)) + self.assertTrue(e2.import_key(keyfile)) + self.assertEqual(e2.encrypt(e1.userid, enc_out, src), not failenc) + self.assertEqual(e1.decrypt(dec_out, enc_out), not faildec) + clear_workfiles() + + def setUp(self): + KeyLocationChooserMixin.__init__(self) + self.rnp = Rnp(RNPDIR, RNP, RNPK) + self.gpg = GnuPG(GPGHOME, GPG) + self.rnp.password = self.gpg.password = PASSWORD + self.rnp.userid = self.gpg.userid = self.test_id + AT_EXAMPLE + + @classmethod + def tearDownClass(cls): + clear_keyrings() + +class EncryptElgamal(Encrypt): + + GPG_GENERATE_DSA_ELGAMAL_PATTERN = """ + Key-Type: dsa + Key-Length: {0} + Key-Usage: sign + Subkey-Type: ELG-E + Subkey-Length: {1} + Subkey-Usage: encrypt + Name-Real: Test Testovich + Expire-Date: 1y + Preferences: aes256 sha256 sha384 sha512 sha1 zlib + Name-Email: {2} + """ + + RNP_GENERATE_DSA_ELGAMAL_PATTERN = "16\n{0}\n" + + @staticmethod + def key_pfx(sign_key_size, enc_key_size): + return "GnuPG_dsa_elgamal_%d_%d" % (sign_key_size, enc_key_size) + + def do_test_encrypt(self, sign_key_size, enc_key_size): + pfx = EncryptElgamal.key_pfx(sign_key_size, enc_key_size) + self.operation_key_location = tuple((key_path(pfx, False), key_path(pfx, True))) + self.rnp.userid = self.gpg.userid = pfx + AT_EXAMPLE + # DSA 1024 key uses SHA-1 as hash but verification would succeed till 2024 + self._encrypt_decrypt(self.gpg, self.rnp) + + def do_test_decrypt(self, sign_key_size, enc_key_size): + pfx = EncryptElgamal.key_pfx(sign_key_size, enc_key_size) + self.operation_key_location = tuple((key_path(pfx, False), key_path(pfx, True))) + self.rnp.userid = self.gpg.userid = pfx + AT_EXAMPLE + self._encrypt_decrypt(self.rnp, self.gpg) + + def test_encrypt_P1024_1024(self): self.do_test_encrypt(1024, 1024) + def test_encrypt_P1024_2048(self): self.do_test_encrypt(1024, 2048) + def test_encrypt_P2048_2048(self): self.do_test_encrypt(2048, 2048) + def test_encrypt_P3072_3072(self): self.do_test_encrypt(3072, 3072) + def test_decrypt_P1024_1024(self): self.do_test_decrypt(1024, 1024) + def test_decrypt_P2048_2048(self): self.do_test_decrypt(2048, 2048) + def test_decrypt_P1234_1234(self): self.do_test_decrypt(1234, 1234) + + def test_generate_elgamal_key1024_in_gpg_and_encrypt(self): + cmd = EncryptElgamal.GPG_GENERATE_DSA_ELGAMAL_PATTERN.format(1024, 1024, self.gpg.userid) + self.operation_key_gencmd = cmd + # Will not fail till 2024 since 1024-bit DSA key uses SHA-1 as hash. + self._encrypt_decrypt(self.gpg, self.rnp) + + def test_generate_elgamal_key1536_in_gpg_and_encrypt(self): + cmd = EncryptElgamal.GPG_GENERATE_DSA_ELGAMAL_PATTERN.format(1536, 1536, self.gpg.userid) + self.operation_key_gencmd = cmd + self._encrypt_decrypt(self.gpg, self.rnp) + + def test_generate_elgamal_key1024_in_rnp_and_decrypt(self): + cmd = EncryptElgamal.RNP_GENERATE_DSA_ELGAMAL_PATTERN.format(1024) + self.operation_key_gencmd = cmd + self._encrypt_decrypt(self.rnp, self.gpg) + + +class EncryptEcdh(Encrypt): + + GPG_GENERATE_ECDH_ECDSA_PATTERN = """ + Key-Type: ecdsa + Key-Curve: {0} + Key-Usage: sign auth + Subkey-Type: ecdh + Subkey-Usage: encrypt + Subkey-Curve: {0} + Name-Real: Test Testovich + Expire-Date: 1y + Preferences: aes256 sha256 sha384 sha512 sha1 zlib + Name-Email: {1}""" + + RNP_GENERATE_ECDH_ECDSA_PATTERN = "19\n{0}\n" + + def test_encrypt_nistP256(self): + self.operation_key_gencmd = EncryptEcdh.GPG_GENERATE_ECDH_ECDSA_PATTERN.format( + "nistp256", self.rnp.userid) + self._encrypt_decrypt(self.gpg, self.rnp) + + def test_encrypt_nistP384(self): + self.operation_key_gencmd = EncryptEcdh.GPG_GENERATE_ECDH_ECDSA_PATTERN.format( + "nistp384", self.rnp.userid) + self._encrypt_decrypt(self.gpg, self.rnp) + + def test_encrypt_nistP521(self): + self.operation_key_gencmd = EncryptEcdh.GPG_GENERATE_ECDH_ECDSA_PATTERN.format( + "nistp521", self.rnp.userid) + self._encrypt_decrypt(self.gpg, self.rnp) + + def test_decrypt_nistP256(self): + self.operation_key_gencmd = EncryptEcdh.RNP_GENERATE_ECDH_ECDSA_PATTERN.format(1) + self._encrypt_decrypt(self.rnp, self.gpg) + + def test_decrypt_nistP384(self): + self.operation_key_gencmd = EncryptEcdh.RNP_GENERATE_ECDH_ECDSA_PATTERN.format(2) + self._encrypt_decrypt(self.rnp, self.gpg) + + def test_decrypt_nistP521(self): + self.operation_key_gencmd = EncryptEcdh.RNP_GENERATE_ECDH_ECDSA_PATTERN.format(3) + self._encrypt_decrypt(self.rnp, self.gpg) + +class Sign(unittest.TestCase, TestIdMixin, KeyLocationChooserMixin): + SIZES = [20, 1000, 5000, 20000, 150000, 1000000] + + def _sign_verify(self, e1, e2, failsign = False, failver = False): + ''' + Helper function for Sign verification + 1. e1 creates/loads key + 2. e1 exports key + 3. e2 imports key + 2. e1 signs message + 3. e2 verifies message + + eX == entityX + ''' + keyfile, src, output = reg_workfiles(self.test_id, '.gpg', '.in', '.out') + random_text(src, 0x1337) + + if not self.operation_key_location and not self.operation_key_gencmd: + print(self.operation_key_gencmd) + raise RuntimeError("key not found") + + if self.operation_key_location: + self.assertTrue(e1.import_key(self.operation_key_location[0])) + self.assertTrue(e1.import_key(self.operation_key_location[1], True)) + else: + self.assertTrue(e1.generate_key_batch(self.operation_key_gencmd)) + self.assertTrue(e1.export_key(keyfile, False)) + self.assertTrue(e2.import_key(keyfile)) + self.assertEqual(e1.sign(output, src), not failsign) + self.assertEqual(e2.verify(output), not failver) + clear_workfiles() + + def setUp(self): + KeyLocationChooserMixin.__init__(self) + self.rnp = Rnp(RNPDIR, RNP, RNPK) + self.gpg = GnuPG(GPGHOME, GPG) + self.rnp.password = self.gpg.password = PASSWORD + self.rnp.userid = self.gpg.userid = self.test_id + AT_EXAMPLE + + @classmethod + def tearDownClass(cls): + clear_keyrings() + +class SignECDSA(Sign): + # {0} must be replaced by ID of the curve 3,4 or 5 (NIST-256,384,521) + #CURVES = ["NIST P-256", "NIST P-384", "NIST P-521"] + GPG_GENERATE_ECDSA_PATTERN = """ + Key-Type: ecdsa + Key-Curve: {0} + Key-Usage: sign auth + Name-Real: Test Testovich + Expire-Date: 1y + Preferences: twofish sha512 zlib + Name-Email: {1}""" + + # {0} must be replaced by ID of the curve 1,2 or 3 (NIST-256,384,521) + RNP_GENERATE_ECDSA_PATTERN = "19\n{0}\n" + + def test_sign_P256(self): + cmd = SignECDSA.RNP_GENERATE_ECDSA_PATTERN.format(1) + self.operation_key_gencmd = cmd + self._sign_verify(self.rnp, self.gpg) + + def test_sign_P384(self): + cmd = SignECDSA.RNP_GENERATE_ECDSA_PATTERN.format(2) + self.operation_key_gencmd = cmd + self._sign_verify(self.rnp, self.gpg) + + def test_sign_P521(self): + cmd = SignECDSA.RNP_GENERATE_ECDSA_PATTERN.format(3) + self.operation_key_gencmd = cmd + self._sign_verify(self.rnp, self.gpg) + + def test_verify_P256(self): + cmd = SignECDSA.GPG_GENERATE_ECDSA_PATTERN.format("nistp256", self.rnp.userid) + self.operation_key_gencmd = cmd + self._sign_verify(self.gpg, self.rnp) + + def test_verify_P384(self): + cmd = SignECDSA.GPG_GENERATE_ECDSA_PATTERN.format("nistp384", self.rnp.userid) + self.operation_key_gencmd = cmd + self._sign_verify(self.gpg, self.rnp) + + def test_verify_P521(self): + cmd = SignECDSA.GPG_GENERATE_ECDSA_PATTERN.format("nistp521", self.rnp.userid) + self.operation_key_gencmd = cmd + self._sign_verify(self.gpg, self.rnp) + + def test_hash_truncation(self): + ''' + Signs message hashed with SHA512 with a key of size 256. Implementation + truncates leftmost 256 bits of a hash before signing (see FIPS 186-4, 6.4) + ''' + cmd = SignECDSA.RNP_GENERATE_ECDSA_PATTERN.format(1) + rnp = self.rnp.copy() + rnp.hash = 'SHA512' + self.operation_key_gencmd = cmd + self._sign_verify(rnp, self.gpg) + +class SignDSA(Sign): + # {0} must be replaced by ID of the curve 3,4 or 5 (NIST-256,384,521) + #CURVES = ["NIST P-256", "NIST P-384", "NIST P-521"] + GPG_GENERATE_DSA_PATTERN = """ + Key-Type: dsa + Key-Length: {0} + Key-Usage: sign auth + Name-Real: Test Testovich + Expire-Date: 1y + Preferences: twofish sha256 sha384 sha512 sha1 zlib + Name-Email: {1}""" + + # {0} must be replaced by ID of the curve 1,2 or 3 (NIST-256,384,521) + RNP_GENERATE_DSA_PATTERN = "17\n{0}\n" + + @staticmethod + def key_pfx(p): return "GnuPG_dsa_elgamal_%d_%d" % (p, p) + + def do_test_sign(self, p_size): + pfx = SignDSA.key_pfx(p_size) + self.operation_key_location = tuple((key_path(pfx, False), key_path(pfx, True))) + self.rnp.userid = self.gpg.userid = pfx + AT_EXAMPLE + # DSA 1024-bit key uses SHA-1 so verification would not fail till 2024 + self._sign_verify(self.rnp, self.gpg) + + def do_test_verify(self, p_size): + pfx = SignDSA.key_pfx(p_size) + self.operation_key_location = tuple((key_path(pfx, False), key_path(pfx, True))) + self.rnp.userid = self.gpg.userid = pfx + AT_EXAMPLE + # DSA 1024-bit key uses SHA-1, but verification would fail since SHA1 is used by GnuPG + self._sign_verify(self.gpg, self.rnp, False, p_size <= 1024) + + def test_sign_P1024_Q160(self): self.do_test_sign(1024) + def test_sign_P2048_Q256(self): self.do_test_sign(2048) + def test_sign_P3072_Q256(self): self.do_test_sign(3072) + def test_sign_P2112_Q256(self): self.do_test_sign(2112) + + def test_verify_P1024_Q160(self): self.do_test_verify(1024) + def test_verify_P2048_Q256(self): self.do_test_verify(2048) + def test_verify_P3072_Q256(self): self.do_test_verify(3072) + def test_verify_P2112_Q256(self): self.do_test_verify(2112) + + def test_sign_P1088_Q224(self): + self.operation_key_gencmd = SignDSA.RNP_GENERATE_DSA_PATTERN.format(1088) + self._sign_verify(self.rnp, self.gpg) + + def test_verify_P1088_Q224(self): + self.operation_key_gencmd = SignDSA.GPG_GENERATE_DSA_PATTERN.format("1088", self.rnp.userid) + self._sign_verify(self.gpg, self.rnp) + + def test_hash_truncation(self): + ''' + Signs message hashed with SHA512 with a key of size 160 bits. Implementation + truncates leftmost 160 bits of a hash before signing (see FIPS 186-4, 4.2) + ''' + rnp = self.rnp.copy() + rnp.hash = 'SHA512' + self.operation_key_gencmd = SignDSA.RNP_GENERATE_DSA_PATTERN.format(1024) + self._sign_verify(rnp, self.gpg) + +class EncryptSignRSA(Encrypt, Sign): + + GPG_GENERATE_RSA_PATTERN = """ + Key-Type: rsa + Key-Length: {0} + Key-Usage: sign auth + Subkey-Type: rsa + Subkey-Length: {0} + Subkey-Usage: encrypt + Name-Real: Test Testovich + Expire-Date: 1y + Preferences: twofish sha256 sha384 sha512 sha1 zlib + Name-Email: {1}""" + + RNP_GENERATE_RSA_PATTERN = "1\n{0}\n" + + @staticmethod + def key_pfx(p): return "GnuPG_rsa_%d_%d" % (p, p) + + def do_encrypt_verify(self, key_size): + pfx = EncryptSignRSA.key_pfx(key_size) + self.operation_key_location = tuple((key_path(pfx, False), key_path(pfx, True))) + self.rnp.userid = self.gpg.userid = pfx + AT_EXAMPLE + self._encrypt_decrypt(self.gpg, self.rnp) + self._sign_verify(self.gpg, self.rnp) + + def do_rnp_decrypt_sign(self, key_size): + pfx = EncryptSignRSA.key_pfx(key_size) + self.operation_key_location = tuple((key_path(pfx, False), key_path(pfx, True))) + self.rnp.userid = self.gpg.userid = pfx + AT_EXAMPLE + self._encrypt_decrypt(self.rnp, self.gpg) + self._sign_verify(self.rnp, self.gpg) + + def test_rnp_encrypt_verify_1024(self): self.do_encrypt_verify(1024) + def test_rnp_encrypt_verify_2048(self): self.do_encrypt_verify(2048) + def test_rnp_encrypt_verify_4096(self): self.do_encrypt_verify(4096) + + def test_rnp_decrypt_sign_1024(self): self.do_rnp_decrypt_sign(1024) + def test_rnp_decrypt_sign_2048(self): self.do_rnp_decrypt_sign(2048) + def test_rnp_decrypt_sign_4096(self): self.do_rnp_decrypt_sign(4096) + + def setUp(self): + Encrypt.setUp(self) + + @classmethod + def tearDownClass(cls): + Encrypt.tearDownClass() + +def test_suites(tests): + if hasattr(tests, '__iter__'): + for x in tests: + for y in test_suites(x): + yield y + else: + yield tests.__class__.__name__ + +# Main thinghy + +if __name__ == '__main__': + main = unittest.main + if not hasattr(main, 'USAGE'): + main.USAGE = '' + main.USAGE += ''.join([ + "\nRNP test client specific flags:\n", + " -w,\t\t Don't remove working directory\n", + " -d,\t\t Enable debug messages\n"]) + + LEAVE_WORKING_DIRECTORY = ("-w" in sys.argv) + if LEAVE_WORKING_DIRECTORY: + # -w must be removed as unittest doesn't expect it + sys.argv.remove('-w') + else: + LEAVE_WORKING_DIRECTORY = os.getenv('RNP_KEEP_TEMP') is not None + + LVL = logging.INFO + if "-d" in sys.argv: + sys.argv.remove('-d') + LVL = logging.DEBUG + + # list suites + if '-ls' in sys.argv: + tests = unittest.defaultTestLoader.loadTestsFromModule(sys.modules[__name__]) + for suite in set(test_suites(tests)): + print(suite) + sys.exit(0) + + setup(LVL) + res = main(exit=False) + + if not LEAVE_WORKING_DIRECTORY: + try: + if RMWORKDIR: + shutil.rmtree(WORKDIR) + else: + shutil.rmtree(RNPDIR) + shutil.rmtree(GPGDIR) + except Exception: + # Ignore exception if something cannot be deleted + pass + + sys.exit(not res.result.wasSuccessful()) |