]> git.proxmox.com Git - pmg-api.git/blob - src/PMG/CertHelpers.pm
acme: allow wildcard domain entries
[pmg-api.git] / src / PMG / CertHelpers.pm
1 package PMG::CertHelpers;
2
3 use strict;
4 use warnings;
5
6 use PVE::Certificate;
7 use PVE::JSONSchema;
8 use PVE::Tools;
9
10 use constant {
11 API_CERT => '/etc/pmg/pmg-api.pem',
12 SMTP_CERT => '/etc/pmg/pmg-tls.pem',
13 };
14
15 my $account_prefix = '/etc/pmg/acme/accounts';
16
17 # TODO: Move `pve-acme-account-name` to common and reuse instead of this.
18 PVE::JSONSchema::register_standard_option('pmg-acme-account-name', {
19 description => 'ACME account config file name.',
20 type => 'string',
21 format => 'pve-configid',
22 format_description => 'name',
23 optional => 1,
24 default => 'default',
25 });
26
27 PVE::JSONSchema::register_standard_option('pmg-acme-account-contact', {
28 type => 'string',
29 format => 'email-list',
30 description => 'Contact email addresses.',
31 });
32
33 PVE::JSONSchema::register_standard_option('pmg-acme-directory-url', {
34 type => 'string',
35 description => 'URL of ACME CA directory endpoint.',
36 pattern => '^https?://.*',
37 });
38
39 PVE::JSONSchema::register_format('pmg-certificate-type', sub {
40 my ($type, $noerr) = @_;
41
42 if ($type =~ /^(?: api | smtp )$/x) {
43 return $type;
44 }
45 return undef if $noerr;
46 die "value '$type' does not look like a valid certificate type\n";
47 });
48
49 PVE::JSONSchema::register_standard_option('pmg-certificate-type', {
50 type => 'string',
51 description => 'The TLS certificate type (API or SMTP certificate).',
52 enum => ['api', 'smtp'],
53 });
54
55 PVE::JSONSchema::register_format('pmg-acme-domain', sub {
56 my ($domain, $noerr) = @_;
57
58 my $label = qr/[a-z0-9][a-z0-9_-]*/i;
59
60 return $domain if $domain =~ /^(?:\*\.)?$label(?:\.$label)+$/;
61 return undef if $noerr;
62 die "value '$domain' does not look like a valid domain name!\n";
63 });
64
65 PVE::JSONSchema::register_format('pmg-acme-alias', sub {
66 my ($alias, $noerr) = @_;
67
68 my $label = qr/[a-z0-9_][a-z0-9_-]*/i;
69
70 return $alias if $alias =~ /^$label(?:\.$label)+$/;
71 return undef if $noerr;
72 die "value '$alias' does not look like a valid alias name!\n";
73 });
74
75 my $local_cert_lock = '/var/lock/pmg-certs.lock';
76 my $local_acme_lock = '/var/lock/pmg-acme.lock';
77
78 sub cert_path : prototype($) {
79 my ($type) = @_;
80 if ($type eq 'api') {
81 return API_CERT;
82 } elsif ($type eq 'smtp') {
83 return SMTP_CERT;
84 } else {
85 die "unknown certificate type '$type'\n";
86 }
87 }
88
89 sub cert_lock {
90 my ($timeout, $code, @param) = @_;
91
92 my $res = PVE::Tools::lock_file($local_cert_lock, $timeout, $code, @param);
93 die $@ if $@;
94 return $res;
95 }
96
97 sub set_cert_file {
98 my ($cert, $cert_path, $force) = @_;
99
100 my ($old_cert, $info);
101
102 my $cert_path_old = "${cert_path}.old";
103
104 die "Custom certificate file exists but force flag is not set.\n"
105 if !$force && -e $cert_path;
106
107 PVE::Tools::file_copy($cert_path, $cert_path_old) if -e $cert_path;
108
109 eval {
110 my $gid = undef;
111 if ($cert_path eq &API_CERT) {
112 $gid = getgrnam('www-data') ||
113 die "user www-data not in group file\n";
114 }
115
116 if (defined($gid)) {
117 my $cert_path_tmp = "${cert_path}.tmp";
118 PVE::Tools::file_set_contents($cert_path_tmp, $cert, 0640);
119 if (!chown(-1, $gid, $cert_path_tmp)) {
120 my $msg =
121 "failed to change group ownership of '$cert_path_tmp' to www-data ($gid): $!\n";
122 unlink($cert_path_tmp);
123 die $msg;
124 }
125 if (!rename($cert_path_tmp, $cert_path)) {
126 my $msg =
127 "failed to rename '$cert_path_tmp' to '$cert_path': $!\n";
128 unlink($cert_path_tmp);
129 die $msg;
130 }
131 } else {
132 PVE::Tools::file_set_contents($cert_path, $cert, 0600);
133 }
134
135 $info = PVE::Certificate::get_certificate_info($cert_path);
136 };
137 my $err = $@;
138
139 if ($err) {
140 if (-e $cert_path_old) {
141 eval {
142 warn "Attempting to restore old certificate file..\n";
143 PVE::Tools::file_copy($cert_path_old, $cert_path);
144 };
145 warn "$@\n" if $@;
146 }
147 die "Setting certificate files failed - $err\n"
148 }
149
150 unlink $cert_path_old;
151
152 return $info;
153 }
154
155 sub lock_acme {
156 my ($account_name, $timeout, $code, @param) = @_;
157
158 my $file = "$local_acme_lock.$account_name";
159
160 my $res = PVE::Tools::lock_file($file, $timeout, $code, @param);
161 die $@ if $@;
162 return $res;
163 }
164
165 sub acme_account_dir {
166 return $account_prefix;
167 }
168
169 sub list_acme_accounts {
170 my $accounts = [];
171
172 return $accounts if ! -d $account_prefix;
173
174 PVE::Tools::dir_glob_foreach($account_prefix, qr/[^.]+.*/, sub {
175 my ($name) = @_;
176
177 push @$accounts, $name
178 if PVE::JSONSchema::pve_verify_configid($name, 1);
179 });
180
181 return $accounts;
182 }