summaryrefslogtreecommitdiff
path: root/src/rexmpp_openpgp.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/rexmpp_openpgp.c')
-rw-r--r--src/rexmpp_openpgp.c464
1 files changed, 464 insertions, 0 deletions
diff --git a/src/rexmpp_openpgp.c b/src/rexmpp_openpgp.c
new file mode 100644
index 0000000..ecb0fc7
--- /dev/null
+++ b/src/rexmpp_openpgp.c
@@ -0,0 +1,464 @@
+/**
+ @file rexmpp_openpgp.c
+ @brief XEP-0373 routines
+ @author defanor <defanor@uberspace.net>
+ @date 2020
+ @copyright MIT license.
+*/
+
+#include <syslog.h>
+#include <string.h>
+
+#include <gpgme.h>
+#include <libxml/tree.h>
+
+#include "rexmpp.h"
+#include "rexmpp_openpgp.h"
+
+
+void rexmpp_pgp_fp_reply (rexmpp_t *s,
+ xmlNodePtr req,
+ xmlNodePtr response,
+ int success)
+{
+ if (! success) {
+ rexmpp_log(s, LOG_WARNING, "Failed to retrieve an OpenpPGP key");
+ return;
+ }
+ xmlNodePtr pubsub =
+ rexmpp_xml_find_child(response, "http://jabber.org/protocol/pubsub",
+ "pubsub");
+ if (pubsub == NULL) {
+ rexmpp_log(s, LOG_ERR, "OpenPGP key retrieval: not a pubsub response");
+ return;
+ }
+ xmlNodePtr items =
+ rexmpp_xml_find_child(pubsub, "http://jabber.org/protocol/pubsub",
+ "items");
+ if (items == NULL) {
+ rexmpp_log(s, LOG_ERR, "OpenPGP key retrieval: no items in pubsub element");
+ return;
+ }
+ xmlNodePtr item =
+ rexmpp_xml_find_child(items, "http://jabber.org/protocol/pubsub", "item");
+ if (item == NULL) {
+ rexmpp_log(s, LOG_ERR, "OpenPGP key retrieval: no item in items");
+ return;
+ }
+ xmlNodePtr pubkey =
+ rexmpp_xml_find_child(item, "urn:xmpp:openpgp:0", "pubkey");
+ if (pubkey == NULL) {
+ rexmpp_log(s, LOG_ERR, "OpenPGP key retrieval: no pubkey in item");
+ return;
+ }
+ xmlNodePtr data =
+ rexmpp_xml_find_child(pubkey, "urn:xmpp:openpgp:0", "data");
+ if (data == NULL) {
+ rexmpp_log(s, LOG_ERR, "OpenPGP key retrieval: no data in pubkey");
+ return;
+ }
+ char *key_base64 = xmlNodeGetContent(data);
+
+ char *key_raw = NULL;
+ size_t key_raw_len = 0;
+ int sasl_err =
+ gsasl_base64_from(key_base64, strlen(key_base64), &key_raw, &key_raw_len);
+ if (sasl_err != GSASL_OK) {
+ rexmpp_log(s, LOG_ERR, "Base-64 key decoding failure: %s",
+ gsasl_strerror(sasl_err));
+ return;
+ }
+
+ gpgme_error_t err;
+ gpgme_data_t key_dh;
+
+ gpgme_data_new_from_mem(&key_dh, key_raw, key_raw_len, 0);
+ err = gpgme_op_import(s->pgp_ctx, key_dh);
+ /* Apparently reading GPGME results is not thread-safe. Fortunately
+ it's not critical. */
+ gpgme_import_result_t r = gpgme_op_import_result(s->pgp_ctx);
+
+ gpgme_data_release(key_dh);
+ if (gpg_err_code(err) != GPG_ERR_NO_ERROR) {
+ rexmpp_log(s, LOG_WARNING, "OpenPGP key import error: %s",
+ gpgme_strerror(err));
+ return;
+ }
+ if (r->imported == 1) {
+ rexmpp_log(s, LOG_DEBUG, "Imported a key");
+ } else {
+ rexmpp_log(s, LOG_WARNING, "Key import failure");
+ }
+}
+
+rexmpp_err_t
+rexmpp_openpgp_check_keys (rexmpp_t *s,
+ const char *jid,
+ xmlNodePtr items)
+{
+ xmlNodePtr item =
+ rexmpp_xml_find_child(items, "http://jabber.org/protocol/pubsub#event",
+ "item");
+ xmlNodePtr list =
+ rexmpp_xml_find_child(item, "urn:xmpp:openpgp:0", "public-keys-list");
+ xmlNodePtr metadata;
+ for (metadata = xmlFirstElementChild(list);
+ metadata != NULL;
+ metadata = xmlNextElementSibling(metadata)) {
+ char *fingerprint = xmlGetProp(metadata, "v4-fingerprint");
+ gpgme_key_t key;
+ gpgme_error_t err;
+ err = gpgme_get_key(s->pgp_ctx, fingerprint, &key, 0);
+ if (key != NULL) {
+ gpgme_key_release(key);
+ }
+ if (gpg_err_code(err) == GPG_ERR_EOF) {
+ rexmpp_log(s, LOG_DEBUG,
+ "Unknown OpenPGP key fingerprint for %s: %s",
+ jid, fingerprint);
+ xmlNodePtr fp_req = xmlNewNode(NULL, "pubsub");
+ xmlNewNs(fp_req, "http://jabber.org/protocol/pubsub", NULL);
+ xmlNodePtr fp_req_items = xmlNewNode(NULL, "items");
+ xmlNewProp(fp_req_items, "max_items", "1");
+ char key_node[72];
+ snprintf(key_node, 72, "urn:xmpp:openpgp:0:public-keys:%s", fingerprint);
+ xmlNewProp(fp_req_items, "node", key_node);
+ xmlAddChild(fp_req, fp_req_items);
+ rexmpp_iq_new(s, "get", jid, fp_req, rexmpp_pgp_fp_reply);
+ } else if (gpg_err_code(err) != GPG_ERR_NO_ERROR) {
+ rexmpp_log(s, LOG_WARNING,
+ "OpenPGP error when looking for a key: %s",
+ gpgme_strerror(err));
+ }
+ free(fingerprint);
+ }
+
+ return REXMPP_SUCCESS;
+}
+
+xmlNodePtr rexmpp_published_fingerprints (rexmpp_t *s, const char *jid) {
+ xmlNodePtr published =
+ rexmpp_find_event(s, jid, "urn:xmpp:openpgp:0:public-keys", NULL);
+ if (published == NULL) {
+ return NULL;
+ }
+ xmlNodePtr event =
+ rexmpp_xml_find_child(published, "http://jabber.org/protocol/pubsub#event",
+ "event");
+ xmlNodePtr items =
+ rexmpp_xml_find_child(event, "http://jabber.org/protocol/pubsub#event",
+ "items");
+ xmlNodePtr item =
+ rexmpp_xml_find_child(items, "http://jabber.org/protocol/pubsub#event",
+ "item");
+ xmlNodePtr list =
+ rexmpp_xml_find_child(item, "urn:xmpp:openpgp:0",
+ "public-keys-list");
+ xmlNodePtr published_fps = xmlFirstElementChild(list);
+ return published_fps;
+}
+
+int rexmpp_openpgp_key_is_published (rexmpp_t *s, const char *fp) {
+ xmlNodePtr metadata;
+ for (metadata = rexmpp_published_fingerprints(s, s->assigned_jid.bare);
+ metadata != NULL;
+ metadata = xmlNextElementSibling(metadata)) {
+ if (! rexmpp_xml_match(metadata, "urn:xmpp:openpgp:0", "pubkey-metadata")) {
+ continue;
+ }
+ char *fingerprint = xmlGetProp(metadata, "v4-fingerprint");
+ if (fingerprint == NULL) {
+ rexmpp_log(s, LOG_WARNING, "No fingerprint found in pubkey-metadata");
+ continue;
+ }
+ int matches = (strcmp(fingerprint, fp) == 0);
+ free(fingerprint);
+ if (matches) {
+ return 1;
+ }
+ }
+ return 0;
+}
+
+void rexmpp_pgp_key_publish_list_iq (rexmpp_t *s,
+ xmlNodePtr req,
+ xmlNodePtr response,
+ int success)
+{
+ if (! success) {
+ rexmpp_log(s, LOG_WARNING, "Failed to publish an OpenpPGP key list");
+ return;
+ }
+ rexmpp_log(s, LOG_INFO, "Published an OpenpPGP key list");
+}
+
+void rexmpp_pgp_key_publish_iq (rexmpp_t *s,
+ xmlNodePtr req,
+ xmlNodePtr response,
+ int success)
+{
+ if (! success) {
+ rexmpp_log(s, LOG_WARNING, "Failed to publish an OpenpPGP key");
+ return;
+ }
+ rexmpp_log(s, LOG_INFO, "Uploaded an OpenpPGP key");
+ xmlNodePtr pubsub = xmlFirstElementChild(req);
+ xmlNodePtr publish = xmlFirstElementChild(pubsub);
+ char *node = xmlGetProp(publish, "node");
+ char *fingerprint = node + 31;
+
+ char time_str[42];
+ time_t t = time(NULL);
+ struct tm *utc_time = gmtime(&t);
+ strftime(time_str, 42, "%FT%TZ", utc_time);
+
+ xmlNodePtr metadata = xmlNewNode(NULL, "pubkey-metadata");
+ xmlNewNs(metadata, "urn:xmpp:openpgp:0", NULL);
+ xmlNewProp(metadata, "date", time_str);
+ xmlNewProp(metadata, "v4-fingerprint", fingerprint);
+
+ free(node);
+
+ xmlNodePtr fps = rexmpp_published_fingerprints(s, s->assigned_jid.bare);
+ if (fps != NULL) {
+ metadata->next = xmlCopyNodeList(fps);
+ }
+
+ xmlNodePtr keylist = xmlNewNode(NULL, "public-keys-list");
+ xmlNewNs(keylist, "urn:xmpp:openpgp:0", NULL);
+ xmlAddChild(keylist, metadata);
+
+ xmlNodePtr item = xmlNewNode(NULL, "item");
+ xmlNewNs(item, "http://jabber.org/protocol/pubsub", NULL);
+ xmlAddChild(item, keylist);
+
+ publish = xmlNewNode(NULL, "publish");
+ xmlNewNs(publish, "http://jabber.org/protocol/pubsub", NULL);
+ xmlNewProp(publish, "node", "urn:xmpp:openpgp:0:public-keys");
+ xmlAddChild(publish, item);
+
+ pubsub = xmlNewNode(NULL, "pubsub");
+ xmlNewNs(pubsub, "http://jabber.org/protocol/pubsub", NULL);
+ xmlAddChild(pubsub, publish);
+
+ rexmpp_iq_new(s, "set", NULL, pubsub, rexmpp_pgp_key_publish_list_iq);
+}
+
+
+rexmpp_err_t rexmpp_openpgp_publish_key (rexmpp_t *s, const char *fp) {
+ if (strlen(fp) != 40) {
+ rexmpp_log(s, LOG_ERR, "Wrong fingerprint length: %d", strlen(fp));
+ return REXMPP_E_PGP;
+ }
+ if (rexmpp_openpgp_key_is_published(s, fp)) {
+ rexmpp_log(s, LOG_DEBUG, "Key %s is already published", fp);
+ return REXMPP_SUCCESS;
+ }
+
+ gpgme_data_t key_dh;
+ gpgme_error_t err;
+
+ err = gpgme_data_new(&key_dh);
+ if (gpg_err_code(err) != GPG_ERR_NO_ERROR) {
+ rexmpp_log(s, LOG_ERR, "Failed to create a gpgme data buffer: %s",
+ gpgme_strerror(err));
+ return REXMPP_E_PGP;
+ }
+ gpgme_data_set_encoding(key_dh, GPGME_DATA_ENCODING_BINARY);
+ err = gpgme_op_export(s->pgp_ctx, fp, GPGME_EXPORT_MODE_MINIMAL, key_dh);
+ if (gpg_err_code(err) == GPG_ERR_EOF) {
+ rexmpp_log(s, LOG_ERR, "No such key found: %s", fp);
+ gpgme_data_release(key_dh);
+ return REXMPP_E_PGP;
+ } else if (gpg_err_code(err) != GPG_ERR_NO_ERROR) {
+ rexmpp_log(s, LOG_ERR, "Failed to read a key: %s", gpgme_strerror(err));
+ gpgme_data_release(key_dh);
+ return REXMPP_E_PGP;
+ }
+ char *key_raw, *key_base64 = NULL;
+ size_t key_raw_len, key_base64_len = 0;
+ key_raw = gpgme_data_release_and_get_mem(key_dh, &key_raw_len);
+ gsasl_base64_to(key_raw, key_raw_len, &key_base64, &key_base64_len);
+ free(key_raw);
+ xmlNodePtr data = xmlNewNode(NULL, "data");
+ xmlNewNs(data, "urn:xmpp:openpgp:0", NULL);
+ xmlNodeAddContent(data, key_base64);
+ free(key_base64);
+
+ xmlNodePtr pubkey = xmlNewNode(NULL, "pubkey");
+ xmlNewNs(pubkey, "urn:xmpp:openpgp:0", NULL);
+ xmlAddChild(pubkey, data);
+
+ char time_str[42];
+ time_t t = time(NULL);
+ struct tm *utc_time = gmtime(&t);
+ strftime(time_str, 42, "%FT%TZ", utc_time);
+
+ xmlNodePtr item = xmlNewNode(NULL, "item");
+ xmlNewNs(item, "http://jabber.org/protocol/pubsub", NULL);
+ xmlNewProp(item, "id", time_str);
+ xmlAddChild(item, pubkey);
+
+ char node_str[72];
+ snprintf(node_str, 72, "urn:xmpp:openpgp:0:public-keys:%s", fp);
+ xmlNodePtr publish = xmlNewNode(NULL, "publish");
+ xmlNewNs(publish, "http://jabber.org/protocol/pubsub", NULL);
+ xmlNewProp(publish, "node", node_str);
+ xmlAddChild(publish, item);
+
+ xmlNodePtr pubsub = xmlNewNode(NULL, "pubsub");
+ xmlNewNs(pubsub, "http://jabber.org/protocol/pubsub", NULL);
+ xmlAddChild(pubsub, publish);
+
+ rexmpp_iq_new(s, "set", NULL, pubsub, rexmpp_pgp_key_publish_iq);
+
+ return REXMPP_SUCCESS;
+}
+
+xmlNodePtr
+rexmpp_openpgp_decrypt_verify (rexmpp_t *s,
+ const char *cipher_base64)
+{
+ gpgme_error_t err;
+ gpgme_data_t cipher_dh, plain_dh;
+ char *cipher_raw = NULL, *plain;
+ size_t cipher_raw_len = 0, plain_len;
+ int sasl_err = gsasl_base64_from(cipher_base64, strlen(cipher_base64),
+ &cipher_raw, &cipher_raw_len);
+ if (sasl_err != GSASL_OK) {
+ rexmpp_log(s, LOG_ERR, "Base-64 cipher decoding failure: %s",
+ gsasl_strerror(sasl_err));
+ return NULL;
+ }
+ gpgme_data_new_from_mem(&cipher_dh, cipher_raw, cipher_raw_len, 0);
+ gpgme_data_new(&plain_dh);
+ err = gpgme_op_decrypt_verify (s->pgp_ctx, cipher_dh, plain_dh);
+ gpgme_data_release(cipher_dh);
+ if (gpg_err_code(err) != GPG_ERR_NO_ERROR) {
+ rexmpp_log(s, LOG_ERR, "Failed to decrypt/verify: %s", gpgme_strerror(err));
+ gpgme_data_release(plain_dh);
+ return NULL;
+ }
+ plain = gpgme_data_release_and_get_mem(plain_dh, &plain_len);
+ if (plain == NULL) {
+ rexmpp_log(s, LOG_ERR, "Failed to release and get memory");
+ return NULL;
+ }
+ xmlNodePtr elem = NULL;
+ xmlDocPtr doc = xmlReadMemory(plain, plain_len, "", "utf-8", XML_PARSE_NONET);
+ if (doc != NULL) {
+ elem = xmlCopyNode(xmlDocGetRootElement(doc), 1);
+ xmlFreeDoc(doc);
+ } else {
+ rexmpp_log(s, LOG_ERR, "Failed to parse an XML document");
+ }
+ free(plain);
+ return elem;
+}
+
+
+char *rexmpp_openpgp_encrypt_sign (rexmpp_t *s,
+ xmlNodePtr payload,
+ char **recipients)
+{
+ int i, nkeys = 0, allocated = 8;
+ gpgme_error_t err;
+
+ /* Locate keys. */
+ gpgme_key_t *keys = malloc(sizeof(gpgme_key_t *) * allocated);
+ keys[0] = NULL;
+ xmlNodePtr metadata;
+ for (i = 0; recipients[i] != NULL; i++) {
+ for (metadata = rexmpp_published_fingerprints(s, recipients[i]);
+ metadata != NULL;
+ metadata = xmlNextElementSibling(metadata)) {
+ char *fingerprint = xmlGetProp(metadata, "v4-fingerprint");
+ err = gpgme_get_key(s->pgp_ctx, fingerprint, &keys[nkeys], 0);
+ if (gpg_err_code(err) == GPG_ERR_NO_ERROR) {
+ nkeys++;
+ if (nkeys == allocated) {
+ allocated *= 2;
+ keys = realloc(keys, sizeof(gpgme_key_t *) * allocated);
+ }
+ keys[nkeys] = NULL;
+ } else if (gpg_err_code(err) == GPG_ERR_EOF) {
+ rexmpp_log(s, LOG_WARNING, "No key %s for %s found",
+ fingerprint, recipients[i]);
+ } else {
+ rexmpp_log(s, LOG_ERR, "Failed to read key %s: %s",
+ fingerprint, gpgme_strerror(err));
+ }
+ free(fingerprint);
+ }
+ }
+
+ /* Prepare a signcrypt element. */
+ xmlNodePtr signcrypt = xmlNewNode(NULL, "signcrypt");
+ xmlNewNs(signcrypt, "urn:xmpp:openpgp:0", NULL);
+
+ /* Add all the recipients. */
+ for (i = 0; recipients[i] != NULL; i++) {
+ xmlNodePtr to = xmlNewNode(NULL, "to");
+ xmlNewNs(to, "urn:xmpp:openpgp:0", NULL);
+ xmlNewProp(to, "jid", recipients[i]);
+ xmlAddChild(signcrypt, to);
+ }
+
+ /* Add timestamp. */
+ char time_str[42];
+ time_t t = time(NULL);
+ struct tm *utc_time = gmtime(&t);
+ strftime(time_str, 42, "%FT%TZ", utc_time);
+
+ xmlNodePtr time = xmlNewNode(NULL, "time");
+ xmlNewNs(time, "urn:xmpp:openpgp:0", NULL);
+ xmlNewProp(time, "stamp", time_str);
+ xmlAddChild(signcrypt, time);
+
+ /* Add a random-length random-content padding. */
+ char *rand_str, rand[256];
+ gsasl_random(rand, 1);
+ size_t rand_str_len = 0, rand_len = (unsigned char)rand[0] + 16;
+ gsasl_random(rand, rand_len);
+ gsasl_base64_to(rand, rand_len, &rand_str, &rand_str_len);
+
+ xmlNodePtr rpad = xmlNewNode(NULL, "rpad");
+ xmlNewNs(rpad, "urn:xmpp:openpgp:0", NULL);
+ xmlNodeAddContent(rpad, rand_str);
+ free(rand_str);
+ xmlAddChild(signcrypt, rpad);
+
+ /* Add the payload. */
+ xmlNodePtr pl = xmlNewNode(NULL, "payload");
+ xmlNewNs(pl, "urn:xmpp:openpgp:0", NULL);
+ xmlAddChild(pl, payload);
+ xmlAddChild(signcrypt, pl);
+
+ /* Serialize the resulting XML. */
+ char *plaintext = rexmpp_xml_serialize(signcrypt);
+ xmlFreeNode(signcrypt);
+
+ /* Encrypt, base64-encode. */
+ gpgme_data_t cipher_dh, plain_dh;
+ gpgme_data_new(&cipher_dh);
+ gpgme_data_new_from_mem(&plain_dh, plaintext, strlen(plaintext), 0);
+ err = gpgme_op_encrypt_sign(s->pgp_ctx, keys, 0, plain_dh, cipher_dh);
+ for (i = 0; i < nkeys; i++) {
+ gpgme_key_release(keys[i]);
+ }
+ free(keys);
+ gpgme_data_release(plain_dh);
+ if (gpg_err_code(err) != GPG_ERR_NO_ERROR) {
+ rexmpp_log(s, LOG_ERR, "Failed to encrypt: %s", gpgme_strerror(err));
+ gpgme_data_release(cipher_dh);
+ return NULL;
+ }
+ char *cipher_raw = NULL, *cipher_base64 = NULL;
+ size_t cipher_raw_len = 0, cipher_base64_len = 0;
+ cipher_raw = gpgme_data_release_and_get_mem(cipher_dh, &cipher_raw_len);
+ gsasl_base64_to(cipher_raw, cipher_raw_len,
+ &cipher_base64, &cipher_base64_len);
+ free(cipher_raw);
+
+ return cipher_base64;
+}