Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Web key directory #121

Open
LecrisUT opened this issue Nov 23, 2021 · 34 comments · May be fixed by #147 or #146
Open

Web key directory #121

LecrisUT opened this issue Nov 23, 2021 · 34 comments · May be fixed by #147 or #146

Comments

@LecrisUT
Copy link

Can this expose the public keys according to GPG Web Key Directory?

It would also be helpful to guide on what DNS records need to be set and other such setups in order to get the keyserver to be auto-detected, and which clients/gpg versions support which setup.

@strausmann
Copy link

i just stumbled across this question, too.

@pravi
Copy link
Contributor

pravi commented May 28, 2024

There is a python script to generate .well-known/openpgpkey/hu/ from a keyring - https://hg.intevation.de/gnupg/wkd-tools/file/tip/generate-openpgpkey-hu, but we should probably use the existing database and map the wkd requests to hkps search requests.

@pravi
Copy link
Contributor

pravi commented May 28, 2024

The format that WKD expects,
https://intevation.de/.well-known/openpgpkey/hu/it5sewh54rxz33fwmr8u6dy4bbz8itz4 is the direct method URL for "[email protected]".
and mailvelope keyserver's format is,
https://keys.puri.sm/pks/lookup?op=get&search=praveen%40debian.org (though this gives a html page and not raw key, so we will need to add another option to get raw keys).

It might be possible to do this url rewriting in web server as well. decode the request to email address and then rewrite the url to search by email address with an option to return raw data (probably in binary since wkd expects binary data - at least the example url returned binary data).

@pravi
Copy link
Contributor

pravi commented May 28, 2024

if we can get the hash generation logic figured out, then it can be simple I think.

  1. generate the hash from email and store in the db when a key is uploaded
  2. add a search option for this hash
  3. do a url rewriting to map between wkd url format and mailvelope search url (in web server)
  4. and add an option to return raw key in addition to current html page

Update: we can already pass options=mr https://keys.puri.sm/pks/lookup?op=get&[email protected]&options=mr

@pravi
Copy link
Contributor

pravi commented May 29, 2024

https://gist.github.com/kafene/0a6e259996862d35845784e6e5dbfc79 this is a useful resource. It shows how to generate the wkd-hash with gpg command itself. We need to find a way to do the same in Javascript.

@pravi
Copy link
Contributor

pravi commented May 29, 2024

https://github.com/openpgpjs/wkd-client/blob/master/src/wkd.js#L50 this has js code for generating the hash.

@pravi
Copy link
Contributor

pravi commented May 29, 2024

Based on wkd-client code, I was able to generate this hash using this code snippet.

const email = "[email protected]"
    if (!email) {
      throw new Error('You must provide an email parameter!');
    }

    if (typeof email !== 'string' || !email.includes('@')) {
      throw new Error('Invalid e-mail address.');
    }

    const [localPart, domain] = email.split('@');
    const localPartEncoded = new TextEncoder().encode(localPart.toLowerCase());
    const localPartHashed = new Uint8Array(await crypto.subtle.digest('SHA-1', localPartEncoded));
    const localPartBase32 = encodeZBase32(localPartHashed);
    console.log(localPartBase32);
/**
 * Encode input buffer using Z-Base32 encoding.
 * See: https://tools.ietf.org/html/rfc6189#section-5.1.6
 *
 * @param {Uint8Array} data - The binary data to encode
 * @returns {String} Binary data encoded using Z-Base32.
 */
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;
}

@pravi
Copy link
Contributor

pravi commented May 30, 2024

I manged to get the hash generated, but unable to pass the value back from util.js to public-key.js. Need to figure out doing this with proper async/await.

diff --git a/src/lib/util.js b/src/lib/util.js
index b871fba..271f0aa 100644
--- a/src/lib/util.js
+++ b/src/lib/util.js
@@ -87,6 +87,53 @@ exports.normalizeEmail = function(email) {
   return email;
 };
 
