]>
Commit | Line | Data |
---|---|---|
1360e6f0 DM |
1 | package PMG::Ticket; |
2 | ||
3 | use strict; | |
4 | use warnings; | |
5 | use Net::SSLeay; | |
6 | use Digest::SHA; | |
7 | ||
9d7f54a3 | 8 | use PVE::Tools; |
1360e6f0 DM |
9 | use PVE::Ticket; |
10 | ||
11 | use Crypt::OpenSSL::RSA; | |
12 | ||
13 | my $min_ticket_lifetime = -60*5; # allow 5 minutes time drift | |
14 | my $max_ticket_lifetime = 60*60*2; # 2 hours | |
15 | ||
d883fac2 DM |
16 | my $basedir = "/etc/proxmox"; |
17 | ||
18 | my $pmg_api_cert_fn = "$basedir/pmg-api.pem"; | |
19 | ||
20 | # this is just a secret accessable by all API servers | |
21 | # and is used for CSRF prevention | |
22 | my $pmg_csrf_key_fn = "$basedir/pmg-csrf.key"; | |
9d7f54a3 | 23 | |
c151b711 DM |
24 | my $authprivkeyfn = "$basedir/pmg-authkey.key"; |
25 | my $authpubkeyfn = "$basedir/pmg-authkey.pub"; | |
1360e6f0 | 26 | |
d883fac2 DM |
27 | # only write output if something fails |
28 | sub run_silent_cmd { | |
29 | my ($cmd) = @_; | |
30 | ||
31 | my $outbuf = ''; | |
32 | ||
33 | my $record_output = sub { | |
34 | $outbuf .= shift; | |
35 | $outbuf .= "\n"; | |
36 | }; | |
37 | ||
38 | eval { | |
39 | PVE::Tools::run_command($cmd, outfunc => $record_output, | |
40 | errfunc => $record_output); | |
41 | }; | |
42 | ||
43 | my $err = $@; | |
44 | ||
45 | if ($err) { | |
46 | print STDERR $outbuf; | |
47 | die $err; | |
48 | } | |
49 | } | |
50 | ||
9d7f54a3 DM |
51 | sub generate_api_cert { |
52 | my ($nodename, $force) = @_; | |
53 | ||
54 | if (-f $pmg_api_cert_fn) { | |
55 | return $pmg_api_cert_fn if !$force; | |
56 | unlink $pmg_api_cert_fn; | |
57 | } | |
58 | ||
c151b711 DM |
59 | my $gid = getgrnam('www-data') || |
60 | die "user www-data not in group file\n"; | |
61 | ||
62 | my $tmp_fn = "$pmg_api_cert_fn.tmp$$"; | |
63 | ||
9d7f54a3 | 64 | my $cmd = ['openssl', 'req', '-batch', '-x509', '-newkey', 'rsa:4096', |
c151b711 | 65 | '-nodes', '-keyout', $tmp_fn, '-out', $tmp_fn, |
9d7f54a3 DM |
66 | '-subj', "/CN=$nodename/", |
67 | '-days', '3650']; | |
68 | ||
c151b711 DM |
69 | eval { |
70 | run_silent_cmd($cmd); | |
71 | chown(0, $gid, $tmp_fn) || die "chown failed - $!\n"; | |
72 | chmod(0640, $tmp_fn) || die "chmod failed - $!\n"; | |
73 | rename($tmp_fn, $pmg_api_cert_fn) || die "rename failed - $!\n"; | |
74 | }; | |
75 | if (my $err = $@) { | |
76 | unlink $tmp_fn; | |
77 | die "unable to generate pmg api cert '$pmg_api_cert_fn':\n$err"; | |
78 | } | |
9d7f54a3 DM |
79 | |
80 | return $pmg_api_cert_fn; | |
81 | } | |
82 | ||
d883fac2 DM |
83 | sub generate_csrf_key { |
84 | ||
85 | return if -f $pmg_csrf_key_fn; | |
86 | ||
c151b711 DM |
87 | my $gid = getgrnam('www-data') || |
88 | die "user www-data not in group file\n"; | |
d883fac2 | 89 | |
c151b711 DM |
90 | my $tmp_fn = "$pmg_csrf_key_fn.tmp$$"; |
91 | my $cmd = ['openssl', 'genrsa', '-out', $tmp_fn, '2048']; | |
92 | ||
93 | eval { | |
94 | run_silent_cmd($cmd); | |
95 | chown(0, $gid, $tmp_fn) || die "chown failed - $!\n"; | |
96 | chmod(0640, $tmp_fn) || die "chmod failed - $!\n"; | |
97 | rename($tmp_fn, $pmg_csrf_key_fn) || die "rename failed - $!\n"; | |
98 | }; | |
99 | if (my $err = $@) { | |
100 | unlink $tmp_fn; | |
101 | die "unable to generate pmg csrf key '$pmg_csrf_key_fn':\n$@"; | |
102 | } | |
d883fac2 | 103 | |
c151b711 DM |
104 | return $pmg_csrf_key_fn; |
105 | } | |
106 | ||
107 | sub generate_auth_key { | |
108 | ||
109 | return if -f "$authprivkeyfn"; | |
110 | ||
111 | eval { | |
112 | run_silent_cmd(['openssl', 'genrsa', '-out', $authprivkeyfn, '2048']); | |
113 | ||
114 | run_silent_cmd(['openssl', 'rsa', '-in', $authprivkeyfn, '-pubout', '-out', $authpubkeyfn]); | |
115 | }; | |
116 | ||
117 | die "unable to generate pmg auth key:\n$@" if $@; | |
118 | } | |
119 | ||
120 | my $pve_auth_priv_key; | |
121 | sub get_privkey { | |
122 | ||
123 | return $pve_auth_priv_key if $pve_auth_priv_key; | |
124 | ||
125 | my $input = PVE::Tools::file_get_contents($authprivkeyfn); | |
126 | ||
127 | $pve_auth_priv_key = Crypt::OpenSSL::RSA->new_private_key($input); | |
128 | ||
129 | return $pve_auth_priv_key; | |
130 | } | |
131 | ||
132 | my $pve_auth_pub_key; | |
133 | sub get_pubkey { | |
134 | ||
135 | return $pve_auth_pub_key if $pve_auth_pub_key; | |
136 | ||
137 | my $input = PVE::Tools::file_get_contents($authpubkeyfn); | |
138 | ||
139 | $pve_auth_pub_key = Crypt::OpenSSL::RSA->new_public_key($input); | |
140 | ||
141 | return $pve_auth_pub_key; | |
d883fac2 DM |
142 | } |
143 | ||
1360e6f0 DM |
144 | my $csrf_prevention_secret; |
145 | my $get_csrfr_secret = sub { | |
146 | if (!$csrf_prevention_secret) { | |
d883fac2 | 147 | my $input = PVE::Tools::file_get_contents($pmg_csrf_key_fn); |
1360e6f0 | 148 | $csrf_prevention_secret = Digest::SHA::sha1_base64($input); |
d883fac2 | 149 | print "SECRET:$csrf_prevention_secret\n"; |
1360e6f0 DM |
150 | } |
151 | return $csrf_prevention_secret; | |
152 | }; | |
153 | ||
154 | ||
155 | sub verify_csrf_prevention_token { | |
156 | my ($username, $token, $noerr) = @_; | |
157 | ||
158 | my $secret = &$get_csrfr_secret(); | |
159 | ||
160 | return PVE::Ticket::verify_csrf_prevention_token( | |
d883fac2 | 161 | $secret, $username, $token, $min_ticket_lifetime, |
1360e6f0 DM |
162 | $max_ticket_lifetime, $noerr); |
163 | } | |
164 | ||
165 | sub assemble_csrf_prevention_token { | |
166 | my ($username) = @_; | |
167 | ||
168 | my $secret = &$get_csrfr_secret(); | |
169 | ||
170 | return PVE::Ticket::assemble_csrf_prevention_token ($secret, $username); | |
171 | } | |
172 | ||
173 | sub assemble_ticket { | |
174 | my ($username) = @_; | |
175 | ||
c151b711 DM |
176 | my $rsa_priv = get_privkey(); |
177 | ||
178 | return PVE::Ticket::assemble_rsa_ticket($rsa_priv, 'PMG', $username); | |
1360e6f0 DM |
179 | } |
180 | ||
181 | sub verify_ticket { | |
182 | my ($ticket, $noerr) = @_; | |
183 | ||
c151b711 DM |
184 | my $rsa_pub = get_pubkey(); |
185 | ||
1360e6f0 | 186 | return PVE::Ticket::verify_rsa_ticket( |
c151b711 | 187 | $rsa_pub, 'PMG', $ticket, undef, |
1360e6f0 DM |
188 | $min_ticket_lifetime, $max_ticket_lifetime, $noerr); |
189 | } | |
190 | ||
191 | # VNC tickets | |
192 | # - they do not contain the username in plain text | |
193 | # - they are restricted to a specific resource path (example: '/vms/100') | |
194 | sub assemble_vnc_ticket { | |
195 | my ($username, $path) = @_; | |
196 | ||
c151b711 DM |
197 | my $rsa_priv = get_privkey(); |
198 | ||
1360e6f0 DM |
199 | my $secret_data = "$username:$path"; |
200 | ||
201 | return PVE::Ticket::assemble_rsa_ticket( | |
c151b711 | 202 | $rsa_priv, 'PMGVNC', undef, $secret_data); |
1360e6f0 DM |
203 | } |
204 | ||
205 | sub verify_vnc_ticket { | |
206 | my ($ticket, $username, $path, $noerr) = @_; | |
207 | ||
c151b711 DM |
208 | my $rsa_pub = get_pubkey(); |
209 | ||
1360e6f0 DM |
210 | my $secret_data = "$username:$path"; |
211 | ||
212 | return PVE::Ticket::verify_rsa_ticket( | |
c151b711 | 213 | $rsa_pub, 'PMGVNC', $ticket, $secret_data, -20, 40, $noerr); |
1360e6f0 DM |
214 | } |
215 | ||
216 | 1; |