caaIdentities was mistakenly labled as a string in a previous patch and not as an array of strings, as it is defined in the rfc [0]. [0] https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.1 Signed-off-by: Folke Gleumes <f.gleumes@proxmox.com>
515 lines
12 KiB
Perl
515 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 => 'array',
|
|
items => {
|
|
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;
|