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 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..bfcae3c 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}); } } @@ -242,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) { @@ -258,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: { @@ -281,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..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) { + 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'); @@ -84,7 +106,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');