1 # Nick's web site: Export GPG public keys for HTTP Keyserver and the
4 # Copyright © 2022 Nick Bowler
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <https://www.gnu.org/licenses/>.
22 @@gpg2 = "/usr/bin/gpg"
23 @@wksclient = "/usr/libexec/gpg-wks-client"
25 def WKD.gpg2; @@gpg2 end
26 def WKD.gpg2=(x); @@gpg2 = x end
27 def WKD.wksclient; @@wksclient end
28 def WKD.wksclient=(x); @@wksclient = x end
30 # Convert a list of keyring filenames into GPG keyring arguments
31 def WKD.keyring_args(args)
32 return [ "--no-default-keyring",
33 *args.map { |x| "--keyring=" + (x['/'] ? x : "./" + x) } ]
36 # Helper for implementing export filters below
37 def WKD.export(item, id, *args)
38 data, result = Open3.capture2(@@gpg2, "--export", *args,
39 *WKD.keyring_args(item[:keyrings]), id.chomp)
40 raise "gpg failed" unless result.success?
44 # Return a list of all key fingerprints known from the given GPG keyrings.
45 def WKD.keys_from_keyrings(*args)
49 "--with-colons", "--list-keys", *WKD.keyring_args(args)
50 ) do |stdin, stdout, result|
53 fields = line.split(":")
54 next if fields[0] != "fpr"
59 raise "gpg failed" unless result.value.success?
65 # Return a list of all UIDs known from the given GPG keyrings.
66 def WKD.uids_from_keyrings(*args)
70 "--with-colons", "--list-keys", *WKD.keyring_args(args)
71 ) do |stdin, stdout, result|
74 fields = line.split(":")
75 next if fields[0] != "uid"
76 fields[9].gsub!(/\\x../) { |x| x[2..].hex.chr }
77 uids[fields[9]] = true
81 raise "gpg failed" unless result.value.success?
87 # Given a list of UIDs, return a dictionary where the keys are UIDs
88 # and the values are the WKS hash.
89 def WKD.hashes_from_uids(*args)
92 consume_output = Proc.new do |s|
93 while l = s.slice!(/([^\n]*)\n/) do
94 hash, uid = l.chomp.split(nil, 2)
99 Open3.popen2(@@wksclient,
101 ) do |stdin, stdout, result|
103 args.flatten.each do |uid|
108 buf += stdout.read_nonblock(100)
109 consume_output.call(buf)
110 rescue EOFError, IO::WaitReadable
117 buf += stdout.readpartial(100)
118 consume_output.call(buf)
124 raise "gpg-wks-client failed" unless result.value.success?
132 # Call during preprocessing to create items for each unique UID found in the
133 # given keyring items. The items have the identifier /gpg/UID and the content
134 # is the same UID. The items are created with the :keyrings attribute set to
135 # the list of keyring files and :wkd_hash is for the Web Key Directory.
136 def create_wkd_items(keyring_items)
138 [*keyring_items].each { |item| keyring_files[item.raw_filename] = true }
140 wkd = WKD.hashes_from_uids(WKD.uids_from_keyrings(*keyring_files.keys))
141 wkd.each do |uid, hash|
143 keyrings: keyring_files.keys,
146 @items.create(uid, attrs, "/gpg/" + uid)
150 def create_hkp_items(keyring_items)
152 [*keyring_items].each { |item| keyring_files[item.raw_filename] = true }
154 fps = WKD.keys_from_keyrings(*keyring_files.keys)
162 keyids_64[id64] = keyids_64[id64].to_i + 1
163 keyids_32[id32] = keyids_32[id32].to_i + 1
170 attrs = { keyrings: keyring_files.keys }
171 attrs[:id64] = id64 if keyids_64[id64] == 1
172 attrs[:id32] = id32 if keyids_32[id32] == 1
174 @items.create("0x"+fp, attrs, "/gpg/" + fp)
178 # Convert items created by create_wkd_items into real GPG keyrings.
179 class WKDExport < Nanoc::Filter
180 identifier :wkd_export
181 type :text => :binary
183 def run(content, params = {})
184 WKD.export(item, content, "--output=" + output_filename)
188 class WKDExportArmor < Nanoc::Filter
189 identifier :wkd_export_armor
192 def run(content, params = {})
193 data = WKD.export(item, content, "--armor")