aboutsummaryrefslogtreecommitdiff
blob: 7392d555bb5bdb38e8f25e3ba973f0c034c28c37 (plain)
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: