Commit 0bf0d9f3 authored by anarcat's avatar anarcat
Browse files

rewrite revocation checking

This was done following discussions with upstream in pull requests,
issues and IRC. Most of this revolves around issue PGPy#225.

It turns out that revocation checking is not that simple: self-signed
revocations can be checked immediately (but fail, PGPy#226) but
delegated checks need extra key material to be checked, which we skip
for now. Anyways, those certificates are not supported yet
upstream (PGPy#198).

We are being careful here: we do not write SSH key material we're
unsure if the key is revoked, for example, if there's a delegated
revoker or if the revocation signature fails because of the above
bugs. This may lead to valid keys with fake revocation certifications
being excluded from the output. Keys with delegated revocations are
also excluded for now. We prefer this to granting access to possibly
revoked keys but this may allow an attacker to forcibly exclude valid
keys from a server, forcibly blocking access.
parent 23f72bf6
Loading
Loading
Loading
Loading
+52 −6
Original line number Diff line number Diff line
@@ -5,8 +5,10 @@ converty it into OpenSSH public keys.

It will process primary and subkeys as long as they have signing
capabilities. It does not check key validity or trust, but it will not
export expired keys. It requires `is_revoked` support in PGPy which is
implemented in PR #223 and related.
export expired keys. Revocation checks may fail in older PGPy
versions, which may lead to valid keys with fake revocation
certifications being excluded from the output. Keys with delegated
revocations are also excluded for now.
"""

import argparse
@@ -15,6 +17,7 @@ import sys

import pgpy
from pgpy.constants import KeyFlags
from pgpy.constants import SignatureType

from cryptography.hazmat.primitives import serialization

@@ -34,14 +37,57 @@ args = parser.parse_args()
logging.getLogger('').setLevel(args.loglevel)


def key2ssh(key):
# return a list of revocations for the given key
# ripped out of https://github.com/SecurityInnovation/PGPy/pull/207
def revocation_signatures(self):
    keyid, keytype = (self.fingerprint.keyid, SignatureType.KeyRevocation) if self.is_primary \
                     else (self.parent.fingerprint.keyid, SignatureType.SubkeyRevocation)
    for sig in iter(sig for sig in self._signatures
                    if all([sig.type == keytype, sig.signer == keyid, not sig.is_expired])):
        yield sig


# shortcut to extract revocation signatures from a public key
# sent as https://github.com/SecurityInnovation/PGPy/pull/227
def revocation_keys(self):
    for sig in self._signatures:
        yield sig.revocation_key


def key2ssh(key, primary=None):
    if KeyFlags.Authentication in key._get_key_flags():
        logging.info("authentication-capable key found: %r", key)
        try:
            revoker = list(revocation_keys(key))
        except NotImplementedError as e:
            # needs https://github.com/SecurityInnovation/PGPy/pull/198
            logging.exception("revocation key delegation missing from PGPy, skipping key %s", key)
            return None
        if key.is_expired:
            logging.warning("key %r is expired, skipping", key)
            return None
        elif key.is_revoked:
            logging.warning("key %r is revoked, skipping", key)
        elif revoker:
            # not supported yet, would need to fetch the key
            logging.warning("key %r has a delegated revoker %r, skipping",
                            key, revoker)
            return None
        else:
            revoker = primary or key
            for sig in revocation_signatures(key):
                logging.debug('checking revocation signature %r', sig)
                try:
                    sv = revoker.verify(key, sig)
                except pgpy.errors.PGPError as e:
                    logging.exception("couldn't verify revocation signature %r on key %r", sig, key)
                    return None
                if sv:
                    logging.warning("key %r has a valid revocation, skipping", key)
                    return None
                else:
                    # XXX: this should not be skipped, but there's a bug in PGPy < 0.4.4
                    # https://github.com/SecurityInnovation/PGPy/issues/226
                    # we'll need versioned dependencies to fix this
                    logging.debug('verification of signature %r failed on key %s, skipping', sig, key)
                    return None
        logging.debug("outputting base64-encoded key material")
        material = key._key.keymaterial.__pubkey__()
@@ -69,7 +115,7 @@ for path in args.path:
    if body:
        print(body, key.fingerprint + main_uid)
    for _, subkey in key.subkeys.items():
        body = key2ssh(subkey)
        body = key2ssh(subkey, key)
        if body:
            print(body, subkey.fingerprint + main_uid)
    f.close()