Skip to content

Commit

Permalink
Implement Attested Certifications
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
dkg committed Sep 18, 2019
1 parent afb2aa1 commit 1c9b55a
Show file tree
Hide file tree
Showing 5 changed files with 160 additions and 3 deletions.
3 changes: 3 additions & 0 deletions docs/source/api/constants.rst.inc
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,9 @@ Constants
.. autoattribute:: Positive_Cert
:annotation:

.. autoattribute:: Attestation
:annotation:

.. autoattribute:: Subkey_Binding
:annotation:

Expand Down
1 change: 1 addition & 0 deletions pgpy/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
103 changes: 102 additions & 1 deletion pgpy/packet/subpackets/signature.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@
'ReasonForRevocation',
'Features',
'EmbeddedSignature',
'IssuerFingerprint']
'IssuerFingerprint',
'AttestedCertifications']


class URI(Signature):
Expand Down Expand Up @@ -957,3 +958,103 @@ def parse(self, packet):

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 changes: 53 additions & 2 deletions pgpy/pgp.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,23 @@ def revocation_reason(self):
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):
"""
Expand Down Expand Up @@ -357,6 +374,14 @@ def __copy__(self):
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()

Expand Down Expand Up @@ -1924,6 +1949,12 @@ def certify(self, subject, level=SignatureType.Generic_Cert, **prefs):
: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``
Expand Down Expand Up @@ -1976,6 +2007,7 @@ def certify(self, subject, level=SignatureType.Generic_Cert, **prefs):
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
Expand Down Expand Up @@ -2006,8 +2038,27 @@ def certify(self, subject, level=SignatureType.Generic_Cert, **prefs):
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
Expand Down
1 change: 1 addition & 0 deletions tests/test_01_packetfields.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ def test_subpacket_header(self, spheader):
# 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',
Expand Down

0 comments on commit 1c9b55a

Please sign in to comment.