From 1d7b0b06448d9e92fcebc812c48b998cbf852f5f Mon Sep 17 00:00:00 2001 From: Pirate Praveen Date: Fri, 31 May 2024 22:36:08 +0530 Subject: [PATCH 1/4] Generate and store wkd hash in the database Thanks to wkd-client npm module, Akshay S Dinesh and Ananthu CV --- src/lib/util.js | 60 +++++++++++++++++++++++++++++++++++++++ src/modules/public-key.js | 2 ++ 2 files changed, 62 insertions(+) diff --git a/src/lib/util.js b/src/lib/util.js index 6b23bb0..4cffb69 100644 --- a/src/lib/util.js +++ b/src/lib/util.js @@ -1,6 +1,25 @@ /** * Copyright (C) 2020 Mailvelope GmbH * Licensed under the GNU Affero General Public License version 3 + * + * genWKDHash and encodeZBase32 are based on wkd-client node module, + * which is WKD client implementation in javascript + * Copyright (C) 2018 Wiktor Kwapisiewicz + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3.0 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA */ 'use strict'; @@ -87,6 +106,47 @@ exports.normalizeEmail = function(email) { return email; }; +exports.genWKDHash = function(email) { + return new Promise((resolve, reject) => { + const [localPart, domain] = email.split('@'); + const localPartHashed = crypto.createHash('sha1') + .update(localPart.toLowerCase()) + .digest(); + const localPartBase32 = encodeZBase32(localPartHashed); + const wkdhash = localPartBase32 + '@' + domain; + resolve(wkdhash); + }); +} + +function encodeZBase32(data) { + if (data.length === 0) { + return ""; + } + const ALPHABET = "ybndrfg8ejkmcpqxot1uwisza345h769"; + const SHIFT = 5; + const MASK = 31; + let buffer = data[0]; + let index = 1; + let bitsLeft = 8; + let result = ''; + while (bitsLeft > 0 || index < data.length) { + if (bitsLeft < SHIFT) { + if (index < data.length) { + buffer <<= 8; + buffer |= data[index++] & 0xff; + bitsLeft += 8; + } else { + const pad = SHIFT - bitsLeft; + buffer <<= pad; + bitsLeft += pad; + } + } + bitsLeft -= SHIFT; + result += ALPHABET[MASK & (buffer >> bitsLeft)]; + } + return result; +} + /** * Generate a cryptographically secure random hex string. If no length is * provided a 32 char hex string will be generated by default. diff --git a/src/modules/public-key.js b/src/modules/public-key.js index f6210ba..e63df5d 100644 --- a/src/modules/public-key.js +++ b/src/modules/public-key.js @@ -162,6 +162,8 @@ class PublicKey { if (userId.notify === true) { // generate nonce for verification userId.nonce = util.random(); + // generate wkd hash + userId.wkdhash = await util.genWKDHash(userId.email); await this._email.send({template: tpl.verifyKey, userId, keyId, origin, publicKeyArmored: userId.publicKeyArmored, i18n}); } } From 9f5e3f650134db89aa60ae3c6effa5205c6a71b5 Mon Sep 17 00:00:00 2001 From: Pirate Praveen Date: Sat, 1 Jun 2024 19:01:31 +0530 Subject: [PATCH 2/4] Add support for search with wkd hash --- src/modules/public-key.js | 21 ++++++++++++++++----- src/route/hkp.js | 5 +++-- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/modules/public-key.js b/src/modules/public-key.js index e63df5d..bfcae3c 100644 --- a/src/modules/public-key.js +++ b/src/modules/public-key.js @@ -244,7 +244,7 @@ class PublicKey { * @param {String} keyId (optional) The public key ID * @return {Promise} The verified key document */ - async getVerified({userIds, fingerprint, keyId}) { + async getVerified({userIds, fingerprint, keyId, wkd}) { let queries = []; // query by fingerprint if (fingerprint) { @@ -260,8 +260,19 @@ class PublicKey { 'userIds.verified': true }); } - // query by user ID - if (userIds) { + // query by wkd hash + if (userIds && wkd) { + queries = queries.concat(userIds.map(uid => ({ + userIds: { + $elemMatch: { + 'wkdhash': uid.email, + 'verified': true + } + } + }))); + } + // query by user id + if (userIds && !wkd) { queries = queries.concat(userIds.map(uid => ({ userIds: { $elemMatch: { @@ -283,10 +294,10 @@ class PublicKey { * @param {Object} i18n i18n object * @return {Promise} The public key document */ - async get({fingerprint, keyId, email, i18n}) { + async get({fingerprint, keyId, email, wkd, i18n}) { // look for verified key const userIds = email ? [{email}] : undefined; - const key = await this.getVerified({keyId, fingerprint, userIds}); + const key = await this.getVerified({keyId, fingerprint, userIds, wkd}); if (!key) { throw Boom.notFound(i18n.__('key_not_found')); } diff --git a/src/route/hkp.js b/src/route/hkp.js index 7f90136..1604702 100644 --- a/src/route/hkp.js +++ b/src/route/hkp.js @@ -46,7 +46,7 @@ class HKP { const params = this.parseQueryString(request); const key = await this._publicKey.get({...params, i18n: request.i18n}); if (params.op === 'get') { - if (params.mr) { + if (params.mr || params.wkd) { return h.response(key.publicKeyArmored) .header('Content-Type', 'application/pgp-keys; charset=utf-8') .header('Content-Disposition', 'attachment; filename=openpgp-key.asc'); @@ -84,7 +84,8 @@ class HKP { parseQueryString({query}) { const params = { op: query.op, // operation ... only 'get' is supported - mr: query.options === 'mr' // machine readable + mr: query.options === 'mr', // machine readable + wkd: query.options === 'wkd' // wkd hash }; if (!['get', 'index', 'vindex'].includes(params.op)) { throw Boom.notImplemented('Method not implemented'); From 59cc6c6ddfa577638e3eca8b06c806086efb2241 Mon Sep 17 00:00:00 2001 From: Pirate Praveen Date: Sat, 1 Jun 2024 19:09:45 +0530 Subject: [PATCH 3/4] Update documentation for web key directory support --- README.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/README.md b/README.md index f36201d..54f9145 100644 --- a/README.md +++ b/README.md @@ -50,13 +50,40 @@ The HKP APIs are not documented here. Please refer to the [HKP specification](ht #### Accepted `options` parameters * mr +* wkd +### Web Key Directory support + +Web Key Directory support can be enabled by adding a rewrite rule to web server +configured as a reverse proxy. + +Example configuration for caddy webserver (for example.com domain), + +``` +openpgpkey.example.com { + header /.well-known/openpgpkey/puri.sm/policy Content-Type text/plain + respond /.well-known/openpgpkey/puri.sm/policy `protocol-version 5` + route /.well-known/openpgpkey/example.com/hu/* { + uri strip_prefix /.well-known/openpgpkey/example.com/hu/ + rewrite * {path}? + rewrite * /pks/lookup?op=get&search={uri}%40example.com&options=wkd + reverse_proxy localhost:3000 + } +} + +openpgpkey.example.com DNS records should be pointing to the mailvelope +keyserver instance. +``` #### Usage example with GnuPG ``` gpg --keyserver hkps://keys.mailvelope.com --search info@mailvelope.com ``` +If Web Key Directory is enabled, +``` +gpg --locate-keys info@mailvelope.com +``` ## REST API ### Lookup a key From b5041cca7a9a49e90b92a66b720ba43daea37c14 Mon Sep 17 00:00:00 2001 From: Pirate Praveen Date: Sun, 2 Jun 2024 23:03:21 +0530 Subject: [PATCH 4/4] Serve dearmored/binary format key for wkd support Using gpg command to dearmor since openpgp.js seems to not support it yet --- src/route/hkp.js | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/route/hkp.js b/src/route/hkp.js index 1604702..42e501d 100644 --- a/src/route/hkp.js +++ b/src/route/hkp.js @@ -46,7 +46,29 @@ class HKP { const params = this.parseQueryString(request); const key = await this._publicKey.get({...params, i18n: request.i18n}); if (params.op === 'get') { - if (params.mr || params.wkd) { + if (params.wkd) { + const fs = require('fs'); + const publicKeyArmoredFile = '/tmp/' + key.fingerprint + '.asc'; + const publicKeyBinaryFile = '/tmp/' + key.fingerprint + '.asc.gpg'; + if (!fs.existsSync(publicKeyArmoredFile)) { + try { + fs.writeFileSync(publicKeyArmoredFile, key.publicKeyArmored); + } catch (err) { + console.error(err); + } + } + if (!fs.existsSync(publicKeyBinaryFile)) { + const {execSync} = require('child_process'); + let output = execSync('gpg --dearmor ' + publicKeyArmoredFile); + } + try { + const publicKeyBinary = fs.readFileSync(publicKeyBinaryFile); + return h.response(publicKeyBinary) + .header('Content-Type', 'application/octet-stream'); + } catch (err) { + console.error(err); + } + } else if (params.mr) { return h.response(key.publicKeyArmored) .header('Content-Type', 'application/pgp-keys; charset=utf-8') .header('Content-Disposition', 'attachment; filename=openpgp-key.asc');