Implement Attested Certifications
This makes the No-modify flag for Key Server Preferences actionable, by allowing the primary key holder the ability to indicate which third-party certifications are acceptable for redistribution. See https://gitlab.com/openpgp-wg/rfc4880bis/merge_requests/20 for more details. Signed-off-by: Daniel Kahn Gillmor <dkg@fifthhorseman.net>
This commit is contained in:
@@ -165,6 +165,9 @@ Constants
|
||||
.. autoattribute:: Positive_Cert
|
||||
:annotation:
|
||||
|
||||
.. autoattribute:: Attestation
|
||||
:annotation:
|
||||
|
||||
.. autoattribute:: Subkey_Binding
|
||||
:annotation:
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -55,7 +55,8 @@ __all__ = ['URI',
|
||||
'ReasonForRevocation',
|
||||
'Features',
|
||||
'EmbeddedSignature',
|
||||
'IssuerFingerprint']
|
||||
'IssuerFingerprint',
|
||||
'AttestedCertifications']
|
||||
|
||||
|
||||
class URI(Signature):
|
||||
@@ -957,3 +958,103 @@ class IssuerFingerprint(Signature):
|
||||
|
||||
self.issuer_fingerprint = 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)]
|
||||
|
||||
55
pgpy/pgp.py
55
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):
|
||||
"""
|
||||
@@ -357,6 +374,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()
|
||||
|
||||
@@ -1924,6 +1949,12 @@ 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``
|
||||
@@ -1976,6 +2007,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
|
||||
@@ -2006,8 +2038,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
|
||||
|
||||
@@ -108,6 +108,7 @@ _sspclasses = {
|
||||
# 0x1f: 'Target', ##TODO: obtain one of these ##TODO: parse this, then uncomment
|
||||
0x20: 'EmbeddedSignature',
|
||||
0x21: 'IssuerFingerprint',
|
||||
0x25: 'AttestedCertifications',
|
||||
# 0x64-0x6e: Private or Experimental
|
||||
0x64: 'Opaque',
|
||||
0x65: 'Opaque',
|
||||
|
||||
Reference in New Issue
Block a user