Merge branch 'master' into intended-recipient

This commit is contained in:
Michael Greene
2019-11-01 16:35:34 -07:00
committed by GitHub
11 changed files with 315 additions and 24 deletions

View File

@@ -165,6 +165,9 @@ Constants
.. autoattribute:: Positive_Cert .. autoattribute:: Positive_Cert
:annotation: :annotation:
.. autoattribute:: Attestation
:annotation:
.. autoattribute:: Subkey_Binding .. autoattribute:: Subkey_Binding
:annotation: :annotation:

View File

@@ -4,6 +4,14 @@
Changelog Changelog
********* *********
v?
==
Bugs Fixed
----------
* Passphrases are now encoded as utf-8 instead of latin-1 (#294)
v0.5.2 v0.5.2
====== ======

View File

@@ -423,6 +423,7 @@ class SignatureType(IntEnum):
Persona_Cert = 0x11 Persona_Cert = 0x11
Casual_Cert = 0x12 Casual_Cert = 0x12
Positive_Cert = 0x13 Positive_Cert = 0x13
Attestation = 0x16
Subkey_Binding = 0x18 Subkey_Binding = 0x18
PrimaryKey_Binding = 0x19 PrimaryKey_Binding = 0x19
DirectlyOnKey = 0x1F DirectlyOnKey = 0x1F
@@ -433,8 +434,7 @@ class SignatureType(IntEnum):
ThirdParty_Confirmation = 0x50 ThirdParty_Confirmation = 0x50
class KeyServerPreferences(IntEnum): class KeyServerPreferences(FlagEnum):
Unknown = 0x00
NoModify = 0x80 NoModify = 0x80

View File

@@ -3,7 +3,7 @@
import contextlib import contextlib
import functools import functools
import six import six
import warnings import logging
try: try:
from singledispatch import singledispatch from singledispatch import singledispatch
@@ -100,7 +100,7 @@ class KeyAction(object):
if _key is not key: if _key is not key:
em['subkeyid'] = _key.fingerprint.keyid em['subkeyid'] = _key.fingerprint.keyid
warnings.warn("Key {keyid:s} does not have the required usage flag {flags:s}; using subkey {subkeyid:s}" logging.debug("Key {keyid:s} does not have the required usage flag {flags:s}; using subkey {subkeyid:s}"
"".format(**em), stacklevel=4) "".format(**em), stacklevel=4)
yield _key yield _key

View File

@@ -1018,7 +1018,8 @@ class String2Key(Field):
# Simple S2K - always done # Simple S2K - always done
hsalt = b'' hsalt = b''
hpass = passphrase.encode('latin-1') ##TODO: we could accept a passphrase that is optionally already `bytes`
hpass = passphrase.encode('utf-8')
# salted, iterated S2K # salted, iterated S2K
if self.specifier >= String2KeyType.Salted: if self.specifier >= String2KeyType.Salted:

View File

@@ -415,6 +415,36 @@ class SignatureV4(Signature):
return _bytes return _bytes
def canonical_bytes(self):
'''Returns a bytearray that is the way the signature packet
should be represented if it is itself being signed.
from RFC 4880 section 5.2.4:
When a signature is made over a Signature packet (type 0x50), the
hash data starts with the octet 0x88, followed by the four-octet
length of the signature, and then the body of the Signature packet.
(Note that this is an old-style packet header for a Signature packet
with the length-of-length set to zero.) The unhashed subpacket data
of the Signature packet being hashed is not included in the hash, and
the unhashed subpacket data length value is set to zero.
'''
_body = bytearray()
_body += self.int_to_bytes(self.header.version)
_body += self.int_to_bytes(self.sigtype)
_body += self.int_to_bytes(self.pubalg)
_body += self.int_to_bytes(self.halg)
_body += self.subpackets.__hashbytearray__()
_body += self.int_to_bytes(0, minlen=2) # empty unhashed subpackets
_body += self.hash2
_body += self.signature.__bytearray__()
_hdr = bytearray()
_hdr += b'\x88'
_hdr += self.int_to_bytes(len(_body), minlen=4)
return _hdr + _body
def __copy__(self): def __copy__(self):
spkt = SignatureV4() spkt = SignatureV4()
spkt.header = copy.copy(self.header) spkt.header = copy.copy(self.header)

View File

@@ -56,7 +56,8 @@ __all__ = ['URI',
'Features', 'Features',
'EmbeddedSignature', 'EmbeddedSignature',
'IssuerFingerprint', 'IssuerFingerprint',
'IntendedRecipient'] 'IntendedRecipient',
'AttestedCertifications']
class URI(Signature): class URI(Signature):
@@ -243,7 +244,7 @@ class CreationTime(Signature):
def __bytearray__(self): def __bytearray__(self):
_bytes = super(CreationTime, self).__bytearray__() _bytes = super(CreationTime, self).__bytearray__()
_bytes += self.int_to_bytes(calendar.timegm(self.created.timetuple()), 4) _bytes += self.int_to_bytes(calendar.timegm(self.created.utctimetuple()), 4)
return _bytes return _bytes
def parse(self, packet): def parse(self, packet):
@@ -691,7 +692,7 @@ class PreferredCompressionAlgorithms(FlagList):
__flags__ = CompressionAlgorithm __flags__ = CompressionAlgorithm
class KeyServerPreferences(FlagList): class KeyServerPreferences(ByteFlag):
__typeid__ = 0x17 __typeid__ = 0x17
__flags__ = _KeyServerPreferences __flags__ = _KeyServerPreferences
@@ -1030,3 +1031,103 @@ class IntendedRecipient(Signature):
self.intended_recipient = packet[:fpr_len] self.intended_recipient = packet[:fpr_len]
del packet[:fpr_len] del packet[:fpr_len]
class AttestedCertifications(Signature):
'''
(from RFC4880bis-08)
5.2.3.30. Attested Certifications
(N octets of certification digests)
This subpacket MUST only appear as a hashed subpacket of an
Attestation Key Signature. It has no meaning in any other signature
type. It is used by the primary key to attest to a set of third-
party certifications over the associated User ID or User Attribute.
This enables the holder of an OpenPGP primary key to mark specific
third-party certifications as re-distributable with the rest of the
Transferable Public Key (see the "No-modify" flag in "Key Server
Preferences", above). Implementations MUST include exactly one
Attested Certification subpacket in any generated Attestation Key
Signature.
The contents of the subpacket consists of a series of digests using
the same hash algorithm used by the signature itself. Each digest is
made over one third-party signature (any Certification, i.e.,
signature type 0x10-0x13) that covers the same Primary Key and User
ID (or User Attribute). For example, an Attestation Key Signature
made by key X over user ID U using hash algorithm SHA256 might
contain an Attested Certifications subpacket of 192 octets (6*32
octets) covering six third-party certification Signatures over <X,U>.
They SHOULD be ordered by binary hash value from low to high (e.g., a
hash with hexadecimal value 037a... precedes a hash with value
0392..., etc). The length of this subpacket MUST be an integer
multiple of the length of the hash algorithm used for the enclosing
Attestation Key Signature.
The listed digests MUST be calculated over the third-party
certification's Signature packet as described in the "Computing
Signatures" section, but without a trailer: the hash data starts with
the octet 0x88, followed by the four-octet length of the Signature,
and then the body of the Signature packet. (Note that this is an
old-style packet header for a Signature packet with the length-of-
length field set to zero.) The unhashed subpacket data of the
Signature packet being hashed is not included in the hash, and the
unhashed subpacket data length value is set to zero.
If an implementation encounters more than one such subpacket in an
Attestation Key Signature, it MUST treat it as a single Attested
Certifications subpacket containing the union of all hashes.
The Attested Certifications subpacket in the most recent Attestation
Key Signature over a given user ID supersedes all Attested
Certifications subpackets from any previous Attestation Key
Signature. However, note that if more than one Attestation Key
Signatures has the same (most recent) Signature Creation Time
subpacket, implementations MUST consider the union of the
attestations of all Attestation Key Signatures (this allows the
keyholder to attest to more third-party certifications than could fit
in a single Attestation Key Signature).
If a keyholder Alice has already attested to third-party
certifications from Bob and Carol and she wants to add an attestation
to a certification from David, she should issue a new Attestation Key
Signature (with a more recent Signature Creation timestamp) that
contains an Attested Certifications subpacket covering all three
third-party certifications.
If she later decides that she does not want Carol's certification to
be redistributed with her certificate, she can issue a new
Attestation Key Signature (again, with a more recent Signature
Creation timestamp) that contains an Attested Certifications
subpacket covering only the certifications from Bob and David.
Note that Certification Revocation Signatures are not relevant for
Attestation Key Signatures. To rescind all attestations, the primary
key holder needs only to publish a more recent Attestation Key
Signature with an empty Attested Certifications subpacket.
'''
__typeid__ = 0x25
@sdproperty
def attested_certifications(self):
return self._attested_certifications
@attested_certifications.register(bytearray)
@attested_certifications.register(bytes)
def attested_certifications_bytearray(self, val):
self._attested_certifications = val
def __init__(self):
super(AttestedCertifications, self).__init__()
self._attested_certifications = bytearray()
def __bytearray__(self):
_bytes = super(AttestedCertifications, self).__bytearray__()
_bytes += self._attested_certifications
return _bytes
def parse(self, packet):
super(AttestedCertifications, self).parse(packet)
self.attested_certifications = packet[:(self.header.length - 1)]
del packet[:(self.header.length - 1)]

