From b4d8a5279ba14c0647a551cd0ce15328b074908a Mon Sep 17 00:00:00 2001 From: missytake Date: Mon, 21 Apr 2025 00:48:22 +0200 Subject: [PATCH] feat: upload PGP keys to keys.openpgp.org --- src/keyserver_bot/hooks.py | 19 ++++++++++++++++--- src/keyserver_bot/koo.py | 32 ++++++++++++++++++++++++++++++++ tests/test_koo.py | 34 +++++++++++++++++++++++++++++++--- 3 files changed, 79 insertions(+), 6 deletions(-) diff --git a/src/keyserver_bot/hooks.py b/src/keyserver_bot/hooks.py index fbe986f..631300c 100644 --- a/src/keyserver_bot/hooks.py +++ b/src/keyserver_bot/hooks.py @@ -4,9 +4,9 @@ from deltachat_rpc_client import events, run_bot_cli, EventType, Message from email_validator import validate_email, EmailNotValidError 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.vcard import construct_vcard, save_vcard +from keyserver_bot.vcard import construct_vcard, parse_vcard, save_vcard from keyserver_bot.errors import KeyNotFound hooks = events.HookCollection() @@ -19,6 +19,9 @@ HELP_MSG = ( "\n\n" "If you send me a public PGP key, " "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": 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 + 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: public_key, email, display_name = handle_command(snapshot.text, snapshot.get("file")) diff --git a/src/keyserver_bot/koo.py b/src/keyserver_bot/koo.py index d44678f..9138a42 100644 --- a/src/keyserver_bot/koo.py +++ b/src/keyserver_bot/koo.py @@ -1,3 +1,6 @@ +import base64 + +import pgpy import requests @@ -27,3 +30,32 @@ def request_from_koo(email: str) -> str: if l != "" and not l.startswith("Comment: ") and not l.startswith("-----"): keyparts.append(l) 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()) diff --git a/tests/test_koo.py b/tests/test_koo.py index 4aa9b7b..47c6563 100644 --- a/tests/test_koo.py +++ b/tests/test_koo.py @@ -2,15 +2,43 @@ import pytest 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( ("email", "public_key"), [ ( - "missytake@systemli.org", - "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_ADDRESS, + SYSTEMLI_KEY, ), - ("adsokasd@ingrdsuf.org", ""), + (NON_EXISTANT_ADDRESS, ""), ], ) def test_request_by_email(email, public_key): 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)