]> git.proxmox.com Git - pve-manager.git/blob - PVE/API2/ACMEAccount.pm
api/acme: deprecate tos endpoint in favor of meta
[pve-manager.git] / PVE / API2 / ACMEAccount.pm
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);
12 use PVE::ACME::Challenge;
13
14 use PVE::API2::ACMEPlugin;
15
16 use base qw(PVE::RESTHandler);
17
18 __PACKAGE__->register_method ({
19 subclass => "PVE::API2::ACMEPlugin",
20 path => 'plugins',
21 });
22
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 ];
33 my $acme_default_directory_url = $acme_directories->[0]->{url};
34 my $account_contact_from_param = sub {
35 my @addresses = PVE::Tools::split_list(extract_param($_[0], 'contact'));
36 return [ map { "mailto:$_" } @addresses ];
37 };
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' },
65 { name => 'meta' },
66 { name => 'directories' },
67 { name => 'plugins' },
68 { name => 'challenge-schema' },
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 => {
111 type => 'string',
112 description => 'URL of CA TermsOfService - setting this indicates agreement.',
113 optional => 1,
114 },
115 directory => get_standard_option('pve-acme-directory-url', {
116 default => $acme_default_directory_url,
117 optional => 1,
118 }),
119 'eab-kid' => {
120 type => 'string',
121 description => 'Key Identifier for External Account Binding.',
122 requires => 'eab-hmac-key',
123 optional => 1,
124 },
125 'eab-hmac-key' => {
126 type => 'string',
127 description => 'HMAC key for External Account Binding.',
128 requires => 'eab-kid',
129 optional => 1,
130 },
131 },
132 },
133 returns => {
134 type => 'string',
135 },
136 code => sub {
137 my ($param) = @_;
138
139 my $rpcenv = PVE::RPCEnvironment::get();
140 my $authuser = $rpcenv->get_user();
141
142 my $account_name = extract_param($param, 'name') // 'default';
143 my $account_file = "${acme_account_dir}/${account_name}";
144 mkdir $acme_account_dir if ! -e $acme_account_dir;
145
146 my $eab_kid = extract_param($param, 'eab-kid');
147 my $eab_hmac_key = extract_param($param, 'eab-hmac-key');
148
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
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";
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
175 if (my $err = $@) {
176 unlink $account_file;
177 die "Registration failed: $err\n";
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
190 my $account_name = extract_param($param, 'name') // 'default';
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();
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,
280 renderer => 'yaml',
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
298 my $account_name = extract_param($param, 'name') // 'default';
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
337 # TODO: deprecated, remove with pve 9
338 __PACKAGE__->register_method ({
339 name => 'get_tos',
340 path => 'tos',
341 method => 'GET',
342 description => "Retrieve ACME TermsOfService URL from CA. Deprecated, please use /cluster/acme/meta.",
343 permissions => { user => 'all' },
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',
355 optional => 1,
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
369 __PACKAGE__->register_method ({
370 name => 'get_meta',
371 path => 'meta',
372 method => 'GET',
373 description => "Retrieve ACME Directory Meta Information",
374 permissions => { user => 'all' },
375 parameters => {
376 additionalProperties => 0,
377 properties => {
378 directory => get_standard_option('pve-acme-directory-url', {
379 default => $acme_default_directory_url,
380 optional => 1,
381 }),
382 },
383 },
384 returns => {
385 type => 'object',
386 additionalProperties => 1,
387 properties => {
388 termsOfService => {
389 type => 'string',
390 optional => 1,
391 description => 'ACME TermsOfService URL.',
392 },
393 externalAccountRequired => {
394 type => 'boolean',
395 optional => 1,
396 description => 'EAB Required'
397 },
398 website => {
399 type => 'string',
400 optional => 1,
401 description => 'URL to more information about the ACME server.'
402 },
403 caaIdentities => {
404 type => 'string',
405 optional => 1,
406 description => 'Hostnames referring to the ACME servers.'
407 },
408 },
409 },
410 code => sub {
411 my ($param) = @_;
412
413 my $directory = extract_param($param, 'directory') // $acme_default_directory_url;
414
415 my $acme = PVE::ACME->new(undef, $directory);
416 my $meta = $acme->get_meta();
417
418 return $meta;
419 }});
420
421 __PACKAGE__->register_method ({
422 name => 'get_directories',
423 path => 'directories',
424 method => 'GET',
425 description => "Get named known ACME directory endpoints.",
426 permissions => { user => 'all' },
427 parameters => {
428 additionalProperties => 0,
429 properties => {},
430 },
431 returns => {
432 type => 'array',
433 items => {
434 type => 'object',
435 additionalProperties => 0,
436 properties => {
437 name => {
438 type => 'string',
439 },
440 url => get_standard_option('pve-acme-directory-url'),
441 },
442 },
443 },
444 code => sub {
445 my ($param) = @_;
446
447 return $acme_directories;
448 }});
449
450 __PACKAGE__->register_method ({
451 name => 'challengeschema',
452 path => 'challenge-schema',
453 method => 'GET',
454 description => "Get schema of ACME challenge types.",
455 permissions => { user => 'all' },
456 parameters => {
457 additionalProperties => 0,
458 properties => {},
459 },
460 returns => {
461 type => 'array',
462 items => {
463 type => 'object',
464 additionalProperties => 0,
465 properties => {
466 id => {
467 type => 'string',
468 },
469 name => {
470 description => 'Human readable name, falls back to id',
471 type => 'string',
472 },
473 type => {
474 type => 'string',
475 },
476 schema => {
477 type => 'object',
478 },
479 },
480 },
481 },
482 code => sub {
483 my ($param) = @_;
484
485 my $plugin_type_enum = PVE::ACME::Challenge->lookup_types();
486
487 my $res = [];
488
489 for my $type (@$plugin_type_enum) {
490 my $plugin = PVE::ACME::Challenge->lookup($type);
491 next if !$plugin->can('get_supported_plugins');
492
493 my $plugin_type = $plugin->type();
494 my $plugins = $plugin->get_supported_plugins();
495 for my $id (sort keys %$plugins) {
496 my $schema = $plugins->{$id};
497 push @$res, {
498 id => $id,
499 name => $schema->{name} // $id,
500 type => $plugin_type,
501 schema => $schema,
502 };
503 }
504 }
505
506 return $res;
507 }});
508
509 1;