# HG changeset patch # User Paul Boddie # Date 1342897276 -7200 # Node ID 24919b9c9bc7b234387d6958335a528f99b63547 # Parent 2c935363b2cf03576826afe18799ac2e2ccfd2d9 Changed the action and test program to support detached signatures, at least in a form acceptable to gpg, even if RFC 3156 is somewhat contradictory on the topic. diff -r 2c935363b2cf -r 24919b9c9bc7 actions/PostMessage.py --- a/actions/PostMessage.py Sat Jul 21 19:58:34 2012 +0200 +++ b/actions/PostMessage.py Sat Jul 21 21:01:16 2012 +0200 @@ -11,6 +11,8 @@ from MoinSupport import * from email.parser import Parser from subprocess import Popen, PIPE +from tempfile import mkstemp +import os try: from cStringIO import StringIO @@ -56,21 +58,25 @@ # Detect PGP/GPG-encoded payloads. # See: http://tools.ietf.org/html/rfc3156 - if mimetype == "multipart/encrypted" and \ - message.get_param("protocol") == "application/pgp-encrypted": + # NOTE: RFC 3156 states that signed messages should employ a detached + # NOTE: signature but then shows "BEGIN PGP MESSAGE" for signatures + # NOTE: instead of "BEGIN PGP SIGNATURE". + + if mimetype == "multipart/signed" and \ + message.get_param("protocol") == "application/pgp-signature": try: - declaration, part = message.get_payload() + content, signature = message.get_payload() except ValueError: writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type") - request.write("There must be a declaration and a content part for signed uploads.") + request.write("There must be a content part and a signature for signed uploads.") return # Verify the message format. - if part.get_content_type() != "application/octet-stream": + if signature.get_content_type() != "application/pgp-signature": writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type") - request.write("Encrypted data must be provided as application/octet-stream.") + request.write("Signature data must be provided in the second part as application/pgp-signature.") return # Locate the keyring. @@ -78,40 +84,53 @@ homedir = getattr(request.cfg, "postmessage_gpg_homedir") if not homedir: writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type") - request.write("Encrypted data cannot currently be understood. Please notify the site administrator.") + request.write("Encoded data cannot currently be understood. Please notify the site administrator.") return - # Decrypt the message text. - - cmd = Popen(["gpg", "--homedir", homedir, "--decrypt"], - stdin=PIPE, stdout=PIPE, stderr=PIPE) + # Write the detached signature and content to files. - cmd.stdin.write(part.get_payload()) - cmd.stdin.close() + signature_fd, signature_filename = mkstemp() + content_fd, content_filename = mkstemp() + try: + signature_fp = os.fdopen(signature_fd, "w") + content_fp = os.fdopen(content_fd, "w") + try: + signature_fp.write(signature.get_payload()) + content_fp.write(content.as_string()) + finally: + signature_fp.close() + content_fp.close() - errors = cmd.stderr.read() - if errors: - getLogger(__name__).warning(errors) + # Verify the message text. - # Handle the embedded message. + cmd = Popen(["gpg", "--homedir", homedir, "--verify", signature_filename, content_filename], + stdout=PIPE, stderr=PIPE) - try: - message_text = cmd.stdout.read() + errors = cmd.stderr.read() + if errors: + getLogger(__name__).warning(errors) - # With a zero return code, accept the message. + # Handle the embedded message. + + try: + # With a zero return code, accept the message. - if not cmd.wait(): - self.handle_plaintext_message(StringIO(message_text)) + if not cmd.wait(): + self.handle_parsed_message(content) + + # Otherwise, reject the unverified message. - # Otherwise, reject the unverified message. + else: + writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden") + request.write("The message could not be verified.") - else: - writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden") - request.write("The message could not be verified.") + finally: + cmd.stdout.close() + cmd.stderr.close() finally: - cmd.stdout.close() - cmd.stderr.close() + os.remove(signature_filename) + os.remove(content_filename) # Reject unsigned payloads. @@ -123,8 +142,14 @@ "Handle the given 'message_text'." + message = Parser().parse(message_text) + self.handle_parsed_message(message) + + def handle_parsed_message(self, message): + + "Handle the given 'message_text'." + request = self.request - message = Parser().parse(message_text) # Handle a single part. diff -r 2c935363b2cf -r 24919b9c9bc7 tests/test_post.py --- a/tests/test_post.py Sat Jul 21 19:58:34 2012 +0200 +++ b/tests/test_post.py Sat Jul 21 21:01:16 2012 +0200 @@ -4,22 +4,51 @@ from email.mime.application import MIMEApplication from email.mime.base import MIMEBase from email.encoders import encode_noop +from email import message_from_string import httplib import sys if __name__ == "__main__": host = sys.argv[1] path = sys.argv[2] + "?action=PostMessage" - text = sys.stdin.read() - message = MIMEMultipart("encrypted", protocol="application/pgp-encrypted") + try: + message = sys.argv[3] + text = open(message).read() + signature = sys.stdin.read() + protocol = "application/pgp-signature" + subtype = "signed" + except IndexError: + text = sys.stdin.read() + signature = None + protocol = "application/pgp-encrypted" + subtype = "encrypted" + + # Make the container for the message. + + message = MIMEMultipart(subtype, protocol=protocol) + + # For encrypted content, add the declaration and content. - declaration = MIMEBase("application", "pgp-encrypted") - declaration.set_payload("Version: 1") - message.attach(declaration) + if not signature: + declaration = MIMEBase("application", "pgp-encrypted") + declaration.set_payload("Version: 1") + message.attach(declaration) + + content = MIMEApplication(text, "octet-stream", encode_noop) + message.attach(content) + + # For signed content, - content = MIMEApplication(text, "octet-stream", encode_noop) - message.attach(content) + else: + submessage = message_from_string(text) + message.attach(submessage) + + signature_part = MIMEBase("application", "pgp-signature") + signature_part.set_payload(signature) + message.attach(signature_part) + + # Show the resulting message text. text = message.as_string()