+exports.genWKDHash = function(email, wkdhash = '') {
+  const [localPart, domain] = email.split('@');
+  crypto.subtle.digest({
+          name: "SHA-1",
+      },
+      new TextEncoder().encode(localPart.toLowerCase())
+  )
+  .then(function(hash){
+     const localPartBase32 = encodeZBase32(new Uint8Array(hash));
+     wkdhash = localPartBase32 + '@' + domain;
+  })
+  .catch(function(err){
+      console.error(err);
+  });
+  console.log(wkdhash); 
+     return 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 bb0b2b1..0fe68f6 100644
--- a/src/modules/public-key.js
+++ b/src/modules/public-key.js
@@ -160,6 +160,10 @@ class PublicKey {
       if (userId.notify && userId.notify === true) {
         // generate nonce for verification
         userId.nonce = util.random();
+        // generate nonce for verification
+        console.log(util.genWKDHash(userId.email));
+        userId.wkdhash = util.genWKDHash(userId.email,userId.wkdhash);
+console.log("wkdhash" + userId.wkdhash);
         await this._email.send({template: tpl.verifyKey, userId, keyId, origin, publicKeyArmored: userId.publicKeyArmored, i18n});
       }
     }

@asdofindia
Copy link

@pravi

exports.genWKDHash = function(email, wkdhash = '') {
  const [localPart, domain] = email.split('@');
  crypto.subtle.digest({
          name: "SHA-1",
      },
      new TextEncoder().encode(localPart.toLowerCase())
  )
  .then(function(hash){
     const localPartBase32 = encodeZBase32(new Uint8Array(hash));
     wkdhash = localPartBase32 + '@' + domain;
  })
  .catch(function(err){
      console.error(err);
  });
     return wkdhash;
}

In the above, crypto.subtle.digest( function starts a new branch of execution (asynchronous) while the main branch continues from that line and jumps to return wkdhash;. At this point in time, wkdhash has not changed, and will be an empty string.

What needs to be done is to return the promise of crypto.subtle.digest itself, like this:

exports.genWKDHash = function(email, wkdhash = '') {
  const [localPart, domain] = email.split('@');
  return crypto.subtle.digest({
          name: "SHA-1",
      },
      new TextEncoder().encode(localPart.toLowerCase())
  )
  .then(function(hash){
     const localPartBase32 = encodeZBase32(new Uint8Array(hash));
     wkdhash = localPartBase32 + '@' + domain;
     return wkdhash;    // this is what the promise resolves to if there's no error
  })
  .catch(function(err){
      console.error(err);
  });
}

And then when you call it, you've to wait for the promise to resolve, like this:

userId.wkdhash = await util.genWKDHash(userId.email,userId.wkdhash);

This hashing operation seems to be destructive in that we cannot get the email address back from the hash. Therefore your latter comment about having to store the hash in the database seems right. Meaning for existing keys, there'll have to be a separate function to calculate the hashes and save to DB. And for new keys being uploaded, the hash can be calculated along with fingerprint, etc.

You could add a wkd endpoint like the hkp endpoint:

'use strict';

const Boom = require('@hapi/boom');
const util = require('../lib/util');
const openpgp = require('openpgp');

/**
 * An implementation of Web Key Directory
 * See https://wiki.gnupg.org/WKD
 */
class WKD {
  /**
   * Create an instance of the WKD server
   * @param  {Object} publicKey - an instance of the public key service
   */
  constructor(publicKey) {
    this._publicKey = publicKey;
  }

  /**
   * Public key lookup via http GET
   * @param {Object} request - hapi request object
   * @param {Object} h - hapi response toolkit
   */
  async lookup(request, h) {
    const hash = request.params.hash;
    // implement lookup
      return h.response(body).type('text/plain');
    }
  }
}

exports.plugin = {
  name: 'WKD',
  async register(server, options) {
    const wkd = new WKD(server.app.publicKey);

    const routeOptions = {
      bind: wkd,
      cors: options.server.cors === 'true',
      security: options.server.security === 'true',
      ext: {
        onPreResponse: {
          method({response}, h) {
            if (!response.isBoom) {
              return h.continue;
            }
            return h.response(response.message).code(response.output.statusCode).type('text/plain');
          }
        }
      }
    };

    server.route({
      method: 'GET',
      path: '/.well-known/openpgpkey/hu/{hash}',
      handler: wkd.lookup,
      options: routeOptions
    });
  }
};

But of course, implementing the lookup requires the hash to be saved.

@pravi
Copy link
Contributor

pravi commented May 31, 2024

@asdofindia thanks, let me try this.

Thanks also to Ananthu CV who also helped making this a promise.

diff --git a/src/lib/util.js b/src/lib/util.js
index b871fba..60d28eb 100644
--- a/src/lib/util.js
+++ b/src/lib/util.js
@@ -87,6 +87,47 @@ exports.normalizeEmail = function(email) {
   return email;
 };
 
+exports.genWKDHash = function(email, wkdhash = '') {
+ return new Promise((resolve, reject) => {
+    const [localPart, domain] = email.split('@');
+    const hash = crypto.createHash('sha1')
+      .update(localPart.toLowerCase())
+      .digest();
+    const localPartBase32 = encodeZBase32(hash);
+    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 bb0b2b1..bd04b5f 100644
--- a/src/modules/public-key.js
+++ b/src/modules/public-key.js
@@ -160,6 +160,8 @@ class PublicKey {
       if (userId.notify && userId.notify === true) {
         // generate nonce for verification
         userId.nonce = util.random();
+        // generate wkd hash
+        userId.wkdhash = await util.genWKDHash(userId.email,userId.wkdhash);
         await this._email.send({template: tpl.verifyKey, userId, keyId, origin, publicKeyArmored: userId.publicKeyArmored, i18n});
       }
     }

@pravi pravi linked a pull request May 31, 2024 that will close this issue
@pravi
Copy link
Contributor

pravi commented May 31, 2024

@asdofindia I tried adding src/route/wkd.js and visiting the .well-known/openpgpkey/hu/g4rf118d16sbj3k77d5txywpm879pszg which resulted in a 404. I think we should try to get a key not found/another debug message printed to get started before implementing the lookup part.

@pravi
Copy link
Contributor

pravi commented Jun 1, 2024

With the following code,

diff --git a/src/modules/public-key.js b/src/modules/public-key.js
index bb0b2b1..8fb8381 100644
--- a/src/modules/public-key.js
+++ b/src/modules/public-key.js
@@ -249,7 +252,7 @@ class PublicKey {
    * @param {string} keyId         (optional) The public key id
    * @return {Object}               The verified key document
    */
-  async getVerified({userIds, fingerprint, keyId}) {
+  async getVerified({userIds, fingerprint, keyId, wkd}) {
     let queries = [];
     // query by fingerprint
     if (fingerprint) {
@@ -265,8 +268,18 @@ class PublicKey {
         'userIds.verified': true
       });
     }
+    // query by wkd hash
+    if (userIds && wkd) {
+      queries = queries.concat(userIds.map(uid => ({
+        'userIds.wkdhash': {
+            $eq: uid.email},
+        'userIds.verified': {
+            $eq: true
+        }
+      })));
+    }
     // query by user id
-    if (userIds) {
+    if (userIds && !wkd) {
       queries = queries.concat(userIds.map(uid => ({
         'userIds.email': {
             $eq: util.normalizeEmail(uid.email)},
@@ -287,10 +300,10 @@ class PublicKey {
    * @param {Object} i18n          i18n object
    * @return {Object}               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');

I'm able to get the raw ascii armored key when looking for /pks/lookup?op=get&search=g4rf118d16sbj3k77d5txywpm879pszg%40puri.sm&options=wkd

Now we just need to either add a wkd route or a url rewriting scheme in web server that will redirect to this url from the wkd direct url.

@pravi
Copy link
Contributor

pravi commented Jun 1, 2024

/pks/lookup?op=get&search=g4rf118d16sbj3k77d5txywpm879pszg%40puri.sm&options=wkd is what is working correctly now, what is expected is /.well-known/openpgpkey/hu/g4rf118d16sbj3k77d5txywpm879pszg

So we have two options to do this mapping,

  1. Add a new route (previous try did not work)
  2. Rewrite the url at web server

Domain name will need to dynamically selected based on the request url since the local part of hash is not unique.

@pravi
Copy link
Contributor

pravi commented Jun 1, 2024

openpgpkey.puri.sm {
        route /.well-known/openpgpkey/puri.sm/hu/* {
                uri strip_prefix /.well-known/openpgpkey/puri.sm/hu/
                rewrite * /pks/lookup?op=get&search={uri}%40puri.sm&options=wkd
                reverse_proxy localhost:3000
        }
}

with this in CaddyFile, I can get the key from https://openpgpkey.puri.sm/.well-known/openpgpkey/puri.sm/hu/g4rf118d16sbj3k77d5txywpm879pszg (this is a restricted url for now).

@pravi
Copy link
Contributor

pravi commented Jun 1, 2024

Though gpg --locate-keys [email protected] seems to be just waiting, no error nor timeout for a long time.


$ gpg --verbose --locate-keys [email protected]
gpg: enabled compatibility flags:
gpg: using pgp trust model
gpg: error retrieving '[email protected]' via Local: No public key

@pravi
Copy link
Contributor

pravi commented Jun 1, 2024

Added

header /.well-known/openpgpkey/puri.sm/policy Content-Type text/plain
        respond /.well-known/openpgpkey/puri.sm/policy `protocol-version 5`

to CaddyFile and
https://openpgpkey.puri.sm/.well-known/openpgpkey/puri.sm/policy can be accessed via browser.

I wonder if gpg is expecting a binary file instead of ascii armored data. I read somewhere gpg can accept ascii armored data as well now.

@pravi
Copy link
Contributor

pravi commented Jun 1, 2024

$ gpg --verbose --locate-keys [email protected]
gpg: enabled compatibility flags:
gpg: using pgp trust model
gpg: error retrieving '[email protected]' via Local: No public key
gpg: error retrieving '[email protected]' via WKD: Network is down
gpg: error reading key: Network is down

The request finally timed out with this error, though the url is accessible via browser (behind a vpn).

https://gist.github.com/kafene/0a6e259996862d35845784e6e5dbfc79 has some hints about mime types.

@pravi
Copy link
Contributor

pravi commented Jun 1, 2024

I wonder if the following line in src/route/hkp.js is the problem

.header('Content-Disposition', 'attachment; filename=openpgp-key.asc');

@pravi
Copy link
Contributor

pravi commented Jun 1, 2024

With gpg 2.4.5, the error is now changed

$ gpg --verbose --locate-keys [email protected]
gpg: enabled compatibility flags:
gpg: using pgp trust model
gpg: error retrieving '[email protected]' via Local: No public key
gpg: error retrieving '[email protected]' via WKD: Server indicated a failure
gpg: error reading key: Server indicated a failure

@pravi
Copy link
Contributor

pravi commented Jun 1, 2024

I think we will need to convert the ascii armored key to binary when options is wkd.

@pravi
Copy link
Contributor

pravi commented Jun 1, 2024

I could not find a way to dearmor with openpgp.js so I used gpg command to dearmor

diff --git a/src/route/hkp.js b/src/route/hkp.js
index 7f90136..836fdda 100644
--- a/src/route/hkp.js
+++ b/src/route/hkp.js
@@ -46,7 +46,23 @@ 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');
+       try {
+               fs.writeFileSync('/tmp/' + key.fingerprint + '.asc', key.publicKeyArmored);
+               } catch (err) {
+                 console.error(err);
+               }
+       const {execSync} = require('child_process');
+       let output = execSync('gpg --dearmor ' + '/tmp/' + key.fingerprint + '.asc');
+       try {
+               const publicKeyBinary = fs.readFileSync('/tmp/' + key.fingerprint + '.asc.gpg');
+               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');

But even then gpg fails to get the key,


$ gpg --verbose --locate-keys [email protected]
gpg: enabled compatibility flags:
gpg: using pgp trust model
gpg: error retrieving '[email protected]' via Local: No public key
gpg: error retrieving '[email protected]' via WKD: No data
gpg: error reading key: No data

@wiktor-k
Copy link

wiktor-k commented Jun 2, 2024

Hmm... I'd advise to use the Web Key Directory checker but I just sunsetted it last month! 😬

GnuPG may cache the result and return error even if you fixed everything. I'd suggest creating the WKD URL manually and visiting it with curl -i -sSL.

I can do more thorough checks but only on Monday.

@pravi
Copy link
Contributor

pravi commented Jun 2, 2024

I have removed the firewall to allow access to everyone and this is the curl output.

$ curl -i -sSL https://openpgpkey.puri.sm/.well-known/openpgpkey/puri.sm/hu/g4rf118d16sbj3k77d5txywpm879pszg
HTTP/2 200 
accept-ranges: bytes
access-control-expose-headers: WWW-Authenticate,Server-Authorization
alt-svc: h3=":443"; ma=2592000
cache-control: no-cache
content-security-policy: default-src 'self'; object-src 'none'; style-src 'self' 'unsafe-inline'; frame-ancestors 'none'; form-action 'self'; base-uri 'self';
content-type: application/octet-stream
date: Sun, 02 Jun 2024 11:10:12 GMT
server: Caddy
strict-transport-security: max-age=15768000
vary: origin
x-content-type-options: nosniff
x-download-options: noopen
x-frame-options: DENY
x-xss-protection: 0
content-length: 2291

@pravi
Copy link
Contributor

pravi commented Jun 2, 2024

mukta@mahishi:~$ gpg-wks-client --check -v --debug=ipc [email protected]
gpg-wks-client: DBG: chan_3 <- # Home: /home/mukta/.gnupg
gpg-wks-client: DBG: chan_3 <- # Config: /home/mukta/.gnupg/dirmngr.conf
gpg-wks-client: DBG: chan_3 <- OK Dirmngr 2.4.5 at your service
gpg-wks-client: DBG: connection to the dirmngr established
gpg-wks-client: DBG: chan_3 -> WKD_GET -- [email protected]
gpg-wks-client: DBG: chan_3 <- S SOURCE https://openpgpkey.puri.sm
gpg-wks-client: DBG: chan_3 <- S PROGRESS tick ? 0 0
gpg-wks-client: DBG: chan_3 <- ERR 167772218 No data <Dirmngr>
gpg-wks-client: DBG: chan_3 -> BYE
gpg-wks-client: public key for '[email protected]' NOT found via WKD

@pravi
Copy link
Contributor

pravi commented Jun 2, 2024

Earlier it was waiting for terminal input before overwriting the .gpg file, I changed it to check for the file before running gpg --dearmor but still no luck with gpg client.

diff --git a/src/route/hkp.js b/src/route/hkp.js
index 7f90136..2376f5a 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');

@pravi
Copy link
Contributor

pravi commented Jun 2, 2024

Using https://sequoia-pgp.org/tools/wkd/

This gave,

Error while checking the key: SyntaxError: JSON.parse: unexpected character at line 1 column 1 of the JSON data

Update: this is probably broken since the same error is shown for [email protected] which has a working WKD implementation.

@wiktor-k
Copy link

wiktor-k commented Jun 2, 2024

Well, Sequoia's WKD checker really uses mine and since I shut down mine it's not working 🤷‍♂️

@asdofindia
Copy link

@pravi

❯ gpg-wks-client --print-wkd-url [email protected] 
https://openpgpkey.puri.sm/.well-known/openpgpkey/puri.sm/hu/g4rf118d16sbj3k77d5txywpm879pszg?l=praveen

It seems like ?l=praveen is (?) expected in the latest spec: https://datatracker.ietf.org/doc/draft-koch-openpgp-webkey-service/

Anyhow, gpg seems to be looking there which in this case returns Not found.

@pravi
Copy link
Contributor

pravi commented Jun 2, 2024

@asdofindia yay! That was the really missing secret sauce!

After adding rewrite * {path}? to strip the query parameter before the previous rewrite rule, gpg now finds the keys correctly!.

mukta@mahishi:~$ gpg-wks-client --check -v --debug=ipc [email protected]
gpg-wks-client: DBG: chan_3 <- # Home: /home/mukta/.gnupg
gpg-wks-client: DBG: chan_3 <- # Config: /home/mukta/.gnupg/dirmngr.conf
gpg-wks-client: DBG: chan_3 <- OK Dirmngr 2.4.5 at your service
gpg-wks-client: DBG: connection to the dirmngr established
gpg-wks-client: DBG: chan_3 -> WKD_GET -- [email protected]
gpg-wks-client: DBG: chan_3 <- S SOURCE https://openpgpkey.puri.sm
gpg-wks-client: DBG: chan_3 <- S PROGRESS tick ? 0 0
gpg-wks-client: DBG: chan_3 <- [ 44 20 c6 c0 cd 04 66 46 20 1c 01 0c 00 9c 40 d1 ...(982 byte(s) skipped) ]
gpg-wks-client: DBG: chan_3 <- [ 44 20 69 6c 40 70 75 72 69 2e 73 6d 3e c2 c1 14 ...(982 byte(s) skipped) ]
gpg-wks-client: DBG: chan_3 <- [ 44 20 b5 6f 40 b1 c1 4c ca b3 b6 43 36 4b 7f a9 ...(369 byte(s) skipped) ]
gpg-wks-client: DBG: chan_3 <- OK
gpg-wks-client: DBG: chan_3 -> BYE
gpg-wks-client: public key for '[email protected]' found via WKD
gpg-wks-client: fingerprint: 07EADF93151C322F27C1AD4A2A6164ED0256C63A
gpg-wks-client:     user-id: Praveen Arimbrathodiyil <[email protected]>
gpg-wks-client:     created: വ്യാഴം 16 മേയ് 2024 08:34:03 വൈകു IST
gpg-wks-client:   addr-spec: [email protected]
gpg-wks-client:     user-id: Praveen Arimbrathodiyil <[email protected]>
gpg-wks-client:     created: വ്യാഴം 16 മേയ് 2024 08:32:52 വൈകു IST
gpg-wks-client:   addr-spec: [email protected]

@pravi
Copy link
Contributor

pravi commented Jun 2, 2024

All the WKD documentation/wiki pages and tutorials will need to be updated.

@pravi
Copy link
Contributor

pravi commented Jun 2, 2024

#146 is ready for review (I don't have a lot of experience with coding, so I'm sure there can be many improvements).

@pravi
Copy link
Contributor

pravi commented Jun 2, 2024

I'm trying to see if we can do this entirely in caddy/web server without generating and storing the wkd hash since we now know the localPart is passed as a query parameter which can be used to search by email address.

I'm trying this,

route /.well-known/openpgpkey/puri.sm/hu/* {
                rewrite * /pks/lookup?op=get&search={query}%40puri.sm&options=mr
        }

But {query} is "l=praveen" so we need a way to strip out "l=" also from that.

I tried uri replace ?l= ?search= but it is still passing l=praveen.

@pravi
Copy link
Contributor

pravi commented Jun 2, 2024

With just a small change, I can get the keys without storing the wkd hash.

diff --git a/src/route/hkp.js b/src/route/hkp.js
index 7f90136..79cfda6 100644
--- a/src/route/hkp.js
+++ b/src/route/hkp.js
@@ -89,7 +89,14 @@ class HKP {
     if (!['get', 'index', 'vindex'].includes(params.op)) {
       throw Boom.notImplemented('Method not implemented');
     }
-    this.parseSearch(query.search, params);
+    let searchString = query.search;
+/**
+ * Search string for Web Key Directory lookups is l
+ */
+    if (query.l) {
+      searchString = query.l;
+    }
+    this.parseSearch(searchString, params);
     if (!params.keyId && !params.fingerprint && !params.email) {
       throw Boom.badRequest('Invalid search parameter');
     }

@pravi pravi linked a pull request Jun 2, 2024 that will close this issue
@pravi
Copy link
Contributor

pravi commented Jun 2, 2024

#147 submitted as a simpler method for wkd support.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
5 participants