View File

@@ -265,6 +265,23 @@ class PGPSignature(Armorable, ParentRef, PGPObject):
return self._reason_for_revocation(subpacket.code, subpacket.string) return self._reason_for_revocation(subpacket.code, subpacket.string)
return None return None
@property
def attested_certifications(self):
"""
Returns a set of all the hashes of attested certifications covered by this Attestation Key Signature.
Unhashed subpackets are ignored.
"""
if self._signature.sigtype != SignatureType.Attestation:
return set()
ret = set()
hlen = self.hash_algorithm.digest_size
for n in self._signature.subpackets['h_AttestedCertifications']:
attestations = bytes(n.attested_certifications)
for i in range(0, len(attestations), hlen):
ret.add(attestations[i:i+hlen])
return ret
@property @property
def signer(self): def signer(self):
""" """
@@ -364,6 +381,14 @@ class PGPSignature(Armorable, ParentRef, PGPObject):
sig |= copy.copy(self._signature) sig |= copy.copy(self._signature)
return sig return sig
def attests_to(self, othersig):
'returns True if this signature attests to othersig (acknolwedges it for redistribution)'
if not isinstance(othersig, PGPSignature):
raise TypeError
h = self.hash_algorithm.hasher
h.update(othersig._signature.canonical_bytes())
return h.digest() in self.attested_certifications
def hashdata(self, subject): def hashdata(self, subject):
_data = bytearray() _data = bytearray()
@@ -634,6 +659,53 @@ class PGPUID(ParentRef):
if self.is_ua: if self.is_ua:
return self._uid.subpackets.__bytearray__() return self._uid.subpackets.__bytearray__()
@property
def third_party_certifications(self):
'''
A generator returning all third-party certifications
'''
if self.parent is None:
return
fpr = self.parent.fingerprint
keyid = self.parent.fingerprint.keyid
for sig in self._signatures:
if (sig.signer_fingerprint != '' and fpr != sig.signer_fingerprint) or (sig.signer != keyid):
yield sig
def attested_to(self, certifications):
'''filter certifications, only returning those that have been attested to by the first party'''
# first find the set of the most recent valid Attestation Key Signatures:
if self.parent is None:
return
mostrecent = None
attestations = []
now = datetime.utcnow()
fpr = self.parent.fingerprint
keyid = self.parent.fingerprint.keyid
for sig in self._signatures:
if sig._signature.sigtype == SignatureType.Attestation and \
((sig.signer_fingerprint == fpr) or (sig.signer == keyid)) and \
self.parent.verify(self, sig) and \
sig.created <= now:
if mostrecent is None or sig.created > mostrecent:
attestations = [sig]
mostrecent = sig.created
elif sig.created == mostrecent:
attestations.append(sig)
# now filter the certifications:
for certification in certifications:
for a in attestations:
if a.attests_to(certification):
yield certification
@property
def attested_third_party_certifications(self):
'''
A generator that provides a list of all third-party certifications attested to
by the primary key.
'''
return self.attested_to(self.third_party_certifications)
@classmethod @classmethod
def new(cls, pn, comment="", email=""): def new(cls, pn, comment="", email=""):
""" """
@@ -1847,6 +1919,10 @@ class PGPKey(Armorable, ParentRef, PGPObject):
if sig.type == SignatureType.Timestamp and len(sig._signature.subpackets._hashed_sp) > 1: if sig.type == SignatureType.Timestamp and len(sig._signature.subpackets._hashed_sp) > 1:
sig._signature.sigtype = SignatureType.Standalone sig._signature.sigtype = SignatureType.Standalone
if prefs.pop('include_issuer_fingerprint', True):
if isinstance(self._key, PrivKeyV4):
sig._signature.subpackets.addnew('IssuerFingerprint', hashed=True, _version=4, _issuer_fpr=self.fingerprint)
sigdata = sig.hashdata(subject) sigdata = sig.hashdata(subject)
h2 = sig.hash_algorithm.hasher h2 = sig.hash_algorithm.hasher
h2.update(sigdata) h2.update(sigdata)
@@ -1892,6 +1968,9 @@ class PGPKey(Armorable, ParentRef, PGPObject):
:type created: :py:obj:`~datetime.datetime` :type created: :py:obj:`~datetime.datetime`
:keyword intended_recipients: Specify a list of :py:obj:`PGPKey` objects that will be encrypted to. :keyword intended_recipients: Specify a list of :py:obj:`PGPKey` objects that will be encrypted to.
:type intended_recipients: ``list`` :type intended_recipients: ``list``
:keyword include_issuer_fingerprint: Whether to include a hashed subpacket indicating the issuer fingerprint.
(only for v4 keys, defaults to True)
:type include_issuer_fingerprint: ``bool``
""" """
sig_type = SignatureType.BinaryDocument sig_type = SignatureType.BinaryDocument
hash_algo = prefs.pop('hash', None) hash_algo = prefs.pop('hash', None)
@@ -1905,8 +1984,7 @@ class PGPKey(Armorable, ParentRef, PGPObject):
subject = subject.message subject = subject.message
created = prefs.pop('created', None) sig = PGPSignature.new(sig_type, self.key_algorithm, hash_algo, self.fingerprint.keyid, created=prefs.pop('created', None))
sig = PGPSignature.new(sig_type, self.key_algorithm, hash_algo, self.fingerprint.keyid, created=created)
return self._sign(subject, sig, **prefs) return self._sign(subject, sig, **prefs)
@@ -1945,9 +2023,17 @@ class PGPKey(Armorable, ParentRef, PGPObject):
:py:obj:`~datetime.timedelta` of how long after the key was created it should expire. :py:obj:`~datetime.timedelta` of how long after the key was created it should expire.
This keyword is ignored for non-self-certifications. This keyword is ignored for non-self-certifications.
:type key_expiration: :py:obj:`datetime.datetime`, :py:obj:`datetime.timedelta` :type key_expiration: :py:obj:`datetime.datetime`, :py:obj:`datetime.timedelta`
:keyword attested_certifications: A list of third-party certifications, as :py:obj:`PGPSignature`, that
the certificate holder wants to attest to for redistribution with the certificate.
Alternatively, any element in the list can be a ``bytes`` or ``bytearray`` object
of the appropriate length (the length of this certification's digest).
This keyword is only used for signatures of type Attestation.
:type attested_certifications: ``list``
:keyword keyserver: Specify the URI of the preferred key server of the user. :keyword keyserver: Specify the URI of the preferred key server of the user.
This keyword is ignored for non-self-certifications. This keyword is ignored for non-self-certifications.
:type keyserver: ``str``, ``unicode``, ``bytes`` :type keyserver: ``str``, ``unicode``, ``bytes``
:keyword keyserver_flags: A set of Key Server Preferences, as :py:obj:`~constants.KeyServerPreferences`.
:type keyserver_flags: ``set``
:keyword primary: Whether or not to consider the certified User ID as the primary one. :keyword primary: Whether or not to consider the certified User ID as the primary one.
This keyword is ignored for non-self-certifications, and any certifications directly on keys. This keyword is ignored for non-self-certifications, and any certifications directly on keys.
:type primary: ``bool`` :type primary: ``bool``
@@ -1964,13 +2050,15 @@ class PGPKey(Armorable, ParentRef, PGPObject):
this regular expression. this regular expression.
This is meaningless without also specifying trust level and amount. This is meaningless without also specifying trust level and amount.
:type regex: ``str`` :type regex: ``str``
:keyword exportable: Whether this certification is exportable or not.
:type exportable: ``bool``
""" """
hash_algo = prefs.pop('hash', None) hash_algo = prefs.pop('hash', None)
sig_type = level sig_type = level
if isinstance(subject, PGPKey): if isinstance(subject, PGPKey):
sig_type = SignatureType.DirectlyOnKey sig_type = SignatureType.DirectlyOnKey
sig = PGPSignature.new(sig_type, self.key_algorithm, hash_algo, self.fingerprint.keyid) sig = PGPSignature.new(sig_type, self.key_algorithm, hash_algo, self.fingerprint.keyid, created=prefs.pop('created', None))
# signature options that only make sense in certifications # signature options that only make sense in certifications
usage = prefs.pop('usage', None) usage = prefs.pop('usage', None)
@@ -1997,6 +2085,7 @@ class PGPKey(Armorable, ParentRef, PGPObject):
keyserver_flags = prefs.pop('keyserver_flags', None) keyserver_flags = prefs.pop('keyserver_flags', None)
keyserver = prefs.pop('keyserver', None) keyserver = prefs.pop('keyserver', None)
primary_uid = prefs.pop('primary', None) primary_uid = prefs.pop('primary', None)
attested_certifications = prefs.pop('attested_certifications', [])
if key_expires is not None: if key_expires is not None:
# key expires should be a timedelta, so if it's a datetime, turn it into a timedelta # key expires should be a timedelta, so if it's a datetime, turn it into a timedelta
@@ -2027,8 +2116,27 @@ class PGPKey(Armorable, ParentRef, PGPObject):
if primary_uid is not None: if primary_uid is not None:
sig._signature.subpackets.addnew('PrimaryUserID', hashed=True, primary=primary_uid) sig._signature.subpackets.addnew('PrimaryUserID', hashed=True, primary=primary_uid)
# Features is always set on self-signatures cert_sigtypes = {SignatureType.Generic_Cert, SignatureType.Persona_Cert,
sig._signature.subpackets.addnew('Features', hashed=True, flags=Features.pgpy_features) SignatureType.Casual_Cert, SignatureType.Positive_Cert,
SignatureType.CertRevocation}
# Features is always set on certifications:
if sig._signature.sigtype in cert_sigtypes:
sig._signature.subpackets.addnew('Features', hashed=True, flags=Features.pgpy_features)
# If this is an attestation, then we must include a Attested Certifications subpacket:
if sig._signature.sigtype == SignatureType.Attestation:
attestations = set()
for attestation in attested_certifications:
if isinstance(attestation, PGPSignature) and attestation.type in cert_sigtypes:
h = sig.hash_algorithm.hasher
h.update(attestation._signature.canonical_bytes())
attestations.add(h.digest())
elif isinstance(attestation, (bytes,bytearray)) and len(attestation) == sig.hash_algorithm.digest_size:
attestations.add(attestation)
else:
warnings.warn("Attested Certification element is neither a PGPSignature certification nor " +
"a bytes object of size %d, ignoring"%(sig.hash_algorithm.digest_size))
sig._signature.subpackets.addnew('AttestedCertifications', hashed=True, attested_certifications=b''.join(sorted(attestations)))
else: else:
# signature options that only make sense in non-self-certifications # signature options that only make sense in non-self-certifications
@@ -2080,7 +2188,7 @@ class PGPKey(Armorable, ParentRef, PGPObject):
else: # pragma: no cover else: # pragma: no cover
raise TypeError raise TypeError
sig = PGPSignature.new(sig_type, self.key_algorithm, hash_algo, self.fingerprint.keyid) sig = PGPSignature.new(sig_type, self.key_algorithm, hash_algo, self.fingerprint.keyid, created=prefs.pop('created', None))
# signature options that only make sense when revoking # signature options that only make sense when revoking
reason = prefs.pop('reason', RevocationReason.NotSpecified) reason = prefs.pop('reason', RevocationReason.NotSpecified)
@@ -2109,7 +2217,7 @@ class PGPKey(Armorable, ParentRef, PGPObject):
""" """
hash_algo = prefs.pop('hash', None) hash_algo = prefs.pop('hash', None)
sig = PGPSignature.new(SignatureType.DirectlyOnKey, self.key_algorithm, hash_algo, self.fingerprint.keyid) sig = PGPSignature.new(SignatureType.DirectlyOnKey, self.key_algorithm, hash_algo, self.fingerprint.keyid, created=prefs.pop('created', None))
# signature options that only make sense when adding a revocation key # signature options that only make sense when adding a revocation key
sensitive = prefs.pop('sensitive', False) sensitive = prefs.pop('sensitive', False)
@@ -2150,7 +2258,7 @@ class PGPKey(Armorable, ParentRef, PGPObject):
else: # pragma: no cover else: # pragma: no cover
raise PGPError raise PGPError
sig = PGPSignature.new(sig_type, self.key_algorithm, hash_algo, self.fingerprint.keyid) sig = PGPSignature.new(sig_type, self.key_algorithm, hash_algo, self.fingerprint.keyid, created=prefs.pop('created', None))
if sig_type == SignatureType.Subkey_Binding: if sig_type == SignatureType.Subkey_Binding:
# signature options that only make sense in subkey binding signatures # signature options that only make sense in subkey binding signatures
@@ -2228,9 +2336,6 @@ class PGPKey(Armorable, ParentRef, PGPObject):
sigv = SignatureVerification() sigv = SignatureVerification()
for sig, subj in sspairs: for sig, subj in sspairs:
if self.fingerprint.keyid != sig.signer and sig.signer in self.subkeys: if self.fingerprint.keyid != sig.signer and sig.signer in self.subkeys:
warnings.warn("Signature was signed with this key's subkey: {:s}. "
"Verifying with subkey...".format(sig.signer),
stacklevel=2)
sigv &= self.subkeys[sig.signer].verify(subj, sig) sigv &= self.subkeys[sig.signer].verify(subj, sig)
else: else:
@@ -2329,9 +2434,6 @@ class PGPKey(Armorable, ParentRef, PGPObject):
mis = set(message.encrypters) mis = set(message.encrypters)
if sks & mis: if sks & mis:
skid = list(sks & mis)[0] skid = list(sks & mis)[0]
warnings.warn("Message was encrypted with this key's subkey: {:s}. "
"Decrypting with that...".format(skid),
stacklevel=2)
return self.subkeys[skid].decrypt(message) return self.subkeys[skid].decrypt(message)
raise PGPError("Cannot decrypt the provided message with this key") raise PGPError("Cannot decrypt the provided message with this key")

