Merge pull request #287 from dkg/attested-certifications

Implement First-Party Attested Third-Party Certifications (1PA3PC)
This commit is contained in:
Michael Greene
2019-11-01 16:33:10 -07:00
committed by GitHub
6 changed files with 237 additions and 3 deletions

View File

@@ -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

View File

@@ -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)

View File

@@ -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)]

View File

@@ -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()
@@ -627,6 +652,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=""):
"""
@@ -1930,6 +2002,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``
@@ -1986,6 +2064,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
@@ -2016,8 +2095,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