From f395e44a7bf831cf9286623039525c05c7d00570 Mon Sep 17 00:00:00 2001 From: Patrick Uiterwijk Date: Apr 23 2018 19:48:20 +0000 Subject: Implement decryption Signed-off-by: Patrick Uiterwijk --- diff --git a/ChangeLog b/ChangeLog index ac41882..226cbea 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,4 +1,6 @@ 2018-04-23 Patrick Uiterwijk + * src/*: Add decrypt method + * tests/: Updates for the new RPM signature status output * src/server_common.py: Rename gpg_decrypt to gpg_decrypt_symmetric diff --git a/src/bridge.py b/src/bridge.py index 2ed3203..e2006b8 100644 --- a/src/bridge.py +++ b/src/bridge.py @@ -1071,6 +1071,7 @@ request_types = { 'sign-data': RT((SF('key'), BoolField('armor', optional=True)), max_payload=1024 * 1024 * 1024 * 1024), + 'decrypt': RT((SF('key'),), max_payload=1024 * 1024 * 1024), 'sign-git-tag': RT((SF('key'),), max_payload=1024 * 1024 * 1024), 'sign-container': RT((SF('key'), diff --git a/src/client.py b/src/client.py index 5d5107e..318ae79 100644 --- a/src/client.py +++ b/src/client.py @@ -1077,6 +1077,43 @@ def cmd_sign_data(conn, args): o2.output, e.strerror)) +def cmd_decrypt(conn, args): + p2 = optparse.OptionParser(usage='%prog decrypt [options] input_file', + description='Decrypt a file') + p2.add_option('-o', '--output', metavar='FILE', + help='Write output to this file') + (o2, args) = p2.parse_args(args) + if len(args) != 2: + p2.error('key name and input file path expected') + if o2.output is None and sys.stdout.isatty(): + p2.error('won\'t write output to a TTY, specify a file name') + + passphrase = read_key_passphrase(conn.config) + try: + f = open(args[1], 'rb') + except IOError as e: + raise ClientError( + 'Error opening {0!s}: {1!s}'.format( + args[1], e.strerror)) + + try: + conn.connect('decrypt', {'key': safe_string(args[0])}) + conn.send_payload_from_file(f) + finally: + f.close() + conn.send_inner({'passphrase': passphrase}) + conn.read_response() + try: + if o2.output is None: + conn.write_payload_to_file(sys.stdout) + else: + utils.write_new_file(o2.output, conn.write_payload_to_file) + except IOError as e: + raise ClientError( + 'Error writing to {0!s}: {1!s}'.format( + o2.output, e.strerror)) + + def call_git(args, stdin=None, ignore_error=False, strip_newline=False): cmd = ['git'] cmd.extend(args) @@ -1583,6 +1620,7 @@ command_handlers = { 'change-passphrase': (cmd_change_passphrase, 'Change key passphrase'), 'sign-text': (cmd_sign_text, 'Output a cleartext signature of a text'), 'sign-data': (cmd_sign_data, 'Create a detached signature'), + 'decrypt': (cmd_decrypt, 'Decrypt an encrypted file'), 'sign-git-tag': (cmd_sign_git_tag, 'Sign a git tag'), 'sign-container': (cmd_sign_container, 'Sign an atomic docker container'), 'sign-ostree': (cmd_sign_ostree, 'Sign an OSTree commit object'), diff --git a/src/errors.py b/src/errors.py index 7b5a7b5..d03348d 100644 --- a/src/errors.py +++ b/src/errors.py @@ -30,6 +30,7 @@ CORRUPT_RPM = 11 UNAUTHENTICATED_RPM = 12 INVALID_IMPORT = 13 IMPORT_PASSPHRASE_ERROR = 14 +DECRYPT_FAILED = 15 _messages = { OK: 'No error', @@ -47,6 +48,7 @@ _messages = { UNAUTHENTICATED_RPM: 'Missing RPM file authentication by client', INVALID_IMPORT: 'Invalid import file', IMPORT_PASSPHRASE_ERROR: 'Import passphrase does not match', + DECRYPT_FAILED: 'Decryption failed', } diff --git a/src/server.py b/src/server.py index 9f01a11..6f1cdd3 100644 --- a/src/server.py +++ b/src/server.py @@ -1369,6 +1369,27 @@ def cmd_sign_data(db, conn): @request_handler(payload_storage=RequestHandler.PAYLOAD_FILE) +def cmd_decrypt(db, conn): + (access, key_passphrase) = conn.authenticate_user(db) + cleartext_file = tempfile.TemporaryFile() + try: + server_common.gpg_decrypt(conn.config, + cleartext_file, + conn.payload_file, + access.key.fingerprint, + key_passphrase) + logging.info('Decrypted data with key %s', + access.key.name) + conn.send_reply_header(errors.OK, {}) + conn.send_reply_payload_from_file(cleartext_file) + except (server_common.GPGError, gpgme.GpgmeError): + conn.send_reply_header(errors.DECRYPT_FAILED, {}) + conn.send_reply_ok_only() + finally: + cleartext_file.close() + + +@request_handler(payload_storage=RequestHandler.PAYLOAD_FILE) def cmd_sign_git_tag(db, conn): # An annotated tag object looks like: # object xxxxxxx diff --git a/src/server_common.py b/src/server_common.py index 455fcb9..c6bc4a6 100644 --- a/src/server_common.py +++ b/src/server_common.py @@ -69,9 +69,19 @@ class ServerBaseConfiguration(utils.DaemonIDConfiguration, 'not an existing file' % self.database_path) -# Database + +# General utilities +def _handle_errlist(errlist): + if len(errlist) == 1: + raise errlist[0] + elif len(errlist) == 0: + logging.error('Raising original') + raise + elif len(errlist) > 1: + raise Exception('Multiple errors occured: %s' % errlist) +# Database class User(object): def __init__(self, name, clear_password=None, admin=False): @@ -309,11 +319,25 @@ def _gpg_open(config): return ctx -def _gpg_set_passphrase(ctx, passphrase): +def _gpg_set_passphrase(ctx, passphrase, fingerprint=None, errlist=None): '''Let ctx use passphrase.''' - def cb(unused_uid_int, unused_info, prev_was_bad, fd): + def cb(unused_uid_int, info, prev_was_bad, fd): if prev_was_bad: - return gpgme.ERR_CANCELED + if fingerprint: + correct_key = False + for kid in info.split()[:2]: + if fingerprint.endswith(kid): + correct_key = True + break + if not correct_key: + error = GPGError( + 'Requested key %s not in unlocked keys %s' + % (fingerprint, info)) + if errlist is not None: + errlist.append(error) + raise error + else: + raise GPGError('Key passphrase incorrect?') data = passphrase + '\n' while len(data) > 0: run = os.write(fd, data) @@ -558,7 +582,7 @@ def gpg_signature(config, signature_file, cleartext_file, fingerprint, ''' ctx = _gpg_open(config) - _gpg_set_passphrase(ctx, passphrase) + _gpg_set_passphrase(ctx, passphrase, fingerprint) key = ctx.get_key(fingerprint, True) ctx.signers = (key,) ctx.armor = armor @@ -566,6 +590,25 @@ def gpg_signature(config, signature_file, cleartext_file, fingerprint, ctx.sign(cleartext_file, signature_file, gpgme.SIG_MODE_NORMAL) +def gpg_decrypt(config, cleartext_file, encrypted_file, fingerprint, + passphrase): + '''Decrypt an encrypted file. + + Decrypt contents of encrypyted_file, write the cleartext to cleartext_file. + Use key with fingerprint and passphrase. + + ''' + errlist = [] + ctx = _gpg_open(config) + _gpg_set_passphrase(ctx, passphrase, fingerprint, errlist) + key = ctx.get_key(fingerprint, True) + ctx.textmode = False + try: + ctx.decrypt(encrypted_file, cleartext_file) + except: + _handle_errlist(errlist) + + def gpg_clearsign( config, signed_file, @@ -579,7 +622,7 @@ def gpg_clearsign( ''' ctx = _gpg_open(config) - _gpg_set_passphrase(ctx, passphrase) + _gpg_set_passphrase(ctx, passphrase, fingerprint) key = ctx.get_key(fingerprint, True) ctx.signers = (key,) ctx.sign(cleartext_file, signed_file, gpgme.SIG_MODE_CLEAR) @@ -594,7 +637,7 @@ def gpg_detached_signature(config, signature_file, cleartext_file, fingerprint, ''' ctx = _gpg_open(config) - _gpg_set_passphrase(ctx, passphrase) + _gpg_set_passphrase(ctx, passphrase, fingerprint) key = ctx.get_key(fingerprint, True) ctx.signers = (key,) ctx.armor = armor diff --git a/tests/decrypt.at b/tests/decrypt.at new file mode 100644 index 0000000..29d14f1 --- /dev/null +++ b/tests/decrypt.at @@ -0,0 +1,79 @@ +# Copyright (C) 2018 Red Hat, Inc. All rights reserved. +# +# This copyrighted material is made available to anyone wishing to use, modify, +# copy, or redistribute it subject to the terms and conditions of the GNU +# General Public License v.2. This program is distributed in the hope that it +# will be useful, but WITHOUT ANY WARRANTY expressed or implied, including the +# implied warranties of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU General Public License for more details. You should have +# received a copy of the GNU General Public License along with this program; if +# not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth +# Floor, Boston, MA 02110-1301, USA. Any Red Hat trademarks that are +# incorporated in the source code or documentation are not subject to the GNU +# General Public License and may only be used or replicated with the express +# permission of Red Hat, Inc. +# +# Red Hat Author: Patrick Uiterwijk + +AT_SETUP([Decrypt check]) + +m4_include([include_setup.at]) +m4_include([include_start.at]) +m4_include([include_importedkey.at]) + +# Import new-key-substitute.asc +AT_CHECK([[printf 'rootroot\0keyroot\0imported-key-pw\0' | \ + sigul -c client/client.conf --batch -v -v \ + import-key secondary-key \ + "$abs_srcdir/tests/new-key-substitute.asc"]]) +AT_CHECK([printf 'rootroot\0' | \ + sigul -c client/client.conf --batch -v -v \ + list-keys], , +[imported-key +secondary-key +]) +AT_CHECK([gpg -q --homedir gnupg --import \ + $abs_srcdir/tests/imported-public-key.asc]) + +# Correctly decrypt armored text +AT_CHECK([printf 'imported-key-pw\0' | \ + sigul -c client/client.conf --batch -v -v \ + decrypt imported-key $abs_srcdir/tests/encrypted-armored.asc], + 0, + [Some foo Text bar +], + []) + +# Correctly decrypt binary text +AT_CHECK([printf 'imported-key-pw\0' | \ + sigul -c client/client.conf --batch -v -v \ + decrypt imported-key $abs_srcdir/tests/encrypted-raw.asc], + 0, + [Some other encrypted text +], + []) + +# NOTE: BE CAREFUL: The following two tests need to always be identical in +# their returned error message/code. Otherwise, a malicious user could make +# conclusions regarding which keys this sigul server has or does not have. + +# Fail decrypting encrypted for wrong key +AT_CHECK([printf 'imported-key-pw\0' | \ + sigul -c client/client.conf --batch -v -v \ + decrypt imported-key $abs_srcdir/tests/encrypted-wrongkey.asc], + 1, + [], + [Error: Decryption failed +]) + +# Fail decrypting encrypted for unknown key altogether +AT_CHECK([printf 'imported-key-pw\0' | \ + sigul -c client/client.conf --batch -v -v \ + decrypt imported-key $abs_srcdir/tests/encrypted-unknown.asc], + 1, + [], + [Error: Decryption failed +]) + + +m4_include([include_cleanup.at]) diff --git a/tests/encrypted-armored.asc b/tests/encrypted-armored.asc new file mode 100644 index 0000000..bd4e2b3 --- /dev/null +++ b/tests/encrypted-armored.asc @@ -0,0 +1,12 @@ +-----BEGIN PGP MESSAGE----- + +hQEMA51KpmxgEb/6AQf9Gr9rDREQrjKyzbguQfdnuxDxU3+TURL1gScgMHok+ihK +2aWYimH/Y3Wk1JM6tjVVEFYwLQSfn/dcMBi5vxpYbSvjk8nEzi/vbJ/y0vPtLRxg +fV9U+kqPavoQIVh1a5R9C5LImCaEieV6nPlywlLLljv8jeiRrexKyYDZlbfllCMG +rdoowIn2tTvAu52ujczcHdSJ40kxpB7TNDhYe3gsHz6bpY1SxhwWaHnNreBB9H8T +5rvFIlo2F4We1q7hTgmWSXu0UhkYrVmKxSXx/aa0+Qa6IvnTYNnc2i+g0JWDzJih +bLY4jO+MG+S0Ttf6Ycqjve3R8AsCLfUKnNUgbdG2g9JNAUwyI868CJzwS4kkrFzj +bIRk18Ebw5gMBypCOC8dJNiWjbcvirkLrmPdhFOB3EnSoU+z5nGHkvSCDdK6zUhv +7Ge0n5Kb0cMmzuS5hSY= +=HpvF +-----END PGP MESSAGE----- diff --git a/tests/encrypted-raw.asc b/tests/encrypted-raw.asc new file mode 100644 index 0000000..321a3b5 Binary files /dev/null and b/tests/encrypted-raw.asc differ diff --git a/tests/encrypted-unknown.asc b/tests/encrypted-unknown.asc new file mode 100644 index 0000000..c6f1c15 --- /dev/null +++ b/tests/encrypted-unknown.asc @@ -0,0 +1,17 @@ +-----BEGIN PGP MESSAGE----- + +hQIMA8FvU2ezxxEmARAAy2+juNmNuN38xWrLnrwkSphnDxs21BisgkMqSTEuaXdf +zom+0X4hCbPn2JEI0ZZMvyprNVuWwZZI/AaDbaYOxU4pobGjKwEjS1V38EuhO7eb +FCDjHLOT0tRzaLy6hxnJScG0Z1Q0ZC8AkanB/y8KSYsy9riXvm57u3SaX+MdgUMp +TX7J2VeMhL+Is9ryLNUfqZw3D2muzBwFg3VEztWaPtqIw9C4YF2Z8Iel2arBVGvN +SeFiN6guCIJkRLcACHxA8WROd4XJz4OQgKjRkgKKJcn6u5hqWve5m3cEO8zINpkX +lC7Ikp+sGDM0+3VQVs5laSUgiaCG9lGfE1eYrYTdospM5tKrPmWITfO07UNztZ9x +LGIAvHAI0D0uCHrPMHDkoGIvZjtYXfOwutYGDqPrysWc1vhKZA0eZAm4dDYHSBpd +eo5bz4OWNjTvsNLqcuEKW3XKJwKmNgVCIJIC/qBxT7TkaQ/B0iv/y3yzQG1l4XVG +Alao6VWr6h1VGxSrCb8Z6HptqI6zzf0C/F0zNQQ8ipFUWNLdYB8b3GclKoyJPcR9 +Qo2HN+dG1gZEYo7BiAH08tIwdLl03OM+VPfAnKRmP22JWhcdwyQapDsF2sldz/I6 +Q6rYtXA6/aEI0XBI4vv+ogkoLtwQGyZB6HZJHQ5cmWH7h+nyqbTJ45DHuOpGmi3S +UwGv2srWwWjhM3gSn0ULweAuJW9pbfnwz+AYBbgJTotSAr4jXw1e18lD4KaljcgE +G5nzmPjPVBgXNmGA+0STSDKL6ERh3BY8GMVmfH4kz+/W8L56 +=a5PS +-----END PGP MESSAGE----- diff --git a/tests/encrypted-wrongkey.asc b/tests/encrypted-wrongkey.asc new file mode 100644 index 0000000..491d7bb --- /dev/null +++ b/tests/encrypted-wrongkey.asc @@ -0,0 +1,12 @@ +-----BEGIN PGP MESSAGE----- + +hQEMA5s2fFaYj3u0AQgAnJfGuIR9kDYZ9ik4rJa9BxkayftiJc8ua8SUxOVkXuCS +DbocUEYeWViDY/s7M/6cHcGydcyKzyj8HtOZOjMxvMq19mfE4D7yqGZbYK1UUepu +GTeU51j7l45229PymMmIjtEQgxX1gjxMdOjFcbLGSmpwQx40/fmLuPcFNBirP8Lk +WZlmjtUy+RezZqzYE9GNKinypiUeKhqRbKlMm7w9Y1C7DQg7hVoTB+NcLB/gtenq +/uK+daxPsnXHqjERIIe696LCc6NUfJw+OewSBMBqgo63YdHzPIfmzWLYXTYVnjo+ +X1a40Xjk5rTV0sNdXWuVMS3gJGoAojWjyiz3556IvNJdAZGmWLbzaxEP7s/iVhWc +engZiOd6tJuOLg9YwYeiUU/GpvnoAesTJUFgO5H/A7RcnHRue9Mt0i6+9mDDoMG6 +JpfhyWXTDfOxscGwphT0oa4UOb9n2whIWxkBIpkL +=j1Ds +-----END PGP MESSAGE----- diff --git a/tests/testsuite.at b/tests/testsuite.at index fe66236..8f4f9e6 100644 --- a/tests/testsuite.at +++ b/tests/testsuite.at @@ -21,6 +21,7 @@ AT_TESTED([sigul sigul_bridge sigul_server sigul_server_create_db] dnl m4_include([analysis.at]) m4_include([basic.at]) +m4_include([decrypt.at]) m4_include([versions.at]) m4_include([sign-rpms.at]) m4_include([ostree.at])