CZ-NIC / envelope

Insert a message and attachments and send e-mail / sign / encrypt contents by a single line.
Other
171 stars 12 forks source link

PGP-encrypt for key without matching email address #9

Closed clonejo closed 2 years ago

clonejo commented 3 years ago

I'm trying to send some PGP-encrypted emails, while hard coding the key fingerprint using Envelope(encrypt="abc1234…"). However, gpg it complaining about a missing key for the recipient email address, which is not actually in the key. Is there a way to encrypt for an arbitrary key? In raw GPG that's certainly possible. The use case is that some people use a lot of different email addresses, but only PGP keys for a subset of those.


PS: This library is super helpful, i would not have been able to send PGP-encrypted or -signed emails with attachments this quickly without it!

e3rd commented 3 years ago

Hi, thanks!

Could you post the exact code (with the sensitive data garbled)? I'd check you specify both the encryption key for the sender and the recipient and try to simulate the issue locally.

clonejo commented 3 years ago

Sure thing!

#!/usr/bin/python3
import envelope

env = envelope.Envelope(
    message="some body",
    subject=f"some subject",
    sign=True,
    encrypt="EA7B1135184C913E22222BCC78EC8541FB9C94B1",
    sender="clonejo@shakik.de",
    to="clonejo+address.not.in.pgp.key@shakik.de",
)
env.smtp(**smtp_params())
sent_mail = env.send()
if not sent_mail:
    raise Exception("There has been a problem during mail delivery!")

The output:

[… lots of gpg output removed …]
[GNUPG:] KEY_CONSIDERED EA7B1135184C913E22222BCC78EC8541FB9C94B1 0
[GNUPG:] KEY_CONSIDERED EA7B1135184C913E22222BCC78EC8541FB9C94B1 2
gpg: error retrieving 'clonejo+address.not.in.pgp.key@shakik.de' via WKD: No data
gpg: clonejo+address.not.in.pgp.key@shakik.de: skipped: No data
[GNUPG:] INV_RECP 0 clonejo+address.not.in.pgp.key@shakik.de
[GNUPG:] FAILURE sign-encrypt 167772218
gpg: [stdin]: sign+encrypt failed: No data

Secret key not found in None home folder. Create one.
Key for clonejo+address.not.in.pgp.key@shakik.de seems missing.
See gpg --list-keys
Signing/encrypting failed.
Traceback (most recent call last):
  File "/home/clonejo/cccac/vorstand/kasse/buchhaltung_2017/./envelope-issue-9.py", line 17, in <module>
    raise Exception("There has been a problem during mail delivery!")
Exception: There has been a problem during mail delivery!

If you instead set to=clonejo@shakik.de, it will work.

There is a second use case i found: the sender and recipient mail addresses are not listed in the key given in encrypt=. The email will be encrypted and sent, using a key from the keyring matching the sender and recipient mail addresses. The key given in encrypt= will be ignored, as far as i can tell using gpg --batch --list-packets on the generated PGP message. Is the value of encrypt= actually used in anyway (besides truthiness)?

Also, i guess the email will always also be encrypted for the sender? Or is one supposed to specify a list of all encryption keys in encrypt=? The documentation does not suggest a list could be passed. In my use case, i only need to use an arbitrary encryption key for the recipient, the sender encryption key can be chosen just by mail address (and is the same as the signing key).

e3rd commented 3 years ago

Sorry I had not yet the time to investigate, I did not forget

clonejo commented 3 years ago

No need to be sorry, you owe me nothing. So when you spend your time for this project, i'm already grateful. :) To be honest, i should also be looking into this myself.

e3rd commented 3 years ago

Is there a way to encrypt for an arbitrary key?

Yes, that's the main purpose envelope has been originally created for.

while hard coding the key fingerprint using Envelope(encrypt="abc1234…")

