1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
|
# OpenPGP Web Key Directory implementation
# https://www.ietf.org/id/draft-koch-openpgp-webkey-service-06.txt
require 'base32'
require 'digest'
module Gentoo
class WKDGenerator < Jekyll::Generator
ACTIVE_DEV_KEYRING = '_data/active-devs.gpg'.freeze
SERVICE_KEYRING = '_data/service-keys.gpg'.freeze
ALL_DEV_KEYRING = '_data/all-devs.gpg'.freeze
# Need all keyrings here, for export-clean
KEYRINGS = [
ACTIVE_DEV_KEYRING,
SERVICE_KEYRING,
ALL_DEV_KEYRING,
]
WKD_DIR = '.well-known/openpgpkey/'.freeze
GPG_BASE_COMMAND = ['gpg',
'--no-auto-check-trustdb',
'--no-comments',
'--no-default-keyring',
'--no-emit-version',
'--no-greeting',
'--no-permission-warning',
'--no-secmem-warning',
'--preserve-permissions',
'--quiet',
'--with-colon',
].freeze
GPG_SHRINK_KEYS = [
# Some dev keys exceed the 256K buffer of MAX_WKD_RESULT_LENGTH
# https://github.com/gpg/gnupg/blob/master/g10/call-dirmngr.c#L44-L47
# This causes an error:
# gpg: error retrieving '...@gentoo.org' via WKD: Provided object is too large
#
# To mitigate it:
# export-clean: removes non-usable userIDs, signatures.
# no-export-attributes turns off Photo UIDs, which can easily get large.
'--export-options', 'export-clean,no-export-attributes',
].freeze
def generate_each_nick(site, keyring, nick, fps, email_domain)
# Do not run if we have no fingerprints to do
# otherwise GPG will print 'gpg: WARNING: nothing exported'
return if fps.empty?
gpg = GPG_BASE_COMMAND + Array(keyring).flatten.map {|k_| %w(--keyring) + Array(File.absolute_path(k_))}.flatten
keydata = nil
cmd = gpg + ['--export', *fps]
STDERR.puts("# generate_each_nick command: #{cmd.inspect}")
IO.popen(cmd, 'rb') do |p|
keydata = p.read
end
# If it's larger than 256K, it will trip the too large error, so only minimize selectively.
if keydata.length >= 256*1024 then
cmd = gpg + GPG_SHRINK_KEYS + ['--export', *fps]
STDERR.puts("# Key for #{nick}@#{email_domain} with #{fps.inspect} is too large, #{keydata.length} bytes; using export-clean")
STDERR.puts("# generate_each_nick command: #{cmd.inspect}")
keydata = ''
IO.popen(cmd, 'rb') do |p|
keydata = p.read
end
end
return if keydata.empty?
site.pages << WKDFile.new(site, nick, keydata)
site.pages << WKDFile.new(site, nick, keydata, email_domain)
end
def get_fingerprints_from_keyring(keyring)
gpg = GPG_BASE_COMMAND + ['--keyring', keyring]
# build a quick list of all fingerprints in this keyring
# IO.popen in a non-block context returns a list of lines
IO.popen(gpg + ['--list-keys'], 'rt',
&:readlines).grep(/^fpr:/).map(&:strip).map do |line|
line.split(':')[9]
end.compact.map(&:upcase)
end
def generate(site)
return if site.data['userinfo'].nil?
# WKD uses z-Base32; replace the alphabet since the standard
# Base32 module supports that and the zBase32 modules are hard to get
old_base32_table = Base32.table
Base32.table = 'ybndrfg8ejkmcpqxot1uwisza345h769'.freeze
[['current', ACTIVE_DEV_KEYRING], ['system', SERVICE_KEYRING]].each do |group, keyring|
keyring_fps = get_fingerprints_from_keyring(keyring)
# Now loop over users
site.data['userinfo'][group].each do |nick, details|
begin
fps = details['gpgfp'].map { |fp| fp.gsub(/\s+/, '').upcase }
# Run only on the intersection of fingerprints we want and fingerprints we have
# TODO: extract the domain here to use for WKD Advanced, for future
# cases where we have @FOO.gentoo.org emails.
# Must provide *all* keyrings here because of export-clean:
# otherwise it will exclude signatures that cross keyrings.
generate_each_nick(site, KEYRINGS, nick, (keyring_fps & fps), 'gentoo.org')
rescue
# fail them silently
end
end
end
# policy file is required
# Need BOTH:
# https://example.org/.well-known/openpgpkey/policy
site.pages << WKDPolicyFile.new(site, email_domain=nil)
# https://openpgpkey.example.org/.well-known/openpgpkey/example.org/policy
site.pages << WKDPolicyFile.new(site, email_domain='gentoo.org')
Base32.table = old_base32_table
end
end
class WKDFile < Jekyll::Page
def initialize(site, nick, keydata, email_domain=nil)
@site = site
@base = @site.source
# IF email_domain is empty, then you get WKD direct
# https://openpgpkey.example.org/.well-known/openpgpkey/example.org/hu/iy9q119eutrkn8s1mk4r39qejnbu3n5q?l=Joe.Doe
# https://example.org/.well-known/openpgpkey/hu/iy9q119eutrkn8s1mk4r39qejnbu3n5q?l=Joe.Doe
dir_parts = [WKDGenerator::WKD_DIR]
dir_parts << email_domain if !email_domain.nil? && email_domain != ''
dir_parts << 'hu'
dir_parts << '' # extra trailing empty element to get trailing slash
@dir = File.join(dir_parts)
@name = Base32.encode(Digest::SHA1.digest(nick.downcase))
process(@name)
read_yaml(File.join(@base, '_layouts'), 'passthrough.html')
@content = keydata
end
def render_with_liquid?
false
end
end
class WKDPolicyFile < Jekyll::Page
def initialize(site, email_domain=nil)
@site = site
@base = @site.source
dir_parts = [WKDGenerator::WKD_DIR]
dir_parts << email_domain if !email_domain.nil? && email_domain != ''
dir_parts << '' # extra trailing empty element to get trailing slash
@dir = File.join(dir_parts)
@name = 'policy'
process(@name)
read_yaml(File.join(@base, '_layouts'), 'passthrough.html')
@content = ''
end
end
end
# vim:et ts=2 sts=2:
|