# HG changeset patch # User Paul Boddie # Date 1350773020 -7200 # Node ID ceadc72b61774edeed75a79130a6fd9000e75763 # Parent 4de4d2c11f523f56ce215a7a5b8c66a452955472 Improved the handling of gpg invocation failures. Added support for authenticating contributions sent by messages. Added test programs for various GPG instance methods. Added some documentation. diff -r 4de4d2c11f52 -r ceadc72b6177 MoinMessage.py --- a/MoinMessage.py Sat Oct 20 19:27:25 2012 +0200 +++ b/MoinMessage.py Sun Oct 21 00:43:40 2012 +0200 @@ -72,14 +72,23 @@ cmd = Popen(["gpg"] + self.conf_args + list(args), stdin=PIPE, stdout=PIPE, stderr=PIPE) - if text: - cmd.stdin.write(text) - cmd.stdin.close() + try: + # Attempt to write input to the command and to read output from the + # command. + + try: + if text: + cmd.stdin.write(text) + cmd.stdin.close() - self.errors = cmd.stderr.read() + text = cmd.stdout.read() + + # I/O errors can indicate the failure of the command. - try: - text = cmd.stdout.read() + except IOError: + pass + + self.errors = cmd.stderr.read() # Test for a zero result. @@ -117,16 +126,24 @@ # Return the details of the signing key. + identity = None + fingerprint = None + for line in text.split("\n"): try: - prefix, msgtype, fingerprint, details = line.strip().split(" ", 3) + prefix, msgtype, digest, details = line.strip().split(" ", 3) except ValueError: continue # Return the fingerprint and identity details. if msgtype == "GOODSIG": - return fingerprint, details + identity = details + elif msgtype == "VALIDSIG": + fingerprint = digest + + if identity and fingerprint: + return fingerprint, identity return None diff -r 4de4d2c11f52 -r ceadc72b6177 README.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/README.txt Sun Oct 21 00:43:40 2012 +0200 @@ -0,0 +1,156 @@ +Introduction +------------ + +MoinMessage provides a library for creating, signing, encrypting, decrypting, +verifying PGP/GPG content in Python along with mechanisms for updating +MoinMoin Wiki instances with such content such that contributors can be +identified from their PGP signatures and such details used to authenticate +their contributions. + +Configuring GPG for a Wiki +-------------------------- + +Initialise a homedir for GPG and configure it using ACL (access control list) +properties: + +./scripts/init_wiki_keyring.sh + +To be in any way useful, signing keys must be made available within this +homedir so that incoming messages can have their senders verified. + +To see the keys available to you in your own environment: + +gpg --list-keys --with-fingerprint + +The full fingerprints are used when defining a user mapping in the Wiki, and +the --with-fingerprint option is used to show them. Otherwise, only the last +eight characters of the fingerprints are shown. + +Export the public key used when signing messages from your own environment: + +gpg --armor --output 1C1AAF83.asc --export 1C1AAF83 + +Import the key into the Wiki's GPG homedir: + +gpg --homedir wiki/gnupg --import 1C1AAF83.asc + +For the Wiki to receive encrypted data, a key for the Wiki must be created: + +gpg --homedir wiki/gnupg --gen-key + +Export the Wiki's key for encrypting messages sent to the Wiki: + +gpg --homedir wiki/gnupg --armor --output 0891463A.asc --export 0891463A + +This exported key can now be imported into your own environment: + +gpg --import 0891463A.asc + +Configuring the Wiki +-------------------- + +In the Wiki configuration, define the following settings: + + moinmessage_gpg_homedir + This sets the path to the homedir initialised above. + + moinmessage_gpg_users_page (optional, default is MoinMessageUserDict) + This provides a mapping from key fingerprints to Moin usernames. + +The Fingerprint-to-Username Mapping +----------------------------------- + +The mapping from fingerprints to usernames is a WikiDict page having the +following general format: + + fingerprint:: username + +Each fingerprint must exclude space characters and correspond to the +fingerprint shown for a key in the available key listing generated above. + +Each username must correspond to a registered user in the Wiki. + +Quick Start: Signing, Encrypting and Sending Messages +----------------------------------------------------- + +To send a message signed and encrypted to a resource on localhost: + +python tests/test_send.py 1C1AAF83 0891463A localhost /wiki/ShareTest \ + 'An update to the Wiki.' 'Another update.' + +Here, the first identifier is a reference to the signing key (over which you +have complete control), and the second identifier is a reference to the +encryption key (which is a public key published for the Wiki). + +This needs password protection to be removed from the secret key in the Web +server environment, and so uses a modified trust model when invoking gpg. + +Below, the mechanisms employed are illustrated through the use of the other +test programs. + +Signing +------- + +Prepare a message signed with a "detached signature" (note that this does not +seem to be what gpg calls a detached signature with the --detach-sig option): + + python tests/test_message.py 'An update to the Wiki.' 'Another update.' \ +| python tests/test_sign.py 1C1AAF83 + +The complicated recipe based on the individual operations is as follows: + + python tests/test_message.py 'An update to the Wiki.' 'Another update.' \ +> test.txt \ +&& cat test.txt \ +| gpg --armor -u 1C1AAF83 --detach-sig \ +| python tests/test_sign_wrap.py test.txt + +Encryption +---------- + +Prepare a message with an encrypted payload using the above key: + + python tests/test_message.py 'An update to the Wiki.' 'Another update.' \ +| python tests/test_encrypt.py 0891463A + +The complicated recipe based on the individual operations is as follows: + + python tests/test_message.py 'An update to the Wiki.' 'Another update.' \ +> test.txt \ +&& cat test.txt \ +| gpg --armor -r 0891463A --encrypt --trust-model always \ +| python tests/test_encrypt_wrap.py + +Note that "--trust-model always" is used only to avoid prompting issues. + +Signing and Encrypting +---------------------- + +Send a message signed and encrypted: + +python tests/test_send.py 1C1AAF83 0891463A localhost /wiki/ShareTest + + python tests/test_message.py 'An update to the Wiki.' 'Another update.' \ +| python tests/test_sign.py 1C1AAF83 \ +| python tests/test_encrypt.py 0891463A + +The complicated recipe based on the individual operations is as follows: + + python tests/test_message.py 'An update to the Wiki.' 'Another update.' \ +> test.txt \ +&& cat test.txt \ +| gpg --armor -u 1C1AAF83 --detach-sig \ +| python tests/test_sign_wrap.py test.txt \ +| gpg --armor -r 0891463A --encrypt --trust-model always \ +| python tests/test_encrypt_wrap.py + +Posting a Message +----------------- + +To post a signed and/or encrypted message, output from the above activities +can be piped into the following command: + +python tests/test_post.py localhost /wiki/ShareTest + +Here, the resource "/wiki/ShareTest" on localhost is presented with the +message. diff -r 4de4d2c11f52 -r ceadc72b6177 actions/PostMessage.py --- a/actions/PostMessage.py Sat Oct 20 19:27:25 2012 +0200 +++ b/actions/PostMessage.py Sun Oct 21 00:43:40 2012 +0200 @@ -8,6 +8,7 @@ from MoinMoin.PageEditor import PageEditor from MoinMoin.log import getLogger +from MoinMoin.user import User from MoinSupport import * from MoinMessage import GPG, MoinMessageError from email.parser import Parser @@ -156,16 +157,41 @@ # Verify the message. try: - gpg.verifyMessage(signature.get_payload(), content.as_string()) + fingerprint, identity = gpg.verifyMessage(signature.get_payload(), content.as_string()) + + # Map the fingerprint to a Wiki user. + + old_user = None + request = self.request - # Log non-fatal errors. + try: + if fingerprint: + gpg_users = getWikiDict( + getattr(request.cfg, "moinmessage_gpg_users_page", "MoinMessageUserDict"), + request + ) + + # With a user mapping and a fingerprint corresponding to a known + # user, temporarily switch user in order to make the edit. - if gpg.errors: - getLogger(__name__).warning(gpg.errors) + if gpg_users and gpg_users.has_key(fingerprint): + old_user = request.user + request.user = User(request, auth_method="gpg", auth_username=gpg_users[fingerprint]) + + # Log non-fatal errors. + + if gpg.errors: + getLogger(__name__).warning(gpg.errors) - # Handle the embedded message. + # Handle the embedded message. + + self.handle_message_content(content) - self.handle_message_content(content) + # Restore any user identity. + + finally: + if old_user: + request.user = old_user # Otherwise, reject the unverified message. @@ -232,11 +258,15 @@ page_editor = PageEditor(self.request, self.pagename) page_editor.saveText("\n\n".join(body), 0) + # Refresh the page. + + self.page = Page(self.request, self.pagename) + def get_homedir(self): "Locate the GPG home directory." - homedir = getattr(self.request.cfg, "postmessage_gpg_homedir") + homedir = getattr(self.request.cfg, "moinmessage_gpg_homedir") if not homedir: writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type") request.write("Encoded data cannot currently be understood. Please notify the site administrator.") diff -r 4de4d2c11f52 -r ceadc72b6177 tests/test_encrypt.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test_encrypt.py Sun Oct 21 00:43:40 2012 +0200 @@ -0,0 +1,18 @@ +#!/usr/bin/env python + +from MoinMessage import GPG +from email.parser import Parser +import sys + +if __name__ == "__main__": + keyid = sys.argv[1] + message = Parser().parse(sys.stdin) + + gpg = GPG() + text = gpg.encryptMessage(message, keyid) + + # Show the resulting message text. + + print text + +# vim: tabstop=4 expandtab shiftwidth=4 diff -r 4de4d2c11f52 -r ceadc72b6177 tests/test_message.py --- a/tests/test_message.py Sat Oct 20 19:27:25 2012 +0200 +++ b/tests/test_message.py Sun Oct 21 00:43:40 2012 +0200 @@ -2,16 +2,14 @@ from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText +import sys if __name__ == "__main__": message = MIMEMultipart() - text1 = MIMEText("An update to the Wiki.", "moin") - - text2 = MIMEText("Another update to the Wiki.", "moin") - - message.attach(text1) - message.attach(text2) + for arg in sys.argv[1:]: + text = MIMEText(arg, "moin") + message.attach(text) text = message.as_string() print text diff -r 4de4d2c11f52 -r ceadc72b6177 tests/test_send.py --- a/tests/test_send.py Sat Oct 20 19:27:25 2012 +0200 +++ b/tests/test_send.py Sun Oct 21 00:43:40 2012 +0200 @@ -9,10 +9,17 @@ recipient = sys.argv[2] host = sys.argv[3] path = sys.argv[4] + "?action=PostMessage" + args = sys.argv[5:] + + if not args: + print >>sys.stderr, "Need some updates as arguments to this program." + sys.exit(1) message = Message() - message.add_update([MIMEText("An update to the Wiki.", "moin")]) - message.add_update([MIMEText("Another update to the Wiki.", "moin")]) + + for arg in args: + message.add_update([MIMEText(arg, "moin")]) + email_message = message.get_payload() gpg = GPG() diff -r 4de4d2c11f52 -r ceadc72b6177 tests/test_sign.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test_sign.py Sun Oct 21 00:43:40 2012 +0200 @@ -0,0 +1,18 @@ +#!/usr/bin/env python + +from MoinMessage import GPG +from email.parser import Parser +import sys + +if __name__ == "__main__": + keyid = sys.argv[1] + message = Parser().parse(sys.stdin) + + gpg = GPG() + text = gpg.signMessage(message, keyid) + + # Show the resulting message text. + + print text + +# vim: tabstop=4 expandtab shiftwidth=4