feat: upload PGP keys to keys.openpgp.org

This commit is contained in:
missytake 2025-04-21 00:48:22 +02:00
parent d55cf64953
commit b4d8a5279b
Signed by: missytake
GPG key ID: 04CC6658320518DF
3 changed files with 79 additions and 6 deletions

View file

@ -4,9 +4,9 @@ from deltachat_rpc_client import events, run_bot_cli, EventType, Message
from email_validator import validate_email, EmailNotValidError from email_validator import validate_email, EmailNotValidError
from keyserver_bot.wkd import request_from_wkd, WKD_TIMEOUT from keyserver_bot.wkd import request_from_wkd, WKD_TIMEOUT
from keyserver_bot.koo import request_from_koo from keyserver_bot.koo import request_from_koo, upload_to_koo
from keyserver_bot.attachment import import_key_from_attachment from keyserver_bot.attachment import import_key_from_attachment
from keyserver_bot.vcard import construct_vcard, save_vcard from keyserver_bot.vcard import construct_vcard, parse_vcard, save_vcard
from keyserver_bot.errors import KeyNotFound from keyserver_bot.errors import KeyNotFound
hooks = events.HookCollection() hooks = events.HookCollection()
@ -19,6 +19,9 @@ HELP_MSG = (
"\n\n" "\n\n"
"If you send me a public PGP key, " "If you send me a public PGP key, "
"you will also get a contact for it." "you will also get a contact for it."
"\n\n"
"With /publish you can upload your key "
"or one of your contacts' keys to keys.openpgp.org."
) )
@ -28,8 +31,18 @@ def command(event):
if snapshot.text == "/generate-invite": if snapshot.text == "/generate-invite":
return snapshot.chat.send_text(snapshot.chat.account.get_qr_code()) return snapshot.chat.send_text(snapshot.chat.account.get_qr_code())
if snapshot.text == "Messages are guaranteed to be end-to-end encrypted from now on.": elif snapshot.text == "Messages are guaranteed to be end-to-end encrypted from now on.":
return return
elif snapshot.text == "/publish":
vcard_to_upload = snapshot.sender.make_vcard()
if snapshot.get("file"):
with open(snapshot.get("file"), "r") as f:
attachment = f.read()
if attachment.startswith("BEGIN:VCARD\nVERSION:4.0\n"):
vcard_to_upload = attachment
email, public_key, _ = parse_vcard(vcard_to_upload)
reply = upload_to_koo(email, public_key)
return snapshot.chat.send_text(reply)
try: try:
public_key, email, display_name = handle_command(snapshot.text, snapshot.get("file")) public_key, email, display_name = handle_command(snapshot.text, snapshot.get("file"))

View file

@ -1,3 +1,6 @@
import base64
import pgpy
import requests import requests
@ -27,3 +30,32 @@ def request_from_koo(email: str) -> str:
if l != "" and not l.startswith("Comment: ") and not l.startswith("-----"): if l != "" and not l.startswith("Comment: ") and not l.startswith("-----"):
keyparts.append(l) keyparts.append(l)
return "".join(keyparts) return "".join(keyparts)
def upload_to_koo(email: str, public_key: str) -> str:
"""Uploads a key to keys.openpgp.org and requests a verification email to publish it.
:param email: the email address to publish a key for
:param public_key: the PGP key to publish
:return: the reply to the user
"""
if not public_key:
return "Sorry, this contact contains no PGP key."
binary_key = base64.b64decode(public_key)
key_object = pgpy.PGPKey().from_blob(binary_key)[0]
r = requests.post("https://keys.openpgp.org/vks/v1/upload", json={"keytext": str(key_object)})
if r.json().get("status"):
if r.json().get("status").get(email) == "published":
public_key_fpr = key_object.fingerprint
if r.json().get("key_fpr") == public_key_fpr:
return f"This key is already published for {email} :)"
if r.json().get("token"):
json = {
"token": r.json()["token"],
"addresses": [email],
}
requests.post("https://keys.openpgp.org/vks/v1/request-verify", json=json)
return f"Thanks! {email} should receive a confirmation link from keyserver@keys.openpgp.org, use it to publish the key."
return str(r.json())