If the fingerprint defined in the encrypt parameter contained in the key ring? It seems that it is not (Key for clonejo+address.not.in.pgp.key@shakik.de seems missing). If so, put the key itself to the encrypt parameter, not only its fingerprint. You can put it as a string, bytes or a pathlib.Path object (https://github.com/CZ-NIC/envelope/#encrypting ). I've just tested this still works – the key passed in the encrypt parameter will get imported into the keyring and will be used for the encryption. The keyring may be defined by gpg parameter.

Is the value of encrypt= actually used in anyway (besides truthiness)?

If it contains a key, that one is imported. The fingerprint is ignored. That is the behaviour of the underlying library https://pythonhosted.org/python-gnupg/#encryption too, there is no possibility to specify the key the user wants to encrypt with. It counts on the fact all keys are already imported in the key ring, Envelope relieved that burden.

the email will always also be encrypted for the sender?

If the From header exists, yes. Because I believe this is the behaviour the programmer expects to happen.

https://github.com/CZ-NIC/envelope/#recipients

image

e3rd commented 2 years ago

No reaction, closing for now

clonejo commented 2 years ago

Sorry for not coming back to you last summer.

That is the behaviour of the underlying library https://pythonhosted.org/python-gnupg/#encryption too, there is no possibility to specify the key the user wants to encrypt with.

That is not true, gnupg.encrypt(recipients=) can take an array of key fingerprints, eg. gnupg.encrypt(recipients=["EA7B1135184C913E22222BCC78EC8541FB9C94B1"]).

I would like to extend envelope so that recipients is exposed, what would be a good way to do that? Add a new function+parameter encrypt_for_keys, that can override the existing method of generating the recipient list?

e3rd commented 2 years ago

I suspect this is the very same request as in https://github.com/CZ-NIC/envelope/issues/14#issuecomment-988142510 (I admire your helpfulness and reckon this is solvable but I'm not able to tell a begin-to guide yet.)

thannaske commented 2 years ago

I also stumbled across this issue recently. In my case I need to communicate with a recipient that uses a single PGP key for all their mailboxes. So the e-mail address of the key is just some generic "pgp@someagency.gov" address and won't ever match the "To" header. Because we need to archive those outgoing e-mails I would like to use such a generic key on my side as well, so I don't have to deal with multiple decryption keys on the archiving solution.

Basically it would be nice if you could just provide a plain list public keys to encrypt a GPG MIME message with. Maybe an extra method where the library doesn't do "the thinking" for you, where it just encrypts the message how you want it.

e3rd commented 2 years ago

(Thanks for writing this down, raising this issue proves real interest. And as I now get the usecase, I'm definitely willing to solve this.)

e3rd commented 2 years ago

You can now put any keys/fingerprits/addresses/raw keys into the encrypt and friends method. Would you please test that the current implementation covers your use case?

clonejo commented 2 years ago

Thank you, mostly it works great!

However, when i run .encryption(key=True) in order to force encryption without specifying a particular key (as the README suggests), i get this stacktrace:

Traceback (most recent call last):
  […]
  File "…/envelope/envelope/envelope.py", line 919, in send
    self._start(sign=sign, encrypt=encrypt, send=send)
  File "…/envelope/envelope/envelope.py", line 972, in _start
    encrypt, sign, gpg_on = self._determine_gpg(encrypt, sign)
  File "…/envelope/envelope/envelope.py", line 1078, in _determine_gpg
    if is_gpg_importable_key(item):
  File "…/envelope/envelope/utils.py", line 396, in is_gpg_importable_key
    return len(key) >= 512  # 512 is the smallest possible GPG key
TypeError: object of type 'bool' has no len()

Setting key= to "auto", False, or a list of fingerprints works. One could expect a set of fingerprints to also work, but i guess it is not supported.

(tested on 61859a8944e5c073a03869d34bfee4c72c4cf7f6)

e3rd commented 2 years ago

Thanks for spotting out this missing test! I know exactly where the problem lies and will repair this

e3rd commented 2 years ago

I was wrong, I don't have a clue.
Will you post the code that raises this exception please?

clonejo commented 2 years ago

Turns out i messed up myself, i was passing key=[True, "fingerprint"]. Sorry! key=True works as documented.

Thanks again for the implementation! From my view, this issue can be closed.

e3rd commented 2 years ago

Thanks a lot for having pleaded for and tested out this useful feature!