Update GPG instructions for packages.
authorNick Bowler <nbowler@draconx.ca>
Sat, 26 Mar 2022 01:34:12 +0000 (21:34 -0400)
committerNick Bowler <nbowler@draconx.ca>
Sat, 26 Mar 2022 02:34:41 +0000 (22:34 -0400)
It seems the keys.gnupg.net keyserver has been dead for some time, so
it is not a viable option for public key distribution.

The new-ish Web Key Directory gives similar functionality but unlike
third party keyservers is self-hosted.

Rules
content/pubring.gpg [new file with mode: 0644]
lib/gpg-wkd.rb [new file with mode: 0644]
lib/project-readme.rb

diff --git a/Rules b/Rules
index 26bcb40ac7b1a8f0dbe86ba9f22b5edbc44d6300..7acf71db99c727cf4afc93c38f0c4f7f1a89fc2e 100644 (file)
--- a/Rules
+++ b/Rules
@@ -1,6 +1,6 @@
 #!/usr/bin/env ruby
 #
-# Copyright © 2018-2020 Nick Bowler
+# Copyright © 2018-2022 Nick Bowler
 #
 # This program is free software: you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -68,6 +68,8 @@ preprocess do
 
         @items.create("", attrs, "#{dir}/index.lst")
     end
+
+    create_wkd_items(@items["/pubring.gpg"])
 end
 
 postprocess do
@@ -216,6 +218,19 @@ compile '/**/*.scss' do
     write @item.identifier.without_ext + '.css'
 end
 
+compile '/gpg/*' do
+    filter :wkd_export, armor: true
+    write "/pubring/" + @item.identifier.components.last + ".asc"
+end
+
+compile '/gpg/*', rep: :hu do
+    filter :wkd_export
+    write "/pubring/wkd/" + @item[:wkd_hash]
+end
+
+compile '/*.gpg' do
+end
+
 compile '/**/*.svg' do
     filter :scour, comment_stripping: true
     write @item.identifier.to_s
