summaryrefslogtreecommitdiffstats
path: root/lualib/rspamadm/dkim_keygen.lua
blob: 3d51903dcca0fdef498f1b011053bf47204c4c88 (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
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
--[[
Copyright (c) 2023, Vsevolod Stakhov <vsevolod@rspamd.com>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
]]--

local argparse = require "argparse"
local rspamd_util = require "rspamd_util"
local rspamd_cryptobox = require "rspamd_cryptobox"

local parser = argparse()
    :name 'rspamadm dkim_keygen'
    :description 'Create key pairs for dkim signing'
    :help_description_margin(30)
parser:option '-d --domain'
      :description 'Create a key for a specific domain'
      :default "example.com"
parser:option '-s --selector'
      :description 'Create a key for a specific DKIM selector'
      :default "mail"
parser:option '-k --privkey'
      :description 'Save private key to file instead of printing it to stdout'
parser:option '-b --bits'
      :convert(function(input)
  local n = tonumber(input)
  if not n or n < 512 or n > 4096 then
    return nil
  end
  return n
end)
      :description 'Generate an RSA key with the specified number of bits (512 to 4096)'
      :default(1024)
parser:option '-t --type'
      :description 'Key type: RSA, ED25519 or ED25119-seed'
      :convert {
  ['rsa'] = 'rsa',
  ['RSA'] = 'rsa',
  ['ed25519'] = 'ed25519',
  ['ED25519'] = 'ed25519',
  ['ed25519-seed'] = 'ed25519-seed',
  ['ED25519-seed'] = 'ed25519-seed',
}
      :default 'rsa'
parser:option '-o --output'
      :description 'Output public key in the following format: dns, dnskey or plain'
      :convert {
  ['dns'] = 'dns',
  ['plain'] = 'plain',
  ['dnskey'] = 'dnskey',
}
      :default 'dns'
parser:option '--priv-output'
      :description 'Output private key in the following format: PEM or DER (for RSA)'
      :convert {
  ['pem'] = 'pem',
  ['der'] = 'der',
}
      :default 'pem'
parser:flag '-f --force'
      :description 'Force overwrite of existing files'

local function split_string(input, max_length)
  max_length = max_length or 253
  local pieces = {}
  local index = 1

  while index <= #input do
    local piece = input:sub(index, index + max_length - 1)
    table.insert(pieces, piece)
    index = index + max_length
  end

  return pieces
end

local function print_public_key_dns(opts, base64_pk)
  local key_type = opts.type == 'rsa' and 'rsa' or 'ed25519'
  if #base64_pk < 255 - 2 then
    io.write(string.format('%s._domainkey IN TXT ( "v=DKIM1; k=%s;" \n\t"p=%s" ) ;\n',
        opts.selector, key_type, base64_pk))
  else
    -- Split it  by parts
    local parts = split_string(base64_pk)
    io.write(string.format('%s._domainkey IN TXT ( "v=DKIM1; k=%s; "\n', opts.selector, key_type))
    for i, part in ipairs(parts) do
      if i == 1 then
        io.write(string.format('\t"p=%s"\n', part))
      else
        io.write(string.format('\t"%s"\n', part))
      end
    end
    io.write(") ; \n")
  end

end

local function print_public_key(opts, pk)
  local key_type = opts.type == 'rsa' and 'rsa' or 'ed25519'
  local base64_pk = tostring(rspamd_util.encode_base64(pk))
  if opts.output == 'plain' then
    io.write(base64_pk)
    io.write("\n")
  elseif opts.output == 'dns' then
    print_public_key_dns(opts, base64_pk)
  elseif opts.output == 'dnskey' then
    io.write(string.format('v=DKIM1; k=%s; p=%s\n', key_type, base64_pk))
  end
end

local function gen_rsa_key(opts)
  local rsa = require "rspamd_rsa"

  local sk, pk = rsa.keypair(opts.bits or 1024)
  if opts.privkey then
    if opts.force then
      os.remove(opts.privkey)
    end
    sk:save(opts.privkey, opts.priv_output)
  else
    sk:save("-", opts.priv_output)
  end

  print_public_key(opts, tostring(pk))
end

local function gen_eddsa_key(opts)
  local sk, pk = rspamd_cryptobox.gen_dkim_keypair(opts.type)

  if opts.privkey and opts.force then
    os.remove(opts.privkey)
  end
  if not sk:save_in_file(opts.privkey, tonumber('0600', 8)) then
    io.stderr:write('cannot save private key to ' .. (opts.privkey or 'stdout') .. '\n')
    os.exit(1)
  end

  if not opts.privkey then
    io.write("\n")
    io.flush()
  end

  print_public_key(opts, tostring(pk))
end

local function handler(args)
  local opts = parser:parse(args)

  if not opts then
    os.exit(1)
  end

  if opts.type == 'rsa' then
    gen_rsa_key(opts)
  else
    gen_eddsa_key(opts)
  end
end

return {
  name = 'dkim_keygen',
  aliases = { 'dkimkeygen' },
  handler = handler,
  description = parser._description
}