wiktor-k / pysequoia

OpenPGP in Python using Sequoia PGP
https://pypi.org/project/pysequoia/
Apache License 2.0
9 stars 2 forks source link

Trying to certify someone else's public key. #18

Open Mihara opened 6 months ago

Mihara commented 6 months ago

I'm trying to cook up a smartcard-using service that would certify incoming public keys by signing specific user IDs in a specific manner (by adding notations).

I've been looking at pysequoia as a potential solution, because so far, the only thing that did let me do that painlessly was pgpy, which doesn't do smartcards at all. Interjecting inside it to hijack the actual signing function is problematic, assuming I can even find a suitable smartcard library in the first place.

Unfortunately, due to pysequoia being written entirely in Rust, I can't even tell if there's a way for me to sign a user ID with, well, anything, and the extant documentation isn't sufficiently detailed. It describes adding notations to a user ID, but not signing them.

Is this possible with pysequoia at all, or should I look for something else?

wiktor-k commented 6 months ago

Is this possible with pysequoia at all, or should I look for something else?

It's not possible right now but your use-case seems to be similar to what set_notations does and as such I could make it more generic.

Just so that I got you right you'd want something like this:

from pysequoia import Notation, Card

card = Card.open("0000:00000000")
signer = card.signer("123456") # the certifying key is on a smart-card

cert = Cert.from_file("target-cert-to-be-signed.asc")

cert = cert.sign_user_id(signer = signer, user_id = cert.user_ids[0], notations = [Notation("name@example.com", "test")])

# now you serialize cert to file...

Did I get that right?

