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