Merge branch 'master' into intended-recipient
This commit is contained in:
@@ -165,6 +165,9 @@ Constants
|
||||
.. autoattribute:: Positive_Cert
|
||||
:annotation:
|
||||
|
||||
.. autoattribute:: Attestation
|
||||
:annotation:
|
||||
|
||||
.. autoattribute:: Subkey_Binding
|
||||
:annotation:
|
||||
|
||||
|
||||
@@ -4,6 +4,14 @@
|
||||
Changelog
|
||||
*********
|
||||
|
||||
v?
|
||||
==
|
||||
|
||||
Bugs Fixed
|
||||
----------
|
||||
|
||||
* Passphrases are now encoded as utf-8 instead of latin-1 (#294)
|
||||
|
||||
v0.5.2
|
||||
======
|
||||
|
||||
|
||||
@@ -423,6 +423,7 @@ class SignatureType(IntEnum):
|
||||
Persona_Cert = 0x11
|
||||
Casual_Cert = 0x12
|
||||
Positive_Cert = 0x13
|
||||
Attestation = 0x16
|
||||
Subkey_Binding = 0x18
|
||||
PrimaryKey_Binding = 0x19
|
||||
DirectlyOnKey = 0x1F
|
||||
@@ -433,8 +434,7 @@ class SignatureType(IntEnum):
|
||||
ThirdParty_Confirmation = 0x50
|
||||
|
||||
|
||||
class KeyServerPreferences(IntEnum):
|
||||
Unknown = 0x00
|
||||
class KeyServerPreferences(FlagEnum):
|
||||
NoModify = 0x80
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import contextlib
|
||||
import functools
|
||||
import six
|
||||
import warnings
|
||||
import logging
|
||||
|
||||
try:
|
||||
from singledispatch import singledispatch
|
||||
@@ -100,7 +100,7 @@ class KeyAction(object):
|
||||
|
||||
if _key is not key:
|
||||
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)
|
||||
|
||||
yield _key
|
||||
|
||||
@@ -1018,7 +1018,8 @@ class String2Key(Field):
|
||||
|
||||
# Simple S2K - always done
|
||||
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
|
||||
if self.specifier >= String2KeyType.Salted:
|
||||
|
||||
@@ -415,6 +415,36 @@ class SignatureV4(Signature):
|
||||
|
||||
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):
|
||||
spkt = SignatureV4()
|
||||
spkt.header = copy.copy(self.header)
|
||||
|
||||
@@ -56,7 +56,8 @@ __all__ = ['URI',
|
||||
'Features',
|
||||
'EmbeddedSignature',
|
||||
'IssuerFingerprint',
|
||||
'IntendedRecipient']
|
||||
'IntendedRecipient',
|
||||
'AttestedCertifications']
|
||||
|
||||
|
||||
class URI(Signature):
|
||||
@@ -243,7 +244,7 @@ class CreationTime(Signature):
|
||||
|
||||
def __bytearray__(self):
|
||||
_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
|
||||
|
||||
def parse(self, packet):
|
||||
@@ -691,7 +692,7 @@ class PreferredCompressionAlgorithms(FlagList):
|
||||
__flags__ = CompressionAlgorithm
|
||||
|
||||
|
||||
class KeyServerPreferences(FlagList):
|
||||
class KeyServerPreferences(ByteFlag):
|
||||
__typeid__ = 0x17
|
||||
__flags__ = _KeyServerPreferences
|
||||
|
||||
@@ -1030,3 +1031,103 @@ class IntendedRecipient(Signature):
|
||||
|
||||
self.intended_recipient = 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)]
|
||||
|
||||
130
pgpy/pgp.py
130
pgpy/pgp.py
@@ -265,6 +265,23 @@ class PGPSignature(Armorable, ParentRef, PGPObject):
|
||||
return self._reason_for_revocation(subpacket.code, subpacket.string)
|
||||
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
|
||||
def signer(self):
|
||||
"""
|
||||
@@ -364,6 +381,14 @@ class PGPSignature(Armorable, ParentRef, PGPObject):
|
||||
sig |= copy.copy(self._signature)
|
||||
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):
|
||||
_data = bytearray()
|
||||
|
||||
@@ -634,6 +659,53 @@ class PGPUID(ParentRef):
|
||||
if self.is_ua:
|
||||
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
|
||||
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:
|
||||
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)
|
||||
h2 = sig.hash_algorithm.hasher
|
||||
h2.update(sigdata)
|
||||
@@ -1892,6 +1968,9 @@ class PGPKey(Armorable, ParentRef, PGPObject):
|
||||
:type created: :py:obj:`~datetime.datetime`
|
||||
:keyword intended_recipients: Specify a list of :py:obj:`PGPKey` objects that will be encrypted to.
|
||||
: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
|
||||
hash_algo = prefs.pop('hash', None)
|
||||
@@ -1905,8 +1984,7 @@ class PGPKey(Armorable, ParentRef, PGPObject):
|
||||
|
||||
subject = subject.message
|
||||
|
||||
created = prefs.pop('created', None)
|
||||
sig = PGPSignature.new(sig_type, self.key_algorithm, hash_algo, self.fingerprint.keyid, created=created)
|
||||
sig = PGPSignature.new(sig_type, self.key_algorithm, hash_algo, self.fingerprint.keyid, created=prefs.pop('created', None))
|
||||
|
||||
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.
|
||||
This keyword is ignored for non-self-certifications.
|
||||
: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.
|
||||
This keyword is ignored for non-self-certifications.
|
||||
: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.
|
||||
This keyword is ignored for non-self-certifications, and any certifications directly on keys.
|
||||
:type primary: ``bool``
|
||||
@@ -1964,13 +2050,15 @@ class PGPKey(Armorable, ParentRef, PGPObject):
|
||||
this regular expression.
|
||||
This is meaningless without also specifying trust level and amount.
|
||||
:type regex: ``str``
|
||||
:keyword exportable: Whether this certification is exportable or not.
|
||||
:type exportable: ``bool``
|
||||
"""
|
||||
hash_algo = prefs.pop('hash', None)
|
||||
sig_type = level
|
||||
if isinstance(subject, PGPKey):
|
||||
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
|
||||
usage = prefs.pop('usage', None)
|
||||
@@ -1997,6 +2085,7 @@ class PGPKey(Armorable, ParentRef, PGPObject):
|
||||
keyserver_flags = prefs.pop('keyserver_flags', None)
|
||||
keyserver = prefs.pop('keyserver', None)
|
||||
primary_uid = prefs.pop('primary', None)
|
||||
attested_certifications = prefs.pop('attested_certifications', [])
|
||||
|
||||
if key_expires is not None:
|
||||
# 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:
|
||||
sig._signature.subpackets.addnew('PrimaryUserID', hashed=True, primary=primary_uid)
|
||||
|
||||
# Features is always set on self-signatures
|
||||
sig._signature.subpackets.addnew('Features', hashed=True, flags=Features.pgpy_features)
|
||||
cert_sigtypes = {SignatureType.Generic_Cert, SignatureType.Persona_Cert,
|
||||
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:
|
||||
# signature options that only make sense in non-self-certifications
|
||||
@@ -2080,7 +2188,7 @@ class PGPKey(Armorable, ParentRef, PGPObject):
|
||||
else: # pragma: no cover
|
||||
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
|
||||
reason = prefs.pop('reason', RevocationReason.NotSpecified)
|
||||
@@ -2109,7 +2217,7 @@ class PGPKey(Armorable, ParentRef, PGPObject):
|
||||
"""
|
||||
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
|
||||
sensitive = prefs.pop('sensitive', False)
|
||||
@@ -2150,7 +2258,7 @@ class PGPKey(Armorable, ParentRef, PGPObject):
|
||||
else: # pragma: no cover
|
||||
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:
|
||||
# signature options that only make sense in subkey binding signatures
|
||||
@@ -2228,9 +2336,6 @@ class PGPKey(Armorable, ParentRef, PGPObject):
|
||||
sigv = SignatureVerification()
|
||||
for sig, subj in sspairs:
|
||||
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)
|
||||
|
||||
else:
|
||||
@@ -2329,9 +2434,6 @@ class PGPKey(Armorable, ParentRef, PGPObject):
|
||||
mis = set(message.encrypters)
|
||||
if sks & mis:
|
||||
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)
|
||||
|
||||
raise PGPError("Cannot decrypt the provided message with this key")
|
||||
|
||||
@@ -109,6 +109,7 @@ _sspclasses = {
|
||||
0x20: 'EmbeddedSignature',
|
||||
0x21: 'IssuerFingerprint',
|
||||
0x23: 'IntendedRecipient',
|
||||
0x25: 'AttestedCertifications',
|
||||
# 0x64-0x6e: Private or Experimental
|
||||
0x64: 'Opaque',
|
||||
0x65: 'Opaque',
|
||||
|
||||
@@ -335,7 +335,7 @@ class TestPGPKey_Management(object):
|
||||
hashes=[HashAlgorithm.SHA384],
|
||||
compression=[CompressionAlgorithm.ZLIB],
|
||||
key_expiration=expiration,
|
||||
keyserver_flags=0x80,
|
||||
keyserver_flags={KeyServerPreferences.NoModify},
|
||||
keyserver='about:none',
|
||||
primary=False)
|
||||
|
||||
@@ -348,7 +348,7 @@ class TestPGPKey_Management(object):
|
||||
assert sig.features == {Features.ModificationDetection}
|
||||
assert sig.key_expiration == expiration - key.created
|
||||
assert sig.keyserver == 'about:none'
|
||||
assert sig.keyserverprefs == [KeyServerPreferences.NoModify]
|
||||
assert sig.keyserverprefs == {KeyServerPreferences.NoModify}
|
||||
|
||||
assert uid.is_primary is False
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ try:
|
||||
except (ModuleNotFoundError, NameError):
|
||||
gpg = None
|
||||
import os
|
||||
import datetime
|
||||
import pytest
|
||||
import glob
|
||||
import warnings
|
||||
@@ -418,3 +419,47 @@ def test_preference_unsupported_ciphers():
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter('ignore')
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user