1.1 --- a/MoinMessage.py Sat Oct 20 19:27:25 2012 +0200
1.2 +++ b/MoinMessage.py Sun Oct 21 00:43:40 2012 +0200
1.3 @@ -72,14 +72,23 @@
1.4
1.5 cmd = Popen(["gpg"] + self.conf_args + list(args), stdin=PIPE, stdout=PIPE, stderr=PIPE)
1.6
1.7 - if text:
1.8 - cmd.stdin.write(text)
1.9 - cmd.stdin.close()
1.10 + try:
1.11 + # Attempt to write input to the command and to read output from the
1.12 + # command.
1.13 +
1.14 + try:
1.15 + if text:
1.16 + cmd.stdin.write(text)
1.17 + cmd.stdin.close()
1.18
1.19 - self.errors = cmd.stderr.read()
1.20 + text = cmd.stdout.read()
1.21 +
1.22 + # I/O errors can indicate the failure of the command.
1.23
1.24 - try:
1.25 - text = cmd.stdout.read()
1.26 + except IOError:
1.27 + pass
1.28 +
1.29 + self.errors = cmd.stderr.read()
1.30
1.31 # Test for a zero result.
1.32
1.33 @@ -117,16 +126,24 @@
1.34
1.35 # Return the details of the signing key.
1.36
1.37 + identity = None
1.38 + fingerprint = None
1.39 +
1.40 for line in text.split("\n"):
1.41 try:
1.42 - prefix, msgtype, fingerprint, details = line.strip().split(" ", 3)
1.43 + prefix, msgtype, digest, details = line.strip().split(" ", 3)
1.44 except ValueError:
1.45 continue
1.46
1.47 # Return the fingerprint and identity details.
1.48
1.49 if msgtype == "GOODSIG":
1.50 - return fingerprint, details
1.51 + identity = details
1.52 + elif msgtype == "VALIDSIG":
1.53 + fingerprint = digest
1.54 +
1.55 + if identity and fingerprint:
1.56 + return fingerprint, identity
1.57
1.58 return None
1.59
2.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
2.2 +++ b/README.txt Sun Oct 21 00:43:40 2012 +0200
2.3 @@ -0,0 +1,156 @@
2.4 +Introduction
2.5 +------------
2.6 +
2.7 +MoinMessage provides a library for creating, signing, encrypting, decrypting,
2.8 +verifying PGP/GPG content in Python along with mechanisms for updating
2.9 +MoinMoin Wiki instances with such content such that contributors can be
2.10 +identified from their PGP signatures and such details used to authenticate
2.11 +their contributions.
2.12 +
2.13 +Configuring GPG for a Wiki
2.14 +--------------------------
2.15 +
2.16 +Initialise a homedir for GPG and configure it using ACL (access control list)
2.17 +properties:
2.18 +
2.19 +./scripts/init_wiki_keyring.sh
2.20 +
2.21 +To be in any way useful, signing keys must be made available within this
2.22 +homedir so that incoming messages can have their senders verified.
2.23 +
2.24 +To see the keys available to you in your own environment:
2.25 +
2.26 +gpg --list-keys --with-fingerprint
2.27 +
2.28 +The full fingerprints are used when defining a user mapping in the Wiki, and
2.29 +the --with-fingerprint option is used to show them. Otherwise, only the last
2.30 +eight characters of the fingerprints are shown.
2.31 +
2.32 +Export the public key used when signing messages from your own environment:
2.33 +
2.34 +gpg --armor --output 1C1AAF83.asc --export 1C1AAF83
2.35 +
2.36 +Import the key into the Wiki's GPG homedir:
2.37 +
2.38 +gpg --homedir wiki/gnupg --import 1C1AAF83.asc
2.39 +
2.40 +For the Wiki to receive encrypted data, a key for the Wiki must be created:
2.41 +
2.42 +gpg --homedir wiki/gnupg --gen-key
2.43 +
2.44 +Export the Wiki's key for encrypting messages sent to the Wiki:
2.45 +
2.46 +gpg --homedir wiki/gnupg --armor --output 0891463A.asc --export 0891463A
2.47 +
2.48 +This exported key can now be imported into your own environment:
2.49 +
2.50 +gpg --import 0891463A.asc
2.51 +
2.52 +Configuring the Wiki
2.53 +--------------------
2.54 +
2.55 +In the Wiki configuration, define the following settings:
2.56 +
2.57 + moinmessage_gpg_homedir
2.58 + This sets the path to the homedir initialised above.
2.59 +
2.60 + moinmessage_gpg_users_page (optional, default is MoinMessageUserDict)
2.61 + This provides a mapping from key fingerprints to Moin usernames.
2.62 +
2.63 +The Fingerprint-to-Username Mapping
2.64 +-----------------------------------
2.65 +
2.66 +The mapping from fingerprints to usernames is a WikiDict page having the
2.67 +following general format:
2.68 +
2.69 + fingerprint:: username
2.70 +
2.71 +Each fingerprint must exclude space characters and correspond to the
2.72 +fingerprint shown for a key in the available key listing generated above.
2.73 +
2.74 +Each username must correspond to a registered user in the Wiki.
2.75 +
2.76 +Quick Start: Signing, Encrypting and Sending Messages
2.77 +-----------------------------------------------------
2.78 +
2.79 +To send a message signed and encrypted to a resource on localhost:
2.80 +
2.81 +python tests/test_send.py 1C1AAF83 0891463A localhost /wiki/ShareTest \
2.82 + 'An update to the Wiki.' 'Another update.'
2.83 +
2.84 +Here, the first identifier is a reference to the signing key (over which you
2.85 +have complete control), and the second identifier is a reference to the
2.86 +encryption key (which is a public key published for the Wiki).
2.87 +
2.88 +This needs password protection to be removed from the secret key in the Web
2.89 +server environment, and so uses a modified trust model when invoking gpg.
2.90 +
2.91 +Below, the mechanisms employed are illustrated through the use of the other
2.92 +test programs.
2.93 +
2.94 +Signing
2.95 +-------
2.96 +
2.97 +Prepare a message signed with a "detached signature" (note that this does not
2.98 +seem to be what gpg calls a detached signature with the --detach-sig option):
2.99 +
2.100 + python tests/test_message.py 'An update to the Wiki.' 'Another update.' \
2.101 +| python tests/test_sign.py 1C1AAF83
2.102 +
2.103 +The complicated recipe based on the individual operations is as follows:
2.104 +
2.105 + python tests/test_message.py 'An update to the Wiki.' 'Another update.' \
2.106 +> test.txt \
2.107 +&& cat test.txt \
2.108 +| gpg --armor -u 1C1AAF83 --detach-sig \
2.109 +| python tests/test_sign_wrap.py test.txt
2.110 +
2.111 +Encryption
2.112 +----------
2.113 +
2.114 +Prepare a message with an encrypted payload using the above key:
2.115 +
2.116 + python tests/test_message.py 'An update to the Wiki.' 'Another update.' \
2.117 +| python tests/test_encrypt.py 0891463A
2.118 +
2.119 +The complicated recipe based on the individual operations is as follows:
2.120 +
2.121 + python tests/test_message.py 'An update to the Wiki.' 'Another update.' \
2.122 +> test.txt \
2.123 +&& cat test.txt \
2.124 +| gpg --armor -r 0891463A --encrypt --trust-model always \
2.125 +| python tests/test_encrypt_wrap.py
2.126 +
2.127 +Note that "--trust-model always" is used only to avoid prompting issues.
2.128 +
2.129 +Signing and Encrypting
2.130 +----------------------
2.131 +
2.132 +Send a message signed and encrypted:
2.133 +
2.134 +python tests/test_send.py 1C1AAF83 0891463A localhost /wiki/ShareTest
2.135 +
2.136 + python tests/test_message.py 'An update to the Wiki.' 'Another update.' \
2.137 +| python tests/test_sign.py 1C1AAF83 \
2.138 +| python tests/test_encrypt.py 0891463A
2.139 +
2.140 +The complicated recipe based on the individual operations is as follows:
2.141 +
2.142 + python tests/test_message.py 'An update to the Wiki.' 'Another update.' \
2.143 +> test.txt \
2.144 +&& cat test.txt \
2.145 +| gpg --armor -u 1C1AAF83 --detach-sig \
2.146 +| python tests/test_sign_wrap.py test.txt \
2.147 +| gpg --armor -r 0891463A --encrypt --trust-model always \
2.148 +| python tests/test_encrypt_wrap.py
2.149 +
2.150 +Posting a Message
2.151 +-----------------
2.152 +
2.153 +To post a signed and/or encrypted message, output from the above activities
2.154 +can be piped into the following command:
2.155 +
2.156 +python tests/test_post.py localhost /wiki/ShareTest
2.157 +
2.158 +Here, the resource "/wiki/ShareTest" on localhost is presented with the
2.159 +message.
3.1 --- a/actions/PostMessage.py Sat Oct 20 19:27:25 2012 +0200
3.2 +++ b/actions/PostMessage.py Sun Oct 21 00:43:40 2012 +0200
3.3 @@ -8,6 +8,7 @@
3.4
3.5 from MoinMoin.PageEditor import PageEditor
3.6 from MoinMoin.log import getLogger
3.7 +from MoinMoin.user import User
3.8 from MoinSupport import *
3.9 from MoinMessage import GPG, MoinMessageError
3.10 from email.parser import Parser
3.11 @@ -156,16 +157,41 @@
3.12 # Verify the message.
3.13
3.14 try:
3.15 - gpg.verifyMessage(signature.get_payload(), content.as_string())
3.16 + fingerprint, identity = gpg.verifyMessage(signature.get_payload(), content.as_string())
3.17 +
3.18 + # Map the fingerprint to a Wiki user.
3.19 +
3.20 + old_user = None
3.21 + request = self.request
3.22
3.23 - # Log non-fatal errors.
3.24 + try:
3.25 + if fingerprint:
3.26 + gpg_users = getWikiDict(
3.27 + getattr(request.cfg, "moinmessage_gpg_users_page", "MoinMessageUserDict"),
3.28 + request
3.29 + )
3.30 +
3.31 + # With a user mapping and a fingerprint corresponding to a known
3.32 + # user, temporarily switch user in order to make the edit.
3.33
3.34 - if gpg.errors:
3.35 - getLogger(__name__).warning(gpg.errors)
3.36 + if gpg_users and gpg_users.has_key(fingerprint):
3.37 + old_user = request.user
3.38 + request.user = User(request, auth_method="gpg", auth_username=gpg_users[fingerprint])
3.39 +
3.40 + # Log non-fatal errors.
3.41 +
3.42 + if gpg.errors:
3.43 + getLogger(__name__).warning(gpg.errors)
3.44
3.45 - # Handle the embedded message.
3.46 + # Handle the embedded message.
3.47 +
3.48 + self.handle_message_content(content)
3.49
3.50 - self.handle_message_content(content)
3.51 + # Restore any user identity.
3.52 +
3.53 + finally:
3.54 + if old_user:
3.55 + request.user = old_user
3.56
3.57 # Otherwise, reject the unverified message.
3.58
3.59 @@ -232,11 +258,15 @@
3.60 page_editor = PageEditor(self.request, self.pagename)
3.61 page_editor.saveText("\n\n".join(body), 0)
3.62
3.63 + # Refresh the page.
3.64 +
3.65 + self.page = Page(self.request, self.pagename)
3.66 +
3.67 def get_homedir(self):
3.68
3.69 "Locate the GPG home directory."
3.70
3.71 - homedir = getattr(self.request.cfg, "postmessage_gpg_homedir")
3.72 + homedir = getattr(self.request.cfg, "moinmessage_gpg_homedir")
3.73 if not homedir:
3.74 writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type")
3.75 request.write("Encoded data cannot currently be understood. Please notify the site administrator.")
4.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
4.2 +++ b/tests/test_encrypt.py Sun Oct 21 00:43:40 2012 +0200
4.3 @@ -0,0 +1,18 @@
4.4 +#!/usr/bin/env python
4.5 +
4.6 +from MoinMessage import GPG
4.7 +from email.parser import Parser
4.8 +import sys
4.9 +
4.10 +if __name__ == "__main__":
4.11 + keyid = sys.argv[1]
4.12 + message = Parser().parse(sys.stdin)
4.13 +
4.14 + gpg = GPG()
4.15 + text = gpg.encryptMessage(message, keyid)
4.16 +
4.17 + # Show the resulting message text.
4.18 +
4.19 + print text
4.20 +
4.21 +# vim: tabstop=4 expandtab shiftwidth=4
5.1 --- a/tests/test_message.py Sat Oct 20 19:27:25 2012 +0200
5.2 +++ b/tests/test_message.py Sun Oct 21 00:43:40 2012 +0200
5.3 @@ -2,16 +2,14 @@
5.4
5.5 from email.mime.multipart import MIMEMultipart
5.6 from email.mime.text import MIMEText
5.7 +import sys
5.8
5.9 if __name__ == "__main__":
5.10 message = MIMEMultipart()
5.11
5.12 - text1 = MIMEText("An update to the Wiki.", "moin")
5.13 -
5.14 - text2 = MIMEText("Another update to the Wiki.", "moin")
5.15 -
5.16 - message.attach(text1)
5.17 - message.attach(text2)
5.18 + for arg in sys.argv[1:]:
5.19 + text = MIMEText(arg, "moin")
5.20 + message.attach(text)
5.21
5.22 text = message.as_string()
5.23 print text
6.1 --- a/tests/test_send.py Sat Oct 20 19:27:25 2012 +0200
6.2 +++ b/tests/test_send.py Sun Oct 21 00:43:40 2012 +0200
6.3 @@ -9,10 +9,17 @@
6.4 recipient = sys.argv[2]
6.5 host = sys.argv[3]
6.6 path = sys.argv[4] + "?action=PostMessage"
6.7 + args = sys.argv[5:]
6.8 +
6.9 + if not args:
6.10 + print >>sys.stderr, "Need some updates as arguments to this program."
6.11 + sys.exit(1)
6.12
6.13 message = Message()
6.14 - message.add_update([MIMEText("An update to the Wiki.", "moin")])
6.15 - message.add_update([MIMEText("Another update to the Wiki.", "moin")])
6.16 +
6.17 + for arg in args:
6.18 + message.add_update([MIMEText(arg, "moin")])
6.19 +
6.20 email_message = message.get_payload()
6.21 gpg = GPG()
6.22
7.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
7.2 +++ b/tests/test_sign.py Sun Oct 21 00:43:40 2012 +0200
7.3 @@ -0,0 +1,18 @@
7.4 +#!/usr/bin/env python
7.5 +
7.6 +from MoinMessage import GPG
7.7 +from email.parser import Parser
7.8 +import sys
7.9 +
7.10 +if __name__ == "__main__":
7.11 + keyid = sys.argv[1]
7.12 + message = Parser().parse(sys.stdin)
7.13 +
7.14 + gpg = GPG()
7.15 + text = gpg.signMessage(message, keyid)
7.16 +
7.17 + # Show the resulting message text.
7.18 +
7.19 + print text
7.20 +
7.21 +# vim: tabstop=4 expandtab shiftwidth=4