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