1.1 --- a/MoinMessage.py Tue Jun 04 14:59:47 2013 +0200
1.2 +++ b/MoinMessage.py Fri Jun 07 01:34:51 2013 +0200
1.3 @@ -162,6 +162,15 @@
1.4 class MoinMessageError(Exception):
1.5 pass
1.6
1.7 +class MoinMessageDecodingError(Exception):
1.8 + pass
1.9 +
1.10 +class MoinMessageMissingPart(MoinMessageDecodingError):
1.11 + pass
1.12 +
1.13 +class MoinMessageBadContent(MoinMessageDecodingError):
1.14 + pass
1.15 +
1.16 class GPG:
1.17
1.18 "A wrapper around the gpg command using a particular configuration."
1.19 @@ -216,7 +225,7 @@
1.20 cmd.stdout.close()
1.21 cmd.stderr.close()
1.22
1.23 - def verifyMessage(self, signature, content):
1.24 + def verifyMessageText(self, signature, content):
1.25
1.26 "Using the given 'signature', verify the given message 'content'."
1.27
1.28 @@ -266,6 +275,28 @@
1.29 os.remove(signature_filename)
1.30 os.remove(content_filename)
1.31
1.32 + def verifyMessage(self, message):
1.33 +
1.34 + """
1.35 + Verify the given RFC 3156 'message', returning a tuple of the form
1.36 + (fingerprint, identity, content).
1.37 + """
1.38 +
1.39 + try:
1.40 + content, signature = message.get_payload()
1.41 + except ValueError:
1.42 + raise MoinMessageMissingPart
1.43 +
1.44 + # Verify the message format.
1.45 +
1.46 + if signature.get_content_type() != "application/pgp-signature":
1.47 + raise MoinMessageBadContent
1.48 +
1.49 + # Verify the message.
1.50 +
1.51 + fingerprint, identity = self.verifyMessageText(signature.get_payload(), content.as_string())
1.52 + return fingerprint, identity, content
1.53 +
1.54 def signMessage(self, message, keyid):
1.55
1.56 """
1.57 @@ -286,12 +317,32 @@
1.58
1.59 return signed_message
1.60
1.61 - def decryptMessage(self, message):
1.62 + def decryptMessageText(self, message):
1.63
1.64 "Return a decrypted version of 'message'."
1.65
1.66 return self.run(["--decrypt"], message)
1.67
1.68 + def decryptMessage(self, message):
1.69 +
1.70 + """
1.71 + Decrypt the given RFC 3156 'message', returning the message text.
1.72 + """
1.73 +
1.74 + try:
1.75 + declaration, content = message.get_payload()
1.76 + except ValueError:
1.77 + raise MoinMessageMissingPart
1.78 +
1.79 + # Verify the message format.
1.80 +
1.81 + if content.get_content_type() != "application/octet-stream":
1.82 + raise MoinMessageBadContent
1.83 +
1.84 + # Return the decrypted message text.
1.85 +
1.86 + return self.decryptMessageText(content.get_payload())
1.87 +
1.88 def encryptMessage(self, message, keyid):
1.89
1.90 """
1.91 @@ -316,6 +367,25 @@
1.92
1.93 return encrypted_message
1.94
1.95 +# Message decoding functions.
1.96 +
1.97 +# Detect PGP/GPG-encoded payloads.
1.98 +# See: http://tools.ietf.org/html/rfc3156
1.99 +
1.100 +def is_signed(message):
1.101 + mimetype = message.get_content_type()
1.102 + encoding = message.get_content_charset()
1.103 +
1.104 + return mimetype == "multipart/signed" and \
1.105 + message.get_param("protocol") == "application/pgp-signature"
1.106 +
1.107 +def is_encrypted(message):
1.108 + mimetype = message.get_content_type()
1.109 + encoding = message.get_content_charset()
1.110 +
1.111 + return mimetype == "multipart/encrypted" and \
1.112 + message.get_param("protocol") == "application/pgp-encrypted"
1.113 +
1.114 # Communications functions.
1.115
1.116 def timestamp(message):
2.1 --- a/MoinMessageSupport.py Tue Jun 04 14:59:47 2013 +0200
2.2 +++ b/MoinMessageSupport.py Fri Jun 07 01:34:51 2013 +0200
2.3 @@ -11,7 +11,7 @@
2.4 from MoinMoin.user import User
2.5 from MoinMoin import wikiutil
2.6 from MoinSupport import ItemStore, getHeader, getMetadata, getWikiDict, writeHeaders
2.7 -from MoinMessage import GPG, Message, MoinMessageError
2.8 +from MoinMessage import GPG, Message, MoinMessageError, is_signed, is_encrypted
2.9 from email.parser import Parser
2.10 import time
2.11
2.12 @@ -57,26 +57,18 @@
2.13
2.14 "Handle the given 'message'."
2.15
2.16 - request = self.request
2.17 - mimetype = message.get_content_type()
2.18 - encoding = message.get_content_charset()
2.19 -
2.20 # Detect PGP/GPG-encoded payloads.
2.21 # See: http://tools.ietf.org/html/rfc3156
2.22
2.23 - if mimetype == "multipart/signed" and \
2.24 - message.get_param("protocol") == "application/pgp-signature":
2.25 -
2.26 + if is_signed(message):
2.27 self.handle_signed_message(message)
2.28 -
2.29 - elif mimetype == "multipart/encrypted" and \
2.30 - message.get_param("protocol") == "application/pgp-encrypted":
2.31 -
2.32 + elif is_encrypted(message):
2.33 self.handle_encrypted_message(message)
2.34
2.35 - # Reject unsigned payloads.
2.36 + # Reject unsigned and unencrypted payloads.
2.37
2.38 else:
2.39 + request = self.request
2.40 writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type")
2.41 request.write("Only PGP/GPG-signed payloads are supported.")
2.42
2.43 @@ -86,45 +78,44 @@
2.44
2.45 request = self.request
2.46
2.47 - try:
2.48 - declaration, content = message.get_payload()
2.49 - except ValueError:
2.50 - writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type")
2.51 - request.write("There must be a declaration and a content part for encrypted uploads.")
2.52 - return
2.53 -
2.54 - # Verify the message format.
2.55 -
2.56 - if content.get_content_type() != "application/octet-stream":
2.57 - writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type")
2.58 - request.write("Encrypted data must be provided as application/octet-stream.")
2.59 - return
2.60 -
2.61 homedir = self.get_homedir()
2.62 if not homedir:
2.63 return
2.64
2.65 gpg = GPG(homedir)
2.66
2.67 - # Get the decrypted message text.
2.68 + try:
2.69 + text = gpg.decryptMessage(message)
2.70
2.71 - try:
2.72 - text = gpg.decryptMessage(content.get_payload())
2.73 -
2.74 - # Log non-fatal errors.
2.75 + # Reject messages without a declaration.
2.76
2.77 - if gpg.errors:
2.78 - getLogger(__name__).warning(gpg.errors)
2.79 + except MoinMessageMissingPart:
2.80 + writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type")
2.81 + request.write("There must be a declaration and a content part for encrypted uploads.")
2.82 + return
2.83 +
2.84 + # Reject messages without appropriate content.
2.85
2.86 - # Handle the embedded message.
2.87 + except MoinMessageBadContent:
2.88 + writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type")
2.89 + request.write("Encrypted data must be provided as application/octet-stream.")
2.90 + return
2.91
2.92 - self.handle_message_text(text)
2.93 -
2.94 - # Otherwise, reject the unverified message.
2.95 + # Reject any unencryptable message.
2.96
2.97 except MoinMessageError:
2.98 writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden")
2.99 request.write("The message could not be decrypted.")
2.100 + return
2.101 +
2.102 + # Log non-fatal errors.
2.103 +
2.104 + if gpg.errors:
2.105 + getLogger(__name__).warning(gpg.errors)
2.106 +
2.107 + # Handle the embedded message which may itself be a signed message.
2.108 +
2.109 + self.handle_message_text(text)
2.110
2.111 def handle_signed_message(self, message):
2.112
2.113 @@ -132,75 +123,74 @@
2.114
2.115 request = self.request
2.116
2.117 + homedir = self.get_homedir()
2.118 + if not homedir:
2.119 + return
2.120 +
2.121 + gpg = GPG(homedir)
2.122 +
2.123 # NOTE: RFC 3156 states that signed messages should employ a detached
2.124 # NOTE: signature but then shows "BEGIN PGP MESSAGE" for signatures
2.125 # NOTE: instead of "BEGIN PGP SIGNATURE".
2.126 # NOTE: The "micalg" parameter is currently not supported.
2.127
2.128 try:
2.129 - content, signature = message.get_payload()
2.130 - except ValueError:
2.131 + fingerprint, identity, content = gpg.verifyMessage(message)
2.132 +
2.133 + # Reject messages without a declaration.
2.134 +
2.135 + except MoinMessageMissingPart:
2.136 writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type")
2.137 request.write("There must be a content part and a signature for signed uploads.")
2.138 return
2.139
2.140 - # Verify the message format.
2.141 + # Reject messages without appropriate content.
2.142
2.143 - if signature.get_content_type() != "application/pgp-signature":
2.144 + except MoinMessageBadContent:
2.145 writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type")
2.146 request.write("Signature data must be provided in the second part as application/pgp-signature.")
2.147 return
2.148
2.149 - homedir = self.get_homedir()
2.150 - if not homedir:
2.151 - return
2.152 -
2.153 - gpg = GPG(homedir)
2.154 -
2.155 - # Verify the message.
2.156 -
2.157 - try:
2.158 - fingerprint, identity = gpg.verifyMessage(signature.get_payload(), content.as_string())
2.159 -
2.160 - # Map the fingerprint to a Wiki user.
2.161 -
2.162 - old_user = None
2.163 - request = self.request
2.164 -
2.165 - try:
2.166 - if fingerprint:
2.167 - gpg_users = getWikiDict(
2.168 - getattr(request.cfg, "moinmessage_gpg_users_page", "MoinMessageUserDict"),
2.169 - request
2.170 - )
2.171 -
2.172 - # With a user mapping and a fingerprint corresponding to a known
2.173 - # user, temporarily switch user in order to make the edit.
2.174 -
2.175 - if gpg_users and gpg_users.has_key(fingerprint):
2.176 - old_user = request.user
2.177 - request.user = User(request, auth_method="gpg", auth_username=gpg_users[fingerprint])
2.178 -
2.179 - # Log non-fatal errors.
2.180 -
2.181 - if gpg.errors:
2.182 - getLogger(__name__).warning(gpg.errors)
2.183 -
2.184 - # Handle the embedded message.
2.185 -
2.186 - self.handle_message_content(content)
2.187 -
2.188 - # Restore any user identity.
2.189 -
2.190 - finally:
2.191 - if old_user:
2.192 - request.user = old_user
2.193 -
2.194 - # Otherwise, reject the unverified message.
2.195 + # Reject any unverified message.
2.196
2.197 except MoinMessageError:
2.198 writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden")
2.199 request.write("The message could not be verified.")
2.200 + return
2.201 +
2.202 + # Log non-fatal errors.
2.203 +
2.204 + if gpg.errors:
2.205 + getLogger(__name__).warning(gpg.errors)
2.206 +
2.207 + # Map the fingerprint to a Wiki user.
2.208 +
2.209 + old_user = None
2.210 + request = self.request
2.211 +
2.212 + try:
2.213 + if fingerprint:
2.214 + gpg_users = getWikiDict(
2.215 + getattr(request.cfg, "moinmessage_gpg_users_page", "MoinMessageUserDict"),
2.216 + request
2.217 + )
2.218 +
2.219 + # With a user mapping and a fingerprint corresponding to a known
2.220 + # user, temporarily switch user in order to make the edit.
2.221 +
2.222 + if gpg_users and gpg_users.has_key(fingerprint):
2.223 + old_user = request.user
2.224 + request.user = User(request, auth_method="gpg", auth_username=gpg_users[fingerprint])
2.225 +
2.226 + # Handle the embedded message.
2.227 +
2.228 + self.handle_message_content(content)
2.229 +
2.230 + # Restore any user identity.
2.231 +
2.232 + finally:
2.233 + if old_user:
2.234 + request.user = old_user
2.235
2.236 def handle_message_content(self, content):
2.237