]>
Commit | Line | Data |
---|---|---|
5c3fd6ac FG |
1 | package PVE::API2::ACMEAccount; |
2 | ||
3 | use strict; | |
4 | use warnings; | |
5 | ||
6 | use PVE::ACME; | |
7 | use PVE::CertHelpers; | |
8 | use PVE::Exception qw(raise_param_exc); | |
9 | use PVE::JSONSchema qw(get_standard_option); | |
10 | use PVE::RPCEnvironment; | |
11 | use PVE::Tools qw(extract_param); | |
75a2be66 | 12 | use PVE::ACME::Challenge; |
5c3fd6ac | 13 | |
83847084 FG |
14 | use PVE::API2::ACMEPlugin; |
15 | ||
69060f1a TL |
16 | use base qw(PVE::RESTHandler); |
17 | ||
83847084 FG |
18 | __PACKAGE__->register_method ({ |
19 | subclass => "PVE::API2::ACMEPlugin", | |
20 | path => 'plugins', | |
21 | }); | |
22 | ||
5c3fd6ac FG |
23 | my $acme_directories = [ |
24 | { | |
25 | name => 'Let\'s Encrypt V2', | |
26 | url => 'https://acme-v02.api.letsencrypt.org/directory', | |
27 | }, | |
28 | { | |
29 | name => 'Let\'s Encrypt V2 Staging', | |
30 | url => 'https://acme-staging-v02.api.letsencrypt.org/directory', | |
31 | }, | |
32 | ]; | |
5c3fd6ac | 33 | my $acme_default_directory_url = $acme_directories->[0]->{url}; |
5c3fd6ac | 34 | my $account_contact_from_param = sub { |
69060f1a TL |
35 | my @addresses = PVE::Tools::split_list(extract_param($_[0], 'contact')); |
36 | return [ map { "mailto:$_" } @addresses ]; | |
5c3fd6ac | 37 | }; |
5c3fd6ac FG |
38 | my $acme_account_dir = PVE::CertHelpers::acme_account_dir(); |
39 | ||
40 | __PACKAGE__->register_method ({ | |
41 | name => 'index', | |
42 | path => '', | |
43 | method => 'GET', | |
44 | permissions => { user => 'all' }, | |
45 | description => "ACMEAccount index.", | |
46 | parameters => { | |
47 | additionalProperties => 0, | |
48 | properties => { | |
49 | }, | |
50 | }, | |
51 | returns => { | |
52 | type => 'array', | |
53 | items => { | |
54 | type => "object", | |
55 | properties => {}, | |
56 | }, | |
57 | links => [ { rel => 'child', href => "{name}" } ], | |
58 | }, | |
59 | code => sub { | |
60 | my ($param) = @_; | |
61 | ||
62 | return [ | |
63 | { name => 'account' }, | |
64 | { name => 'tos' }, | |
bd3f27ad | 65 | { name => 'meta' }, |
5c3fd6ac | 66 | { name => 'directories' }, |
83847084 | 67 | { name => 'plugins' }, |
96d4c3b4 | 68 | { name => 'challenge-schema' }, |
5c3fd6ac FG |
69 | ]; |
70 | }}); | |
71 | ||
72 | __PACKAGE__->register_method ({ | |
73 | name => 'account_index', | |
74 | path => 'account', | |
75 | method => 'GET', | |
76 | permissions => { user => 'all' }, | |
77 | description => "ACMEAccount index.", | |
78 | protected => 1, | |
79 | parameters => { | |
80 | additionalProperties => 0, | |
81 | properties => { | |
82 | }, | |
83 | }, | |
84 | returns => { | |
85 | type => 'array', | |
86 | items => { | |
87 | type => "object", | |
88 | properties => {}, | |
89 | }, | |
90 | links => [ { rel => 'child', href => "{name}" } ], | |
91 | }, | |
92 | code => sub { | |
93 | my ($param) = @_; | |
94 | ||
95 | my $accounts = PVE::CertHelpers::list_acme_accounts(); | |
96 | return [ map { { name => $_ } } @$accounts ]; | |
97 | }}); | |
98 | ||
99 | __PACKAGE__->register_method ({ | |
100 | name => 'register_account', | |
101 | path => 'account', | |
102 | method => 'POST', | |
103 | description => "Register a new ACME account with CA.", | |
104 | protected => 1, | |
105 | parameters => { | |
106 | additionalProperties => 0, | |
107 | properties => { | |
108 | name => get_standard_option('pve-acme-account-name'), | |
109 | contact => get_standard_option('pve-acme-account-contact'), | |
110 | tos_url => { | |
5c3fd6ac | 111 | description => 'URL of CA TermsOfService - setting this indicates agreement.', |
0231e304 | 112 | type => 'string', |
5c3fd6ac FG |
113 | optional => 1, |
114 | }, | |
115 | directory => get_standard_option('pve-acme-directory-url', { | |
116 | default => $acme_default_directory_url, | |
117 | optional => 1, | |
118 | }), | |
fe64969b | 119 | 'eab-kid' => { |
fe64969b | 120 | description => 'Key Identifier for External Account Binding.', |
0231e304 | 121 | type => 'string', |
fe64969b FG |
122 | requires => 'eab-hmac-key', |
123 | optional => 1, | |
124 | }, | |
125 | 'eab-hmac-key' => { | |
fe64969b | 126 | description => 'HMAC key for External Account Binding.', |
0231e304 | 127 | type => 'string', |
fe64969b FG |
128 | requires => 'eab-kid', |
129 | optional => 1, | |
130 | }, | |
5c3fd6ac FG |
131 | }, |
132 | }, | |
133 | returns => { | |
134 | type => 'string', | |
135 | }, | |
136 | code => sub { | |
137 | my ($param) = @_; | |
138 | ||
7ffd1550 TL |
139 | my $rpcenv = PVE::RPCEnvironment::get(); |
140 | my $authuser = $rpcenv->get_user(); | |
141 | ||
5c3fd6ac FG |
142 | my $account_name = extract_param($param, 'name') // 'default'; |
143 | my $account_file = "${acme_account_dir}/${account_name}"; | |
7ffd1550 | 144 | mkdir $acme_account_dir if ! -e $acme_account_dir; |
5c3fd6ac | 145 | |
fe64969b FG |
146 | my $eab_kid = extract_param($param, 'eab-kid'); |
147 | my $eab_hmac_key = extract_param($param, 'eab-hmac-key'); | |
148 | ||
5c3fd6ac FG |
149 | raise_param_exc({'name' => "ACME account config file '${account_name}' already exists."}) |
150 | if -e $account_file; | |
151 | ||
152 | my $directory = extract_param($param, 'directory') // $acme_default_directory_url; | |
153 | my $contact = $account_contact_from_param->($param); | |
154 | ||
5c3fd6ac FG |
155 | my $realcmd = sub { |
156 | PVE::Cluster::cfs_lock_acme($account_name, 10, sub { | |
157 | die "ACME account config file '${account_name}' already exists.\n" | |
158 | if -e $account_file; | |
159 | ||
160 | my $acme = PVE::ACME->new($account_file, $directory); | |
161 | print "Generating ACME account key..\n"; | |
162 | $acme->init(4096); | |
163 | print "Registering ACME account..\n"; | |
fe64969b FG |
164 | |
165 | my %info = (contact => $contact); | |
166 | if (defined($eab_kid)) { | |
167 | $info{eab} = { | |
168 | kid => $eab_kid, | |
169 | hmac_key => $eab_hmac_key | |
170 | }; | |
171 | } | |
172 | ||
173 | eval { $acme->new_account($param->{tos_url}, %info); }; | |
174 | ||
7ffd1550 | 175 | if (my $err = $@) { |
5c3fd6ac | 176 | unlink $account_file; |
7ffd1550 | 177 | die "Registration failed: $err\n"; |
5c3fd6ac FG |
178 | } |
179 | print "Registration successful, account URL: '$acme->{location}'\n"; | |
180 | }); | |
181 | die $@ if $@; | |
182 | }; | |
183 | ||
184 | return $rpcenv->fork_worker('acmeregister', undef, $authuser, $realcmd); | |
185 | }}); | |
186 | ||
187 | my $update_account = sub { | |
188 | my ($param, $msg, %info) = @_; | |
189 | ||
bed7626a | 190 | my $account_name = extract_param($param, 'name') // 'default'; |
5c3fd6ac FG |
191 | my $account_file = "${acme_account_dir}/${account_name}"; |
192 | ||
193 | raise_param_exc({'name' => "ACME account config file '${account_name}' does not exist."}) | |
194 | if ! -e $account_file; | |
195 | ||
196 | ||
197 | my $rpcenv = PVE::RPCEnvironment::get(); | |
5c3fd6ac FG |
198 | my $authuser = $rpcenv->get_user(); |
199 | ||
200 | my $realcmd = sub { | |
201 | PVE::Cluster::cfs_lock_acme($account_name, 10, sub { | |
202 | die "ACME account config file '${account_name}' does not exist.\n" | |
203 | if ! -e $account_file; | |
204 | ||
205 | my $acme = PVE::ACME->new($account_file); | |
206 | $acme->load(); | |
207 | $acme->update_account(%info); | |
208 | if ($info{status} && $info{status} eq 'deactivated') { | |
209 | my $deactivated_name; | |
210 | for my $i (0..100) { | |
211 | my $candidate = "${acme_account_dir}/_deactivated_${account_name}_${i}"; | |
212 | if (! -e $candidate) { | |
213 | $deactivated_name = $candidate; | |
214 | last; | |
215 | } | |
216 | } | |
217 | if ($deactivated_name) { | |
218 | print "Renaming account file from '$account_file' to '$deactivated_name'\n"; | |
219 | rename($account_file, $deactivated_name) or | |
220 | warn ".. failed - $!\n"; | |
221 | } else { | |
222 | warn "No free slot to rename deactivated account file '$account_file', leaving in place\n"; | |
223 | } | |
224 | } | |
225 | }); | |
226 | die $@ if $@; | |
227 | }; | |
228 | ||
229 | return $rpcenv->fork_worker("acme${msg}", undef, $authuser, $realcmd); | |
230 | }; | |
231 | ||
232 | __PACKAGE__->register_method ({ | |
233 | name => 'update_account', | |
234 | path => 'account/{name}', | |
235 | method => 'PUT', | |
236 | description => "Update existing ACME account information with CA. Note: not specifying any new account information triggers a refresh.", | |
237 | protected => 1, | |
238 | parameters => { | |
239 | additionalProperties => 0, | |
240 | properties => { | |
241 | name => get_standard_option('pve-acme-account-name'), | |
242 | contact => get_standard_option('pve-acme-account-contact', { | |
243 | optional => 1, | |
244 | }), | |
245 | }, | |
246 | }, | |
247 | returns => { | |
248 | type => 'string', | |
249 | }, | |
250 | code => sub { | |
251 | my ($param) = @_; | |
252 | ||
253 | my $contact = $account_contact_from_param->($param); | |
254 | if (scalar @$contact) { | |
255 | return $update_account->($param, 'update', contact => $contact); | |
256 | } else { | |
257 | return $update_account->($param, 'refresh'); | |
258 | } | |
259 | }}); | |
260 | ||
261 | __PACKAGE__->register_method ({ | |
262 | name => 'get_account', | |
263 | path => 'account/{name}', | |
264 | method => 'GET', | |
265 | description => "Return existing ACME account information.", | |
266 | protected => 1, | |
267 | parameters => { | |
268 | additionalProperties => 0, | |
269 | properties => { | |
270 | name => get_standard_option('pve-acme-account-name'), | |
271 | }, | |
272 | }, | |
273 | returns => { | |
274 | type => 'object', | |
275 | additionalProperties => 0, | |
276 | properties => { | |
277 | account => { | |
278 | type => 'object', | |
279 | optional => 1, | |
7fa666d2 | 280 | renderer => 'yaml', |
5c3fd6ac FG |
281 | }, |
282 | directory => get_standard_option('pve-acme-directory-url', { | |
283 | optional => 1, | |
284 | }), | |
285 | location => { | |
286 | type => 'string', | |
287 | optional => 1, | |
288 | }, | |
289 | tos => { | |
290 | type => 'string', | |
291 | optional => 1, | |
292 | }, | |
293 | }, | |
294 | }, | |
295 | code => sub { | |
296 | my ($param) = @_; | |
297 | ||
bed7626a | 298 | my $account_name = extract_param($param, 'name') // 'default'; |
5c3fd6ac FG |
299 | my $account_file = "${acme_account_dir}/${account_name}"; |
300 | ||
301 | raise_param_exc({'name' => "ACME account config file '${account_name}' does not exist."}) | |
302 | if ! -e $account_file; | |
303 | ||
304 | my $acme = PVE::ACME->new($account_file); | |
305 | $acme->load(); | |
306 | ||
307 | my $res = {}; | |
308 | $res->{account} = $acme->{account}; | |
309 | $res->{directory} = $acme->{directory}; | |
310 | $res->{location} = $acme->{location}; | |
311 | $res->{tos} = $acme->{tos}; | |
312 | ||
313 | return $res; | |
314 | }}); | |
315 | ||
316 | __PACKAGE__->register_method ({ | |
317 | name => 'deactivate_account', | |
318 | path => 'account/{name}', | |
319 | method => 'DELETE', | |
320 | description => "Deactivate existing ACME account at CA.", | |
321 | protected => 1, | |
322 | parameters => { | |
323 | additionalProperties => 0, | |
324 | properties => { | |
325 | name => get_standard_option('pve-acme-account-name'), | |
326 | }, | |
327 | }, | |
328 | returns => { | |
329 | type => 'string', | |
330 | }, | |
331 | code => sub { | |
332 | my ($param) = @_; | |
333 | ||
334 | return $update_account->($param, 'deactivate', status => 'deactivated'); | |
335 | }}); | |
336 | ||
bd3f27ad | 337 | # TODO: deprecated, remove with pve 9 |
5c3fd6ac FG |
338 | __PACKAGE__->register_method ({ |
339 | name => 'get_tos', | |
340 | path => 'tos', | |
341 | method => 'GET', | |
bd3f27ad | 342 | description => "Retrieve ACME TermsOfService URL from CA. Deprecated, please use /cluster/acme/meta.", |
16e393ab | 343 | permissions => { user => 'all' }, |
5c3fd6ac FG |
344 | parameters => { |
345 | additionalProperties => 0, | |
346 | properties => { | |
347 | directory => get_standard_option('pve-acme-directory-url', { | |
348 | default => $acme_default_directory_url, | |
349 | optional => 1, | |
350 | }), | |
351 | }, | |
352 | }, | |
353 | returns => { | |
354 | type => 'string', | |
29a6f858 | 355 | optional => 1, |
5c3fd6ac FG |
356 | description => 'ACME TermsOfService URL.', |
357 | }, | |
358 | code => sub { | |
359 | my ($param) = @_; | |
360 | ||
361 | my $directory = extract_param($param, 'directory') // $acme_default_directory_url; | |
362 | ||
363 | my $acme = PVE::ACME->new(undef, $directory); | |
364 | my $meta = $acme->get_meta(); | |
365 | ||
366 | return $meta ? $meta->{termsOfService} : undef; | |
367 | }}); | |
368 | ||
bd3f27ad FG |
369 | __PACKAGE__->register_method ({ |
370 | name => 'get_meta', | |
371 | path => 'meta', | |
372 | method => 'GET', | |
373 | description => "Retrieve ACME Directory Meta Information", | |
dab65f73 TL |
374 | permissions => { |
375 | check => ['perm', '/nodes/{node}', [ 'Sys.Audit' ]], | |
376 | }, | |
bd3f27ad FG |
377 | parameters => { |
378 | additionalProperties => 0, | |
379 | properties => { | |
380 | directory => get_standard_option('pve-acme-directory-url', { | |
381 | default => $acme_default_directory_url, | |
382 | optional => 1, | |
383 | }), | |
384 | }, | |
385 | }, | |
386 | returns => { | |
387 | type => 'object', | |
388 | additionalProperties => 1, | |
389 | properties => { | |
390 | termsOfService => { | |
0231e304 | 391 | description => 'ACME TermsOfService URL.', |
bd3f27ad FG |
392 | type => 'string', |
393 | optional => 1, | |
bd3f27ad FG |
394 | }, |
395 | externalAccountRequired => { | |
c0ab227a | 396 | description => 'EAB Required', |
bd3f27ad FG |
397 | type => 'boolean', |
398 | optional => 1, | |
bd3f27ad FG |
399 | }, |
400 | website => { | |
c0ab227a | 401 | description => 'URL to more information about the ACME server.', |
bd3f27ad FG |
402 | type => 'string', |
403 | optional => 1, | |
bd3f27ad FG |
404 | }, |
405 | caaIdentities => { | |
c0ab227a | 406 | description => 'Hostnames referring to the ACME servers.', |
bd3f27ad FG |
407 | type => 'string', |
408 | optional => 1, | |
bd3f27ad FG |
409 | }, |
410 | }, | |
411 | }, | |
412 | code => sub { | |
413 | my ($param) = @_; | |
414 | ||
415 | my $directory = extract_param($param, 'directory') // $acme_default_directory_url; | |
416 | ||
417 | my $acme = PVE::ACME->new(undef, $directory); | |
418 | my $meta = $acme->get_meta(); | |
419 | ||
420 | return $meta; | |
421 | }}); | |
422 | ||
5c3fd6ac FG |
423 | __PACKAGE__->register_method ({ |
424 | name => 'get_directories', | |
425 | path => 'directories', | |
426 | method => 'GET', | |
427 | description => "Get named known ACME directory endpoints.", | |
16e393ab | 428 | permissions => { user => 'all' }, |
5c3fd6ac FG |
429 | parameters => { |
430 | additionalProperties => 0, | |
431 | properties => {}, | |
432 | }, | |
433 | returns => { | |
434 | type => 'array', | |
435 | items => { | |
436 | type => 'object', | |
437 | additionalProperties => 0, | |
438 | properties => { | |
439 | name => { | |
440 | type => 'string', | |
441 | }, | |
442 | url => get_standard_option('pve-acme-directory-url'), | |
443 | }, | |
444 | }, | |
445 | }, | |
446 | code => sub { | |
447 | my ($param) = @_; | |
448 | ||
449 | return $acme_directories; | |
450 | }}); | |
451 | ||
75a2be66 DC |
452 | __PACKAGE__->register_method ({ |
453 | name => 'challengeschema', | |
454 | path => 'challenge-schema', | |
455 | method => 'GET', | |
456 | description => "Get schema of ACME challenge types.", | |
457 | permissions => { user => 'all' }, | |
458 | parameters => { | |
459 | additionalProperties => 0, | |
460 | properties => {}, | |
461 | }, | |
462 | returns => { | |
463 | type => 'array', | |
464 | items => { | |
465 | type => 'object', | |
466 | additionalProperties => 0, | |
467 | properties => { | |
468 | id => { | |
469 | type => 'string', | |
470 | }, | |
471 | name => { | |
472 | description => 'Human readable name, falls back to id', | |
473 | type => 'string', | |
474 | }, | |
475 | type => { | |
476 | type => 'string', | |
477 | }, | |
478 | schema => { | |
479 | type => 'object', | |
480 | }, | |
481 | }, | |
482 | }, | |
483 | }, | |
484 | code => sub { | |
485 | my ($param) = @_; | |
486 | ||
487 | my $plugin_type_enum = PVE::ACME::Challenge->lookup_types(); | |
488 | ||
489 | my $res = []; | |
490 | ||
491 | for my $type (@$plugin_type_enum) { | |
492 | my $plugin = PVE::ACME::Challenge->lookup($type); | |
493 | next if !$plugin->can('get_supported_plugins'); | |
494 | ||
495 | my $plugin_type = $plugin->type(); | |
496 | my $plugins = $plugin->get_supported_plugins(); | |
497 | for my $id (sort keys %$plugins) { | |
498 | my $schema = $plugins->{$id}; | |
499 | push @$res, { | |
500 | id => $id, | |
501 | name => $schema->{name} // $id, | |
502 | type => $plugin_type, | |
503 | schema => $schema, | |
504 | }; | |
505 | } | |
506 | } | |
507 | ||
508 | return $res; | |
509 | }}); | |
510 | ||
5c3fd6ac | 511 | 1; |