View File

@@ -109,6 +109,7 @@ _sspclasses = {
0x20: 'EmbeddedSignature', 0x20: 'EmbeddedSignature',
0x21: 'IssuerFingerprint', 0x21: 'IssuerFingerprint',
0x23: 'IntendedRecipient', 0x23: 'IntendedRecipient',
0x25: 'AttestedCertifications',
# 0x64-0x6e: Private or Experimental # 0x64-0x6e: Private or Experimental
0x64: 'Opaque', 0x64: 'Opaque',
0x65: 'Opaque', 0x65: 'Opaque',

View File

@@ -335,7 +335,7 @@ class TestPGPKey_Management(object):
hashes=[HashAlgorithm.SHA384], hashes=[HashAlgorithm.SHA384],
compression=[CompressionAlgorithm.ZLIB], compression=[CompressionAlgorithm.ZLIB],
key_expiration=expiration, key_expiration=expiration,
keyserver_flags=0x80, keyserver_flags={KeyServerPreferences.NoModify},
keyserver='about:none', keyserver='about:none',
primary=False) primary=False)
@@ -348,7 +348,7 @@ class TestPGPKey_Management(object):
assert sig.features == {Features.ModificationDetection} assert sig.features == {Features.ModificationDetection}
assert sig.key_expiration == expiration - key.created assert sig.key_expiration == expiration - key.created
assert sig.keyserver == 'about:none' assert sig.keyserver == 'about:none'
assert sig.keyserverprefs == [KeyServerPreferences.NoModify] assert sig.keyserverprefs == {KeyServerPreferences.NoModify}
assert uid.is_primary is False assert uid.is_primary is False

