Monkeysphere in Python
This is a scratch area to do some tests with the PGPy library in
order to address certain limitations in Monkeysphere. The first of
those is the lack of ECC support which may be resolved with a
rewrite of the keytransfer
component in Python.
This shouldn't be taken as an attempt to take over or rewrite Monkeysphere, but simply as a space to experiment with new technologies and share them with the world.
Core concepts
The core concept of Monkeysphere is to leverage the OpenPGP web of trust to authenticate user and server public keys, both for SSH servers but eventually other services like HTTPS.
The monkeysphere(7) manpage describes those fundamental concepts and outlines key acceptability criteria:
-
capability: we check the
authentication
usage flag is set -
validity: we check that the key is not expired and not revoked, and PGPy takes care of the key being well-formed. we do not enforce other requirements like key length or algorithms. There are also limitations in the key types supported by PGPy, see "Limitations" below.
-
certification: we do NOT check that keys are signed by a trusted identity certifier, see "Limitations"
Monkeysphere architecture
Monkeysphere is made out of those 3 core programs which cover all the functionality offered to users by the project. Entries marked with a checkmark (✓) are implemented here.
-
monkeysphere(1) - "client user interface"
-
takes care of updating the monkeysphere-managed
known_hosts
andauthorized_keys
file for a user -
generates an authentication subkey
-
act as a "proxy server" for SSH to authenticate hosts
-
transfer subkeys to ssh-agent
-
✓ generate SSH public keys from OpenPGP authentication keys, implemented by the
openpgp2ssh
programs which is a symlink to the more elaboratekeytransfer
program. this is partially implemented in Python with PGPy, see below.
-
-
monkeysphere-host(8) - host key administration tool
-
import a PEM key as a new OpenPGP key attached to a UID based on the host URI chosen by the user
-
manage expiration date, UIDs, revocations and revokers
-
publishes keys on keyservers
-
-
monkeysphere-authentication(8) - authentication admin tool
-
similar to the client user interface (monkeysphere(1)) but can operate on all users at once
-
refresh keys from the keyserver
-
manage certifier keys
-
OpenPGP to SSH
The first part of this effort is the openpgp2ssh
script, which
currently only takes a public OpenPGP key and transforms it in a
series of authorized_keys
lines.
It does nothing more: it doesn't check key trust or validity (beyond expiration date and revocation) and blindly transforms any key given on the commandline or stdin. It also doesn't process private keys, nor does it convert OpenSSH keys back into OpenPGP keys. It also does not output PEM-formatted keys, although that can probably be implemented easily. See below for more details on those limitations.
Limitations
PGPy cannot (currently) talk to keyservers to fetch key updates, but this should be fairly easy to implement. A core issue there is to avoid trusting the key material sent by the keyservers, but certification checks implemented below should take care of that. See also keyserver search and get support and update status of network interactions protocols.
PGPy doesn't have an easy way to check if a key is revoked (issue
#225). I made a crude patch to enable revocation checks with a
is_revoked()
function, but it doesn't do its job properly, because
it actually needs to have access to the public key material of the
revoker to function properly. Again, if keyserver fetch is implemented
this becomes possible.
ECC keys
It also requires two patches (#221 and #222), to be shipped with 0.4.4, to properly read ECC keys. Furthermore, only the development version of OpenSSL (from June 2017 at least) supports those curves, so PGPy (and the underlying Cryptography library both need to be linked against such a version for ECC curves to work. As such, there's no release of PGPy that correctly supports ECC at the time of writing.
Also note there are concerns with the actual implementation, see for example this PR which patches the implementation to correctly pad some fields as required by the RFC...
PEM and SSH to OpenPGP
The other key part of Monkeysphere is to do the reverse conversion: take a SSH (or PEM certificate) key and turn it into an OpenPGP keypair that can then be integrated in the Web of Trust. There seems to be some functionality missing in PGPy for this to happen, unfortunately.
While the Cryptography library can probably read SSH and PEM key material without too much trouble, the PGPy lacks a way to synthesize OpenPGP keys from scratch (see issue #220). There may be a way to do it, but it is not obvious from the API right now.
Certification checks
We do not check certificates right now: any keyring passed to
openpgp2ssh
is assumed to be fully trusted and certification checks
are currently considered "out of scope". The way Monkeysphere does
those checks is described in the monkeysphere(7) manpage, but
essentially boils down to those certification methods:
-
ultimate certification: a user identifier (UID, e.g. the user's email address or the host URI) is signed by a single, ultimately trusted key. authentication keys bound to this UID is then considered certified. this could be easily implemented by checking UID signatures in PGPy.
-
marginal certification: a UID needs to be signed by multiple marginally trusted keys before it is considered certified. it is unclear how Monkeysphere does this, but it may be replicated by counting the number of valid certification from trusted keys.
-
scoped certification: an overlap with the previous two. certifying keys are restricted to sign identities only within a given domain name.
In practice, Monkeysphere leverages GPG's validity
field as provided
by its --with-colons
output, in the process_user_id()
function
stored in the src/share/common
. That function in turns calls the
gpg2ssh
function which is a simple wrapper around openpgp2ssh
,
called on the output of gpg --export $fingerprint
,
essentially.
All this is bound by a ltsign
command which makes a non-exportable
"trusted signature" using an automatically generated public/private
keypair named the "Monkeysphere authentication trust core UID (random
string +...)". Scoped certifications are implemented with the "regular
expressions" described in RFC4880 section 5.2.3.13, but it seems
that never quite worked, see bug T2923. Key validity is deduced
form there by GnuPG.
With PGPy, it's possible to inspect UIDs and look at their signatures. For example:
In [40]: u = pgpy.PGPKey.from_file('anarcat.gpg')[0].userids[0]
In [41]: u
Out[41]: <PGPUID [UserID] at 0x7FF370CF5400>
In [42]: u.__sig__
Out[42]: [<PGPSignature [Positive_Cert] object at 0x7ff370cf55c0>]
In [43]: u.signers
Out[43]: {'792152527B75921E'}
One concern here is that only the 8-byte key ID is shown here. This is part of the specification (RFC4880 section 5.2.3.5) but it could be a security issue. The proper way of doing this verification seems to be by loading the certifier's OpenPGP key material and using that to verify the key. For example, to check the signatures I have made on dkg's key:
In [66]: k, _ = pgpy.PGPKey.from_file('dkg.gpg')
In [67]: a, _ = pgpy.PGPKey.from_file('anarcat.gpg')
In [81]: for uid in k.userids:
...: if uid.name == 'Daniel Kahn Gillmor' and uid.email == 'dkg@fifthhorseman.net':
...: print(list(a.verify(uid).good_signatures))
...: else:
...: print('uid no match: ', uid, uid.name, uid.email)
...:
[sigsubj(verified=True, by='792152527B75921E', signature=<PGPSignature [Generic_Cert] object at 0x7ff370bad470>, subject=<PGPUID [UserID][2016-12-21 17:44:57] at 0x7FF370D444E0>)]
uid no match: <PGPUID [UserID][2016-12-21 17:45:07] at 0x7FF370A7AEB8> Daniel Kahn Gillmor dkg@aclu.org
uid no match: <PGPUID [UserID][2016-12-21 17:45:05] at 0x7FF370B65400> Daniel Kahn Gillmor dkg@debian.org
uid no match: <PGPUID [UserID][2016-12-21 17:45:00] at 0x7FF370D44550> Daniel Kahn Gillmor dkg@openflows.com
uid no match: <PGPUID [UserID][2009-06-02 17:45:53] at 0x7FF370B3B048> Daniel Kahn Gillmor dkg-debian.org@fifthhorseman.net
uid no match: <PGPUID [UserID][2009-06-02 17:44:32] at 0x7FF370BFCAC8> Daniel Kahn Gillmor dkg@astro.columbia.edu
So implementing this filtering in the code should be simple enough, provided that we know what restrictions we want to impose. An added benefit here is that could pass a complete OpenPGP key material to the program instead of an unreliable key ID or a fingerprint, and directly verify the signature cryptographically instead of having to do keyring management.
Obviously, this approach would be different than the current Monkeysphere approach: we wouldn't have an automatically-generated "trust anchor" and would only rely on the integrity of the public key material provided to establish trust, which we would compute using an algorithm like the above, directly with PGPy and without resorting to GPG's algorithms. This would make domain-scoped certifications more difficult, however, as there would need to be out-of-band storage for that information somehow, if it is not stored in a signature.
Credits
Anarcat wrote this, but Michael Greene wrote the awesome PGPy library.