View file

@ -2,15 +2,43 @@ import pytest
import keyserver_bot.koo import keyserver_bot.koo
SYSTEMLI_ADDRESS = "missytake@systemli.org"
SYSTEMLI_KEY = "xjMEXcLIEBYJKwYBBAHaRw8BAQdAmlYU7TEgGL3eq2WXC95tQtZYHjpJOCjb7qq3vJd1lG7NIm1pc3N5dGFrZSA8bWlzc3l0YWtlQHN5c3RlbWxpLm9yZz7CkAQTFggAOAIbAwULCQgHAgYVCgkICwIEFgIDAQIeAQIXgBYhBEX/6kI9pCw7MYVUWQTMZlgyBRjfBQJnDuTLAAoJEATMZlgyBRjfeUUA/0P0E/quL71dn5Zjc4ewsnykT1GODazGmk+xprSwAvEyAPwJy+uJgJz5pSFdmi2lGRrOERmKUu8AdQ/M7dSm6kt3AMKQBBMWCAA4AhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAFiEERf/qQj2kLDsxhVRZBMxmWDIFGN8FAmcO5FkACgkQBMxmWDIFGN+0EAD+PVALMtR4PDB4aaxpJ5L1p6mGmq1lb8wqZtdAyHK9+7EA/jPU12/e368B6OHknY1YzGxQxyPETcL8hUS26CU6wnUEzRZtaXNzeXRha2VAc3lzdGVtbGkub3JnwpAEExYIADgCGwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AWIQRF/+pCPaQsOzGFVFkEzGZYMgUY3wUCZw7kywAKCRAEzGZYMgUY37s7AP4uOSeC9cwDKnPnov7L6Sp0axUNncX+n5sjepkPpwgVIQD/Zt85kzYrodvsx4QdolweWDFrH9DFxTsTSw2GWIIE6Q3CkAQTFggAOAIbAwULCQgHAgYVCgkICwIEFgIDAQIeAQIXgBYhBEX/6kI9pCw7MYVUWQTMZlgyBRjfBQJnDuRZAAoJEATMZlgyBRjf3lkBAPdeXSQY9oPO4wHv+pYE5d6+4ij8plA6tSReaqhneOKtAQCJguCnqcH8A9KKZ97n1gIBFnJ7xhdNTPLPoAbYE1BAB8KWBBMWCAA+FiEERf/qQj2kLDsxhVRZBMxmWDIFGN8FAl3CyBACGwMFCQlmAYAFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQBMxmWDIFGN9eHAD/SR+CRWWJ5km4pfeU48Y88+6nKA/T6egmav9NIZTjw9cBAIEk6yInsxZj/2Ot9AW8vr+cyuiG9FtZOuWvbNh4oYUJzjMEXcLKyhYJKwYBBAHaRw8BAQdAtUIjrOUmkTbVVkwIXeAkC/s3Z1wrW2/+KIgkQbbi38XCeAQYFggAIAIbIBYhBEX/6kI9pCw7MYVUWQTMZlgyBRjfBQJnDuULAAoJEATMZlgyBRjf19ABALR4jIhWK/5e87V3+xuX9R0MAyBDztZjb8nfXJ3HWK65AQC6S7JybxebS+jHSavTIF0nuaaqBXZx3me1edqwVVBAD8J+BBgWCAAmFiEERf/qQj2kLDsxhVRZBMxmWDIFGN8FAl3CysoCGyAFCQlmAYAACgkQBMxmWDIFGN/IQgEA8WwKDEEtbyIiCr5pLD/eqJ2m1xIsKKP/0sH0ADPMwjEA/jvMjOjbrh5WZuUnf+ddcVB7GStu3SZtenkB/rK1+s0EzjgEXcLIEBIKKwYBBAGXVQEFAQEHQGTy3kLa+rSeHhK35fDN/k46zZFh+LDZQ0a2552FuPJ6AwEIB8J4BBgWCAAgAhsMFiEERf/qQj2kLDsxhVRZBMxmWDIFGN8FAmcO5P8ACgkQBMxmWDIFGN/jJgEA5+ESV8PtiUcwxml7GpTGwbIv8GoDA1YlPcUeku/S20QA/iVvzlf8Oj5Wvhet8VMzU37wWsFJ2n6aAyM2WmOrPVoPwn4EGBYIACYWIQRF/+pCPaQsOzGFVFkEzGZYMgUY3wUCXcLIEAIbDAUJCWYBgAAKCRAEzGZYMgUY3zZnAP9JMp+PI+1H4x3D62Qg4udjL6zypFKTrcrUnyWNcoWzoQD/W3uJ7M2sbOdAdHcj246koYP32BrR/7Wtc0/7yJPfqQk==oePr"
NON_EXISTANT_ADDRESS = "adsokasd@ingrdsuf.org"
NINE_ADDRESS = "ap3m5cs63@nine.testrun.org"
NINE_KEY = "xjMEaAVqNxYJKwYBBAHaRw8BAQdAnQ1KcTZYpcfbGyXkgPHJsCJQn/mn2a4F5SH7tccFNF/NHDxhcDNtNWNzNjNAbmluZS50ZXN0cnVuLm9yZz7CjQQQFggANQIZAQUCaAVqNwIbAwQLCQgHBhUICQoLAgMWAgEBJxYhBJCKqRpWzC6rZWWG76Wa27/U0TWBAAoJEKWa27/U0TWBKV8A/RoUFaB7YYc0zLkZWkJr9xTy5jN8T3VsGNJRi2IN1wQTAQDAsLwZkTf4pax2Hu/S0P11e+hsK+7TqF8/YP/toT5zAc44BGgFajcSCisGAQQBl1UBBQEBB0Dw5cbj6CDXHYKJDHvqfCPE1oDcO0194OYjXPf3foYTGAMBCAfCeAQYFggAIAUCaAVqNwIbDBYhBJCKqRpWzC6rZWWG76Wa27/U0TWBAAoJEKWa27/U0TWB2c8BAON7SIbWGpzhCoLP/pKsVycxdH3lc4bAAKwLP2X/cnFyAQCO43AeusNcRYetQJfvtmI9avQkEKWw54QBfFWIiabpDg=="
@pytest.mark.parametrize( @pytest.mark.parametrize(
("email", "public_key"), ("email", "public_key"),
[ [
( (
"missytake@systemli.org", SYSTEMLI_ADDRESS,
"xjMEXcLIEBYJKwYBBAHaRw8BAQdAmlYU7TEgGL3eq2WXC95tQtZYHjpJOCjb7qq3vJd1lG7NIm1pc3N5dGFrZSA8bWlzc3l0YWtlQHN5c3RlbWxpLm9yZz7CkAQTFggAOAIbAwULCQgHAgYVCgkICwIEFgIDAQIeAQIXgBYhBEX/6kI9pCw7MYVUWQTMZlgyBRjfBQJnDuTLAAoJEATMZlgyBRjfeUUA/0P0E/quL71dn5Zjc4ewsnykT1GODazGmk+xprSwAvEyAPwJy+uJgJz5pSFdmi2lGRrOERmKUu8AdQ/M7dSm6kt3AMKQBBMWCAA4AhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAFiEERf/qQj2kLDsxhVRZBMxmWDIFGN8FAmcO5FkACgkQBMxmWDIFGN+0EAD+PVALMtR4PDB4aaxpJ5L1p6mGmq1lb8wqZtdAyHK9+7EA/jPU12/e368B6OHknY1YzGxQxyPETcL8hUS26CU6wnUEzRZtaXNzeXRha2VAc3lzdGVtbGkub3JnwpAEExYIADgCGwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AWIQRF/+pCPaQsOzGFVFkEzGZYMgUY3wUCZw7kywAKCRAEzGZYMgUY37s7AP4uOSeC9cwDKnPnov7L6Sp0axUNncX+n5sjepkPpwgVIQD/Zt85kzYrodvsx4QdolweWDFrH9DFxTsTSw2GWIIE6Q3CkAQTFggAOAIbAwULCQgHAgYVCgkICwIEFgIDAQIeAQIXgBYhBEX/6kI9pCw7MYVUWQTMZlgyBRjfBQJnDuRZAAoJEATMZlgyBRjf3lkBAPdeXSQY9oPO4wHv+pYE5d6+4ij8plA6tSReaqhneOKtAQCJguCnqcH8A9KKZ97n1gIBFnJ7xhdNTPLPoAbYE1BAB8KWBBMWCAA+FiEERf/qQj2kLDsxhVRZBMxmWDIFGN8FAl3CyBACGwMFCQlmAYAFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQBMxmWDIFGN9eHAD/SR+CRWWJ5km4pfeU48Y88+6nKA/T6egmav9NIZTjw9cBAIEk6yInsxZj/2Ot9AW8vr+cyuiG9FtZOuWvbNh4oYUJzjMEXcLKyhYJKwYBBAHaRw8BAQdAtUIjrOUmkTbVVkwIXeAkC/s3Z1wrW2/+KIgkQbbi38XCeAQYFggAIAIbIBYhBEX/6kI9pCw7MYVUWQTMZlgyBRjfBQJnDuULAAoJEATMZlgyBRjf19ABALR4jIhWK/5e87V3+xuX9R0MAyBDztZjb8nfXJ3HWK65AQC6S7JybxebS+jHSavTIF0nuaaqBXZx3me1edqwVVBAD8J+BBgWCAAmFiEERf/qQj2kLDsxhVRZBMxmWDIFGN8FAl3CysoCGyAFCQlmAYAACgkQBMxmWDIFGN/IQgEA8WwKDEEtbyIiCr5pLD/eqJ2m1xIsKKP/0sH0ADPMwjEA/jvMjOjbrh5WZuUnf+ddcVB7GStu3SZtenkB/rK1+s0EzjgEXcLIEBIKKwYBBAGXVQEFAQEHQGTy3kLa+rSeHhK35fDN/k46zZFh+LDZQ0a2552FuPJ6AwEIB8J4BBgWCAAgAhsMFiEERf/qQj2kLDsxhVRZBMxmWDIFGN8FAmcO5P8ACgkQBMxmWDIFGN/jJgEA5+ESV8PtiUcwxml7GpTGwbIv8GoDA1YlPcUeku/S20QA/iVvzlf8Oj5Wvhet8VMzU37wWsFJ2n6aAyM2WmOrPVoPwn4EGBYIACYWIQRF/+pCPaQsOzGFVFkEzGZYMgUY3wUCXcLIEAIbDAUJCWYBgAAKCRAEzGZYMgUY3zZnAP9JMp+PI+1H4x3D62Qg4udjL6zypFKTrcrUnyWNcoWzoQD/W3uJ7M2sbOdAdHcj246koYP32BrR/7Wtc0/7yJPfqQk==oePr", SYSTEMLI_KEY,
), ),
("adsokasd@ingrdsuf.org", ""), (NON_EXISTANT_ADDRESS, ""),
], ],
) )
def test_request_by_email(email, public_key): def test_request_by_email(email, public_key):
assert public_key == keyserver_bot.koo.request_from_koo(email) assert public_key == keyserver_bot.koo.request_from_koo(email)
@pytest.mark.parametrize(
("email", "public_key", "result"),
[
(SYSTEMLI_ADDRESS, SYSTEMLI_KEY, f"This key is already published for {SYSTEMLI_ADDRESS} :)"),
(NON_EXISTANT_ADDRESS, "", "Sorry, this contact contains no PGP key."),
(
NON_EXISTANT_ADDRESS,
SYSTEMLI_KEY,
f"Thanks! {NON_EXISTANT_ADDRESS} should receive a confirmation link from keyserver@keys.openpgp.org, use it to publish the key.",
),
(
NINE_ADDRESS,
NINE_KEY,
f"Thanks! {NINE_ADDRESS} should receive a confirmation link from keyserver@keys.openpgp.org, use it to publish the key.",
),
],
)
def test_upload_to_koo(email, public_key, result):
assert result == keyserver_bot.koo.upload_to_koo(email, public_key)