mirror of
https://git.proxmox.com/git/pve-manager
synced 2025-07-16 21:09:52 +00:00

As even though restricted to some specific endpoints and formats, one can still scan HTTP, potentially also on the LAN. We can do this here as the API call is new and was never packaged since introduced, so this isn't a breaking change. The TOS one will be removed with the next major release, so not a problem anymore from then one. Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
512 lines
12 KiB
Perl
512 lines
12 KiB
Perl
package PVE::API2::ACMEAccount;
|
|
|
|
use strict;
|
|
use warnings;
|
|
|
|
use PVE::ACME;
|
|
use PVE::CertHelpers;
|
|
use PVE::Exception qw(raise_param_exc);
|
|
use PVE::JSONSchema qw(get_standard_option);
|
|
use PVE::RPCEnvironment;
|
|
use PVE::Tools qw(extract_param);
|
|
use PVE::ACME::Challenge;
|
|
|
|
use PVE::API2::ACMEPlugin;
|
|
|
|
use base qw(PVE::RESTHandler);
|
|
|
|
__PACKAGE__->register_method ({
|
|
subclass => "PVE::API2::ACMEPlugin",
|
|
path => 'plugins',
|
|
});
|
|
|
|
my $acme_directories = [
|
|
{
|
|
name => 'Let\'s Encrypt V2',
|
|
url => 'https://acme-v02.api.letsencrypt.org/directory',
|
|
},
|
|
{
|
|
name => 'Let\'s Encrypt V2 Staging',
|
|
url => 'https://acme-staging-v02.api.letsencrypt.org/directory',
|
|
},
|
|
];
|
|
my $acme_default_directory_url = $acme_directories->[0]->{url};
|
|
my $account_contact_from_param = sub {
|
|
my @addresses = PVE::Tools::split_list(extract_param($_[0], 'contact'));
|
|
return [ map { "mailto:$_" } @addresses ];
|
|
};
|
|
my $acme_account_dir = PVE::CertHelpers::acme_account_dir();
|
|
|
|
__PACKAGE__->register_method ({
|
|
name => 'index',
|
|
path => '',
|
|
method => 'GET',
|
|
permissions => { user => 'all' },
|
|
description => "ACMEAccount index.",
|
|
parameters => {
|
|
additionalProperties => 0,
|
|
properties => {
|
|
},
|
|
},
|
|
returns => {
|
|
type => 'array',
|
|
items => {
|
|
type => "object",
|
|
properties => {},
|
|
},
|
|
links => [ { rel => 'child', href => "{name}" } ],
|
|
},
|
|
code => sub {
|
|
my ($param) = @_;
|
|
|
|
return [
|
|
{ name => 'account' },
|
|
{ name => 'tos' },
|
|
{ name => 'meta' },
|
|
{ name => 'directories' },
|
|
{ name => 'plugins' },
|
|
{ name => 'challenge-schema' },
|
|
];
|
|
}});
|
|
|
|
__PACKAGE__->register_method ({
|
|
name => 'account_index',
|
|
path => 'account',
|
|
method => 'GET',
|
|
permissions => { user => 'all' },
|
|
description => "ACMEAccount index.",
|
|
protected => 1,
|
|
parameters => {
|
|
additionalProperties => 0,
|
|
properties => {
|
|
},
|
|
},
|
|
returns => {
|
|
type => 'array',
|
|
items => {
|
|
type => "object",
|
|
properties => {},
|
|
},
|
|
links => [ { rel => 'child', href => "{name}" } ],
|
|
},
|
|
code => sub {
|
|
my ($param) = @_;
|
|
|
|
my $accounts = PVE::CertHelpers::list_acme_accounts();
|
|
return [ map { { name => $_ } } @$accounts ];
|
|
}});
|
|
|
|
__PACKAGE__->register_method ({
|
|
name => 'register_account',
|
|
path => 'account',
|
|
method => 'POST',
|
|
description => "Register a new ACME account with CA.",
|
|
protected => 1,
|
|
parameters => {
|
|
additionalProperties => 0,
|
|
properties => {
|
|
name => get_standard_option('pve-acme-account-name'),
|
|
contact => get_standard_option('pve-acme-account-contact'),
|
|
tos_url => {
|
|
description => 'URL of CA TermsOfService - setting this indicates agreement.',
|
|
type => 'string',
|
|
optional => 1,
|
|
},
|
|
directory => get_standard_option('pve-acme-directory-url', {
|
|
default => $acme_default_directory_url,
|
|
optional => 1,
|
|
}),
|
|
'eab-kid' => {
|
|
description => 'Key Identifier for External Account Binding.',
|
|
type => 'string',
|
|
requires => 'eab-hmac-key',
|
|
optional => 1,
|
|
},
|
|
'eab-hmac-key' => {
|
|
description => 'HMAC key for External Account Binding.',
|
|
type => 'string',
|
|
requires => 'eab-kid',
|
|
optional => 1,
|
|
},
|
|
},
|
|
},
|
|
returns => {
|
|
type => 'string',
|
|
},
|
|
code => sub {
|
|
my ($param) = @_;
|
|
|
|
my $rpcenv = PVE::RPCEnvironment::get();
|
|
my $authuser = $rpcenv->get_user();
|
|
|
|
my $account_name = extract_param($param, 'name') // 'default';
|
|
my $account_file = "${acme_account_dir}/${account_name}";
|
|
mkdir $acme_account_dir if ! -e $acme_account_dir;
|
|
|
|
my $eab_kid = extract_param($param, 'eab-kid');
|
|
my $eab_hmac_key = extract_param($param, 'eab-hmac-key');
|
|
|
|
raise_param_exc({'name' => "ACME account config file '${account_name}' already exists."})
|
|
if -e $account_file;
|
|
|
|
my $directory = extract_param($param, 'directory') // $acme_default_directory_url;
|
|
my $contact = $account_contact_from_param->($param);
|
|
|
|
my $realcmd = sub {
|
|
PVE::Cluster::cfs_lock_acme($account_name, 10, sub {
|
|
die "ACME account config file '${account_name}' already exists.\n"
|
|
if -e $account_file;
|
|
|
|
my $acme = PVE::ACME->new($account_file, $directory);
|
|
print "Generating ACME account key..\n";
|
|
$acme->init(4096);
|
|
print "Registering ACME account..\n";
|
|
|
|
my %info = (contact => $contact);
|
|
if (defined($eab_kid)) {
|
|
$info{eab} = {
|
|
kid => $eab_kid,
|
|
hmac_key => $eab_hmac_key
|
|
};
|
|
}
|
|
|
|
eval { $acme->new_account($param->{tos_url}, %info); };
|
|
|
|
if (my $err = $@) {
|
|
unlink $account_file;
|
|
die "Registration failed: $err\n";
|
|
}
|
|
print "Registration successful, account URL: '$acme->{location}'\n";
|
|
});
|
|
die $@ if $@;
|
|
};
|
|
|
|
return $rpcenv->fork_worker('acmeregister', undef, $authuser, $realcmd);
|
|
}});
|
|
|
|
my $update_account = sub {
|
|
my ($param, $msg, %info) = @_;
|
|
|
|
my $account_name = extract_param($param, 'name') // 'default';
|
|
my $account_file = "${acme_account_dir}/${account_name}";
|
|
|
|
raise_param_exc({'name' => "ACME account config file '${account_name}' does not exist."})
|
|
if ! -e $account_file;
|
|
|
|
|
|
my $rpcenv = PVE::RPCEnvironment::get();
|
|
my $authuser = $rpcenv->get_user();
|
|
|
|
my $realcmd = sub {
|
|
PVE::Cluster::cfs_lock_acme($account_name, 10, sub {
|
|
die "ACME account config file '${account_name}' does not exist.\n"
|
|
if ! -e $account_file;
|
|
|
|
my $acme = PVE::ACME->new($account_file);
|
|
$acme->load();
|
|
$acme->update_account(%info);
|
|
if ($info{status} && $info{status} eq 'deactivated') {
|
|
my $deactivated_name;
|
|
for my $i (0..100) {
|
|
my $candidate = "${acme_account_dir}/_deactivated_${account_name}_${i}";
|
|
if (! -e $candidate) {
|
|
$deactivated_name = $candidate;
|
|
last;
|
|
}
|
|
}
|
|
if ($deactivated_name) {
|
|
print "Renaming account file from '$account_file' to '$deactivated_name'\n";
|
|
rename($account_file, $deactivated_name) or
|
|
warn ".. failed - $!\n";
|
|
} else {
|
|
warn "No free slot to rename deactivated account file '$account_file', leaving in place\n";
|
|
}
|
|
}
|
|
});
|
|
die $@ if $@;
|
|
};
|
|
|
|
return $rpcenv->fork_worker("acme${msg}", undef, $authuser, $realcmd);
|
|
};
|
|
|
|
__PACKAGE__->register_method ({
|
|
name => 'update_account',
|
|
path => 'account/{name}',
|
|
method => 'PUT',
|
|
description => "Update existing ACME account information with CA. Note: not specifying any new account information triggers a refresh.",
|
|
protected => 1,
|
|
parameters => {
|
|
additionalProperties => 0,
|
|
properties => {
|
|
name => get_standard_option('pve-acme-account-name'),
|
|
contact => get_standard_option('pve-acme-account-contact', {
|
|
optional => 1,
|
|
}),
|
|
},
|
|
},
|
|
returns => {
|
|
type => 'string',
|
|
},
|
|
code => sub {
|
|
my ($param) = @_;
|
|
|
|
my $contact = $account_contact_from_param->($param);
|
|
if (scalar @$contact) {
|
|
return $update_account->($param, 'update', contact => $contact);
|
|
} else {
|
|
return $update_account->($param, 'refresh');
|
|
}
|
|
}});
|
|
|
|
__PACKAGE__->register_method ({
|
|
name => 'get_account',
|
|
path => 'account/{name}',
|
|
method => 'GET',
|
|
description => "Return existing ACME account information.",
|
|
protected => 1,
|
|
parameters => {
|
|
additionalProperties => 0,
|
|
properties => {
|
|
name => get_standard_option('pve-acme-account-name'),
|
|
},
|
|
},
|
|
returns => {
|
|
type => 'object',
|
|
additionalProperties => 0,
|
|
properties => {
|
|
account => {
|
|
type => 'object',
|
|
optional => 1,
|
|
renderer => 'yaml',
|
|
},
|
|
directory => get_standard_option('pve-acme-directory-url', {
|
|
optional => 1,
|
|
}),
|
|
location => {
|
|
type => 'string',
|
|
optional => 1,
|
|
},
|
|
tos => {
|
|
type => 'string',
|
|
optional => 1,
|
|
},
|
|
},
|
|
},
|
|
code => sub {
|
|
my ($param) = @_;
|
|
|
|
my $account_name = extract_param($param, 'name') // 'default';
|
|
my $account_file = "${acme_account_dir}/${account_name}";
|
|
|
|
raise_param_exc({'name' => "ACME account config file '${account_name}' does not exist."})
|
|
if ! -e $account_file;
|
|
|
|
my $acme = PVE::ACME->new($account_file);
|
|
$acme->load();
|
|
|
|
my $res = {};
|
|
$res->{account} = $acme->{account};
|
|
$res->{directory} = $acme->{directory};
|
|
$res->{location} = $acme->{location};
|
|
$res->{tos} = $acme->{tos};
|
|
|
|
return $res;
|
|
}});
|
|
|
|
__PACKAGE__->register_method ({
|
|
name => 'deactivate_account',
|
|
path => 'account/{name}',
|
|
method => 'DELETE',
|
|
description => "Deactivate existing ACME account at CA.",
|
|
protected => 1,
|
|
parameters => {
|
|
additionalProperties => 0,
|
|
properties => {
|
|
name => get_standard_option('pve-acme-account-name'),
|
|
},
|
|
},
|
|
returns => {
|
|
type => 'string',
|
|
},
|
|
code => sub {
|
|
my ($param) = @_;
|
|
|
|
return $update_account->($param, 'deactivate', status => 'deactivated');
|
|
}});
|
|
|
|
# TODO: deprecated, remove with pve 9
|
|
__PACKAGE__->register_method ({
|
|
name => 'get_tos',
|
|
path => 'tos',
|
|
method => 'GET',
|
|
description => "Retrieve ACME TermsOfService URL from CA. Deprecated, please use /cluster/acme/meta.",
|
|
permissions => { user => 'all' },
|
|
parameters => {
|
|
additionalProperties => 0,
|
|
properties => {
|
|
directory => get_standard_option('pve-acme-directory-url', {
|
|
default => $acme_default_directory_url,
|
|
optional => 1,
|
|
}),
|
|
},
|
|
},
|
|
returns => {
|
|
type => 'string',
|
|
optional => 1,
|
|
description => 'ACME TermsOfService URL.',
|
|
},
|
|
code => sub {
|
|
my ($param) = @_;
|
|
|
|
my $directory = extract_param($param, 'directory') // $acme_default_directory_url;
|
|
|
|
my $acme = PVE::ACME->new(undef, $directory);
|
|
my $meta = $acme->get_meta();
|
|
|
|
return $meta ? $meta->{termsOfService} : undef;
|
|
}});
|
|
|
|
__PACKAGE__->register_method ({
|
|
name => 'get_meta',
|
|
path => 'meta',
|
|
method => 'GET',
|
|
description => "Retrieve ACME Directory Meta Information",
|
|
permissions => {
|
|
check => ['perm', '/nodes/{node}', [ 'Sys.Audit' ]],
|
|
},
|
|
parameters => {
|
|
additionalProperties => 0,
|
|
properties => {
|
|
directory => get_standard_option('pve-acme-directory-url', {
|
|
default => $acme_default_directory_url,
|
|
optional => 1,
|
|
}),
|
|
},
|
|
},
|
|
returns => {
|
|
type => 'object',
|
|
additionalProperties => 1,
|
|
properties => {
|
|
termsOfService => {
|
|
description => 'ACME TermsOfService URL.',
|
|
type => 'string',
|
|
optional => 1,
|
|
},
|
|
externalAccountRequired => {
|
|
description => 'EAB Required',
|
|
type => 'boolean',
|
|
optional => 1,
|
|
},
|
|
website => {
|
|
description => 'URL to more information about the ACME server.',
|
|
type => 'string',
|
|
optional => 1,
|
|
},
|
|
caaIdentities => {
|
|
description => 'Hostnames referring to the ACME servers.',
|
|
type => 'string',
|
|
optional => 1,
|
|
},
|
|
},
|
|
},
|
|
code => sub {
|
|
my ($param) = @_;
|
|
|
|
my $directory = extract_param($param, 'directory') // $acme_default_directory_url;
|
|
|
|
my $acme = PVE::ACME->new(undef, $directory);
|
|
my $meta = $acme->get_meta();
|
|
|
|
return $meta;
|
|
}});
|
|
|
|
__PACKAGE__->register_method ({
|
|
name => 'get_directories',
|
|
path => 'directories',
|
|
method => 'GET',
|
|
description => "Get named known ACME directory endpoints.",
|
|
permissions => { user => 'all' },
|
|
parameters => {
|
|
additionalProperties => 0,
|
|
properties => {},
|
|
},
|
|
returns => {
|
|
type => 'array',
|
|
items => {
|
|
type => 'object',
|
|
additionalProperties => 0,
|
|
properties => {
|
|
name => {
|
|
type => 'string',
|
|
},
|
|
url => get_standard_option('pve-acme-directory-url'),
|
|
},
|
|
},
|
|
},
|
|
code => sub {
|
|
my ($param) = @_;
|
|
|
|
return $acme_directories;
|
|
}});
|
|
|
|
__PACKAGE__->register_method ({
|
|
name => 'challengeschema',
|
|
path => 'challenge-schema',
|
|
method => 'GET',
|
|
description => "Get schema of ACME challenge types.",
|
|
permissions => { user => 'all' },
|
|
parameters => {
|
|
additionalProperties => 0,
|
|
properties => {},
|
|
},
|
|
returns => {
|
|
type => 'array',
|
|
items => {
|
|
type => 'object',
|
|
additionalProperties => 0,
|
|
properties => {
|
|
id => {
|
|
type => 'string',
|
|
},
|
|
name => {
|
|
description => 'Human readable name, falls back to id',
|
|
type => 'string',
|
|
},
|
|
type => {
|
|
type => 'string',
|
|
},
|
|
schema => {
|
|
type => 'object',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
code => sub {
|
|
my ($param) = @_;
|
|
|
|
my $plugin_type_enum = PVE::ACME::Challenge->lookup_types();
|
|
|
|
my $res = [];
|
|
|
|
for my $type (@$plugin_type_enum) {
|
|
my $plugin = PVE::ACME::Challenge->lookup($type);
|
|
next if !$plugin->can('get_supported_plugins');
|
|
|
|
my $plugin_type = $plugin->type();
|
|
my $plugins = $plugin->get_supported_plugins();
|
|
for my $id (sort keys %$plugins) {
|
|
my $schema = $plugins->{$id};
|
|
push @$res, {
|
|
id => $id,
|
|
name => $schema->{name} // $id,
|
|
type => $plugin_type,
|
|
schema => $schema,
|
|
};
|
|
}
|
|
}
|
|
|
|
return $res;
|
|
}});
|
|
|
|
1;
|