Changeset

3501:1df139b157fb

mod_pubsub_post: Add support for WebSub authentication
author Kim Alvefur <zash@zash.se>
date Fri, 24 Aug 2018 14:52:09 +0200
parents 3500:e86315c9b5c4
children 3502:42e9e3c5eb02
files mod_pubsub_post/README.markdown mod_pubsub_post/mod_pubsub_post.lua
diffstat 2 files changed, 42 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- a/mod_pubsub_post/README.markdown	Fri Mar 29 17:06:18 2019 +0100
+++ b/mod_pubsub_post/README.markdown	Fri Aug 24 14:52:09 2018 +0200
@@ -43,6 +43,24 @@
 to attempt to publish to a non-existant node becomes owner of it, which
 includes publishing rights.
 
+## WebSub
+
+``` {.lua}
+-- Per node secrets
+pubsub_post_secrets = {
+    my_node = "shared secret"
+}
+
+-- Same secret for all nodes
+pubsub_post_secret = "shared secret"
+```
+
+This enables the
+[WebSub](https://www.w3.org/TR/2018/REC-websub-20180123/) [Authenticated
+Content
+Distribution](https://www.w3.org/TR/2018/REC-websub-20180123/#authenticated-content-distribution)
+authentication method, where payloads are signed using a shared secret.
+
 ## Setting up affiliations
 
 Prosodys PubSub module supports [setting affiliations via
--- a/mod_pubsub_post/mod_pubsub_post.lua	Fri Mar 29 17:06:18 2019 +0100
+++ b/mod_pubsub_post/mod_pubsub_post.lua	Fri Aug 24 14:52:09 2018 +0200
@@ -5,6 +5,14 @@
 local xml = require "util.xml";
 local uuid_generate = require "util.uuid".generate;
 local timestamp_generate = require "util.datetime".datetime;
+local hashes = require "util.hashes";
+local from_hex = require "util.hex".from;
+local hmacs = {
+	sha1 = hashes.hmac_sha1;
+	sha256 = hashes.hmac_sha256;
+	sha384 = hashes.hmac_sha384;
+	sha512 = hashes.hmac_sha512;
+};
 
 local pubsub_service = module:depends("pubsub").service;
 
@@ -68,6 +76,17 @@
 end
 
 local actor_source = module:get_option_string("pubsub_post_actor", "superuser");
+local actor_secret = module:get_option_string("pubsub_post_secret");
+local actor_secrets = module:get_option("pubsub_post_secrets");
+
+local function verify_signature(secret, body, signature)
+	if not signature then return false; end
+	local algo, digest = signature:match("^([^=]+)=(%x+)");
+	if not algo then return false; end
+	local hmac = hmacs[algo];
+	if not algo then return false; end
+	return hmac(secret, body) == from_hex(digest);
+end
 
 function handle_POST(event, path)
 	local request = event.request;
@@ -76,6 +95,11 @@
 	local content_type = request.headers.content_type or "application/octet-stream";
 	local actor;
 
+	local secret = actor_secrets and actor_secrets[path] or actor_secret;
+	if secret and not verify_signature(secret, request.body, request.headers.x_hub_signature) then
+		return 401;
+	end
+
 	if actor_source == "request.ip" then
 		actor = request.ip or request.conn:ip();
 	elseif actor_source == "superuser" then