summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordefanor <defanor@uberspace.net>2020-11-19 07:02:48 +0300
committerdefanor <defanor@uberspace.net>2020-11-19 07:02:48 +0300
commitf56e6496224eb5b997facfe80d9d6262c9296f93 (patch)
tree97375b7996902026c1448e43bb74426d0a1e2725
parent19428c30ba198bf96c875f1574db99a1677df00d (diff)
Implement XEP-0373: OpenPGP for XMPP
Various checks and utility functions should still be added, and it currently relies on gpg(1) for key generation and validation, but PEP-based key distribution and basic OpenPGP functionality are there.
-rw-r--r--README5
-rw-r--r--configure.ac2
-rw-r--r--examples/basic.c2
-rw-r--r--examples/weechat.c102
-rw-r--r--src/Makefile.am9
-rw-r--r--src/rexmpp.c43
-rw-r--r--src/rexmpp.h12
-rw-r--r--src/rexmpp_openpgp.c464
-rw-r--r--src/rexmpp_openpgp.h28
9 files changed, 651 insertions, 16 deletions
diff --git a/README b/README
index 47de5e7..36b9daa 100644
--- a/README
+++ b/README
@@ -14,7 +14,8 @@ rely on any particular UI, should be flexible and not stay in the way
of implementing additional XEPs on top of it, and should try to make
it easy to implement a decent client application using it.
-Current dependencies: libunbound, libxml2, gnutls, gnutls-dane, gsasl.
+Current dependencies: libunbound, libxml2, gnutls, gnutls-dane, gsasl,
+gpgme.
A rough roadmap:
@@ -61,7 +62,7 @@ A rough roadmap:
[+] XEP-0030: Service Discovery (replying to queries)
[+] XEP-0115: Entity Capabilities (including into initial presence)
[+] XEP-0172: User Nickname
-[ ] XEP-0373: OpenPGP for XMPP
+[+] XEP-0373: OpenPGP for XMPP
[ ] XEP-0402: PEP Native Bookmarks (autojoin conferences)
[ ] XEP-0166: Jingle
[ ] XEP-0234: Jingle File Transfer
diff --git a/configure.ac b/configure.ac
index b8e3786..02ea44c 100644
--- a/configure.ac
+++ b/configure.ac
@@ -36,6 +36,8 @@ PKG_CHECK_MODULES([UNBOUND], [libunbound])
AC_SUBST(UNBOUND_CFLAGS)
AC_SUBST(UNBOUND_LIBS)
+AM_PATH_GPGME
+
# Checks for header files.
AC_CHECK_HEADERS([arpa/inet.h netdb.h netinet/in.h sys/socket.h syslog.h])
diff --git a/examples/basic.c b/examples/basic.c
index 09a98c4..1d19c90 100644
--- a/examples/basic.c
+++ b/examples/basic.c
@@ -139,6 +139,8 @@ main (int argc, char **argv) {
} else if (strcmp(input, ".") == 0) {
/* Exit. */
rexmpp_stop(&s);
+ } else if (strlen(input) == 42 && input[0] == 'k' && input[1] == ' ') {
+ rexmpp_openpgp_publish_key(&s, input + 2);
} else {
/* A test message for a fixed JID. */
xmlNodePtr msg = rexmpp_xml_add_id(&s, xmlNewNode(NULL, "message"));
diff --git a/examples/weechat.c b/examples/weechat.c
index 46b9093..b2cee3a 100644
--- a/examples/weechat.c
+++ b/examples/weechat.c
@@ -25,10 +25,12 @@
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
+#include <libxml/tree.h>
#include "weechat-plugin.h"
#include "rexmpp.h"
#include "rexmpp_roster.h"
#include "rexmpp_jid.h"
+#include "rexmpp_openpgp.h"
WEECHAT_PLUGIN_NAME("rexmpp");
WEECHAT_PLUGIN_DESCRIPTION("XMPP plugin using librexmpp");
@@ -138,6 +140,19 @@ int muc_close_cb (const void *ptr, void *data,
return WEECHAT_RC_OK;
}
+void display_message (struct t_gui_buffer *buf,
+ const char *display_name,
+ xmlNodePtr body)
+{
+ xmlChar *str = xmlNodeGetContent(body);
+ if (str != NULL) {
+ char tags[4096];
+ snprintf(tags, 4096, "nick_%s", display_name);
+ weechat_printf_date_tags(buf, 0, tags, "%s\t%s\n", display_name, str);
+ xmlFree(str);
+ }
+}
+
int my_xml_in_cb (rexmpp_t *s, xmlNodePtr node) {
struct weechat_rexmpp *wr = (struct weechat_rexmpp *)s;
char *xml_buf = rexmpp_xml_serialize(node);
@@ -161,15 +176,32 @@ int my_xml_in_cb (rexmpp_t *s, xmlNodePtr node) {
weechat_buffer_set(buf, "nicklist", "1");
}
xmlNodePtr body = rexmpp_xml_find_child(node, "jabber:client", "body");
- if (body != NULL) {
- xmlChar *str = xmlNodeGetContent(body);
- if (str != NULL) {
- char tags[4096];
- snprintf(tags, 4096, "nick_%s", display_name);
- weechat_printf_date_tags(buf, 0, tags, "%s\t%s\n", display_name, str);
- xmlFree(str);
+
+ xmlNodePtr openpgp = rexmpp_xml_find_child(node, "urn:xmpp:openpgp:0", "openpgp");
+ if (openpgp != NULL) {
+ /* todo: verify it */
+ const char *openpgp_content = xmlNodeGetContent(openpgp);
+ if (openpgp_content != NULL) {
+ xmlNodePtr elem = rexmpp_openpgp_decrypt_verify(s, openpgp_content);
+ if (elem != NULL) {
+ xmlNodePtr payload =
+ rexmpp_xml_find_child(elem, "urn:xmpp:openpgp:0", "payload");
+ if (payload != NULL) {
+ xmlNodePtr pl_body =
+ rexmpp_xml_find_child(payload, "jabber:client", "body");
+ if (pl_body != NULL) {
+ display_message(buf, display_name, pl_body);
+ body = NULL;
+ }
+ }
+ xmlFreeNode(elem);
+ }
}
}
+
+ if (body != NULL) {
+ display_message(buf, display_name, body);
+ }
}
}
if (rexmpp_xml_match(node, "jabber:client", "presence")) {
@@ -427,6 +459,55 @@ void iter (struct weechat_rexmpp *wr, fd_set *rfds, fd_set *wfds) {
}
int
+command_sc_cb (const void *wr_ptr, void *data,
+ struct t_gui_buffer *buffer,
+ int argc, char **argv, char **argv_eol)
+{
+ struct weechat_rexmpp *wr = (void*)wr_ptr;
+ rexmpp_t *s = &wr->rexmpp_state;
+ const char *to = weechat_buffer_get_string(buffer, "name");
+ xmlNodePtr body = xmlNewNode(NULL, "body");
+ xmlNewNs(body, "jabber:client", NULL);
+ xmlNodeAddContent(body, argv_eol[1]);
+
+ char *rcpt[3];
+ rcpt[0] = s->initial_jid.bare;
+ rcpt[1] = to;
+ rcpt[2] = NULL;
+
+ char *b64 = rexmpp_openpgp_encrypt_sign(s, body, rcpt);
+ if (b64 == NULL) {
+ weechat_printf(buffer, "Failed to encrypt a message.");
+ return WEECHAT_RC_OK;
+ }
+
+ xmlNodePtr openpgp = xmlNewNode(NULL, "openpgp");
+ xmlNewNs(openpgp, "urn:xmpp:openpgp:0", NULL);
+ xmlNodeAddContent(openpgp, b64);
+ free(b64);
+
+ xmlNodePtr msg = rexmpp_xml_add_id(s, xmlNewNode(NULL, "message"));
+ xmlNewProp(msg, "to", to);
+ xmlNewProp(msg, "type", "chat");
+ xmlAddChild(msg, openpgp);
+
+ body = xmlNewNode(NULL, "body");
+ xmlNewNs(body, "jabber:client", NULL);
+ xmlNodeAddContent(body, "This is a secret message.");
+ xmlAddChild(msg, body);
+
+ /* XEP-0380: Explicit Message Encryption */
+ xmlNodePtr eme = xmlNewNode(NULL, "encryption");
+ xmlNewNs(eme, "urn:xmpp:eme:0", NULL);
+ xmlNewProp(eme, "namespace", "urn:xmpp:openpgp:0");
+ xmlAddChild(msg, eme);
+
+ rexmpp_send(s, msg);
+ weechat_printf_date_tags(buffer, 0, "self_msg", "%s\t%s\n", ">", argv_eol[1]);
+ return WEECHAT_RC_OK;
+}
+
+int
command_xmpp_cb (const void *pointer, void *data,
struct t_gui_buffer *buffer,
int argc, char **argv, char **argv_eol)
@@ -450,6 +531,13 @@ command_xmpp_cb (const void *pointer, void *data,
FD_ZERO(&read_fds);
FD_ZERO(&write_fds);
iter(wr, &read_fds, &write_fds);
+
+ weechat_hook_command ("sc",
+ "Sign and encrypt a message",
+ "<message>",
+ "message: a message to send",
+ NULL,
+ &command_sc_cb, wr, NULL);
}
return WEECHAT_RC_OK;
}
diff --git a/src/Makefile.am b/src/Makefile.am
index e66a276..488b96d 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -13,10 +13,11 @@ librexmpp_la_SOURCES = rexmpp_roster.h rexmpp_roster.c \
rexmpp_socks.h rexmpp_socks.c \
rexmpp.h rexmpp.c \
rexmpp_dns.h rexmpp_dns.c \
- rexmpp_jid.h rexmpp_jid.c
+ rexmpp_jid.h rexmpp_jid.c \
+ rexmpp_openpgp.h rexmpp_openpgp.c
include_HEADERS = rexmpp_roster.h rexmpp_tcp.h rexmpp_socks.h rexmpp.h \
- rexmpp_dns.h rexmpp_jid.h
+ rexmpp_dns.h rexmpp_jid.h rexmpp_openpgp.h
librexmpp_la_CFLAGS = $(AM_CFLAGS) $(LIBXML_CFLAGS) $(GNUTLS_CFLAGS) \
- $(LIBDANE_CFLAGS) $(GSASL_CFLAGS) $(UNBOUND_CFLAGS)
+ $(LIBDANE_CFLAGS) $(GSASL_CFLAGS) $(UNBOUND_CFLAGS) $(GPGME_CFLAGS)
librexmpp_la_LIBADD = $(LIBXML_LIBS) $(GNUTLS_LIBS) $(LIBDANE_LIBS) \
- $(GSASL_LIBS) $(UNBOUND_LIBS)
+ $(GSASL_LIBS) $(UNBOUND_LIBS) $(GPGME_LIBS)
diff --git a/src/rexmpp.c b/src/rexmpp.c
index 431ec32..c75f9b2 100644
--- a/src/rexmpp.c
+++ b/src/rexmpp.c
@@ -22,6 +22,7 @@
#include <gnutls/dane.h>
#include <gsasl.h>
#include <unbound.h>
+#include <gpgme.h>
#include "rexmpp.h"
#include "rexmpp_tcp.h"
@@ -29,6 +30,7 @@
#include "rexmpp_roster.h"
#include "rexmpp_dns.h"
#include "rexmpp_jid.h"
+#include "rexmpp_openpgp.h"
void rexmpp_sax_start_elem_ns (rexmpp_t *s,
const char *localname,
@@ -187,6 +189,9 @@ xmlNodePtr rexmpp_find_event (rexmpp_t *s,
cur != NULL;
prev = cur, cur = xmlNextElementSibling(cur)) {
char *cur_from = xmlGetProp(cur, "from");
+ if (cur_from == NULL) {
+ continue;
+ }
xmlNodePtr cur_event =
rexmpp_xml_find_child(cur,
"http://jabber.org/protocol/pubsub#event",
@@ -195,7 +200,14 @@ xmlNodePtr rexmpp_find_event (rexmpp_t *s,
rexmpp_xml_find_child(cur_event,
"http://jabber.org/protocol/pubsub#event",
"items");
+ if (cur_items == NULL) {
+ free(cur_from);
+ continue;
+ }
char *cur_node = xmlGetProp(cur_items, "node");
+ if (cur_node == NULL) {
+ continue;
+ }
int match = (strcmp(cur_from, from) == 0 && strcmp(cur_node, node) == 0);
free(cur_node);
free(cur_from);
@@ -289,6 +301,11 @@ xmlNodePtr rexmpp_disco_info (rexmpp_t *s) {
prev->next = cur;
prev = cur;
}
+ if (s->retrieve_openpgp_keys) {
+ cur = rexmpp_xml_feature("urn:xmpp:openpgp:0:public-keys+notify");
+ prev->next = cur;
+ prev = cur;
+ }
cur = rexmpp_xml_feature("urn:xmpp:ping");
prev->next = cur;
prev = cur;
@@ -333,6 +350,7 @@ rexmpp_err_t rexmpp_init (rexmpp_t *s, const char *jid)
s->track_roster_presence = 1;
s->track_roster_events = 1;
s->nick_notifications = 1;
+ s->retrieve_openpgp_keys = 1;
s->send_buffer = NULL;
s->send_queue = NULL;
s->resolver_ctx = NULL;
@@ -440,6 +458,17 @@ rexmpp_err_t rexmpp_init (rexmpp_t *s, const char *jid)
gsasl_callback_hook_set(s->sasl_ctx, s);
gsasl_callback_set(s->sasl_ctx, rexmpp_sasl_cb);
+ gpgme_check_version(NULL);
+ err = gpgme_new(&(s->pgp_ctx));
+ if (gpg_err_code(err) != GPG_ERR_NO_ERROR) {
+ rexmpp_log(s, LOG_CRIT, "gpgme initialisation error: %s",
+ gpgme_strerror(err));
+ gsasl_done(s->sasl_ctx);
+ gnutls_certificate_free_credentials(s->gnutls_cred);
+ xmlFreeParserCtxt(s->xml_parser);
+ return REXMPP_E_PGP;
+ }
+
return REXMPP_SUCCESS;
}
@@ -508,6 +537,7 @@ void rexmpp_cleanup (rexmpp_t *s) {
/* Frees the things that persist through reconnects. */
void rexmpp_done (rexmpp_t *s) {
rexmpp_cleanup(s);
+ gpgme_release(s->pgp_ctx);
gsasl_done(s->sasl_ctx);
gnutls_certificate_free_credentials(s->gnutls_cred);
if (s->resolver_ctx != NULL) {
@@ -654,8 +684,8 @@ xmlNodePtr rexmpp_xml_set_delay (rexmpp_t *s, xmlNodePtr node) {
}
char buf[42];
time_t t = time(NULL);
- struct tm *local_time = localtime(&t);
- strftime(buf, 42, "%FT%T%z", local_time);
+ struct tm *utc_time = gmtime(&t);
+ strftime(buf, 42, "%FT%TZ", utc_time);
xmlNodePtr delay = xmlNewChild(node, NULL, "delay", NULL);
xmlNewProp(delay, "stamp", buf);
if (s != NULL && s->assigned_jid.full[0]) {
@@ -1760,12 +1790,19 @@ rexmpp_err_t rexmpp_process_element (rexmpp_t *s, xmlNodePtr elem) {
prev->next = cur->next;
}
}
- free(node);
/* Add the new message. */
xmlNodePtr message = xmlCopyNode(elem, 1);
message->next = s->roster_events;
s->roster_events = message;
+
+ /* Process the node at once. */
+ if (s->retrieve_openpgp_keys &&
+ strcmp(node, "urn:xmpp:openpgp:0:public-keys") == 0) {
+ rexmpp_openpgp_check_keys(s, from_jid.bare, items);
+ }
+
+ free(node);
}
}
}
diff --git a/src/rexmpp.h b/src/rexmpp.h
index 53f2cba..e1cfe0e 100644
--- a/src/rexmpp.h
+++ b/src/rexmpp.h
@@ -14,6 +14,7 @@
#include <gnutls/gnutls.h>
#include <gsasl.h>
#include <libxml/tree.h>
+#include <gpgme.h>
#include "rexmpp_tcp.h"
#include "rexmpp_socks.h"
#include "rexmpp_dns.h"
@@ -177,6 +178,8 @@ enum rexmpp_err {
REXMPP_E_SEND_BUFFER_NOT_EMPTY,
/** SASL-related error. */
REXMPP_E_SASL,
+ /** OpenGPG-related error. */
+ REXMPP_E_PGP,
/** TLS-related error. */
REXMPP_E_TLS,
/** TCP-related error. */
@@ -243,6 +246,7 @@ struct rexmpp
int track_roster_presence;
int track_roster_events;
int nick_notifications;
+ int retrieve_openpgp_keys;
/* Resource limits. */
uint32_t stanza_queue_size;
@@ -336,6 +340,9 @@ struct rexmpp
/* SASL structures. */
Gsasl *sasl_ctx;
Gsasl_session *sasl_session;
+
+ /* OpenPGP structures */
+ gpgme_ctx_t pgp_ctx;
};
/**
@@ -482,4 +489,9 @@ int rexmpp_xml_match (xmlNodePtr node,
xmlNodePtr rexmpp_xml_find_child (xmlNodePtr node,
const char *namespace,
const char *name);
+
+xmlNodePtr rexmpp_find_event (rexmpp_t *s,
+ const char *from,
+ const char *node,
+ xmlNodePtr *prev_event);
#endif
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;
+}
diff --git a/src/rexmpp_openpgp.h b/src/rexmpp_openpgp.h
new file mode 100644
index 0000000..9d00a6e
--- /dev/null
+++ b/src/rexmpp_openpgp.h
@@ -0,0 +1,28 @@
+/**
+ @file rexmpp_openpgp.h
+ @brief XEP-0373 routines
+ @author defanor <defanor@uberspace.net>
+ @date 2020
+ @copyright MIT license.
+*/
+#ifndef REXMPP_OPENPGP_H
+#define REXMPP_OPENPGP_H
+
+#include "rexmpp.h"
+
+rexmpp_err_t
+rexmpp_openpgp_check_keys (rexmpp_t *s,
+ const char *jid,
+ xmlNodePtr items);
+
+rexmpp_err_t rexmpp_openpgp_publish_key (rexmpp_t *s, const char *fp);
+
+xmlNodePtr
+rexmpp_openpgp_decrypt_verify (rexmpp_t *s,
+ const char *cipher_base64);
+
+char *rexmpp_openpgp_encrypt_sign (rexmpp_t *s,
+ xmlNodePtr payload,
+ char **recipients);
+
+#endif