View File

@@ -7,6 +7,7 @@ try:
except (ModuleNotFoundError, NameError): except (ModuleNotFoundError, NameError):
gpg = None gpg = None
import os import os
import datetime
import pytest import pytest
import glob import glob
import warnings import warnings
@@ -418,3 +419,47 @@ def test_preference_unsupported_ciphers():
with warnings.catch_warnings(): with warnings.catch_warnings():
warnings.simplefilter('ignore') warnings.simplefilter('ignore')
pubkey.encrypt(msg) pubkey.encrypt(msg)
@pytest.mark.regression(issue=291)
def test_sig_timezone():
from pgpy import PGPKey, PGPSignature
# from https://tools.ietf.org/html/draft-bre-openpgp-samples-00#section-2.2:
alice_sec = '''-----BEGIN PGP PRIVATE KEY BLOCK-----
Comment: Alice's OpenPGP Transferable Secret Key
lFgEXEcE6RYJKwYBBAHaRw8BAQdArjWwk3FAqyiFbFBKT4TzXcVBqPTB3gmzlC/U
b7O1u10AAP9XBeW6lzGOLx7zHH9AsUDUTb2pggYGMzd0P3ulJ2AfvQ4RtCZBbGlj
ZSBMb3ZlbGFjZSA8YWxpY2VAb3BlbnBncC5leGFtcGxlPoiQBBMWCAA4AhsDBQsJ
CAcCBhUKCQgLAgQWAgMBAh4BAheAFiEE64W7X6M6deFelE5j8jFVDE9H444FAl2l
nzoACgkQ8jFVDE9H447pKwD6A5xwUqIDprBzrHfahrImaYEZzncqb25vkLV2arYf
a78A/R3AwtLQvjxwLDuzk4dUtUwvUYibL2sAHwj2kGaHnfICnF0EXEcE6RIKKwYB
BAGXVQEFAQEHQEL/BiGtq0k84Km1wqQw2DIikVYrQrMttN8d7BPfnr4iAwEIBwAA
/3/xFPG6U17rhTuq+07gmEvaFYKfxRB6sgAYiW6TMTpQEK6IeAQYFggAIBYhBOuF
u1+jOnXhXpROY/IxVQxPR+OOBQJcRwTpAhsMAAoJEPIxVQxPR+OOWdABAMUdSzpM
hzGs1O0RkWNQWbUzQ8nUOeD9wNbjE3zR+yfRAQDbYqvtWQKN4AQLTxVJN5X5AWyb
Pnn+We1aTBhaGa86AQ==
=n8OM
-----END PGP PRIVATE KEY BLOCK-----
'''
alice_key, _ = PGPKey.from_blob(alice_sec)
class FixedOffset(datetime.tzinfo):
def __init__(self, hours, name):
self.__offset = datetime.timedelta(hours=hours)
self.__name = name
def utcoffset(self, dt):
return self.__offset
def tzname(self, dt):
return self.__name
def dst(self, dt):
return datetime.timedelta(0)
# America/New_York during DST:
tz = FixedOffset(-4, 'EDT')
# 2019-10-20T09:18:11-0400
when = datetime.datetime.fromtimestamp(1571577491, tz)
pgpsig = alice_key.sign('this is a test', created=when)
roundtrip = PGPSignature.from_blob(str(pgpsig))
assert pgpsig.created.utctimetuple() == roundtrip.created.utctimetuple()