Merge branch 'master' into intended-recipient
This commit is contained in:
@@ -165,6 +165,9 @@ Constants
|
|||||||
.. autoattribute:: Positive_Cert
|
.. autoattribute:: Positive_Cert
|
||||||
:annotation:
|
:annotation:
|
||||||
|
|
||||||
|
.. autoattribute:: Attestation
|
||||||
|
:annotation:
|
||||||
|
|
||||||
.. autoattribute:: Subkey_Binding
|
.. autoattribute:: Subkey_Binding
|
||||||
:annotation:
|
:annotation:
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
======
|
======
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)]
|
||||||
|
|||||||
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 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")
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user