pgp: force protocol/micalg ordering in doctest output.
[pgp-mime.git] / pgp_mime / pgp.py
1 # Copyright (C) 2012 W. Trevor King <wking@tremily.us>
2 #
3 # This file is part of pgp-mime.
4 #
5 # pgp-mime is free software: you can redistribute it and/or modify it under the
6 # terms of the GNU General Public License as published by the Free Software
7 # Foundation, either version 3 of the License, or (at your option) any later
8 # version.
9 #
10 # pgp-mime is distributed in the hope that it will be useful, but WITHOUT ANY
11 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12 # A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License along with
15 # pgp-mime.  If not, see <http://www.gnu.org/licenses/>.
16
17 import copy as _copy
18 from email import message_from_bytes as _message_from_bytes
19 from email.encoders import encode_7or8bit as _encode_7or8bit
20 from email.generator import BytesGenerator as _BytesGenerator
21 from email.mime.application import MIMEApplication as _MIMEApplication
22 from email.mime.multipart import MIMEMultipart as _MIMEMultipart
23 from email import policy as _email_policy
24 import io as _io
25
26 from . import LOG as _LOG
27 from .crypt import sign_and_encrypt_bytes as _sign_and_encrypt_bytes
28 from .crypt import verify_bytes as _verify_bytes
29 from .email import email_targets as _email_targets
30 from .email import strip_bcc as _strip_bcc
31
32
33 def _flatten(message):
34     r"""Flatten a message to bytes.
35
36     >>> from pgp_mime.email import encodedMIMEText
37     >>> message = encodedMIMEText('Hi\nBye')
38     >>> _flatten(message)  # doctest: +ELLIPSIS
39     b'Content-Type: text/plain; charset="us-ascii"\r\nMIME-Version: ...'
40     """
41     bytesio = _io.BytesIO()
42     generator = _BytesGenerator(bytesio, policy=_email_policy.SMTP)
43     generator.flatten(message)
44     return bytesio.getvalue()
45
46 def sign(message, **kwargs):
47     r"""Sign a ``Message``, returning the signed version.
48
49     multipart/signed
50     +-> text/plain                 (body)
51     +-> application/pgp-signature  (signature)
52
53     >>> from pgp_mime.email import encodedMIMEText
54     >>> message = encodedMIMEText('Hi\nBye')
55     >>> signed = sign(message, signers=['pgp-mime@invalid.com'])
56     >>> signed.set_boundary('boundsep')
57     >>> print(signed.as_string().replace(
58     ...     'micalg="pgp-sha1"; protocol="application/pgp-signature"',
59     ...     'protocol="application/pgp-signature"; micalg="pgp-sha1"'))
60     ... # doctest: +ELLIPSIS, +REPORT_UDIFF
61     Content-Type: multipart/signed; protocol="application/pgp-signature"; micalg="pgp-sha1"; boundary="boundsep"
62     MIME-Version: 1.0
63     Content-Disposition: inline
64     <BLANKLINE>
65     --boundsep
66     Content-Type: text/plain; charset="us-ascii"
67     MIME-Version: 1.0
68     Content-Transfer-Encoding: 7bit
69     Content-Disposition: inline
70     <BLANKLINE>
71     Hi
72     Bye
73     --boundsep
74     MIME-Version: 1.0
75     Content-Transfer-Encoding: 7bit
76     Content-Description: OpenPGP digital signature
77     Content-Type: application/pgp-signature; name="signature.asc"; charset="us-ascii"
78     <BLANKLINE>
79     -----BEGIN PGP SIGNATURE-----
80     Version: GnuPG...
81     -----END PGP SIGNATURE-----
82     <BLANKLINE>
83     --boundsep--
84
85     >>> from email.mime.multipart import MIMEMultipart
86     >>> message = MIMEMultipart()
87     >>> message.attach(encodedMIMEText('Part A'))
88     >>> message.attach(encodedMIMEText('Part B'))
89     >>> signed = sign(message, signers=['pgp-mime@invalid.com'])
90     >>> signed.set_boundary('boundsep')
91     >>> print(signed.as_string().replace(
92     ...     'micalg="pgp-sha1"; protocol="application/pgp-signature"',
93     ...     'protocol="application/pgp-signature"; micalg="pgp-sha1"'))
94     ... # doctest: +ELLIPSIS, +REPORT_UDIFF
95     Content-Type: multipart/signed; protocol="application/pgp-signature"; micalg="pgp-sha1"; boundary="boundsep"
96     MIME-Version: 1.0
97     Content-Disposition: inline
98     <BLANKLINE>
99     --boundsep
100     Content-Type: multipart/mixed; boundary="===============...=="
101     MIME-Version: 1.0
102     <BLANKLINE>
103     --===============...==
104     Content-Type: text/plain; charset="us-ascii"
105     MIME-Version: 1.0
106     Content-Transfer-Encoding: 7bit
107     Content-Disposition: inline
108     <BLANKLINE>
109     Part A
110     --===============...==
111     Content-Type: text/plain; charset="us-ascii"
112     MIME-Version: 1.0
113     Content-Transfer-Encoding: 7bit
114     Content-Disposition: inline
115     <BLANKLINE>
116     Part B
117     --===============...==--
118     --boundsep
119     MIME-Version: 1.0
120     Content-Transfer-Encoding: 7bit
121     Content-Description: OpenPGP digital signature
122     Content-Type: application/pgp-signature; name="signature.asc"; charset="us-ascii"
123     <BLANKLINE>
124     -----BEGIN PGP SIGNATURE-----
125     Version: GnuPG...
126     -----END PGP SIGNATURE-----
127     <BLANKLINE>
128     --boundsep--
129     """
130     body = _flatten(message)
131     signature = str(_sign_and_encrypt_bytes(data=body, **kwargs), 'us-ascii')
132     sig = _MIMEApplication(
133         _data=signature,
134         _subtype='pgp-signature; name="signature.asc"',
135         _encoder=_encode_7or8bit)
136     sig['Content-Description'] = 'OpenPGP digital signature'
137     sig.set_charset('us-ascii')
138
139     msg = _MIMEMultipart(
140         'signed', micalg='pgp-sha1', protocol='application/pgp-signature')
141     msg.attach(message)
142     msg.attach(sig)
143     msg['Content-Disposition'] = 'inline'
144     return msg
145
146 def encrypt(message, recipients=None, **kwargs):
147     r"""Encrypt a ``Message``, returning the encrypted version.
148
149     multipart/encrypted
150     +-> application/pgp-encrypted  (control information)
151     +-> application/octet-stream   (body)
152
153     >>> from pgp_mime.email import encodedMIMEText
154     >>> message = encodedMIMEText('Hi\nBye')
155     >>> message['To'] = 'pgp-mime-test <pgp-mime@invalid.com>'
156     >>> encrypted = encrypt(message, always_trust=True)
157     >>> encrypted.set_boundary('boundsep')
158     >>> print(encrypted.as_string().replace(
159     ...     'micalg="pgp-sha1"; protocol="application/pgp-encrypted"',
160     ...     'protocol="application/pgp-encrypted"; micalg="pgp-sha1"'))
161     ... # doctest: +ELLIPSIS, +REPORT_UDIFF
162     Content-Type: multipart/encrypted; protocol="application/pgp-encrypted"; micalg="pgp-sha1"; boundary="boundsep"
163     MIME-Version: 1.0
164     Content-Disposition: inline
165     <BLANKLINE>
166     --boundsep
167     MIME-Version: 1.0
168     Content-Transfer-Encoding: 7bit
169     Content-Type: application/pgp-encrypted; charset="us-ascii"
170     <BLANKLINE>
171     Version: 1
172     <BLANKLINE>
173     --boundsep
174     MIME-Version: 1.0
175     Content-Transfer-Encoding: 7bit
176     Content-Description: OpenPGP encrypted message
177     Content-Type: application/octet-stream; name="encrypted.asc"; charset="us-ascii"
178     <BLANKLINE>
179     -----BEGIN PGP MESSAGE-----
180     Version: GnuPG...
181     -----END PGP MESSAGE-----
182     <BLANKLINE>
183     --boundsep--
184
185     >>> from email.mime.multipart import MIMEMultipart
186     >>> message = MIMEMultipart()
187     >>> message.attach(encodedMIMEText('Part A'))
188     >>> message.attach(encodedMIMEText('Part B'))
189     >>> encrypted = encrypt(
190     ...     message, recipients=['pgp-mime@invalid.com'], always_trust=True)
191     >>> encrypted.set_boundary('boundsep')
192     >>> print(encrypted.as_string().replace(
193     ...     'micalg="pgp-sha1"; protocol="application/pgp-encrypted"',
194     ...     'protocol="application/pgp-encrypted"; micalg="pgp-sha1"'))
195     ... # doctest: +ELLIPSIS, +REPORT_UDIFF
196     Content-Type: multipart/encrypted; protocol="application/pgp-encrypted"; micalg="pgp-sha1"; boundary="boundsep"
197     MIME-Version: 1.0
198     Content-Disposition: inline
199     <BLANKLINE>
200     --boundsep
201     MIME-Version: 1.0
202     Content-Transfer-Encoding: 7bit
203     Content-Type: application/pgp-encrypted; charset="us-ascii"
204     <BLANKLINE>
205     Version: 1
206     <BLANKLINE>
207     --boundsep
208     MIME-Version: 1.0
209     Content-Transfer-Encoding: 7bit
210     Content-Description: OpenPGP encrypted message
211     Content-Type: application/octet-stream; name="encrypted.asc"; charset="us-ascii"
212     <BLANKLINE>
213     -----BEGIN PGP MESSAGE-----
214     Version: GnuPG...
215     -----END PGP MESSAGE-----
216     <BLANKLINE>
217     --boundsep--
218     """
219     body = _flatten(message)
220     if recipients is None:
221         recipients = [email for name,email in _email_targets(message)]
222         _LOG.debug('extracted encryption recipients: {}'.format(recipients))
223     encrypted = str(_sign_and_encrypt_bytes(
224             data=body, recipients=recipients, **kwargs), 'us-ascii')
225     enc = _MIMEApplication(
226         _data=encrypted,
227         _subtype='octet-stream; name="encrypted.asc"',
228         _encoder=_encode_7or8bit)
229     enc['Content-Description'] = 'OpenPGP encrypted message'
230     enc.set_charset('us-ascii')
231     control = _MIMEApplication(
232         _data='Version: 1\n',
233         _subtype='pgp-encrypted',
234         _encoder=_encode_7or8bit)
235     control.set_charset('us-ascii')
236     msg = _MIMEMultipart(
237         'encrypted',
238         micalg='pgp-sha1',
239         protocol='application/pgp-encrypted')
240     msg.attach(control)
241     msg.attach(enc)
242     msg['Content-Disposition'] = 'inline'
243     return msg
244
245 def sign_and_encrypt(message, signers=None, recipients=None, **kwargs):
246     r"""Sign and encrypt a ``Message``, returning the encrypted version.
247
248     multipart/encrypted
249      +-> application/pgp-encrypted  (control information)
250      +-> application/octet-stream   (body)
251
252     >>> from pgp_mime.email import encodedMIMEText
253     >>> message = encodedMIMEText('Hi\nBye')
254     >>> message['To'] = 'pgp-mime-test <pgp-mime@invalid.com>'
255     >>> encrypted = sign_and_encrypt(
256     ...     message, signers=['pgp-mime@invalid.com'], always_trust=True)
257     >>> encrypted.set_boundary('boundsep')
258     >>> print(encrypted.as_string().replace(
259     ...     'micalg="pgp-sha1"; protocol="application/pgp-encrypted"',
260     ...     'protocol="application/pgp-encrypted"; micalg="pgp-sha1"'))
261     ... # doctest: +ELLIPSIS, +REPORT_UDIFF
262     Content-Type: multipart/encrypted; protocol="application/pgp-encrypted"; micalg="pgp-sha1"; boundary="boundsep"
263     MIME-Version: 1.0
264     Content-Disposition: inline
265     <BLANKLINE>
266     --boundsep
267     MIME-Version: 1.0
268     Content-Transfer-Encoding: 7bit
269     Content-Type: application/pgp-encrypted; charset="us-ascii"
270     <BLANKLINE>
271     Version: 1
272     <BLANKLINE>
273     --boundsep
274     MIME-Version: 1.0
275     Content-Transfer-Encoding: 7bit
276     Content-Description: OpenPGP encrypted message
277     Content-Type: application/octet-stream; name="encrypted.asc"; charset="us-ascii"
278     <BLANKLINE>
279     -----BEGIN PGP MESSAGE-----
280     Version: GnuPG...
281     -----END PGP MESSAGE-----
282     <BLANKLINE>
283     --boundsep--
284
285     >>> from email.mime.multipart import MIMEMultipart
286     >>> message = MIMEMultipart()
287     >>> message.attach(encodedMIMEText('Part A'))
288     >>> message.attach(encodedMIMEText('Part B'))
289     >>> encrypted = sign_and_encrypt(
290     ...     message, signers=['pgp-mime@invalid.com'],
291     ...     recipients=['pgp-mime@invalid.com'], always_trust=True)
292     >>> encrypted.set_boundary('boundsep')
293     >>> print(encrypted.as_string().replace(
294     ...     'micalg="pgp-sha1"; protocol="application/pgp-encrypted"',
295     ...     'protocol="application/pgp-encrypted"; micalg="pgp-sha1"'))
296     ... # doctest: +ELLIPSIS, +REPORT_UDIFF
297     Content-Type: multipart/encrypted; protocol="application/pgp-encrypted"; micalg="pgp-sha1"; boundary="boundsep"
298     MIME-Version: 1.0
299     Content-Disposition: inline
300     <BLANKLINE>
301     --boundsep
302     MIME-Version: 1.0
303     Content-Transfer-Encoding: 7bit
304     Content-Type: application/pgp-encrypted; charset="us-ascii"
305     <BLANKLINE>
306     Version: 1
307     <BLANKLINE>
308     --boundsep
309     MIME-Version: 1.0
310     Content-Transfer-Encoding: 7bit
311     Content-Description: OpenPGP encrypted message
312     Content-Type: application/octet-stream; name="encrypted.asc"; charset="us-ascii"
313     <BLANKLINE>
314     -----BEGIN PGP MESSAGE-----
315     Version: GnuPG...
316     -----END PGP MESSAGE-----
317     <BLANKLINE>
318     --boundsep--
319     """
320     _strip_bcc(message=message)
321     body = _flatten(message)
322     if recipients is None:
323         recipients = [email for name,email in _email_targets(message)]
324         _LOG.debug('extracted encryption recipients: {}'.format(recipients))
325     encrypted = str(
326         _sign_and_encrypt_bytes(
327             data=body, signers=signers, recipients=recipients, **kwargs),
328         'us-ascii')
329     enc = _MIMEApplication(
330         _data=encrypted,
331         _subtype='octet-stream; name="encrypted.asc"',
332         _encoder=_encode_7or8bit)
333     enc['Content-Description'] = 'OpenPGP encrypted message'
334     enc.set_charset('us-ascii')
335     control = _MIMEApplication(
336         _data='Version: 1\n',
337         _subtype='pgp-encrypted',
338         _encoder=_encode_7or8bit)
339     control.set_charset('us-ascii')
340     msg = _MIMEMultipart(
341         'encrypted',
342         micalg='pgp-sha1',
343         protocol='application/pgp-encrypted')
344     msg.attach(control)
345     msg.attach(enc)
346     msg['Content-Disposition'] = 'inline'
347     return msg
348
349 def _get_encrypted_parts(message):
350     ct = message.get_content_type()
351     assert ct == 'multipart/encrypted', ct
352     params = dict(message.get_params())
353     assert params.get('protocol', None) == 'application/pgp-encrypted', params
354     assert message.is_multipart(), message
355     control = body = None
356     for part in message.get_payload():
357         if part == message:
358             continue
359         assert part.is_multipart() == False, part
360         ct = part.get_content_type()
361         if ct == 'application/pgp-encrypted':
362             if control:
363                 raise ValueError('multiple application/pgp-encrypted parts')
364             control = part
365         elif ct == 'application/octet-stream':
366             if body:
367                 raise ValueError('multiple application/octet-stream parts')
368             body = part
369         else:
370             raise ValueError('unnecessary {} part'.format(ct))
371     if not control:
372         raise ValueError('missing application/pgp-encrypted part')
373     if not body:
374         raise ValueError('missing application/octet-stream part')
375     return (control, body)
376
377 def _get_signed_parts(message):
378     ct = message.get_content_type()
379     assert ct == 'multipart/signed', ct
380     params = dict(message.get_params())
381     assert params.get('protocol', None) == 'application/pgp-signature', params
382     assert message.is_multipart(), message
383     body = signature = None
384     for part in message.get_payload():
385         if part == message:
386             continue
387         ct = part.get_content_type()
388         if ct == 'application/pgp-signature':
389             if signature:
390                 raise ValueError('multiple application/pgp-signature parts')
391             signature = part
392         else:
393             if body:
394                 raise ValueError('multiple non-signature parts')
395             body = part
396     if not body:
397         raise ValueError('missing body part')
398     if not signature:
399         raise ValueError('missing application/pgp-signature part')
400     return (body, signature)
401
402 def decrypt(message, **kwargs):
403     r"""Decrypt a multipart/encrypted message.
404
405     >>> from pgp_mime.email import encodedMIMEText
406     >>> message = encodedMIMEText('Hi\nBye')
407     >>> encrypted = encrypt(
408     ...     message, recipients=['<pgp-mime@invalid.com>'], always_trust=True)
409     >>> decrypted = decrypt(encrypted)
410     >>> print(decrypted.as_string().replace('\r\n', '\n'))
411     ... # doctest: +ELLIPSIS, +REPORT_UDIFF
412     Content-Type: text/plain; charset="us-ascii"
413     MIME-Version: 1.0
414     Content-Transfer-Encoding: 7bit
415     Content-Disposition: inline
416     <BLANKLINE>
417     Hi
418     Bye
419
420     >>> from email.mime.multipart import MIMEMultipart
421     >>> message = MIMEMultipart()
422     >>> message.attach(encodedMIMEText('Part A'))
423     >>> message.attach(encodedMIMEText('Part B'))
424     >>> encrypted = encrypt(
425     ...     message, recipients=['pgp-mime@invalid.com'], always_trust=True)
426     >>> decrypted = decrypt(encrypted)
427     >>> decrypted.set_boundary('boundsep')
428     >>> print(decrypted.as_string()) # doctest: +ELLIPSIS, +REPORT_UDIFF
429     Content-Type: multipart/mixed; boundary="boundsep"
430     MIME-Version: 1.0
431     <BLANKLINE>
432     --boundsep
433     Content-Type: text/plain; charset="us-ascii"
434     MIME-Version: 1.0
435     Content-Transfer-Encoding: 7bit
436     Content-Disposition: inline
437     <BLANKLINE>
438     Part A
439     --boundsep
440     Content-Type: text/plain; charset="us-ascii"
441     MIME-Version: 1.0
442     Content-Transfer-Encoding: 7bit
443     Content-Disposition: inline
444     <BLANKLINE>
445     Part B
446     --boundsep--
447     <BLANKLINE>
448     """
449     control,body = _get_encrypted_parts(message)
450     encrypted = body.get_payload(decode=True)
451     if not isinstance(encrypted, bytes):
452         encrypted = encrypted.encode('us-ascii')
453     decrypted,verified,result = _verify_bytes(encrypted, **kwargs)
454     return _message_from_bytes(decrypted)
455
456 def verify(message, **kwargs):
457     r"""Verify a signature on ``message``, possibly decrypting first.
458
459     >>> from pgp_mime.email import encodedMIMEText
460     >>> message = encodedMIMEText('Hi\nBye')
461     >>> message['To'] = 'pgp-mime-test <pgp-mime@invalid.com>'
462     >>> encrypted = sign_and_encrypt(message, signers=['pgp-mime@invalid.com'],
463     ...     always_trust=True)
464     >>> decrypted,verified,signatures = verify(encrypted)
465     >>> print(decrypted.as_string().replace('\r\n', '\n'))
466     ... # doctest: +ELLIPSIS, +REPORT_UDIFF
467     Content-Type: text/plain; charset="us-ascii"
468     MIME-Version: 1.0
469     Content-Transfer-Encoding: 7bit
470     Content-Disposition: inline
471     To: pgp-mime-test <pgp-mime@invalid.com>
472     <BLANKLINE>
473     Hi
474     Bye
475     >>> verified
476     False
477     >>> for s in signatures:
478     ...     print(s.dumps())  # doctest: +REPORT_UDIFF
479     ... # doctest: +REPORT_UDIFF, +ELLIPSIS
480     DECC812C8795ADD60538B0CD171008BA2F73DE2E signature:
481       summary:
482         CRL missing: False
483         CRL too old: False
484         bad policy: False
485         green: False
486         key expired: False
487         key missing: False
488         key revoked: False
489         red: False
490         signature expired: False
491         system error: False
492         valid: False
493       status: success
494       timestamp: ...
495       expiration timestamp: None
496       wrong key usage: False
497       pka trust: not available
498       chain model: False
499       validity: unknown
500       validity reason: success
501       public key algorithm: RSA
502       hash algorithm: SHA256
503
504     >>> from email.mime.multipart import MIMEMultipart
505     >>> message = MIMEMultipart()
506     >>> message.attach(encodedMIMEText('Part A'))
507     >>> message.attach(encodedMIMEText('Part B'))
508     >>> signed = sign(message, signers=['pgp-mime@invalid.com'])
509     >>> decrypted,verified,signatures = verify(signed)
510     >>> decrypted.set_boundary('boundsep')
511     >>> print(decrypted.as_string())  # doctest: +ELLIPSIS, +REPORT_UDIFF
512     Content-Type: multipart/mixed; boundary="boundsep"
513     MIME-Version: 1.0
514     <BLANKLINE>
515     --boundsep
516     Content-Type: text/plain; charset="us-ascii"
517     MIME-Version: 1.0
518     Content-Transfer-Encoding: 7bit
519     Content-Disposition: inline
520     <BLANKLINE>
521     Part A
522     --boundsep
523     Content-Type: text/plain; charset="us-ascii"
524     MIME-Version: 1.0
525     Content-Transfer-Encoding: 7bit
526     Content-Disposition: inline
527     <BLANKLINE>
528     Part B
529     --boundsep--
530     >>> verified
531     False
532     >>> for s in signatures:
533     ...     print(s.dumps())  # doctest: +REPORT_UDIFF
534     ... # doctest: +REPORT_UDIFF, +ELLIPSIS
535     DECC812C8795ADD60538B0CD171008BA2F73DE2E signature:
536       summary:
537         CRL missing: False
538         CRL too old: False
539         bad policy: False
540         green: False
541         key expired: False
542         key missing: False
543         key revoked: False
544         red: False
545         signature expired: False
546         system error: False
547         valid: False
548       status: success
549       timestamp: ...
550       expiration timestamp: None
551       wrong key usage: False
552       pka trust: not available
553       chain model: False
554       validity: unknown
555       validity reason: success
556       public key algorithm: RSA
557       hash algorithm: SHA1
558
559     Test a message generated by Mutt (for sanity):
560
561     >>> from email import message_from_bytes
562     >>> message_bytes = b'\n'.join([
563     ...   b'Return-Path: <pgp-mime@invalid.com>',
564     ...   b'Received: by invalid; Tue, 24 Apr 2012 19:46:59 -0400',
565     ...   b'Date: Tue, 24 Apr 2012 19:46:59 -0400',
566     ...   b'From: pgp-mime-test <pgp-mime@invalid.com',
567     ...   b'To: pgp-mime@invalid.com',
568     ...   b'Subject: test',
569     ...   b'Message-ID: <20120424233415.GA27788@invalid>',
570     ...   b'MIME-Version: 1.0',
571     ...   b'Content-Type: multipart/signed; micalg=pgp-sha1;',
572     ...   b'  protocol="application/pgp-signature";',
573     ...   b'  boundary="kORqDWCi7qDJ0mEj"',
574     ...   b'Content-Disposition: inline',
575     ...   b'User-Agent: Mutt/1.5.21 (2010-09-15)',
576     ...   b'Content-Length: 740',
577     ...   b'',
578     ...   b'',
579     ...   b'--kORqDWCi7qDJ0mEj',
580     ...   b'Content-Type: text/plain; charset=us-ascii',
581     ...   b'Content-Disposition: inline',
582     ...   b'',
583     ...   b'ping!',
584     ...   b'',
585     ...   b'--kORqDWCi7qDJ0mEj',
586     ...   b'Content-Type: application/pgp-signature; name="signature.asc"',
587     ...   b'Content-Description: OpenPGP digital signature',
588     ...   b'',
589     ...   b'-----BEGIN PGP SIGNATURE-----',
590     ...   b'Version: GnuPG v2.0.17 (GNU/Linux)',
591     ...   b'',
592     ...   b'iQEcBAEBAgAGBQJPlztxAAoJEFEa7aZDMrbjwT0H/i9eN6CJ2FIinK7Ps04XYEbL',
593     ...   b'PSQV1xCxb+2bk7yA4zQnjAKOPSuMDXfVG669Pbj8yo4DOgUqIgh+lK+voec9uwsJ',
594     ...   b'ZgUJcMozSmEFSTPO+Fiyx0S+NjnaLsas6IQrQTVDc6lWiIZttgxuN0crH5DcLomB',
595     ...   b'Ip90+ELbzVN3yBAjMJ1Y6xnKd7C0IOKm7VunYu9eCzJ/Rik5qZ0+IacQQnnrFJEN',
596     ...   b'04nDvDUzfaKy80Ke7VAQBIRi85XCsM2h0KDXOGUZ0xPQ8L/4eUK9tL6DJaqKqFPl',
597     ...   b'zNiwfpue01o6l6kngrQdXZ3tuv0HbLGc4ACzfz5XuGvE5PYTNEsylKLUMiSCIFc=',
598     ...   b'=xP0S',
599     ...   b'-----END PGP SIGNATURE-----',
600     ...   b'',
601     ...   b'--kORqDWCi7qDJ0mEj--',
602     ...   b''])
603     >>> message = message_from_bytes(message_bytes)
604     >>> decrypted,verified,signatures = verify(message)
605     >>> print(decrypted.as_string())  # doctest: +ELLIPSIS, +REPORT_UDIFF
606     Content-Type: text/plain; charset=us-ascii
607     Content-Disposition: inline
608     <BLANKLINE>
609     ping!
610     <BLANKLINE>
611     >>> verified
612     False
613     >>> for s in signatures:
614     ...     print(s.dumps())  # doctest: +REPORT_UDIFF
615     ... # doctest: +REPORT_UDIFF, +ELLIPSIS
616     B2EDBE0E771A4B8708DD16A7511AEDA64332B6E3 signature:
617       summary:
618         CRL missing: False
619         CRL too old: False
620         bad policy: False
621         green: False
622         key expired: False
623         key missing: False
624         key revoked: False
625         red: False
626         signature expired: False
627         system error: False
628         valid: False
629       status: success
630       timestamp: Tue Apr 24 23:46:57 2012
631       expiration timestamp: None
632       wrong key usage: False
633       pka trust: not available
634       chain model: False
635       validity: unknown
636       validity reason: success
637       public key algorithm: RSA
638       hash algorithm: SHA1
639     """
640     ct = message.get_content_type()
641     if ct == 'multipart/encrypted':
642         control,body = _get_encrypted_parts(message)
643         encrypted = body.get_payload(decode=True)
644         if not isinstance(encrypted, bytes):
645             encrypted = encrypted.encode('us-ascii')
646         decrypted,verified,message = _verify_bytes(encrypted)
647         return (_message_from_bytes(decrypted), verified, message)
648     body,signature = _get_signed_parts(message)
649     sig_data = signature.get_payload(decode=True)
650     if not isinstance(sig_data, bytes):
651         sig_data = sig_data.encode('us-ascii')
652     decrypted,verified,result = _verify_bytes(
653         _flatten(body), signature=sig_data, **kwargs)
654     return (_copy.deepcopy(body), verified, result)