diff --git a/content/pubring.gpg b/content/pubring.gpg
new file mode 100644 (file)
index 0000000..456deef
Binary files /dev/null and b/content/pubring.gpg differ
diff --git a/lib/gpg-wkd.rb b/lib/gpg-wkd.rb
new file mode 100644 (file)
index 0000000..c6e5fcc
--- /dev/null
@@ -0,0 +1,131 @@
+# Nick's web site: Export public keys for the Web Key Directory
+#
+# Copyright © 2022 Nick Bowler
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+module WKD
+    require 'open3'
+
+    @@gpg2 = "/usr/bin/gpg"
+    @@wksclient = "/usr/libexec/gpg-wks-client"
+
+    def WKD.gpg2; @@gpg2 end
+    def WKD.gpg2=(x); @@gpg2 = x end
+    def WKD.wksclient; @@wksclient end
+    def WKD.wksclient=(x); @@wksclient = x end
+
+    # Return a list list of all UIDs known from the given GPG keyrings.
+    def WKD.uids_from_keyrings(*args)
+        uids = {}
+
+        Open3.popen2(@@gpg2,
+            "--no-default-keyring", "--with-colons", "--list-keys",
+            *args.map { |x| "--keyring=" + (x['/'] ? x : "./" + x) }
+        ) do |stdin, stdout, result|
+            stdin.close
+            stdout.each do |line|
+                fields = line.split(":")
+                next if fields[0] != "uid"
+                fields[9].gsub!(/\\x../) { |x| x[2..].hex.chr }
+                uids[fields[9]] = true
+            end
+            stdout.close
+
+            raise "gpg failed" unless result.value.success?
+        end
+
+        return uids.keys
+    end
+
+    # Given a list of UIDs, return a dictionary where the keys are UIDs
+    # and the values are the WKS hash.
+    def WKD.hashes_from_uids(*args)
+        wkd_hash = {}
+
+        consume_output = Proc.new do |s|
+            while l = s.slice!(/([^\n]*)\n/) do
+                hash, uid = l.chomp.split(nil, 2)
+                wkd_hash[uid] = hash
+            end
+        end
+
+        Open3.popen2(@@wksclient,
+            "--print-wkd-hash"
+        ) do |stdin, stdout, result|
+            buf = ""
+            args.flatten.each do |uid|
+                stdin.puts(uid)
+                stdin.flush
+
+                loop do
+                    buf += stdout.read_nonblock(100)
+                    consume_output.call(buf)
+                rescue EOFError, IO::WaitReadable
+                    break
+                end
+            end
+            stdin.close
+
+            loop do
+                buf += stdout.readpartial(100)
+                consume_output.call(buf)
+            rescue EOFError
+                break
+            end
+            stdout.close
+
+            raise "gpg-wks-client failed" unless result.value.success?
+        end
+
+        return wkd_hash
+    end
+
+end
+
+# Call during preprocessing to create items for each unique UID found in the
+# given keyring items.  The items have the identifier /gpg/UID and the content
+# is the same UID.  The items are created with the :keyrings attribute set to
+# the list of keyring files and :wkd_hash is for the Web Key Directory.
+def create_wkd_items(keyring_items)
+    keyring_files = {}
+    [*keyring_items].each { |item| keyring_files[item.raw_filename] = true }
+
+    wkd = WKD.hashes_from_uids(WKD.uids_from_keyrings(*keyring_files.keys))
+    wkd.each do |uid, hash|
+        attrs = {
+            keyrings: keyring_files.keys,
+            wkd_hash: hash
+        }
+        @items.create(uid, attrs, "/gpg/" + uid)
+    end
+end
+
+# Convert items created by create_wkd_items into real GPG keyrings.
+class WKDExport < Nanoc::Filter
+    identifier :wkd_export
+    type :text => :binary
+
+    def run(content, params = {})
+        args = @item[:keyrings].map { |x|
+            "--keyring=" + (x['/'] ? x : "./" + x)
+        };
+        args.append("--armor") if params[:armor]
+
+        dummy, result = Open3.capture2(WKD.gpg2, "--export",
+            "--no-default-keyring", "--output=" + output_filename,
+            *args, content.chomp)
+        raise "gpg failed" unless result.success?
+    end
+end
index 3338e8a436558759a81a129abdc937d1d2142506..ffe3e3f3679b9e1f2dbd187d057007a87ac6c5a6 100644 (file)
@@ -1,6 +1,6 @@
 # Nick's web site: Generate a project page from a submodule README.md
 #
-# Copyright © 2021 Nick Bowler
+# Copyright © 2021-2022 Nick Bowler
 #
 # This program is free software: you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -82,21 +82,25 @@ def project_readme(item = @item)
             obtaining += line.lstrip
         end
 
+        gpg_uid = "nbowler@draconx.ca"
         obtaining += "\n\n" + <<~EOF
             [gpg]: https://gnupg.org/
+            [keyring]: /pubring/#{gpg_uid}.asc
 
             Use the signature file to verify that the corresponding source
             bundle is intact.  After downloading both files, if [GnuPG][gpg]
             is installed, the signature can be verified with a command like:
 
-            <kbd>gpg --verify #{targz}.sig</kbd>
+            <kbd>gpg --verify #{targz}.sig #{targz}</kbd>
 
             If the verification fails because you don't have the required
             public key, that key can be imported with a command such as:
 
-            <kbd>gpg --keyserver keys.gnupg.net --recv-keys 5B45D3D185B8E1F6</kbd>
+            <kbd>gpg --locate-external-keys #{gpg_uid}</kbd>
 
-            Then run the verify command again.
+            Then run the verify command again.  Alternatively, you can
+            [download the public keyring][keyring] manually and import it
+            using `gpg --import`.
         EOF
     end