(I hope you don't mind us two iterating on this design a bit)

Mihara commented 6 months ago

Did I get that right?

Yes, that looks like what I want to happen, though ability to add a revocation of such uid certification signatures would be needed too.

I figured out how to do that with pgpy anyway, though. It involves manually constructing a signature object, pulling its binary representation, hashing it, and feeding that through the card. Which is pretty crude, but also leaves me the option to use a different, non-openpgp key storage system, which I might end up having to use anyway due to availability. So there's definitely no rush. :)

wiktor-k commented 6 months ago

Ack! :+1:

Which is pretty crude, but also leaves me the option to use a different, non-openpgp key storage system, which I might end up having to use anyway due to availability.

Just out of curiosity is the thing you're working on open-source? It looks potentially cool :)

Mihara commented 6 months ago

Just out of curiosity is the thing you're working on open-source? It looks potentially cool :)

It's actually extremely niche. While I'm going to be pushing for opening it far and wide as much as possible, it's not entirely up to me.

If you're interested in the particulars, though, here's some:

Everything in OpenPGP seems to be bent around email. The system I'm cooking up has hardly anything to do with email, or even encryption -- it's all about signatures, which must be available to verify, occasionally, decades later. (I can hear someone say "blockchain!" but that's a bit too advanced for the niche involved.)

We don't need to certify a key as belonging to a specific individual, either, we certify it as belonging to someone who is (or was!) authorized to use a certain identifier (I.e. UID) during a certain period of time. Which may be far in the past! It's normal to make signed statements regarding what happened to this identifier during this time period, years after the fact.

I was tempted to avoid OpenPGP, and make something similar to minisign, instead. But then, if I want other organizations to participate in this whole system, I'd have to roll my own keyserver, keyserver protocol, key merging logic, etc, etc, etc, debug and support all of that, which was an effective deterrent so far. Anyone being able to mirror and pick up should we fall off the face of the earth is a key requirement. So OpenPGP it is.

So I need to add notations to specific UIDs on a key, be able to revoke them in case someone lost their key, (In case the key actually gets stolen, there's no way forward other than revoking the entire key.) and I very much need to keep a key as hard to just steal as possible with a non-airgapped machine. Hence I need a server with a smartcard plugged into it, so that at least you'd have to steal the physical dongle itself.

But every library I look at in any of the programming languages I'm halfway good at, none of them does everything I need for the CA server part of the system, (For the end-user parts, OpenPGP.js is good, fortunately.) there's always something missing here or there, because it's all about email.

pgpy was the closest, but one thing it doesn't do at all is smartcards. And it is structured in such a way that you can't just delegate signing to a smartcard. I didn't find a practical way to add my code onto it to create a class representing a smartcard-based key, either, not without forking the whole thing.

But what I can do is partially duplicate what it's doing for the narrow and specific use case I have. So it's sufficient to do this:


import OpenPGPpy
# ....
from pgpy import PGPSignature, PGPKey
from pgpy.constants import (
    SignatureType,
    HashAlgorithm,
    NotationDataFlags,
    RevocationReason,
)

# ....

class KeySigner:

    # ....

    preferred_hash = HashAlgorithm.SHA256

    # ....

    key = None
    card = None
    card_pin = None

    # ....

    def smartcard_sign(self, data):
        """Invoke our smartcard to sign a hash with OpenPGPy."""

        # We must input the pin every time.
        # "1" is the number of key slot, signing in our case.
        self.card.verify_pin(1, self.card_pin)

        # In theory we can replace the smartcard library
        # with any other smartcard library
        # as long as it exposes an ed25519 signing function.
        return self.card.sign(data)

    def smartcard_sign_mechanics(self, target, sig):
        """
        Low level mechanics of signing things with a smartcard.
        """
        # This gets us the bytes we actually sign.
        sigdata = sig.hashdata(target)

        # as well as some other data demanded by the standard
        # that goes into specific places in the sig object.
        h2 = sig.hash_algorithm.hasher
        h2.update(sigdata)
        sig._signature.hash2 = bytearray(h2.digest()[:2])

        # What we really sign is the hash we just produced.
        sigdata = bytearray(h2.digest())

        # Now that we have that, we can actually poke our smartcard.
        _sig = self.smartcard_sign(sigdata)

        # Then we stick the result back into the sig structure.
        sig._signature.signature.from_signer(_sig)
        sig._signature.update_hlen()

        # and return the completed sig object
        return sig

    # .........

                    # When time comes to actually sign UIDs,
                    # we do it in this semi-manual way
                    # by creating an empty sig object.

                    sig = PGPSignature.new(
                        SignatureType.Generic_Cert,
                        self.key.key_algorithm,
                        self.preferred_hash,
                        self.key.fingerprint.keyid,
                    )

                    # Then we pile on packets in the same order pgpy normally does it when certifying uids.
                    sig._signature.subpackets.addnew(
                        "NotationData",
                        hashed=True,
                        flags=NotationDataFlags.HumanReadable,
                        name=self.notation_name,
                        value=notation_text,
                    )
                    sig._signature.subpackets.addnew(
                        "IssuerFingerprint",
                        hashed=True,
                        _version=4,
                        _issuer_fpr=self.key.fingerprint,
                    )

                    # Everything else can be encapsulated for reuse
                    # in other signing operations.
                    sig = self.smartcard_sign_mechanics(sign_this.userids[index], sig)

                    # Finally we can stick it back onto the signed key 
                    # and get a result identical to what pgpy does.
                    sign_this.userids[index] |= sig

Now, once you have the isolated operation of a smart card signing something with its onboard ed25519 key, like here -- which pkcs11 smart cards are also supposed to be able to do -- as long as you can stuff the same key bytes into that card as you would use in an openpgp key, you can get it to do the same job.

Doing it properly would involve hacking smartcard support into pgpy in a general way, which is a lot more work.

wiktor-k commented 6 months ago

Doing it properly would involve hacking smartcard support into pgpy in a general way, which is a lot more work.

Just for the record I've experimented with extracting the smartcard features into a shared object (for C/C++ projects but I believe it could be reused for Python): https://github.com/wiktor-k/openpgp-card-ffi