]> git.draconx.ca Git - homepage.git/blob - lib/gpg-wkd.rb
Release cdecl99-1.2.
[homepage.git] / lib / gpg-wkd.rb
1 # Nick's web site: Export GPG public keys for HTTP Keyserver and the
2 # Web Key Directory
3 #
4 # Copyright © 2022 Nick Bowler
5 #
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.
10 #
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.
15 #
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/>.
18
19 module WKD
20     require 'open3'
21
22     @@gpg2 = "/usr/bin/gpg"
23     @@wksclient = "/usr/libexec/gpg-wks-client"
24
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
29
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) } ]
34     end
35
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?
41         return data
42     end
43
44     # Return a list of all key fingerprints known from the given GPG keyrings.
45     def WKD.keys_from_keyrings(*args)
46         fps = {}
47
48         Open3.popen2(@@gpg2,
49             "--with-colons", "--list-keys", *WKD.keyring_args(args)
50         ) do |stdin, stdout, result|
51             stdin.close
52             stdout.each do |line|
53                 fields = line.split(":")
54                 next if fields[0] != "fpr"
55                 fps[fields[9]] = true
56             end
57             stdout.close
58
59             raise "gpg failed" unless result.value.success?
60         end
61
62         return fps.keys
63     end
64
65     # Return a list of all UIDs known from the given GPG keyrings.
66     def WKD.uids_from_keyrings(*args)
67         uids = {}
68
69         Open3.popen2(@@gpg2,
70             "--with-colons", "--list-keys", *WKD.keyring_args(args)
71         ) do |stdin, stdout, result|
72             stdin.close
73             stdout.each do |line|
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
78             end
79             stdout.close
80
81             raise "gpg failed" unless result.value.success?
82         end
83
84         return uids.keys
85     end
86
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)
90         wkd_hash = {}
91
92         consume_output = Proc.new do |s|
93             while l = s.slice!(/([^\n]*)\n/) do
94                 hash, uid = l.chomp.split(nil, 2)
95                 wkd_hash[uid] = hash
96             end
97         end
98
99         Open3.popen2(@@wksclient,
100             "--print-wkd-hash"
101         ) do |stdin, stdout, result|
102             buf = ""
103             args.flatten.each do |uid|
104                 stdin.puts(uid)
105                 stdin.flush
106
107                 loop do
108                     buf += stdout.read_nonblock(100)
109                     consume_output.call(buf)
110                 rescue EOFError, IO::WaitReadable
111                     break
112                 end
113             end
114             stdin.close
115
116             loop do
117                 buf += stdout.readpartial(100)
118                 consume_output.call(buf)
119             rescue EOFError
120                 break
121             end
122             stdout.close
123
124             raise "gpg-wks-client failed" unless result.value.success?
125         end
126
127         return wkd_hash
128     end
129
130 end
131
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)
137     keyring_files = {}
138     [*keyring_items].each { |item| keyring_files[item.raw_filename] = true }
139
140     wkd = WKD.hashes_from_uids(WKD.uids_from_keyrings(*keyring_files.keys))
141     wkd.each do |uid, hash|
142         attrs = {
143             keyrings: keyring_files.keys,
144             wkd_hash: hash
145         }
146         @items.create(uid, attrs, "/gpg/" + uid)
147     end
148 end
149
150 def create_hkp_items(keyring_items)
151     keyring_files = {}
152     [*keyring_items].each { |item| keyring_files[item.raw_filename] = true }
153
154     fps = WKD.keys_from_keyrings(*keyring_files.keys)
155     keyids_64 = {}
156     keyids_32 = {}
157
158     fps.each do |fp|
159         id64 = fp[-16..]
160         id32 = fp[-8..]
161
162         keyids_64[id64] = keyids_64[id64].to_i + 1
163         keyids_32[id32] = keyids_32[id32].to_i + 1
164     end
165
166     fps.each do |fp|
167         id64 = fp[-16..]
168         id32 = fp[-8..]
169
170         attrs = { keyrings: keyring_files.keys }
171         attrs[:id64] = id64 if keyids_64[id64] == 1
172         attrs[:id32] = id32 if keyids_32[id32] == 1
173
174         @items.create("0x"+fp, attrs, "/gpg/" + fp)
175     end
176 end
177
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
182
183     def run(content, params = {})
184         WKD.export(item, content, "--output=" + output_filename)
185     end
186 end
187
188 class WKDExportArmor < Nanoc::Filter
189     identifier :wkd_export_armor
190     type :text
191
192     def run(content, params = {})
193         data = WKD.export(item, content, "--armor")
194         return data
195     end
196 end